MVC binding to model with list property ignores other properties - asp.net-mvc-3

I have a basic ViewModel with a property that is a List of complex types. When binding, I seem to be stuck with getting either the list of values, OR the other model properties depending on the posted values (i.e. the view arrangement).
The view model:
public class MyViewModel
{
public int Id { get; set; }
public string Property1 { get; set; }
public string Property2 { get; set; }
public List<MyDataItem> Data { get; set; }
}
public class MyDataItem
{
public int Id { get; set; }
public int ParentId { get; set; }
public string Name { get; set; }
public string Value { get; set; }
}
The controller actions:
public ActionResult MyForm()
{
MyViewModel model = new MyViewModel();
model.Id = 1;
model.Data = new List<MyDataItem>()
{
new MyDataItem{ Id = 1, ParentId = 1, Name = "MyListItem1", Value = "SomeValue"}
};
return View(model);
}
[HttpPost]
public ActionResult MyForm(MyViewModel model)
{
//...
return View(model);
}
Here is the basic view (without the list mark-up)
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>My View Model</legend>
#Html.HiddenFor(model => model.Id)
<div class="editor-label">
#Html.LabelFor(model => model.Property1)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Property1)
#Html.ValidationMessageFor(model => model.Property1)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Property2)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Property2)
#Html.ValidationMessageFor(model => model.Property2)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
When posted back to the controller, I get the 2 property values and a null value for the 'Data' property as expected.
If I add the mark-up for the List as follows (based on the information in this Scott Hanselman post and Phil Haack's post):
<div class="editor-field">
#for (int i = 0; i < Model.Data.Count(); i++)
{
MyDataItem data = Model.Data[i];
#Html.Hidden("model.Data[" + i + "].Id", data.Id)
#Html.Hidden("model.Data[" + i + "].ParentId", data.ParentId)
#Html.Hidden("model.Data[" + i + "].Name", data.Name)
#Html.TextBox("model.Data[" + i + "].Value", data.Value)
}
</div>
The 'Data' property of the model is successfully bound but the other properties are null.
The form values posted are as follows:
Id=1&Property1=test1&Property2=test2&model.Data%5B0%5D.Id=1&model.Data%5B0%5D.ParentId=1&model.Data%5B0%5D.Name=MyListItem1&model.Data%5B0%5D.Value=SomeValue
Is there a way to get both sets of properties populated or am I just missing something obvious?
EDIT:
For those of you who are curious. Based on the answer from MartinHN, the original generated mark-up was:
<div class="editor-field">
<input id="model_Data_0__Id" name="model.Data[0].Id" type="hidden" value="1" />
<input id="model_Data_0__ParentId" name="model.Data[0].ParentId" type="hidden" value="1" />
<input id="model_Data_0__Name" name="model.Data[0].Name" type="hidden" value="MyListItem1" />
<input id="model_Data_0__Value" name="model.Data[0].Value" type="text" value="SomeValue" />
</div>
The new generated mark-up is:
<div class="editor-field">
<input id="Data_0__Id" data-val="true" name="Data[0].Id" type="hidden" value="1" data-val-number="The field Id must be a number." data-val-required="The Id field is required." />
<input id="Data_0__ParentId" name="Data[0].ParentId" type="hidden" value="1" data-val="true" data-val-number="The field ParentId must be a number." data-val-required="The ParentId field is required." />
<input id="Data_0__Name" name="Data[0].Name" type="hidden" value="MyListItem1" />
<input id="Data_0__Value" name="Data[0].Value" type="text" value="SomeValue" />
</div>
Which results in the following posted values:
Id=1&Property1=test1&Property2=test2&Data%5B0%5D.Id=1&Data%5B0%5D.ParentId=1&Data%5B0%5D.Name=MyListItem1&Data%5B0%5D.Value=SomeValue
Notice there's no 'model.' in the name and posted values...

Try to change the code for the Data collection to this, and let MVC take care of the naming:
<div class="editor-field">
#for (int i = 0; i < Model.Data.Count(); i++)
{
#Html.HiddenFor(m => m.Data[i].Id)
#Html.HiddenFor(m => m.Data[i].ParentId)
#Html.HiddenFor(m => m.Data[i].Name)
#Html.TextBoxFor(m => m.Data[i].Value)
}
</div>

Alternatively you could have created an EditorTemplate for your nested ViewModel as follows.
#model MyDataItem
#Html.HiddenFor(model => model.Id)
#Html.HiddenFor(model => model.ParentId)
#Html.HiddenFor(model => model.Name)
#Html.TextBoxFor(model => model.Value)
Create a folder named 'EditorTemplates' in your 'Shared' folder and save the above as 'MyDataItem.cshtml'.
Then in your View, just call the following instead of the foreach loop:
#Html.EditorFor(model => model.Data)
Feels a bit less hackier IMO :)

