How to persist an object in view model on post back [duplicate] - asp.net-mvc

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

Unobtrusive validation on multiple models with same property names

I have a view containing multiple partial views bind to different models.
#model MyApp.ViewModels.ParentViewModel
#Html.Partial("_PartialView1", Model.PartialView1)
#Html.Partial("_PartialView2", Model.PartialView2)
Unobtrusive validation works, problem is, the models for the views have properties with the same name.
public class ClassA
{
public int SomeProperty { get; set; }
}
public class ClassB
{
public int SomeProperty { get; set; }
}
public class ParentViewModel
{
public int ClassA PartialView1 { get; set; }
public int ClassB PartialView2 { get; set; }
}
Since both properties have the same name, their html name attributes are the same too.
If ClassA.SomeProperty has an error, same error is shown in ClassB.SomeProperty.
Is there a way to have proper validation without changing the property names?
Do not use partials (which result in duplicate name attributes without the correct prefix, and which cannot be bound to you model when you submit the form).
The correct approach is to use an EditorTemplate. Rename _PartialView1.cshtml to ClassA.cshtml and move it to the /Views/Shared/EditorTemplates folder (ditto for _PartialView2 which needs to renamed to ClassB.cshtml - i.e. to match the name of the class). Then in the main view its
#model MyApp.ViewModels.ParentViewModel
....
#Html.EditorFor(m => m.PartialView1)
#Html.EditorFor(m => m.PartialView2)
Your html will now generate the correct name attributes
<input name="PartialView1.SomeProperty" .... />
<input name="PartialView2.SomeProperty" .... />
and the associated #Html.ValidationMessageFor() will also match up correctly
Side note: You can also solve this using a partial by passing the prefix as additional ViewData as per this answer, but the correct approach is to use an EditorTemplate
Unless they are in separate forms I don't think it is possible without giving them a different name.

MVC Passing ViewModel to #Html.Partial

Passing ViewModel to #Html.Partial
Have two ViewModels
public class RegisterVM
{
... some properties
public AddressVM AddressInformation { get; set; } //viewmodel
}
public class AddressVM {
public string Street1 { get; set; }
public string Street2 { get; set; }
public string PostalCode { get; set; }
}
When loading main view using VM:
#model ViewModels.RegisterVM
All field load. But When I add Partial View and pass viewmodel
#Html.Partial("_AddressDetails", Model.AddressInformation)
It fails
Error: Exception Details: System.NullReferenceException: Object reference not set to an instance of an object. Why does it fail?
The partial View _AddressDetails is expecting a
#model ViewModels.AddressVM
Update
Based on changes from Prashant,
When submitting the information The Address information is NULL.
In The controller:
[HttpPost]
public ActionResult Register(RegisterVM vm){
...
//when viewing vm.AddressInformation.Street1 is null. and there is a value
//Is there a different way of retrieving the values from partial view?
}
Thanks for reading.
The error is generated because property AddressInformation is null, and you need to initailize it in a parameterless constructor or in the controller before you pass it to the view, for example
public class RegisterVM
{
public RegisterVM() // default constructor
{
AddressInformation = new AddressVM();
}
public AddressVM AddressInformation { get; set; }
....
}
However you usage means that the controls generated will be
<input name="Street1" .../>
whereas they need to be
<input name="AddressInformation.Street1" .../>
in order to bind to your model. You can either make your partial an EditorTemplate (/Views/Shared/EditorTemplates/AddressVM.cshtml) and use in the main view as
#Html.EditorFor(m => m.AddressInformation)
or pass the prefix to the partial as additional ViewData
#Html.Partial("_AddressDetails", Model.AddressInformation, new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "AddressInformation" }})
This is working for me. You just need to instantiate your VM, attach it and send it to the view.
Page Action
public ActionResult Page(){
RegisterVM vm = new RegisterVM();
vm.AddressInformation = new AddressVM();
return View(vm);
}
Page.cshtml
#model Project.Web.Models.RegisterVM
<!-- loading partial view -->
#Html.Partial("_AddressDetails",Model.AddressInformation)
Partial View File
<input type="text" name="name" value=" " />
I am not having more info about code but as per mention details, can you try this
public ActionResult Register(){ return View(register); }
i know you may tried this but try to assigned explict value. as this is basic MVC implementation. if it not work out then you need to provide more code details.
hope this help.
in Register get Method must instatiate your viewModel because in view, call other partial with viewModel members(proprties);
public ActionResult Register(){
RegisterVM vm = new RegisterVM();
return View(vm);
}

How to post each submodel alone with validation into consideration?

