Missing model collection after submit - c#

Hi I have simple model which looks like this:
public class HomeModel
{
public HomeModel()
{
Buildings = new List<Building>();
}
public List<Building> Buildings { get; set; }
public int SelectedBuildingId { get; set; }
}
and on view i display combo like this:
#Html.DropDownListFor(model => model.SelectedBuildingId, new SelectList(Model.Buildings, "Id", "Name"), "Choose Building... ")
and now when I click submit button, then the buildings list dissapears
so I tryid to keep it with hidden field
#Html.HiddenFor(model => model.Buildings)
but it doesn't work, any help ?

#Html.HiddenFor(model => model.Buildings) -> this is working just for primitive types like int string and so on.
I will not recommend you to serialize a list into the view but if this is necessary for you,you can do it like this:
#for (int i = 0; i < Model.Buildings.Count; i++)
{
#Html.HiddenFor(c => c.Buildings[i])
}
And now after post you will have full list in your view.

If your view is strongly typed to your HomeModel, then you can return your model in your postback event and return the model again to your view:
[HttpPost]
public ActionResult GetHomePostResult(HomeModel model)
{
//do whatever with your model data
return View("YourHomeGetView", model);
}

You don't need the list again... just load it again in the Post action.
EDIT:
If you insist in having it posted along with the important data. You should serialize that list to a string, and then rebuild the list in the server after the post.
Remember you are in MVC not plain ASP.NET... so each action is somehow atomic. For the case of dropdownlists, this is the normal behavior: reload the list in the server (independently from where you load it).

1)
and now when I click submit button, then the buildings list dissapears so I tryid to keep it with hidden field
#Html.HiddenFor(model => model.Buildings)
but it doesn't work, any help ?
Just Inspect element and see what is the name rendered for this hidden field, Make sure you have the same name as in your Model (only then the values get binded). If you see a different name in element, change the syntax to
#Html.HiddenFor(model => model.Buildings, name {Name = Buildings })
and on post back the values will be binded in the model
**OR**
2) If you are retrieving the values from DB using some Id or something. Pass this Id to the view. And keep this in hidden field. On post back catch this value in controller with proper parameter name (Name must be same as the hidden field element in view). Use this Id to retrieve the DropDown again and continue...

Related

Add model into model using Html helper HiddenFor C# MVC

I have a model like
public class Model
{
public int Value { get; set; }
public List<OtherModel> List { get; set; }
}
public class OtherModel
{
public int Value1 { get; set; }
public int Value2 { get; set; }
public bool IsPropTrue { get; set; }
}
I am using Model in a View where I'm looping through the List to show data in a table.
Depending on whether one of the properties (IsPropTrue) in OtherModel is true or false, I want to use the HiddenFor Html helper and send the data to the HttpPost controller.
#model Model
#foreach (var item in Model.List)
{
if (item.IsPropTrue)
{
#Html.HiddenFor(model=> item.Value1)
#Html.HiddenFor(model=> item.Value2)
}
}
I think it doesn't work because I should in some way add these properties to the OtherModel, which is inside the Model; But the way I have it now, I am adding properties to Model.
you can do it like this :
#model Model
#foreach (var item in Model.List)
{
if (item.IsPropTrue)
{
#Html.HiddenFor(model => model.List[Model.List.IndexOf(item)].Value1)
#Html.HiddenFor(model => model.List[Model.List.IndexOf(item)].Value2)
}
}
this way the binding system will bind the hidden fields with your List OtherModel in the Model
if you want send an array to server based on the Model you have to use indexer in #Html.HiddenFor .
#model WebApplication1.Models.MyModel
<form>
#if (Model != null && Model.List != null)
{
for (int i = 0; i < Model.List.Count; i++)
{
if (Model.List[i].IsPropTrue)
{
#Html.HiddenFor(model => Model.List[i].Value1)
#Html.HiddenFor(model => Model.List[i].Value2)
}
}
}
<button type="submit">submit</button>
</form>
if you want know reason of using indexer on model i recommend How does MVC 4 List Model Binding work?
Consider if it the responsibility of the view or the controller action to make the decisions - you can send everything back to the action to do the decision making.
In your Views/Shared folder, create a controller called EditorTemplates
In this folder, add a partial view called OtherModel
In this view, set the model to OtherModel and set the Layout=null
Add the three OtherModel fields in EditorFor (and HiddenFor if not displaying isPropTrue). This partial view displays just one instance of your list.
In your main view, use the above editor model like so. MVC will take care of all rendering and postback of the Model State for your complete list of items. We like one-liners...
#Html.EditorFor(model => model.OtherModel)
When the data is subsequently posted back to an action, Model State has wrapped up all of your displayed items into a list again, so you can check the isPropTrue value for each item on the server.
The only issue with MVC is that is you pass an empty list out to a view, you get a null value back, so just replace this with an empty list when null is returned

