How can I make sure model binding will work as expected? - c#

I'm programming in C# ASP.NET MVC4 (Razor engine). I have a need to create a partial view and reuse it in multiple places. The problem is that the view is a form, and in some cases I will need to use it with a ViewModel. My question is how the model binding will work because in a ViewModel it will be a property of a property. Example:
public class PersonModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class OfficeViewModel
{
public PersonModel Person { get; set; }
// other properties
}
The partial view for the PersonModel would be:
#model SomeNameSpace.PersonModel
#Html.TextBoxFor(m => m.FirstName)
#Html.TextBoxFor(m => m.LastName)
When this view is rendered it would look like this:
<input type="textbox" id="FirstName" name="FirstName" />
<input type="textbox" id="LastName" name="LastName" />
Now I want to use this same view with my OfficeViewModel. In that case I would do this in my Office view:
#{ Html.RenderPartial("Person", Model.Person); }
When this partial view is rendered it will be rendered as shown above. If I were to NOT reuse that view, my Office view will be like this:
#model SomeNameSpace.OfficeViewModel
#Html.TextBoxFor(m => m.Person.FirstName)
#Html.TextBoxFor(m => m.Person.LastName)
That would be rendered as:
<input type="textbox" id="Person_FirstName" name="Person.FirstName" />
<input type="textbox" id="Person_LastName" name="Person.LastName" />
Notice how the name attribute has the Person prefix property. So if I use the RenderPartial option and pass in the Model.Person, will the model binder know where to bind the FirstName and LastName in the OfficeViewModel?
If the model binder is smart enough to check for properties of properties, what happens when I have a ManagerModel and EmployeeModel in the OfficeViewModel and they both have properties named FirstName and LastName?
I hope I have been clear, and thanks in advance.

Unfortunately, Html.RenderPartial does not carry over the information necessary to come up with the correct field names in this situation.
If you want to reuse a partial view in this way, look into using Editor Templates with Html.EditorFor instead. Like the other *For helpers, EditorFor takes a lambda expression which allows it to carry over the name of the property being passed in to the template.

#Gerald is right. Default RenderPartial won't figure it out. Though you can write a custom helper that will take care of that issue:
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper, System.Linq.Expressions.Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
string name = ExpressionHelper.GetExpressionText(expression);
object model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new System.Web.Mvc.TemplateInfo
{
HtmlFieldPrefix = name
}
};
return helper.Partial(partialViewName, model, viewData);
}
Use it in the View like that:
#Html.PartialFor(x => x.Person, "Person")

