I have a ViewModel that has a complex object as one of its members. The complex object has 4 properties (all strings). I'm trying to create a re-usable partial view where I can pass in the complex object and have it generate the html with html helpers for its properties. That's all working great. However, when I submit the form, the model binder isn't mapping the values back to the ViewModel's member so I don't get anything back on the server side. How can I read the values a user types into the html helpers for the complex object.
ViewModel
public class MyViewModel
{
public string SomeProperty { get; set; }
public MyComplexModel ComplexModel { get; set; }
}
MyComplexModel
public class MyComplexModel
{
public int id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
....
}
Controller
public class MyController : Controller
{
public ActionResult Index()
{
MyViewModel model = new MyViewModel();
model.ComplexModel = new MyComplexModel();
model.ComplexModel.id = 15;
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
// model here never has my nested model populated in the partial view
return View(model);
}
}
View
#using(Html.BeginForm("Index", "MyController", FormMethod.Post))
{
....
#Html.Partial("MyPartialView", Model.ComplexModel)
}
Partial View
#model my.path.to.namespace.MyComplexModel
#Html.TextBoxFor(m => m.Name)
...
how can I bind this data on form submission so that the parent model contains the data entered on the web form from the partial view?
thanks
EDIT: I've figured out that I need to prepend "ComplexModel." to all of my control's names in the partial view (textboxes) so that it maps to the nested object, but I can't pass the ViewModel type to the partial view to get that extra layer because it needs to be generic to accept several ViewModel types. I could just rewrite the name attribute with javascript, but that seems overly ghetto to me. How else can I do this?
EDIT 2: I can statically set the name attribute with new { Name="ComplexModel.Name" } so I think I'm in business unless someone has a better method?
You can pass the prefix to the partial using
#Html.Partial("MyPartialView", Model.ComplexModel,
new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "ComplexModel" }})
which will perpend the prefix to you controls name attribute so that <input name="Name" ../> will become <input name="ComplexModel.Name" ../> and correctly bind to typeof MyViewModel on post back
Edit
To make it a little easier, you can encapsulate this in a html helper
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper, 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 = string.IsNullOrEmpty(helper.ViewData.TemplateInfo.HtmlFieldPrefix) ?
name : $"{helper.ViewData.TemplateInfo.HtmlFieldPrefix}.{name}"
}
};
return helper.Partial(partialViewName, model, viewData);
}
and use it as
#Html.PartialFor(m => m.ComplexModel, "MyPartialView")
If you use tag helpers, the partial tag helper accepts a for attribute, which does what you expect.
<partial name="MyPartialView" for="ComplexModel" />
Using the for attribute, rather than the typical model attribute, will cause all of the form fields within the partial to be named with the ComplexModel. prefix.
You can try passing the ViewModel to the partial.
#model my.path.to.namespace.MyViewModel
#Html.TextBoxFor(m => m.ComplexModel.Name)
Edit
You can create a base model and push the complex model in there and pass the based model to the partial.
public class MyViewModel :BaseModel
{
public string SomeProperty { get; set; }
}
public class MyViewModel2 :BaseModel
{
public string SomeProperty2 { get; set; }
}
public class BaseModel
{
public MyComplexModel ComplexModel { get; set; }
}
public class MyComplexModel
{
public int id { get; set; }
public string Name { get; set; }
...
}
Then your partial will be like below :
#model my.path.to.namespace.BaseModel
#Html.TextBoxFor(m => m.ComplexModel.Name)
If this is not an acceptable solution, you may have to think in terms of overriding the model binder. You can read about that here.
I came across the same situation and with the help of such informative posts changed my partial code to have prefix on generated in input elements generated by partial view
I have used Html.partial helper giving partialview name and object of ModelType and an instance of ViewDataDictionary object with Html Field Prefix to constructor of Html.partial.
This results in GET request of "xyz url" of "Main view" and rendering partial view inside it with input elements generated with prefix e.g. earlier Name="Title" now becomes Name="MySubType.Title" in respective HTML element and same for rest of the form input elements.
The problem occurred when POST request is made to "xyz url", expecting the Form which is filled in gets saved in to my database. But the MVC Modelbinder didn't bind my POSTed model data with form values filled in and also ModelState is also lost. The model in viewdata was also coming to null.
Finally I tried to update model data in Posted form using TryUppdateModel method which takes model instance and html prefix which was passed earlier to partial view,and can see now model is bound with values and model state is also present.
Please let me know if this approach is fine or bit diversified!
Related
View Model looks like this:
public class AsmenysInfoViewModel2
{
public asmenys_info Asmenys_info { get; set; }
public List<miestai> Miestai { get; set; }
public string Test { get; set; }
}
And there are two actions. Get and Post.
public ActionResult Index(long? id)
{
var model = new AsmenysInfoViewModel2();
model.Test = "Test";
model.Asmenys_info = BllFactory.DalFactory.AsmenysInfoDal.GetById(id.Value);
return View(model);
}
[HttpPost]
public ActionResult Index(AsmenysInfoViewModel2 asmenys_info)
{
var model = asmenys_info;
return View(model);
}
And my view looks like this:
#model MODELS.AsmenysInfoViewModel2
#{
ViewBag.Title = "Index";
}
#using (Html.BeginForm("Index", "AsmenysInfo", FormMethod.Post))
{
#Html.ValidationSummary()
#Html.TextBoxFor(m => m.Asmenys_info.adresas)
<input type="submit" value="Išsaugoti" />
}
Doesn't matter if I use EditorFor or TextBoxFor - result is same. My model property "Asmenys_info" on posting is always null. If my class AsmenysInfoViewModel2 would not contain asmenys_info type property and would contain only "string, int etc" (no strongly typed) - it would work.
My question is :
How to post View Model which has strongly typed property which on posting would not be null?
Your model has a property named Asmenys_info and the parameter in your POST method is also named asmenys_info. Internally the DefaultModelBinder reads the values of the form data which includes a value for Asmenys_info and attempts to set property Asmenys_info to that value but it fails because there is no conversion from a string to a complex object.
Change the name of the parameter to anything other than a name of a property in your model and it will bind fine, for example
[HttpPost]
public ActionResult Index(AsmenysInfoViewModel2 model)
Change the below line with another object name
public ActionResult Index(AsmenysInfoViewModel2 asmenys_info)
in above method use any other name of object instead of asmenys_info.
because while mvc framework map your model with object there is confution in asmenys_info and Asmenys_info property of AsmenysInfoViewModel2 class.
I am using nested view models to display views based on user roles.
Model:
public class MainVM {
//some properties
public OneVM One {get; set;}
public TwoVM Two {get; set;}
}
public class OneVM {
//properties
}
public class TwoVM {
//properties
}
As written here that only main model is need to be sent controller. I am using Automapper to map properties from received model.
Controller:
public ActionResult EditAction(MainVM model){
var item = db.Table.Find(model.Id);
//automapper to map
AutoMapper.Mapper.Map(model.One, item); //does not work
db.Entry(item).State = EntityState.Modified;
db.SaveChanges();
}
Is this the right way to do that? What am I doing wrong here.
Update:
This was the view I was using to render nested view models from partial views
View:
#model MainVM
#Html.RenderPartial("_OnePartial", Model.One)
This answer https://stackoverflow.com/a/6292180/342095 defines an Html helper which will generate the partial view with right names.
The value of property One will be empty because you are passing an instance of OneVM to the partial (not the main model) so the form controls are not correctly named with the prefix (which need to be name="One.SomeProperty").
You have included a link to a PartialFor() helper (which works) but don't use it. In the main view it needs to be
#Html.PartialFor(m => m.One, "_OnePartial")
Which is the equivalent of
#Html.Partial("_OnePartial", Model.One,
new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "One" }})
The problem probably lies in your HTML. If a model is nested, then the input fields of properties should be like this:
<input type="text" name="SubModel.PropertyName" />
Using HTML helpers, it would look something like this:
#Html.EditorFor(model => model.SubModel.PropertyName)
The ASP.NET MVC Action cannot know, that you want to fill your submodel if it's not in your HTML.
I have a base class for model:
public abstract class ViewModel {
public string Title{ get; set; }
public abstract string Type { get; }
}
and I created two classes:
public class SomeViewModel: ViewModel {
public override string Type { get { return "a type" } }
public int Id { get; set; }
}
public class AnotherModel: ViewModel {
public override string Type { get { return "another type" } }
public string System { get; set; }
}
Now, I have a view which uses these classes ( I send a List<ViewModel> to the view)
#model List<ViewModel>
...
<form>
#Html.DisplayFor(model => model)
</form>
I have a view called ViewModel.cshtml in DisplayTemplates folder
#model ViewModel
#if(Model is AnotherModel) {
// do something and print value
AnotherModel conv = Model as AnotherModel;
#Html.TextboxFor(model => conv.System)
} else {
// put some inputs here
}
#Html.HiddenFor(model => model.Type)
Now I have a javascript ajax. I want to send a list of objects to an action which renders a partial view
The ajax:
$.ajax({
url: 'My/GetData',
dataType: "html",
type: "POST",
data: { id: 3, myList: $("form").serialize() }
success: function(data) {
// print html
}
});
and action looks like:
[HttpPost]
public ActionResult GetData(int id, List<ViewModel> myList){
...
return PartialView("myView", someModel)
}
The problem is myList parameter which always is 0 as length (count)... I expect to be 2...
Where is my mistake ?
The reason myList would contain no elements is no doubt because the control names do not match the property names, but you have more problems than just that. The Type property in the derived models has a getter only, so even if the collection did bind correctly (it wont as discussed below), the value of Type would be an empty string because there is no setter on the property (the IsReadonly property of ModelMetadata is set to true so the DefaultModelBinder ignores it), and you would have no way of knowing what type your objects were.
Next, The DefaultModelBinder will only initialize instances of ViewModel, not your derived types, so only those properties of ViewModel with public getters/setters would be bound (for example the Id property of SomeViewModel wont exist)
I am assuming your models must be far more complex than what you have shown (otherwise there would be no point doing this) which means your template (and it should be an EditorTemplate not a DisplayTemplate!) must be polluted with lots of other logic to determine which properties to render and how (and a nightmare to debug).
The simple solution would be to create a view model with collections for each type (probably also including the id property you want to post, but not sure what that is or how it relates to your model) and then use an EditorTemplate for each type
public class TypeVM
{
public List<SomeViewModel> SomeTypes { get; set; }
public List<AnotherModel> AnotherTypes { get; set; }
}
/Views/Shared/EditorTemplates/AnotherModel.cshtml
#model SomeViewModel
....
#Html.TextBoxFor(m => m.Id)
/Views/Shared/EditorTemplates/SomeViewModel.cshtml
#model SomeViewModel
....
#Html.TextBoxFor(m => m.System)
and in the view
#model TypeVM
....
#Html.EditorFor(m => m.SomeTypes)
#Html.EditorFor(m => m.AnotherTypes)
If you need to submit using ajax, then
var url = '#Url.Action("GetData", "My")'; // don't hard code your url's!
var data = $("form").serialize();
$.post(url, data, function(data) {
// update DOM
});
and post back to
[HttpPost]
public ActionResult GetData(TypeVM model)
This would mean rendering different the types in groups. If that is not suitable, then you need to create a custom ModelBinder where you can use the bindingContext to read the Type property and initialize each derived type and set the appropriate properties. A daunting task, but The Features and Foibles of ASP.NET MVC Model Binding
might help you get started with how to approach it (the section on Abstract Model Binder).
Side note: Do you really need to post back the whole form in order to return a partial view?
I’m working with (ASP.Net MVC4), and I’m stuck in how to get info from partial view.
I mean I have a my viewmodel
public class ShowHomeViewModel
{
public int ID_ClientTypeID { get; set; }
public string ReservationDate { get; set; }
public short ClientNum { get; set; }
public string ClientPhone { get; set; }
public List<HomeViewModel> ReservedHuts { get; set; }
}
I have a partial view where I’m showing all de data from HomeViewModel, it show’s great, the problem is when I try to recover the list of ReservedHuts from the view:
#model HutReservation.ViewModel.ShowHomeViewModel
<table>
<tr>
#{Html.RenderPartial("_Reservations", Model.ReservedHuts);}
</tr>
</table>
In my view I show the list and change some data, but when I click on the button and go to the New Method bellow, turns out that the ReservedHuts (the list of HomeViewModel) is null
[HttpPost] //cambiar la pagina para el partial view
public ActionResult New(ShowHomeViewModel vm) // <- this vm is null
{
foreach (HomeViewModel hvm in vm.ReservedHuts)
{
}
return View("ConfirmNew", vm);
}
I’m really stuck here, Thanks for your help in advance
The problem here is that your form data cannot be bound to ShowHomeViewModel. This is because the context of Model has changed when you render your partial (the model context has changed from ShowHomeViewModel to HomeViewModel). MVC uses conventions to bind the properties by default (you can make your own model binders if you wish, which use whatever methods you want to bind). By Default, when you make an EditorFor a property of your model, MVC will name the form field appropriate so that it can be bound back to the model context it was created from.
So if you skip the partial, you can just run your foreach in the parent view, and you could probably still bind to ShowHomeViewModel because it will name the form fields like this:
<input name="ReservedHuts_HutId">
However, if you use the partial and change your model context, it will render the same input like this:
<input name="HutId">
So, MVC cannot (by default) bind the value of HutId to ShowHomeViewModelbecause there is no property called HutId (But there is HutId property on HomeViewModel, so it will bind successfully!)
I declare a model with 4 string fields. 3 of which are read-only on the form:
public class HomeModel
{
[ReadOnly(true)]
[DisplayName("Service Version")]
public string ServiceVersion { get; set; }
[ReadOnly(true)]
[DisplayName("Session Id")]
public string SessionId { get; set; }
[ReadOnly(true)]
[DisplayName("Visiting from")]
public string Country { get; set; }
[DisplayName("Search")]
public string SearchString { get; set; }
}
I pass the model, after populating it, to my form:
[HttpGet]
public ActionResult Index()
{
var model = new HomeModel
{
Country = "Australia",
SearchString = "Enter a search",
ServiceVersion = "0.1",
SessionId = "76237623763726"
};
return View(model);
}
And the form is displayed as I expect:
<h2>Simple Lookup</h2>
#Html.LabelFor(m=>m.ServiceVersion): #Model.ServiceVersion<br/>
#Html.LabelFor(m=>m.SessionId): #Model.SessionId<br/>
#Html.LabelFor(m=>m.Country): #Model.Country<br/>
<p>
#using(Html.BeginForm())
{
#Html.LabelFor(m => m.SearchString)
#Html.TextBoxFor(m => m.SearchString)
<button type="submit" name="btnSearch">Search</button>
}
</p>
But, when I submit the form, and get the model back from the form, only the value of the SearchString is populated.
[HttpPost]
public ActionResult Index(HomeModel model)
{
return View(model);
}
Is it right that the other fields have been 'lost'? Does MVC not preserve the other members of the model class? And if this is expected - is there a way to re-get these? Or would I need to go back to my database, populate the model with the old values, and then use the new values from the form model?
It's possible the validity of wanting to read 'read-only' fields back from the model is questioned.. which is fair - but in the event that I find something suspect about the posted data, maybe I want to re-show the screen, and not have to re-read the data from a database again?
This is the correct behavior. Only the elements inside form will be posted to your action. Since it is posting the form so your fields should be inside the form in order to get them on your post method.
Update
Also, you cannot read particular field on your action method if you have taken that field readonly on your view. eg: displaying using #Html.LabelFor. In order to get field back on your action use #Html.HiddenFor if field is not to be edited.