editing a model in more than one view

My target is, to modify a model in more than one view.
Since sometimes my models have many properties I want to modify them in more than one view. Something like:
first page edits 2 properties, second page edits 3 other properties,...
the model looks like this:
public class LoadViewModel
{
public int CurrentPage { get; set; } = -1;
public PageViewModel PageViewModel { get; set; }
}
public class PageViewModel
{
public string Param1 { get; set; }
public string Param2 { get; set; }
public int Param3 { get; set; }
}
my view on the Index-page looks like this:
#model LoadViewModel
#using(Ajax.BeginForm("Load", "Home", new AjaxOptions {UpdateTargetId = "page"}, new {lvm = Model}))
{
<div id="page"></div>
<input type="submit"/>
}
and this is my action:
public ActionResult Load(LoadViewModel lvm = null)
{
if (lvm == null) lvm = new LoadViewModel();
lvm.CurrentPage += 1;
TempData["CurrentPage"] = TempData["CurrentPage"] == null ? 0 : (int)TempData["CurrentPage"] + 1;
if (!partialViewDict.ContainsKey((int) TempData["CurrentPage"]))
TempData["CurrentPage"] = 0;
return PartialView(partialViewDict[(int)TempData["CurrentPage"]], lvm);
}
the pages are just partials that are mapped:
private Dictionary<int, string> partialViewDict = new Dictionary<int, string>
{
{0, "Pages/_Page1"},
{1, "Pages/_Page2"},
{2, "Pages/_Page3"},
};
and designed like this:
#using WebApplication1.Controllers
#model LoadViewModel
#{
TempData["CurrentPage"] = 0;
}
#Html.DisplayNameFor(m => m.PageViewModel.Param1)
#Html.EditorFor(m => m.PageViewModel.Param1)
this is working. When switching to Page2 the model is correctly set, but when hitting the submit the value of Param1 (that I set in Page1) is resetted to null and only the values I set in the current partial are correct.
This is Page2:
#using WebApplication1.Controllers
#model LoadViewModel
#{
TempData["CurrentPage"] = 1;
}
#Html.DisplayNameFor(m => m.PageViewModel.Param2)
#Html.EditorFor(m => m.PageViewModel.Param2)
When I add a #Html.HiddenFor(m => m.PageViewModel.Param1) into the partial, the value is still set. But I don't want the values to be resetted. I don't want to add an #Html.HiddenFor for all properties a set in a previous view. How can I prevent that the values are resetted when hitting submit without adding #Html.HiddenFor for all not listed attributes? Or is there any other possibility to catch my target?
There's two pieces to this. First, the post itself, and getting that to validate. For that, each step should have its own view model, containing only the properties it's supposed to modify. This allows you to add all the validation you need without causing other steps to fail. In the end, you'll combine the data from all of these into your entity class or whatever.
Which brings us to the second piece. You need some way of persisting the data from each step. The only data that will exist after a POST is the data that was posted and anything in the session (which includes TempData). You could always create a bunch of hidden fields to store the data from the previous steps, but that can get a little arduous. Most likely, you'll just want to use the session.
TempData is basically a specialized instance of Session, so which you use doesn't really matter. With TempData, you'll need to remember call TempData.Keep() for each of the keys you've set for each step or you'll lose the previous steps on the next request. Session will keep them around for the life of the session, but you should remember to remove the keys at the end with Session.Remove().
Do you use #using (Html.BeginForm()) in your .cshtml?
Unfortunately this is MVC. MVC is stateless, which means if you don't render it then you loose it :(
If you use model binding and scaffolding, then you can save some time and work but at the end it will be the same solution.