Just my 2 cents but something worth noting with this issue - the data member in the View Model must be defined as a public property for the postback model binding to work.
I had a very similar problem to the above but used a public data member in my View Model. The same HTML is generated as shown above and all looks well but the model binder threw back an empty collection. Worth watching for...

Related

ViewModel for multiple file upload in ASP.NET MVC 3

I have multiple file upload Views with ViewModel binding as following:
#model IVRControlPanel.Models.UploadNewsModel
#using (Html.BeginForm("index", "NewsUpload", FormMethod.Post, new { name = "form1", #id = "form1" }))
{
#Html.ValidationSummary(true)
<div class="field fullwidth">
<label for="text-input-normal">
#Html.Label("Select Active Date Time")</label>
<input type="text" id="active" value="#DateTime.Now" />
#Html.ValidationMessageFor(model => model.ActiveDateTime)
</div>
<div class="field fullwidth">
<label>
#Html.Label("Select Language")
</label>
#Html.DropDownList("Language", (SelectList)ViewBag.lang)
</div>
<div class="field">
<label>
#Html.Label("General News")
</label>
#Html.TextBoxFor(model => model.generalnews, new { name = "files", #class="custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.generalnews)
</div>
<div class="field">
<label>
#Html.Label("Sports News")
</label>
#Html.TextBoxFor(model => model.sportsnews, new { name = "files", #class = "custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.sportsnews)
</div>
<div class="field">
<label>
#Html.Label("Business News")
</label>
#Html.TextBoxFor(model => model.businessnews, new { name = "files", #class = "custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.businessnews)
</div>
<div class="field">
<label>
#Html.Label("International News")
</label>
#Html.TextBoxFor(model => model.internationalnews, new { name = "files", #class = "custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.internationalnews)
</div>
<div class="field">
<label>
#Html.Label("Entertaintment News")
</label>
#Html.TextBoxFor(model => model.entertaintmentnews, new { name = "files", #class = "custom-file-input", type = "file" })
#Html.ValidationMessageFor(model => model.entertaintmentnews)
</div>
<footer class="pane">
<input type="submit" class="bt blue" value="Submit" />
</footer>
}
View model with data annotation for validating file upload for allowed extension as follows:
public class UploadNewsModel
{
public DateTime ActiveDateTime { get; set; }
// public IEnumerable<SelectListItem> Language { get; set; }
[File(AllowedFileExtensions = new string[] { ".jpg", ".gif", ".tiff", ".png", ".pdf", ".wav" }, MaxContentLength = 1024 * 1024 * 8, ErrorMessage = "Invalid File")]
public HttpPostedFileBase files { get; set; }
}
Controller: for saving multiple file and return view if error is exist
[HttpPost]
public ActionResult Index(UploadNewsModel news, IEnumerable<HttpPostedFileBase> files)
{
if (ModelState.IsValid)
{
foreach (var file in files)
{
if (file != null && file.ContentLength > 0)
{
var fileName = Path.GetFileName(file.FileName);
var serverpath = Server.MapPath("~/App_Data/uploads/News");
var path = Path.Combine(serverpath, fileName);
if (!Directory.Exists(serverpath))
{
Directory.CreateDirectory(serverpath);
}
file.SaveAs(path);
}
}
}
return View(news);
}
}
Problem explaination
How do I define view model for those five file upload input control so that corresponding error is shown for respective validation error if file extension of file uploaded is not allowed type. I have only one view model items for all five file upload control.
What can be best way to define view model for those multiple file upload control for showing respective validation error instead user try to upload file of unallowed extension???
The real problem here is that MVC doesn't have a decent model binder for http files.
So unless you use a project like mvc futures which has some extra support around file uploads you are going to have to get some what dirty and do the hard work yourself.
Here is an example that might work a bit better for you.
Firstly I would create a ViewModel to represent one file something like this:
public class FileViewModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public bool Delete { get; set; }
public string ExistingUrl { get; set; }
public HttpPostedFileBase FileBase { get; set; }
}
Obviously properties depend on your requirements, the important bit is the FileBase and that it is its own model.
Next, a ViewModel for your page (UploadNewsModel in your case):
public class IndexViewModel
{
public IList<FileViewModel> Files { get; set; }
}
The important bit here is the IList of Files, this is how we capture multiple files (in your current implementation with 'file' you are only capturing one.
Onto the page level view:
#model IndexViewModel
<form method="post" action="#Url.Action("Index")" enctype="multipart/form-data">
#Html.ValidationSummary(true)
#Html.EditorFor(x => x.Files)
<input type="submit" value="Submit" />
</form>
Note the EditorFor, what we will do next is create a EditorTemplate for the FileViewModel which should be used at this point.
Like this:
#model FileViewModel
<h4>#Model.Name</h4>
#Html.HiddenFor(x => x.Id)
#Html.CheckBoxFor(x => x.Delete)
<input #( "name=" + ViewData.TemplateInfo.HtmlFieldPrefix + ".FileBase") type="file" />
Note the usage of ViewData.TemplateInfo.HtmlFieldPrefix, it kinda sucks but like I said this is because of the poor mvc support for the file input type. We have to do it ourselves. We except the name for each file to be something like '[0].FileBase' etc.
This will populate the FileViewModel correctly on the POST action.
So far so good, what about validation?
Well again you can do that manually on the server. Test the file extensions yourself, and simply use the following to add the error to the model like:
ModelState.AddModelError("","Invalid extension.")
On another note, extension validation should be done on the client side (and as well as on the server side)

ASP.Net MVC 3 Data Annotation

I am building an ASP.Net MVC 3 Web application using Entity Framework 4.1. To perform validation within one of my Views which accepts a ViewModel. I am using Data Annotations which I have placed on the properties I wish to validate.
ViewModel
public class ViewModelShiftDate
{
public int shiftDateID { get; set; }
public int shiftID { get; set; }
[DisplayName("Start Date")]
[Required(ErrorMessage = "Please select a Shift Start Date/ Time")]
public DateTime? shiftStartDate { get; set; }
[DisplayName("Assigned Locum")]
public int assignedLocumID { get; set; }
public SelectList AssignedLocum { get; set; }
}
View
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<br />
<div class="editor-label">
#Html.LabelFor(model => model.shiftStartDate)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.shiftStartDate, new { #readonly = "readonly" })
#Html.ValidationMessageFor(model => model.shiftStartDate)
</div>
<br />
<div class="editor-label">
#Html.LabelFor(model => model.assignedLocumID)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.assignedLocumID, Model.AssignedLocum)
#Html.ValidationMessageFor(model => model.assignedLocumID)
</div>
<br />
<p>
<input type="submit" value="Save" />
</p>
<br />
}
The SelectList 'AssignedLocum' is passed into my View for a DropDownList, and the item selected is assigned to the property 'assignedLocumID'.
As you can see from my ViewModel, the only required field is 'shiftStartDate', however, when I hit the Submit button in my View, the drop down list 'AssignedLocum' also acts a required field and will not allow the user to submit until a value is selected.
Does anyone know why this property is acting as a required field even though I have not tagged it to be so?
Thanks.
Try to use default value for dropdown (for example "Please select")
#Html.DropDownListFor(model => model.assignedLocumID, Model.AssignedLocum, "Please select")