I have a view which is bound with a ViewModel which contains multiple viewmodels.
Now, the parent view contains views(rendered by #html.partial) each view bound with its corresponding viewmodel and has its own form action.
My Question:
I could view the data correctly, but i can't submit each subview alone, so how can post each submodel alone?
Also, when there would be modelstate errors how can i refer to the correct subview?
Any idea would be appreciated.
Extra info:
The code sample shows what i did exactly:
ViewModel:
public class ViewModelParent
{
public ViewModelChild1 ViewModelC1 {get; set;}
public ViewModelChild2 ViewModelC2 {get; set;}
public ViewModelChild3 ViewModelC3 {get; set;}
}
Controller:
public ActionResult GetParent()
{
return view(new ViewModelParent());
}
Views:
GetParent.cshtml (contains views for each submodel).
#model Models.ViewModelParent
#Html.Partial("~/Views/Children/GetC1.cshtml", Model.ViewModelC1)
#Html.Partial("~/Views/Children/GetC2.cshtml", Model.ViewModelC2)
#Html.Partial("~/Views/Children/GetC3.cshtml", Model.ViewModelC3)
Children views:
GetC1.cshtml
#model ViewModelChild1
<form action="#Url.Action("GetC1", "Child"" method="POST" class="smart-form" id="frm_child1">
#Html.AntiForgeryToken()
#Html.ValidationSummary()
#* controls here*#
</form>
The same applies for the rest children views GetC2.cshtml & GetC3.cshtml
I've done something similar in the past.
I'd recommend this as a possible approach (assuming you want to stick with full page postbacks instead of going the ajax route).
Use your existing Parent ViewModel class (with child Models)
public class ViewModelParent
{
public ViewModelChild1 ViewModelC1 {get; set;}
public ViewModelChild2 ViewModelC2 {get; set;}
public ViewModelChild3 ViewModelC3 {get; set;}
}
Have the partial views each use the Parent Model
#model Models.ViewModelParent
#Html.Partial("~/Views/Children/GetC1.cshtml", Model)
#Html.Partial("~/Views/Children/GetC2.cshtml", Model)
#Html.Partial("~/Views/Children/GetC3.cshtml", Model)
The Child views each have the parent Model, but only contain form elements for the Child Model of that view. If you want a validation summary in every partial view you have to get a bit creative - I'll explain later...
eg: GetC1.cshtml
#model ViewModelParent
#using(Html.BeginForm("GetParent", "ParentControllerName", null, FormMethod.Post, new {#class="smart-form" id="frm_child1"}))
{
#Html.AntiForgeryToken()
#Html.ValidationSummaryForGroup(ViewBag.ChildType, "Child1") #* I'll explain this later *#
#* controls here - eg... *#
#Html.TextBoxFor(m => m.ViewModelChild1.Property1)
}
Then your controller can simply farm out the child methods if the form is valid (or return if not)
Eg:
public class ParentControllerNameController : Controller
{
public ActionResult GetParent()
{
return View(new ViewModelParent());
}
[HttpPost]
public ActionResult GetParent(ViewModelParent model)
{
if (ModelState.IsValid)
{
if (model.ViewModelC1 != null)
{
return GetC1(model.ViewModelC1);
}
else if (model.ViewModelC2 != null)
{
return GetC2(model.ViewModelC2)
}
else if (model.ViewModelC3 != null)
{
return GetC3(model.ViewModelC3)
}
} else {
// invalid!
if (model.ViewModelC1 != null)
{
ViewBag.ChildType = "Child1";
}
else if (model.ViewModelC2 != null)
{
ViewBag.ChildType = "Child2";
}
else if (model.ViewModelC3 != null)
{
ViewBag.ChildType = "Child3";
}
// needed to prevent null reference errors
if (model.ViewModelC1 == null) model.ViewModelC1 = new ViewModelChild1();
if (model.ViewModelC2 == null) model.ViewModelC2 = new ViewModelChild2();
if (model.ViewModelC3 == null) model.ViewModelC3 = new ViewModelChild3();
}
return View(model);
}
}
The above else-if statements will work, because each child view only contains properties for that child model - hence the other child viewmodels are null.
Note I used a new Html Helper extension above that I created that wraps the Validation Summary so you can display errors specific to the child model. A simple display/not display is insufficient because you'd lose client side validation errors being shown otherwise.
Of course this is only necessary when you have a validation summary in every partial view. If there's just one validation summary then you can stick with a simple #Html.ValidationSummary()
namespace System.Web.Mvc.Html
{
public static class ValidationSummaryForGroupExtensions
{
public static MvcHtmlString ValidationSummaryForGroup(this HtmlHelper html, string testValue, string expectedValue)
{
return ValidationSummaryForGroup(html, testValue, expectedValue, false);
}
/// <summary>
/// Displays a validation summary which shows serverside errors only if the specified testvalue and value are equal. Client side validation will work as normal.
/// <para>The purpose of this is to allow multiple valiation summaries (for multiple forms) on a single page.</para>
/// </summary>
/// <param name="testValue">Value to test (could be a value in viewbag)</param>
/// <param name="expectedValue">Value to expect if the server side errors are to be displayed.</param>
/// <returns></returns>
public static MvcHtmlString ValidationSummaryForGroup(this HtmlHelper html, string testValue, string expectedValue, bool excludePropertyErrors)
{
if (testValue != null && testValue.ToLower() == expectedValue.ToLower())
return html.ValidationSummary(excludePropertyErrors);
return new MvcHtmlString("<div class=\"validation-summary-valid\" data-valmsg-summary=\"true\"><ul><li style=\"display:none\"></li></ul></div>");
}
}
}
Of course you could do partial postbacks using ajax - in which case the child views could be directly for the child models, and each child form postback directly to the relevant method in your controller.
This is simple, in the handler of the post method of the controller, call the name of the parameter after the property name in the view model.
So in you view model you have:
public class ViewModelParent
{
public ViewModelChild1 viewModelC1 {get; set;}
public ViewModelChild2 viewModelC2 {get; set;}
public ViewModelChild3 viewModelC3 {get; set;}
}
In the post handler of your controller you will need something like:
<HttpPost()>
Function GetC1(viewModelC1 As ViewModelChild1 ) As ActionResult
in the html all the properties will be names like 'viewModelC1.nameofsomething' and this helps the model binder map the properties up. The above is VB.net but you should get the idea.
hope that helps
Andy

Display data from database(Entity framwork) into _Layout.cshtml

I'm developping a web site on MVC3 asp.net and I use entities framwork for data base:
I want to display the logo from database on _Layout.cshtml, and I want to display the texte from database into My home page.
this is my model
public class Theme
{
[Required(ErrorMessage = "ID is required.")]
public string ThemeID { get; set; }
public string path { get; set; }
[AllowHtml]
[Required(ErrorMessage = "Text is required.")]
public string texte { get; set; }
}
I put in the _Layout.cshtml
#Html.Partial("~/Views/Shared/_Header.cshtml")
this is my ThemeController.cs
[ChildActionOnly]
public ActionResult Header(string id)
{
var model = db.Themes.ToList();
return View("~/Views/Shared/_Header.cshtml", model);
}
this is the _Header.cshtml
#model ICollection<DSClient.Models.Theme>
#{
<img src="#Href( #Model.ElementAt(#Model-1).path )" />
}
When I type the url of Theme/index
It's OK, BUT the problem is when I load an other page, I have this exception
Object reference not set to an instance of an object.
Please Ineed your help.
Html.Partial is used to include a partial view. Therefore, when you include _Header.cshtml and the Model is not a ICollection<DSClient.Models.Theme>, you're in trouble.
Since you made a method with a Childaction attribute, I assume you may want to use Html.Action instead of Html.Partial. This would execute the child action of the controller which outputs the _Header.cshtml with the appropriate data.
Remove the string argument in the Header action (it's not used) and in _Layout.cshtml, you may call it like this:
#Html.Action("Header", "Theme")

How do I add a Custom Query for a drop down and retain the View Model Pattern?

I've read many articles which they state that querying should not be placed in the Controller, but I can't seem to see where else I would place it.
My Current Code:
public class AddUserViewModel
{
public UserRoleType UserRoleType { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
public ActionResult AddUser()
{
AddUserViewModel model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
The View:
<li>#Html.Label("User Role")#Html.DropDownListFor(x => Model.UserRoleType.UserRoleTypeID, Model.UserRoleTypes)</li>
How do I retain the View Model and Query and exclude the User Type that should not show up?
I think that you are doing it just fine.
Any way... all you can do to remove the querying logic from controller is having a ServiceLayer where you do the query and return the result.
The MVC pattern here is used correctly... what your are lacking is the other 2 layers (BusinessLayer and DataAccessLayer)... since ASP.NET MVC is the UI Layer.
UPDATE, due to comment:
Using var userroletypes = db.UserRoleTypes.Where(u=> u.UserRoleType != 1);
is OK, it will return a list of UserRoleType that satisfy the query.
Then, just create a new SelectList object using the userroletypes collection... and asign it to the corresponding viewmodel property. Then pass that ViewModel to the View.
BTW, I never used the db.XXXX.Select() method before, not really sure what it does... I always use Where clause.
SECOND UPDATE:
A DropDownList is loaded from a SelectList that is a collection of SelectItems.
So you need to convert the collection resulting of your query to a SelectList object.
var userroletypes = new SelectList(db.UserRoleTypes.Where(u=> u.UserRoleType != 1), "idRoleType", "Name");
then you create your ViewModel
var addUserVM = new AddUserViewModel();
addUserVM.UserRoleTypes = userroletypes;
and pass addUserVM to your view:
return View(addUserVM );
Note: I'm assuming your ViewModel has a property of type SelectList... but yours is public IEnumerable<SelectListItem> UserRoleTypes { get; set; } so you could change it or adapt my answer.
I don't see anything wrong with your code other than this db instance that I suppose is some concrete EF context that you have hardcoded in the controller making it impossible to unit test in isolation. Your controller action does exactly what a common GET controller action does:
query the DAL to fetch a domain model
map the domain model to a view model
pass the view model to the view
A further improvement would be to get rid of the UserRoleType domain model type from your view model making it a real view model:
public class AddUserViewModel
{
[DisplayName("User Role")]
public string UserRoleTypeId { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
and then:
public ActionResult AddUser()
{
var model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
and in the view:
#model AddUserViewModel
<li>
#Html.LabelFor(x => x.UserRoleTypeId)
#Html.DropDownListFor(x => x.UserRoleTypeId, Model.UserRoleTypes)
</li>

Resources