Found the answer here: https://stackoverflow.com/a/4807655/78739
Basically I need to set the ViewData.TemplateInfo.HtmlFieldPrefix.
So I added a property to the PersonModel and called it HtmlFieldPrefix. Example:
public class PersonModel
{
public string HtmlFieldPrefix { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Then in my partial view I do this:
#model SomeNameSpace.PersonModel
#{
ViewData.TemplateInfo.HtmlFieldPrefix = Model.HtmlFieldPrefix;
}
#Html.TextBoxFor(m => m.FirstName)
#Html.TextBoxFor(m => m.LastName)
In my controller action I can do this:
model.HtmlFieldPrefix = "Person";
return View("_Person", model);
Now my rendered view will look like this:
<input type="textbox" id="Person_FirstName" name="Person.FirstName" />
<input type="textbox" id="Person_LastName" name="Person.LastName" />
If I leave the model.HtmlFieldPrefix as null, the view will render as:
<input type="textbox" id="FirstName" name="FirstName" />
<input type="textbox" id="LastName" name="LastName" />
So this fixes my issue, and allows me to use the partial view with ViewModels.

Related

ModelState for Model inside Viewmodel is not working

I have a view model as shown below
public class SampleViewModel
{
public Model1 Property1 { get; set; }
}
public class Model1
{
[Required]
public string Name{get; set;}
}
In View We have
#Html.DisplayFor(m=>m.Model1.Name)
#Html.ValidationMessageFor(m=>m.Model1.Name)
In Controller we have
public ActionResult Index()
{
Model1 a= new Model1();
SampleViewModel s= new SampleViewModel();
s.Property1=a;
return View(s);
}
When Name is null ModelState is not getting populated with model errors even though I have Required attribute for Name. Where am I doing wrong?
To make this work you should at least create some <input> tag. For example to use the Html.TextBoxFor():
#Html.ValidationSummary(true)
#Html.DisplayFor(m => m.Property1.Name)
#Html.ValidationMessageFor(m => m.Property1.Name)
<p>#Html.TextBoxFor(m => m.Property1.Name)</p>
<br />
<input type="submit" value="Submit " />
The problem can be discovered if you look at the HTML generated by the MVC. The #Html.TextBoxFor(m => m.Property1.Name) is rendered to the following markup:
<input data-val="true" data-val-required="The Name field is required." id="Property1_Name" name="Property1.Name" type="text" value="" />
With the data-val-required attribute the posted page is validated correctly and the ModelState.IsValid returns false when no data entered.
Or you can use:
#Html.HiddenFor(m => m.Property1.Name)
Similar problem is described here: Required string attribute with null value gives IsValid=true in ASP.NET Core 2 Razor Page

Returning a partial view from controller with proper id and name in inputs

I'm developing a web MVC application with net core 2.2.
I have the following classes:
public class A
{
public IList<B> Bs { get; set; }
}
public class B
{
public string Id { get; set; }
public string Name { get; set; }
}
The following view:
#model A
#for (int i = 0; i < Model.Bs.Count; i++)
{
<partial name="_BsPatialView" for="Bs[i]" />
}
And the following partial view (_BsPatialView.cshtml):
<input type='hidden' asp-for="#Model.Id" />
<input asp-for="#Model.Name" />
Until here, everything it-s been generated fine. An example of the created inputs in the partial view is:
<input type="hidden" id="Bs_3__Id" name="Bs[3].Id" />
<input type="text" id="Bs_3__Name" name="Bs[3].Name" />
With the elements name and ids the model binder in the controller can properly bind everything.
The problem is when I try to return the partial view from the controller. What I do is:
public IActionResult AddBElement(A a)
{
a.Bs.Add(new B() { Id = Guid.NewGuid() });
return PartialView("_BsPatialView", a.Bs.Last());
}
The resulting html is:
<input type="hidden" id="Id" name="Id" />
<input type="text" id="Name" name="Name" />
So then, when I submit a form in which these inputs are, the model binding fails.
So, how should I return the partial view from the controller to fix this? Is there any equivalent to the partial's tag helper for attribute to use on the controller?
Model binding uses field names to map them to Model properties. Now because your name does not contain any information about the parent class, A, model binder does not know how to bind them.
So in other words, model binder would know how to bind this input:
<input type="hidden" id="Bs_3__Id" name="A.Bs[3].Id" />
But not this input:
<input type="hidden" id="Bs_3__Id" name="Bs[3].Id" />
One solution would be pass the prefix, A to the partial view: See this answer
A better solution would be to use EditorTemplate instead of Partial View, which would generate correct name for your input field. To use EditorTemplate Rename your partial view to B.cshtml (this should be your class name) and put it under /Views/Shared/EditorTemplates folder... then you can use it like this:
#Html.EditorFor(m => m.Bs)
Check this tutorial for more info about Editor Templates
Also check this question, which is very similar to yours

How to pass back model to controller using razor dropdownlist

I have a DropDownListFor control that I am wanting to show a display value that resides in a property within a model/class (this is the Rule class.) The view's model is actually a collection of these model/classes. However, when I select the item from the DropDownList, I want to send back the entire model as a parameter. I have this working perfectly with the following code, but the Name property within the parameter is coming back as null. The other properties all have appropriate values.
View Code:
#model List<StockTrader.Web.Data.Rule>
#{
ViewBag.Title = "Configure Rules";
}
<h2>#ViewBag.Title</h2>
<h4>Choose a rule to edit:</h4>
<form method="post" id="rulesform" action="SaveRules">
#Html.DropDownListFor(m => m.First().RuleID, new SelectList(Model.AsEnumerable(), "RuleID", "Name"))
<div style="margin-bottom: 15px;">
<label>Value:</label><br />
<input type="number" name="Value" style="margin-bottom: 15px;" /><br />
<button>Save Value</button>
</div>
Controller Code:
public ActionResult SaveRules(Rule model)
{
//do something
}
Rule Class:
public class Rule
{
public int RuleID { get; set; }
public string Name { get; set; }
public int Value { get; set; }
public bool IsDeleted { get; set; }
}
We do have Kendo controls, so if another control would be more appropriate, that is an option.
I would be glad to provide anymore code or information you might need.
Any thoughts or ideas?
EDIT:
So it turns out this is what I needed to do, the accepted answer got me to this point so I'm going to leave it checked.
View Code (w/script included):
#Html.DropDownListFor(m => m.First().RuleID, new SelectList(Model.AsEnumerable(), "RuleID", "Name"), new { id = "ruleid", #onchange = "CallChangefunc(this)" })
#Html.HiddenFor(m => m.First().Name, new { id = "rulename" })
function CallChangefunc(e) {
var name = e.options[e.selectedIndex].text;
$("#rulename").val(name);
}
You will need a hidden field for it,and use dropdownlist on change event on client side to update hidden field:
#Html.DropDownListFor(m => m.First().RuleID, new SelectList(Model.AsEnumerable(), "RuleID", "Name"),new { id= "ruleid" })
#Html.HiddenFor(m=>m.First().Name,new { id="rulename" })
and jquery code:
$("#ruleid").change(function(){
$("#rulename").val($(this).text());
});
Second option isif Rule collection is coming from database you can fetch RuleName by using id to by querying db in action.
it can be achieved by using UIHint
On your model class, on the RuleID property, add an annotation for UIHint. It basically lets you render a partial (cshtml) for the property. So, on the partial, you can have the template for generating the dropdwon with required styling. When Page is generated. Now you can use the same Html.DropDownListFor for RuleID and UI generates a dropdown for it.
This will avoid having additional jQuery code to get the dropdown value, and code is more concise and testable.