MVC 4 EF - Product / Multiple Categories

EDIT: I think I need some sort of view model, but I'm unsure how to handle this relationship.
I'm trying to understand MVC 4 and EF Code First and I'm trying to map many to many relationships.
I have two classes;
public class Asset
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public virtual ICollection<Category> Categories { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public virtual ICollection<Asset> Assets { get; set; }
}
So I'm trying to allow each Asset to have multiple Categories and each Category may have multiple Assets.
On my create method I have;
public ActionResult Create()
{
var model = new Asset();
model.Categories = _db.Categories.ToList();
return View(model);
}
In my view, the only way I can show these categories is to say; (Note the capital M in Model. I can't use the lower case model as used elsewhere in the view)
#model MyProject.Models.Asset
#using (Html.BeginForm("Create", "Assets", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div>
#foreach (var item in Model.Categories)
{
<p>#item.CategoryName</p>
}
</div>
<div class="form-group">
#Html.LabelFor(model => model.Title, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Title, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Title, "", new { #class = "text-danger" })
</div>
</div>
}
When the initial create is called, I can see my asset and it has categories. On the return create method however, its null. I can't work out why. I understand I'm not doing anything to edit these categories in the View, I can't get that far. What I don't understand though is why my model leaves with categories, but comes back with none.
My create return (here my assets categories is null)
// POST: Assets/Create
[HttpPost]
public ActionResult Create(Asset model)
{
if (!ModelState.IsValid)
{
//error, return to view.
return View();
}
try
{
//do stuff
}
catch
{
return View();
}
}
Ultimately when creating an Asset, I want to be able to list all the categories and allow some selection as to which categories this new asset will belong. If someone could help me work that out, you're my hero. But if I could just understand why what's coming back isn't what I sent out, that would be a start.
In my view, the only way I can show these categories is to say; (Note the capital M in Model. I can't use the lower case model as used elsewhere in the view)
I've always hated that Microsoft uses the model => model.* convention through its generated views and tutorials and articles online; it only leads to confusion. In your view Model is an actual object instance, namely an instance of what you defined as the "model" for the view. The lowercase model you see used in things like Html.EditorFor is actually a parameter to a lambda expression. It can be called anything. For example, Html.EditorFor(x => x.Foo) and even Html.EditorFor(supercalifragilisticexpialidocious => supercalifragilisticexpialidocious.Foo) would work just as well. Although the value that gets passed into this parameter is usually the Model object instance, Model and model are totally different concepts.
When the initial create is called, I can see my asset and it has categories. On the return create method however, its null. I can't work out why. I understand I'm not doing anything to edit these categories in the View, I can't get that far. What I don't understand though is why my model leaves with categories, but comes back with none.
That is why. You're not doing anything to edit these categories in the view. There's no fields for them to be posted along with the form data, and as a result, the class instantiated by the modelbinder in your action does not contain any data for categories. This is key. The class instance that goes into the view is not the same class instance that comes back after something like a post. Each is a unique thing. The post action has no knowledge of anything that came before it; it simply has whatever data was posted. Assuming the action takes a parameter of a particular class, the modelbinder will attempt to new up an instance of that class and bind the posted data to appropriate properties on that class. It doesn't care what was sent to the view originally; it doesn't even care what class it's working with.
Ultimately when creating an Asset, I want to be able to list all the categories and allow some selection as to which categories this new asset will belong. If someone could help me work that out, you're my hero. But if I could just understand why what's coming back isn't what I sent out, that would be a start.
This is the fun part. First, you absolutely must use a view model for this. In case you're not familiar with view models, they're simply classes that are used as a model for a view, hence the name. What you're passing around here, Asset, is technically an "entity", which is a class that is used for data transfer, usually to/from a database. While an entity could be used as a model for a view, as you've done here, it's not really suited for that.
There's a clear conflict of interest, as the needs of a class representing some table schema in a database are vastly different from the needs of a class representing data for a UI layer. That's where view models come in. In the most traditional sense, a view model simply represents the data that will need to be displayed and/or edited in one or more views. It may have many properties in common with a particular entity or it may only have a subset of those properties or even completely different properties. It is the job of your application to "map" from your entity to your view model and vice-versa, so that the logic for saving an entity to a persistence store can be completely abstracted from the logic for how the user interacts with that data.
The reason a view model is so important for your purposes here is that form elements in HTML have certain limitations. They can only work with data that can be represented as a string: things like ints, bools, actual strings, etc. They are particularly unsuited for working with complex objects, like your Category class. In other words, it would be perfectly achievable to post back a list of integer ids, representing Categorys, but it is entirely implausible to post back complete Category instances that have been chosen by a user.
Since your entity expects a list of categories and your view will only feasibly be capable of posting a list of ints, there's a fundamental disconnect. Using a view model provides a way to bridge the gap. Plus, it allows you to have other properties, like a list of category choices to populate your select list with, that would be totally inappropriate to put on your entity class.
For your scenario, you'd need a view model like:
public class AssetViewModel
{
// any other asset properties you need to edit
public List<int> SelectedCategoryIds { get; set; }
public IEnumerable<SelectListItem> CategoryChoices { get; set; }
}
This then allows you to create a multiselect list in your view using:
#Html.ListBoxFor(m => m.SelectedCategoryIds, Model.CategoryChoices)
Now, to populate your view model with data from your entity. In a create view, the entity doesn't exist yet, so you don't need to do any mapping. The only thing you need to do is populate your CategoryChoices property so the select list in the view has some data. However, based on the above discussion about data needing to be posted back or else it will be null, since the actual contents of the select list will never be posted, you'll need to populate this in each of your create and edit actions, both for GET and POST. As a result, it's best to factor this logic out into a private method on your controller that each action can call:
private void PopulateCategoryChoices(AssetViewModel model)
{
model.CategoryChoices = db.Categories.Select(m => new SelectListItem
{
Value = m.Id,
Text = m.Name
};
}
Then, in your create GET action, you'll just new up your view model and populate your category choices:
public ActionResult Create()
{
var model = new AssetViewModel();
PopulateCategoryChoices(model);
return View(model);
}
In the post version, you'll now need to map the posted data onto your Asset entity:
[HttpPost]
public ActionResult Create(AssetViewModel model)
{
if (ModelState.IsValid)
{
var asset = new Asset
{
Title = model.Title,
Description = model.Description,
// etc.
Categories = db.Categories.Where(m => model.SelectedCategoryIds.Contains(m.Id))
}
db.Assets.Add(asset);
db.SaveChanges();
return RedirectToAction("Index");
}
PopulateCategoryChoices(model);
return View(model);
}
The edit GET action is similar to the create version, only this time, you have an existing entity that will need to be mapped onto an instance of your view model:
var asset = db.Assets.Find(id);
if (asset == null)
{
return new HttpNotFoundResult();
}
var model = new AssetViewModel
{
Title = asset.Title,
Description = asset.Description,
// etc.
SelectedCategoryIds = asset.Categories.Select(m => m.Id).ToList()
};
Likewise, the edit POST action is similar to the create version, but you're going to map from your view model on to an existing asset instead of creating a new asset. Additionally, because you have a many to many relationship, you have to take extra care when saving the categories.
// map data
asset.Title = model.Title;
asset.Description = model.Description;
//etc.
// You might be tempted to do the following:
// asset.Categories = db.Categories.Where(m => model.SelectedCategoryIds.Contains(m.Id));
// Instead you must first, remove any categories that the user deselected:
asset.Categories.Where(m => !model.SelectedCategoryIds.Contains(m.Id))
.ToList().ForEach(m => asset.Categories.Remove(m));
// Then you need to add any newly selected categories
var existingCategories = asset.Categories.Select(m => m.Id).ToList();
db.Categories.Where(m => model.SelectedCategoryIds.Except(existingCategories).Contains(m.Id))
.ToList().ForEach(m => asset.Categories.Add(m));
The extra footwork here is necessary to prevent saving the same relationship twice, resulting in an integrity error. By default Entity Framework creates a join table for many to many relationships that consists of a composite primary key composed of the foreign keys to each side of the relationship.
The reason why your categories is null is because you are not binding it on the POST. They are not in fields during the POST.
Try this and see if they are filled out:
#for (int i = 0; i < Model.Categories; i++)
{
#Html.TextBoxFor(model => model.Categories[i].CategoryId)
#Html.TextBoxFor(model => model.Categories[i].CategoryName)
}
1.
return View();
you are not passing back the model in your Create Method that is why you don't see the Model is NULL.
// POST: Assets/Create
[HttpPost]
public ActionResult Create(Asset model)
{
if (!ModelState.IsValid)
{
//error, return to view.
return View(model);
// If you don't pass back the model to you view you will see model is NULL
}
try
{
//do stuff
}
catch
{
return View(model);
}
}
Cateogries Will always be null in your case, as you can't post back a List like you are doing in your case.
Try Displaying them in a loop, Then the MVC model Binder will be able to bind your list to Model:
#for (int i = 0; i < Model.Categories; i++)
{
#Html.HiddenFor(model => model.Categories[i].CategoryId)
}
If you want to save SelectedCategories you will have to use MultiSelect

DropDown not displaying what was previously selected & saved

The customer can view their cust details page where they can change their pre-recorded delivery run (if they wish too) I have a drop down list containing towns for delivery runs:
<div class="editor-label">#Html.DropDownListFor(model => model.DeliveryRunList, Model.DeliveryRunList)</div>
When the customer profile loads it displays the correct town in the drop down(reading from the DB, which they previously selected when registering).
However if they change the town and save it, the user is returned to the home page and the newly selected tow is saved to the DB. But, if the user returns to the customer profile page the drop down displays the previously selected town, as opposed to the new one just previously selected and saved to the DB. Is it being stored in the cache somewhere.
Why is it not updating to what is actually in the DB??
Codebehind:
CustomerPart custPart = _custService.Get(custId);
if (DeliveryRunList.HasValue)
{
custPart.DeliveryRun_Id = DeliveryRunList.Value;
}
_custService.Update(custPart);
Thanks
I suppose model is a CustomerPart instance, and you have defined it more or less in this way.
public class CustomerPart
{
public int DeliveryRun_Id {get; set;}
public SelectList(or some IEnumerable) DeliveryRun_Id
}
I feel your code isn't updating the DB since you use the wrong attributes. The first lambda expression should be model => model.TheAttributeYouWantToUpdate, in this case DeliveryRun_Id.
So it should be:
#Html.DropDownListFor(model => model.DeliveryRun_Id, Model.DeliveryRunList)
rather than
#Html.DropDownListFor(model => model.DeliveryRunList, Model.DeliveryRunList)
It's not even clear where is this code inside the controller:
CustomerPart custPart = _custService.Get(custId);
if (DeliveryRunList.HasValue)
{
custPart.DeliveryRun_Id = DeliveryRunList.Value;
}
_custService.Update(custPart);
A common way of doing it is to have two methods of the same name for editing, one for HttpGet and one for HttpPost, and use a #Html.BeginForm() in the razor view for updating, rather than updating the info in controller.
Example:
public ActionResult Edit(int id = 0) {
InvestmentFund Fund = InvestmentFundData.GetFund(id);
return Fund == null ? (ActionResult)HttpNotFound() : View(Fund);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(InvestmentFund Fund)
{
if (ModelState.IsValid)
{
InvestmentFundData.Update(Fund);
return RedirectToAction("List");
}
return View(Fund);
}
In View
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#* For the attributes of your model *#
#Html.LabelFor ...
#Html.EditorFor ...
#Html.ValidationMessageFor ...
<input type="Submit"m value="Save">
}

MVC validation error with strongly typed view

I have a simple form that I would like to validate on form submission. Note I have stripped out the html for ease of viewing
<%=Html.TextBox("LastName", "")%> //Lastname entry
<%=Html.ValidationMessage("LastName")%>
<%=Html.TextBox("FirstName", "")%>//Firstname entry
<%=Html.ValidationMessage("FirstName")%>
<%=Html.DropDownList("JobRole", Model.JobRoleList)%> //Dropdownlist of job roles
<% foreach (var record in Model.Courses) // Checkboxes of different courses for user to select
{ %>
<li><label><input type="checkbox" name="Courses" value="<%=record.CourseName%>" /><%= record.CourseName%></label></li>
<% } %>
On submission of this form I would like to check that both FirstName and LastName are populated (i.e. non-zero length).
In my controller I have:
public ActionResult Submit(string FirstName, string LastName)
{
if (FirstName.Trim().Length == 0)
ModelState.AddModelError("FirstName", "You must enter a first name");
if (LastName.Trim().Length == 0)
ModelState.AddModelError("LastName", "You must enter a first name");
if (ModelState.IsValid)
{
//Update database + redirect to action
}
return View(); //If ModelState not valid, return to View and show error messages
}
Unfortunately, this code logic produces an error that states that no objects are found for JobRole and Courses.
If I remove the dropdownlist and checkboxes then all works fine.
The issue appears to be that when I return the View the view is expecting objects for the dropwdownlist and checkboxes (which is sensible as that is what is in my View code)
How can I overcome this problem?
Things I have considered:
In my controller I could create a JobRoleList object and Course object to pass to the View so that it has the objects to render. The issue with this is that it will overwrite any dropdownlist / checkbox selections that the user has already made.
In the parameters of my controller method Submit I could aslo capture the JobRoleList object and Course object to pass back to the View. Again, not sure this would capture any items the user has already selected.
I have done much googling and reading but I cannot find a good answer. When I look at examples in books or online (e.g. Nerddinner) all the validation examples involve simple forms with TextBox inputs and don't seems to show instances with multiple checkboxes and dropdownlists.
Have I missed something obvious here? What would be best practice in this situation?
Thanks
The best practice would be to accept a view-specific model. You could have a shared model that has both the properties that are needed to render the page and the properties required on post or separate models for rendering and accepting the post parameters. I usually go with a shared model as that means I can simply return the model I've received, suitably re-populated with any data needed to generate menus (such as JobList and Courses). Often I will have a method that takes a model of this type and returns a view with the menu properties populated.
public ActionResult JobsView( JobsViewModel model )
{
model.JobList = db.Jobs.Select( j => new SelectListItem
{
Text = j.Name,
Value = j.ID.ToString()
});
...
return View( model );
}
Then call this method from any of my actions that require a view with this type of model, to ensure that the model has the proper menu data.
// on error, return the view
return JobsView( model );
When using a model, you can also use DataAnnotations to decorate your model properties and let the model binder do validation for you, as well as enable client-side validation based on the model. Any validation not supported by the existing attributes can be implemented by creating your own attributes or done within the controller action.
public class JobsViewModel
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public int JobRole { get; set; }
[ScaffoldColumn(false)]
public IEnumerable<SelectListItem> JobRoleList { get; set; }
...
}
public ActionResult Submit( JobsViewModel model )
{
if (ModelState.IsValid)
{
... convert model to entity and save to DB...
}
return JobsView( model );
}
Then enable validation in your HTML
...
<script type="text/javascript" src="<%= Url.Content( "~/scripts/MicrosoftMvcValidation.js" ) %>"></script>
<% Html.EnableClientValidation(); %>
... begin form...
<%=Html.TextBoxFor( m=> m.LastName )%>
<%=Html.ValidationMessageFor( m => m.LastName )%>
<%=Html.TextBoxFor( m=> m.FirstName )%>
<%=Html.ValidationMessageFor( m => m.FirstName )%>
<%=Html.DropDownListFor( m=> m.JobRole, Model.JobRoleList)%>
<% foreach (var record in Model.Courses) // Checkboxes of different courses for user to select
{ %>
<li><label><input type="checkbox" name="Courses" value="<%=record.CourseName%>" /><%= record.CourseName%></label></li>
<% } %>
In simple MVC validation your supposed to pass the strongly typed object in other words your views model.
Example:
public ActionResult Update(Employees employee)
{
if (employee.Name.Trim().Length == 0)
ModelState.AddModelError("Name", "Name is required.");
// ETC....
}

Categories

Resources