Why doesn't Html.LabelFor work in a partial view?

I thought I have asked this before, but I am not finding it. I am making partial view for a form so I can use it in multiple places. Here is one short snippet:
#model Permits.Domain.Entities.PermitRequest
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<div class="editor-label">
#Html.LabelFor(model => model.JobAddress)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.JobAddress)
#Html.ValidationMessageFor(model => model.JobAddress)
</div>
<p>
<input type="submit" value="Submit request" />
</p>
</fieldset>
}
My model looks like:
public class PermitRequest
{
[Description("Job address")]
public string JobAddress { get; set; }
}
Why would my label still be "JobAddress" instead of "Job Address" (with the space)? I feel like I am missing something obvious.
[DisplayName("Job address")]
public string JobAddress { get; set; }
or if you prefer:
[Display(Name = "Job address")]
public string JobAddress { get; set; }
Both set the DisplayName property of the ModelMetadata which is used by the LabelFor helper.

MVC3: Portions of Model Not Reconstituted on Postback

Portions of my models are not being correctly reconstructed on postback.
Models
public class DemographicsModel
{
public List<QuestionModel> Questions { get; set; }
}
public abstract class QuestionModel
{
[HiddenInput(DisplayValue = false)]
public int ID { get; set; }
[HiddenInput(DisplayValue = false)]
public string Title { get; set; }
}
public abstract class ChooseQuestionModel : QuestionModel
{
public abstract List<SelectListItem> Items { get; set; }
}
public class ChooseManyQuestionModel : ChooseQuestionModel
{
[Required]
[DataType("CheckBoxList")]
public override List<SelectListItem> Items { get; set; }
}
Views
ChooseManyQuestionModel.cshtml
#model X.Y.Z.ChooseManyQuestionModel
<div class="Form Wide NoLabel">
<div class="Title">#this.Model.Title</div>
#Html.TypeStamp()
#Html.EditorFor(m => m.ID)
#Html.EditorFor(m => m.Title)
#Html.EditorFor(m => m.Items)
</div>
CheckBoxList.cshtml
#model IEnumerable<SelectListItem>
#if (!this.Model.IsNullOrEmpty())
{
foreach (var item in this.Model)
{
<div>
#Html.HiddenFor(m => item.Value)
#Html.HiddenFor(m => item.Text)
#Html.CheckBoxFor(m => item.Selected)
#Html.LabelFor(m => item.Selected, item.Text)
</div>
}
}
I believe the issue lies within CheckBoxList.cshtml since these items are not being re-constituted on postback.
HTML Output
<div class="Form Wide NoLabel">
<div class="Title">Question title displays here?</div>
<input id="Questions_1___xTypeStampx_" name="Questions[1]._xTypeStampx_" type="hidden" value="Hrxh2HjDRorBAZWo18hsC0OvbJwyswpDkfTBfNF2NC8=" />
<input data-val="true" data-val-number="The field ID must be a number." data-val-required="The ID field is required." id="Questions_1__ID" name="Questions[1].ID" type="hidden" value="76" />
<input id="Questions_1__Title" name="Questions[1].Title" type="hidden" value="Question title displays here?" />
<div>
<input id="Questions_1__Items_item_Value" name="Questions[1].Items.item.Value" type="hidden" value="148" />
<input id="Questions_1__Items_item_Text" name="Questions[1].Items.item.Text" type="hidden" value="Organization Type 1" />
<input data-val="true" data-val-required="The Selected field is required." id="Questions_1__Items_item_Selected" name="Questions[1].Items.item.Selected" type="checkbox" value="true" /><input name="Questions[1].Items.item.Selected" type="hidden" value="false" />
<label for="Questions_1__Items_item_Selected">Organization Type 1</label>
</div>
</div>
</div>
Controller
public class AccountController : BaseController
{
public ActionResult Demographics()
{
return this.View(new DemographicsModel());
}
[HttpPost]
public ActionResult Demographics(DemographicsModel model)
{
return this.View(model);
}
}
On postback, the DemographicsModel is populated with the correct types (I'm using MvcContrib to handle abstract type binding). The List<Question> is populated with all of the correct data including the ID and Title of each question from the hidden fields. However, List<SelectListItem> within each question is set to null.
Update 1
The issue is definitely occurring because the fields are not named correctly. For instance, the "item" field names are being generated like this:
Questions_1__Items_item_Value
When they should really look like this (addition of item index and removal of erroneous "item"):
Questions_1__Items_1__Value
Similarly, the field IDs are being generated like this (addition of item index and removal of erroneous "item"):
Questions[1].Items.item.Value
Instead of:
Questions[1].Items[0].Value
Using Fiddler with the correct IDs being posted back, the model is constructed correctly with all radio buttons and checkboxes in place.
Try the following.
In ChooseManyQuestionModel.cshtml, change #Html.EditorFor(m => m.Items) to:
#Html.EditorForModel(m => m.Items)
Then, in CheckBoxList.cshtml, change #model IEnumerable<SelectListItem> to:
#model SelectListItem
Finally, in each item, modify each lambda expression, and change item to m, then remove the foreeach loop. This will allow the Editor to iterate through the collection, and should give you correct id generation for each element.
When foreach loop is used the ids generated in HTML are all same.
When for look is used the ids generated with the for loops index so binding is happening correctly and all the data is available after post back.
In this scenario, it seems the Helper class is not doing what you want it to do. I would suggest writing your own helper class to name your inputs exactly as you require them to be.

custom validator in asp.net mvc3

I have created a custom validator in my asp.net mvc3 application like this:
{
if (customerToValidate.FirstName == customerToValidate.LastName)
return new ValidationResult("First Name and Last Name can not be same.");
return ValidationResult.Success;
}
public static ValidationResult ValidateFirstName(string firstName, ValidationContext context)
{
if (firstName == "Nadeem")
{
return new ValidationResult("First Name can not be Nadeem", new List<string> { "FirstName" });
}
return ValidationResult.Success;
}
and I have decorated my model like this:
[CustomValidation(typeof(CustomerValidator), "ValidateCustomer")]
public class Customer
{
public int Id { get; set; }
[CustomValidation(typeof(CustomerValidator), "ValidateFirstName")]
public string FirstName { get; set; }
public string LastName { get; set; }
}
my view is like this:
#model CustomvalidatorSample.Models.Customer
#{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
#using (#Html.BeginForm())
{
#Html.ValidationSummary(false)
<div class="editor-label">
#Html.LabelFor(model => model.FirstName, "First Name")
</div>
<div class="editor-field">
#Html.EditorFor(model => model.FirstName)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.LastName, "Last Name")
</div>
<div class="editor-field">
#Html.EditorFor(model => model.LastName)
</div>
<div>
<input type="submit" value="Validate" />
</div>
}
But validation doesn't fire. Please suggest solution.
Thanks
How do you know the validation doesn't fire? Are you setting a break point in your controller?
You are not displaying any validation errors in your view. You need to add the following lines to the view.
#Html.ValidationMessageFor(model => model.FirstName)
#Html.ValidationMessageFor(model => model.LastName)
You will want to remove the custom validation from the class. Leave it on the properties though.

Resources