ASP.NET MVC form element name generation

I have a class that looks like this:
public class UserListVM
{
public SearchModel SearchModel { get; set; }
public PagedList<User> Users { get; set; }
}
public class SearchModel
{
public string Text { get; set; }
/* other properties */
}
I send UserListVM to my view but the action accepts SearchModel:
public ActionResult Search(SearchModel filter)
{
UserListVM model = new UserListVM();
model.Users = userService.GetUsers(filter);
model.SearchModel = filter;
return View(model);
}
My view is:
#model UserListVM
<form>
#Html.TextBoxFor(m => Model.SearchModel.Text)
</form>
But this generates:
<input id="SearchModel_Text" name="SearchModel.Text" type="text" value="">
Which sends UserListVM to the action instead of SearchModel. How can I get it to generate this:
<input id="Text" name="Text" type="text" value="">
#Html.TextBoxFor(m => m.SearchModel.Text, new { id = "Text" })
Utilize the overloaded TextBoxFor() method that takes a second object parameter (called htmlAttributes). Here you can specify HTML attributes to apply to the DOM element you are currently utilizing (in this case, your input element).
Edit: I believe your lambda expression is wrong. Change:
#Html.TextBoxFor(m => Model.SearchModel.Text)
To
#Html.TextBoxFor(m => m.SearchModel.Text)
// htmlAttributes omitted to show the issue
Edit Edit: it turns out that even with a specified name attribute, it will be rendered according to what the form is requiring for a POST to the necessary field(s).
Edit Edit Edit: Try to be explicit with FormExtensions.BeginForm():
#using (Html.BeginForm("Search", "YourController", FormMethod.Post, null))
{
#Html.TextBoxFor(m => m.SearchModel.Text)
}
Use this as a substite of your <form /> element.
Create a partial view for your SearchModel, and call it using Html.Partial. Then, from within that partial view, do all of the EditorFor/TextBoxFor Extensions
Your view - UserList.cshtml:
#model UserListVM
#using (Html.BeginForm())
{
#Html.Partial("Search", Model.SearchModel)
}
Your view - Search.cshtml:
#model SearchModel
#Html.TextAreaFor(m => m.Text)
Assumming there is more the the View than you have shown, why not just have your Search method take the UserListVM model. It will just contain a null reference to the users, so there is no extra data sent in the post.
Try doing it manually like this:
#Html.TextBox("Text", Model.SearchModel.Text)

Two fields with the same name

I have a ViewModel class to encapsulate "Personal" and "Business" models. My problem is that both models have a property called "Email" and the model binding is not able to make a distinction between the two.
I read that [Bind(Prefix = ... is used to resolved this issue, but I have not been able to see a concise example on how to achieve this.
public class BusinessFormViewModel
{
public Business Business { get; set; }
public ContactPerson ContactPerson { get; set; }
public BusinessFromView(Business business, ContactPerson contactPerson)
{
Business = business;
ContactPerson = contactPerson;
}
}
How do I use the Bind Prefix to fix this?
I believe that if the form elements that are posted have prefixes included in the name, the binding will be done properly. This is how the templated helpers (i.e. EditorFor) renders the controls, and my nested viewmodels are bound properly. For example, in your case, your view would have form elements something like this:
...
<input type="text" name="Business.Email" value="<%=this.Model.Business.Email %>" />
...
<input type="text" name="ContactPerson.Email" value="<%=this.Model.ContactPerson.Email %>" />
...
Or, using templated helpers (in mvc 2):
...
<%= Html.TextBoxFor(m => m.Business.Email) %>
...
<%= Html.TextBoxFor(m => m.ContactPerson.Email) %>
...
And your controller would simply take a BusinessFormViewModel as a parameter, as so:
public BusinessFromView(BusinessFormViewModel businessForm)
{
Business = businessForm.Business;
ContactPerson = businessForm.ContactPerson;
}

Categories

Resources