"Please feel free to edit the title if it is misleading, since I am not sure if that's the correct way to ask"
I am new to ASP.MVC I am running into a seemly easy problem and having a hard time doing it.
What I am trying to do: I have two sets of scaffolds: View A, Controller A, Model B. and View B, Controller B, Model B.
Controller A looks like this:
public ActionResult LogIn(FormCollection formValues)
{
ModelA Model = new ModelA();
Model.EmailAddress = formValues["EmailAddress"];
}
I want to pass this formValues["EmailAddress"] from controller A to controller B.
In Controller B:
public ActionResult Initiate(FormCollection formValues, string phone, string method)
{
var ModelB = new ModelB();
var ModelA = new ModelA(); ---> This is null.
ModelB.Email = ModelA.EmailAddress --> This is null.
var userId = ModelB.dosomething(ModelB.Email, phone, method);
}
Is there away of doing that?
If the logic of ModelA and ModelB are going to be intertwined, I would recommend creating a new ViewModel, View, and Controller:
ModelAB
public class AB {
public ModelA ModelA { get; set; }
public ModelB ModelB { get; set; }
}
ModelABController
public ActionResult LogInAndInitiate(FormCollection formValues, string phone, string method)
{
var ModelB = new ModelB();
var ModelA = new ModelA(); // passed in, so its not null!
ModelA.EmailAddress = formValues["EmailAddress"];
ModelB.Email = ModelA.EmailAddress // passed in too!
var userId = ModelB.dosomething(ModelB.Email, phone, method);
}
Save the Model in TempData in Controller A like this
ModelA Model = new ModelA();
Model.EmailAddress = formValues["EmailAddress"];
TempData["ModelA"]=Model;
and Then You can Acces it in Controller B like this
ModelA modelA= TempData["ModelA"] as ModelA;
However TempData has very short life and can not be used in subsequent requests
to keep the TempData persistant for subsequent request you can use
TempData.Keep();
If the controller actions are completely unrelated (i.e., they do not call each other) you have two options:
1) Render the data to the client into a form and let the data be re-posted on the next request. This is of course only suitable for small amounts of data AND if you are sure it is neither sensitive nor a problem if the user tampers with the data.
2) Use sessions. This is what sessions are for, i.e., shopping cart contents, etc. You can opt to save this data in a database or have it in-memory in a session object only.
I wouldn't recommend using TempData for this, the idea behind TempData is to contain data which is useful for the very next request only. Think validation messages and that kind of data.
Related
I need to build a ViewModel for my Layout's Web Application, I have tried this solution but it's not based on a URL's Id coming from the URL to generate the Layout's ViewModel.
I tried this but I first had the not existing empty controller error, then I tried to include the id as a parameter but I get the error "Object reference not set to an instance of an object." because the LayoutColorRGB is not set.
public MobileController(int id)
{
Event model = db.Events.Where(s => s.Id == id).FirstOrDefault();
LayoutVM = new LayoutVM()
{
EventId = model.Id,
LayoutColorRGB = model.LayoutColorRGB,
SponsorLogoLink = model.SponsorLogoLink,
SponsorLogoURL = model.SponsorLogoURL
};
ViewData["LayoutVM"] = LayoutVM;
}
There are many cases need to extract data based on the request context and show something on layout pages. For example:
You may want to show logged-in user info
You may want to show number of visitors or online visitors
You you may want to show the current language and let the user to change the language.
You may want to load site menu or side-bar or footer from database
To do so you can consider the following points:
Partial View: You can create some small partial views for those parts having a specific model for each partial view and render them in the layout page.
Use context to get data: You can initialize the model by extracting information from Request, ViewContext, RouteData, ValueProvider and other context objects.
Access to data by HTML Helpers: You can create a HtmlHelper to get data from context and use the helper in the layout or partial views.
Access to data by dependency injection: You can define some services for extracting data and then inject those data to layout pages. In the service, you will initialize the model using context objects. If you are using ASP.NET CORE, this is a good way to go.
Access to data as property of base controller: You can have a property in the base controller and initialize it in constructor of the controller or in OnActionExecuting. Then in the layout, get the property by casting ViewContext.Controller to type of your base controller and read the property.
Access to data by ViewBag: You can initialize an instance of the model in constructor of a base controller or in OnActionExecuting method of the base controller and then put it in ViewBag. Then you can easily use it in view.
Layout Pages: Don't forget you can define different layout pages and use different layouts based on your requirement. You can set the layout in the action or in _ViewStart.
Example
Trying to resolve id in each request, means you need to have id as part of all requests or you need to know what should you do in absence of id. Considering this fact and to keep things simple for a minimal example, I'll define the following model and base controller and try to resolve id in OnActionExecuting method of the base controller and then will derive all my controllers which needs such behavior from this base controller.
You can do the same using an ActionFilter or a global action filter of the OnActionExecuting method of your controller.
Using the following code:
If you browse /home/index you will see a red bar at bottom of the page.
If you browse /home/index/1 you will see a blue bar at the bottom of the page
If you browse /home/index/2 you will see a green bar at the bottom of the page
Layout Model
public class LayoutViewModel
{
public int? Id { get; set; }
public string Color { get; set; }
}
Base Controller
public class BaseControllr : Controller
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
//Get the id from route
var id = int.TryParse(ValueProvider.GetValue("id")?.AttemptedValue, out var temp)
? temp : default(int?);
var model = new LayoutViewModel();
//Your logic to initialize the model, for example
model.Id = id;
if (model.Id == null)
model.Color = "FF0000";
else if (model.Id%2==0)
model.Color = "00FF00";
else
model.Color = "0000FF";
//Set ViewBag
ViewBag.MainLayoutViewModel = model;
base.OnActionExecuting(filterContext);
}
}
Home Controller
public class HomeController : BaseControllr
{
public ActionResult Index(int? id)
{
return View();
}
}
_Layout.cshtml
Then in the _Layout.cshtml, add the following code before closing <body/> tag for test:
#{
string color = ViewBag.MainLayoutViewModel?.Color;
int? id = ViewBag.MainLayoutViewModel?.Id;
}
<div style="background-color:##color;">
Id:#id
</div>
You should have a default value for each property of your layout. Then if your model doe not have some property you can use it from the default layout.
internal static readonly LayoutVM defaultLayout = new LayoutVM()
{
EventId = 0,
LayoutColorRGB = "#FFFFFF",
SponsorLogoLink = "AnyLink",
SponsorLogoURL = "AnyImageUrl"
};
public MobileController(int id)
{
Event model = db.Events.Where(s => s.Id == id).FirstOrDefault();
if (model == null)
{
ViewData["LayoutVM"] = defaultLayout;
return;
}
LayoutVM = new LayoutVM()
{
EventId = model.Id,
LayoutColorRGB = model.LayoutColorRGB ?? defaultLayout.LayoutColorRGB,
SponsorLogoLink = model.SponsorLogoLink ?? defaultLayout.SponsorLogoLink,
SponsorLogoURL = model.SponsorLogoURL ?? defaultLayout.SponsorLogoURL
};
ViewData["LayoutVM"] = LayoutVM;
}
I have the following ViewModel that includes 2 other models:
public class ViewModel
{
public ViewModel1 viewModel1 { get; set; }
public ViewModel2 viewModel2 { get; set; }
}
My View looks like this:
#Html.TextBoxFor(m => m.viewModel1.NameOfCustomer)
#Html.TextBoxFor(m => m.viewModel2.ProductCategory)
And finally the controller:
public ActionResult CreateNewOrder(ViewModel viewModel)
{
Model1 myModel1 = new Model1()
{
NameOfCustomer = viewModel.viewModel1.NameOfCustomer
};
db.Orders.Add(myModel1);
db.SaveChanges();
return View(viewModel);
}
The problem is, if I want to take the data from my ViewModel to pass it to my actual model, it does not work. It just shows null as value. If I do the following change to my controller data is there but this does not work as it should:
public ActionResult CreateNewOrder(ViewModel viewModel)
{
// that works
viewModel.viewModel1.NameOfCustomer = "John John";
Model1 myModel1 = new Model1()
{
myModel1.NameOfCustomer = viewModel.viewModel1.NameOfCustomer
};
db.Orders.Add(myModel1);
db.SaveChanges();
return View(viewModel);
}
So I guess the problem is, my data from my view is not being send correctly to my ViewModel. I hope you guys can give me a hint what Im doing wrong.
Regards,
Edited following #Stephen Muecke's comment, doesn't sound like this is the case. However, I'll leave this answer here, as my first port of call would still be to check out the POST payload in the request to make sure it was going across the wire as expected, so this part may be useful
It looks like it's because the data coming into your MVC action, is essentially a flattened view model, whereas the model expected in the action is hierarchical.
If look at the network tab within your browser's dev tools, find the POST request being made, and check the payload being sent to the server.
Sounds like this is being posted:
{"NameOfCustomer": "SomePerson", ....}
Which won't be autobinding server-side to your model, because ViewModel does not itself have a NameOfCustomer property - instead, it's on a child object held in the viewModel1 property. So, autobinding would work if the following was being posted:
{"viewModel1": {"NameOfCustomer" : "SomePerson", ....}}
Solutions include:
1) construct the payload to post from JS yourself, ensuring the structure reflects the nested objects correctly as above
2) Create a flattened viewmodel, so all properties from ViewModel1 are held directly at top level in the ViewModel class
3) Create a custom model binder
Your code should properly carry the information to the controller(see a similar fiddle here)
The piece:
Model1 myModel1 = new Model1()
{
NameOfCustomer = viewModel.viewModel1.NameOfCustomer
};
db.Orders.Add(myModel1);
db.SaveChanges();
Are you sure you are doing the right thing here? You are creating a Model1 type variable and add it to the Orders in your database. Are you getting any errors? Maybe the right code should be something like:
Order myModel1 = new Order ()
{
NameOfCustomer = viewModel.viewModel1.NameOfCustomer
};
db.Orders.Add(myModel1);
db.SaveChanges();
I'm having some issues with my DropDownLists, because when I post the information and my Model is not valid it comes back "empty" to the page triggering an error exactly like this question.
I've used the solution proposed there and it fixed my problem. Anyway, I wanted to avoid querying the database every time my ModelState is not valid and I came with this approach. I would like to know if it is valid or if there are better ways to do it now, considering that instead of MVC2 (which was the MVC version from the question) I'm now using MVC 5, maybe they added something new to tackle this.
What I've done was to use the TempData to persist the information when my model is not valid.
public class ViewModel
{
[DisplayName("Project")]
public int ProjectID { get; set; }
public List<SelectListItem> Projects { get; set; }
//Other fields
}
Now my Create() Action (that populates Projects)
[HttpGet]
public ActionResult Create()
{
ViewModel vmodel = new ViewModel();
vmodel.Projects = db.GetProjects(User.Identity.Name).Select(x => new SelectListItem { Text = x.Description, Value = x.Id }).ToList();
TempData["Projects"] = vmodel.Projects;
return View(vmodel);
}
And my post would be like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ViewModel vmodel)
{
//Clear TempData (in theory will clear my tempdata when read, so if this controller redirects to another action my tempdata will be clear)
List<SelectListItem> projects = (TempData["Projects"] as List<SelectListItem>);
if (ModelState.IsValid)
{
//...
}
//If it got here it's going back to the screen.
//Repopulate the TempData (allowing it to exist one more trip)
TempData["Projects"] = projects;
vmodel.Projects = projects
return View(atendimento);
}
Is this approach a good one? Is there a better way to achieve that without querying the database every single time?
Thanks a lot!
You don't need to use TempData at all as you have a property in your view model to hold the dropdown items.
public ActionResult Create()
{
ViewModel vmodel = new ViewModel();
vmodel.Projects = GetProjects();
return View(vmodel);
}
private List<SelectListItem> GetProjects()
{
return db.GetProjects(User.Identity.Name)
.Select(x => new SelectListItem { Text = x.Description,
Value = x.Id }).ToList();
}
And in the view
#Html.DropDownListFor(s=>s.ProjectID,Model.Projects)
And in your HttpPost action, If ModelState is not valid, Reload the Projects collection again (because http is stateless)
if(ModelState.IsValid)
{
// to do :Save and redirect
}
model.Projects = GetProjects();
return View(model);
You may cache the Projects so that you do not need to hit the database every time, if you are too much worried about performance.
Personally, I wouldn't worry about querying the database each time for this kind of operation.
What if projects are added/deleted? This could be the reason the save failed (selected project deleted) and the user would never realise it.
I usually write a method to populate all of my view model's SelectListItems and then use this in my Get and in my Post if the validation fails.
Warning: This is my first web app.
I have 4 models, views and controllers. Lets call them A, B, C, D(ex. ModelA, ControllerA, ViewA). They are all basic views with list scaffolding.
/ControllerA/Index
User starts at ViewA and Selects an the first item, which redirects the user to ViewB
/ControllerB/Function?Aid=1
ViewB shows another list based on Selection from ViewA. Then the user Selects again is is redirected to ViewC
/ControllerC/Function?Aid=1&Bid=2
ViewC shows another list based on Selections from ViewA and ViewB. Then the user Selects again is is redirected to ViewD.
/ControllerD/Function?Aid=1&Bid=2&Cid=3
ViewD shows another list based on Selections from ViewA, ViewB, and ViewC, Then the user Selects again.
At this point I would like to POST Aid, Bid, Cid, and Did and save them in my database. Ideally the user would click the link, the data would be posted and then the site would redirect the user back to the homepage. Should I create another model and controller to Handle the post? I thought about trying to do the POST from controllerD but that doesn't seem like the proper way to do this.
The msdn tutorials only show posting directly from a view with a strongly typed model. I kinda stuck and I would prefer not to make this a complete mess.
Edit for Code
Controller
public ActionResult myFunction(int Aid = 0, int Bid, int Cid)
{
//query D stuff here
if (D == null)
{
return HttpNotFound();
}
return View(D.ToList());
}
[HttpPost]
[InitializeSimpleMembership]
public ActionResult CreateQuote(int Aid, int Bid, int Cid, int Did)
{
Quote myQuote = new Quote();
myQuote.Customer_ID_FK = (int)Membership.GetUser().ProviderUserKey;
myQuote.A_ID_FK = Aid;
myQuote.B_ID_FK = Bid;
myQuote.C_ID_FK = Cid;
myQuote.D_ID_FK = Did;
if (ModelState.IsValid)
{
db.Quotes.Add(myQuote);
db.SaveChanges();
db.Quotes.Max();
int mymax = db.Quotes.Max(q => q.ID);
return RedirectToAction();
}
return View(D.ToList());
}
[HttpPost]
[InitializeSimpleMembership]
public ActionResult CreateQuote(Quote myQuote)
{
myQuote.Customer_ID_FK = (int)Membership.GetUser().ProviderUserKey;
if (ModelState.IsValid)
{
db.Quotes.Max();
int mymax = db.Quotes.Max(q => q.ID);
db.Quotes.Add(myQuote);
db.SaveChanges();
return RedirectToAction();
}
return View(D.ToList());
}
It usually makes sense to put your post handler in the controller it's related to. This isn't always the case, as sometimes it would make more sense to make a new controller to handle all posts related to a certain task. You should also understand the distinction between a method IN a controller, and a controller. A controller is just a class that inherits from System.Web.Mvc.Controller and can have methods just like any other class. A perfectly reasonable controller could look like this:
public class DController : Controller
{
//GET /d
public ActionResult Index()
{
//MyModel is a class that would contain the logic to display
//the selections from A, B, and C
var model = new MyModel();
return View(model);
}
//POST /d/saveresults
//We only want this method to accept POST
[HttpPost]
public ActionResult SaveResults(MyEntity data)
{
var model = new MyModel();
model.SaveResultsToDatabase(data);
return Redirect("/");
}
}
The important thing in a controller is to keep logical processing to a minimum. There's nothing wrong with having an if statement here and there, but the majority of your logic should be handled by your model. A controller is there primarily to pass data between your views and models.
My View looks like this:
<%# Control Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<TMS.MVC.BusinessSystemsSupport.Models.SearchDataTypeModel>" %>
<table class="classQueryResultsTable">
<!-- the header -->
<tr class="headerRow">
<td>
<%= Html.ActionLink("Effective Startdate",
"SortDetails",
"DataQryUpdate",
new
{
model = Model,
sortBy = "EffectiveStartDate",
},
new { #class = "classLinkLogDetails" })%>
</td>
</tr>
</table>
My controller action:
public ActionResult SortDetails(SearchDataTypeModel model, String sortBy)
{
The model parameter is null. The sortBy parameter is populated. I can pass in a String property from the model to the action with no problem. I want to pass in the entire model though.
Any ideas what I'm doing wrong?
You can't pass complex objects:
new
{
model = Model,
sortBy = "EffectiveStartDate",
},
model = Model makes no sense and cannot be sent using GET. You might need to use a form with an editor template and/or hidden fields to send all the model properties. Remember only scalar values can be sent in the query string (key1=value1&key2=value2...). Another alternative that comes to mind is to send only the ID:
new
{
modelId = Model.Id,
sortBy = "EffectiveStartDate",
},
and in your controller action fetch the model given this id from your data store:
public ActionResult SortDetails(int modelId, String sortBy)
{
var model = repository.GetModel(modelId);
...
}
Of course this is only true if the user is not supposed to edit the model properties in a form. Depends on your scenario.
And for the sake of completeness let me expose another option: use the Html.Serialize helper from MVC Futures to serialize the entire model into a hidden field which could be passed back to the controller action and deserialized there.
There is another way of passing model or complex objects specifically in ActionLink as RouteValues.
MODEL: Make static Serialize and Deserialize methods in the class like
public class XYZ
{
// Some Fields
public string X { get; set; }
public string Y { get; set; }
public string X { get; set; }
// This will convert the passed XYZ object to JSON string
public static string Serialize(XYZ xyz)
{
var serializer = new JavaScriptSerializer();
return serializer.Serialize(xyz);
}
// This will convert the passed JSON string back to XYZ object
public static XYZ Deserialize(string data)
{
var serializer = new JavaScriptSerializer();
return serializer.Deserialize<XYZ>(data);
}
}
VIEW: Now convert your complex object to JSON string before passing it in Action View
<%= Html.ActionLink(Model.x, "SomeAction", new { modelString = XYZ.Serialize(Model) })%>
CONTROLLER: Get the object as string in Action method and convert it back to object before using
public ActionResult SomeAction(string modelString)
{
XYX xyz = XYX.Deserialize(modelString);
}
Thats All...
Note: Techniques discussed in other answers are well enough in case of Model, But some times you need to pass some complex object (other than database model) back to the controller, as I have such specific case.
Hope this will help some...:)
One other option is to persist the data you need in TempData. This will hand it to the next request, and you can retrieve it there. You should be able to persist the entire model object if you want to.
But it's easier (and better practice) to just retrieve it again from the database, as Darin suggests.
You would have to serialize the object. The url would get ugly and risk becoming to long.
This is kind of close to what you was looking for.
I use custom parameters that persist only within a controller.
Just easy to maintain and they are strong typed. I don't like variable in quotes.
If I use these in a form submit then all is good.
<form><%=Html.TextBox("catid.Value") %></form>
If I use Html.ActionLink then it no worky.
Basically the Url has to look something like this
?catid.Value=31f1a21a-9546-4f2f-8c26-a0273d11b233
The work around is pretty simple since I still remember how to manually write an html A tag.
<a href="?catid.Value=<%= cat.ID %>" ><%: cat.Name %></a>
public ActionResult Index(Core.ControllerPersistence._Guid catid)
{
if (catid.Value.HasValue)
{
Not all but some of the Html helpers are like a pen you would carry in your pocket that automatically signs your name so you don't have to move your wrist. If for some reason the pen one day does not work just grab a normal pen and move your wrist so you can sign your name and move on.
Jeff,
Maybe you could create a View class that has the properties SearchDataTypeModel and sortby and you pass it to the view. When you click the actionlink pass it just the Model.SearchDataTypeModel. HTH
Maybe it is too late. Got some solution. Something similar to this.
Here is my example.
Url generating code:
var rv = new RouteValueDictionary();
rv["sortBy"] = currentSortColumn;
rv["ascending"] = currentSortColumn == sortBy ? !ascending : true;
rv["filter.Id"] = // some value
rv["filter.Creator"] = // some value
var url = url.Action( // url is UrlHelper
actionName,
controllerName,
rv);
// as result it will output something like this:
// http://your_host/yourController/yourAction?sortBy=name&ascending=True&filter.Id=100&filter.Creator=test
Controller code:
public ActionResult YourAction(string sortBy = "name", bool ascending = false, YourFilterModel filter = null)
Filter object class:
public class YourFilterModel
{
public string Id { get; set; }
public string Creator { get; set; }
}