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
Related
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!
The app is designed to allow the user to enter a an IP address for a local machine and and it will then return the HDD information for that machine. It starts out with a default value already in the TextAreaFor box and performs the query for that value. This part works with no problem. But when a user tries to enter in their own value and hit the Refresh button, it keeps coming up with the error Object reference not set to an instance of an object.
I'm not sure why this is happening. It seems to me that clicking the button submits a POST action, which should kick off the second method in the controller. The current model is then passed to the controller with the values in the TextAreaFor attached and the mainCode() method is run on the new values.
Edit: According to What is a NullReferenceException, and how do I fix it? I am pretty sure that I am returning an empty model from my controller. I just don't see how. The form field should be sending the controller everything contained in TextAreaFor so the model should not be empty.
Edit2: I did some testing and the model is getting returned alright, but the values from TextAreaFor are not. When the mainCode() tries to do some logic to startDrives.startingDrives, it can't because that variable is empty for some reason.
Model:
namespace RelengAdmin.Models
{
public class DriveInfo
{
public class DriveHolder
{
public string startingDrives {get; set;}
}
public DriveHolder startDrives = new DriveHolder();
public void mainCode()
{
/****Code to return the HDD size omitted****/
}
}
}
View:
#using (Html.BeginForm())
{
<input type="submit" value="Refresh" />
#Html.TextAreaFor(model => model.startDrives.startingDrives, new {#class = "HDDTextBox"})
}
Controller:
namespace RelengAdmin.Controllers
{
public class HDDCheckerController : Controller
{
[HttpGet]
public ActionResult Index()
{
DriveInfo myDrive = new DriveInfo();
myDrive.startDrives.startingDrives = "148.136.148.53"
myDrive.mainCode();
return View(myDrive);
}
[HttpPost]
public ActionResult Index(DriveInfo model)
{
model.mainCode();
return View(model);
}
}
}
The issue is that your model's startDrives property is not actually declared as a property with getters and setters, so the model binder won't bind to it. I was able to duplicate the issue locally, and solve it by declaring the startDrives as a property and initializing it in the constructor.
public class DriveInfo
{
public class DriveHolder
{
public string startingDrives { get; set; }
}
public DriveHolder startDrives { get; set; }
public DriveInfo()
{
startDrives = new DriveHolder();
}
public void mainCode()
{
/****Code to return the HDD size omitted****/
}
}
Your question is a bit unclear of where the model is actually null.. but I would assume that when you hit your button, it goes to the correct action, but there is nothing in model because you haven't passed any specific values..
so try this:
CSHTML
#using (Html.BeginForm())
{
<input type="submit" value="Refresh" />
#Html.TextArea("startingDrive", "148.136.148.53", new {#class = "HDDTextBox"})
}
Controller
[HttpPost]
public ActionResult Index(string startingDrive)
{
DriveInfo searchThisDrive = new DriveInfo();
searchThisDrive.startDrives.startingDrives = startingDrive;
searchThisDrive.mainCode();
return View(searchThisDrive);
}
Let me know if this helps!
I followed Darin's post at
multi-step registration process issues in asp.net mvc (splitted viewmodels, single model)
Its a very elegant solution, however im having trouble seeing how you would populate the individual step viewmodels with data. Im trying to emulate amazons checkout step-system which starts with selecting an address, then shipping options, then payment information.
For my first viewmodel i require a list of addresses for my current logged in user which i poll the database for to display on screen
In my head, this is the viewmodel that makes sense to me.
[Serializable]
public class ShippingAddressViewModel : IStepViewModel
{
public List<AddressViewModel> Addresses { get; set; }
[Required(ErrorMessage="You must select a shipping address")]
public Int32? SelectedAddressId { get; set; }
#region IStepViewModel Members
private const Int32 stepNumber = 1;
public int GetStepNumber()
{
return stepNumber;
}
#endregion
}
However there seems to be no good way to populate the addresses from the controller.
public class WizardController : Controller
{
public ActionResult Index()
{
var wizard = new WizardViewModel();
wizard.Initialize();
return View(wizard);
}
[HttpPost]
public ActionResult Index(
[Deserialize] WizardViewModel wizard,
IStepViewModel step)
{
wizard.Steps[wizard.CurrentStepIndex] = step;
if (ModelState.IsValid)
{
if (!string.IsNullOrEmpty(Request["next"]))
{
wizard.CurrentStepIndex++;
}
else if (!string.IsNullOrEmpty(Request["prev"]))
{
wizard.CurrentStepIndex--;
}
else
{
// TODO: we have finished: all the step partial
// view models have passed validation => map them
// back to the domain model and do some processing with
// the results
return Content("thanks for filling this form", "text/plain");
}
}
else if (!string.IsNullOrEmpty(Request["prev"]))
{
// Even if validation failed we allow the user to
// navigate to previous steps
wizard.CurrentStepIndex--;
}
return View(wizard);
}
}
So i removed the list of address view models
[Serializable]
public class ShippingAddressViewModel : IStepViewModel
{
[Required(ErrorMessage="You must select a shipping address")]
public Int32? SelectedAddressId { get; set; }
#region IStepViewModel Members
private const Int32 stepNumber = 1;
public int GetStepNumber()
{
return stepNumber;
}
#endregion
}
This is what i came up with a custom editor template for the view model. It calls a Html.RenderAction which returns a partial view from my user controller of all the addresses and uses Jquery to populate a hidden input field for the view model's required SelectedAddressId property.
#model ViewModels.Checkout.ShippingAddressViewModel
<script src="../../Scripts/jquery-1.7.1.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function () {
//Check to see if the shipping id is already set
var shippingID = $("#SelectedAddressId").val();
if (shippingID != null) {
$("#address-id-" + shippingID.toString()).addClass("selected");
}
$(".address-id-link").click(function () {
var shipAddrId = $(this).attr("data-addressid").valueOf();
$("#SelectedAddressId").val(shipAddrId);
$(this).parent("", $("li")).addClass("selected").siblings().removeClass("selected");
});
});
</script>
<div>
#Html.ValidationMessageFor(m => m.SelectedAddressId)
#Html.HiddenFor(s => s.SelectedAddressId)
<div id="ship-to-container">
#{Html.RenderAction("UserAddresses", "User", null);}
</div>
</div>
And the users controller action
[ChildActionOnly]
public ActionResult UserAddresses()
{
var user = db.Users.Include("Addresses").FirstOrDefault(
u => u.UserID == WebSecurity.CurrentUserId);
if (user != null)
{
return PartialView("UserAddressesPartial",
Mapper.Map<List<AddressViewModel>>(user.Addresses));
}
return Content("An error occured");
}
The partial view
#model IEnumerable<AddressViewModel>
<ul>
#foreach (var item in Model)
{
<li id="address-id-#item.AddressID">
#Html.DisplayFor(c => item)
<a class="address-id-link" href="#" data-addressid="#item.AddressID">Ship To this Address
</a></li>
}
</ul>
My solution just seems super out of the way/sloppy to me, is there a better more concise way to populate the viewmodel than using partial views from a different controller for this?
There's nothing wrong with using a child action like this to populate the user's addresses. In fact, I think this is actually the optimal approach. You've got full separation of concerns and single responsibility in play. Just because something requires more "pieces" (extra action, views, etc.) doesn't make it sloppy or otherwise wrong.
The only other way to handle this would be with dependency injection. Namely, your ShippingAddressViewModel would need a dependency of the currently logged in user, so that it could populate the list of addresses from that in its constructor. However, since ShippingAddressViewModel is not exposed in your view, you would have to pass the dependency through Wizard which is a bit of code smell. Wizard is not dependent on a user, but it would have dependence forced upon it by virtue of having your view model abstracted away inside it.
Long and short, while there's a way you could do this without the child actions and partial views, it would actually be nastier and sloppier than with them.
I am aware that complex types are not usually rendered when using EditorForModel but I am using a custom object template that does not do the check and calls Html.Editor for every property including complex types.
Unfortunately, whilst I can see the correct TemplateHint value for the property within the object template, the Editor call doesn't seem to use it and the built in collection template is used instead.
My object template is basically this:
#foreach (var property in ViewData.ModelMetadata.Properties.Where(x => x.ShowForEdit))
{
#Html.Editor(property.PropertyName)
}
If I force the use of the template by passing the name to the Editor call then the ModelMetadata is empty in the template.
Is this a bug / are there any workarounds?
Some more info:
So my view model contains the following:
[ACustomAttribute("Something")]
public IEnumerable<int> SelectedOptions { get; set; }
The attribute implements IMetadataAware and adds some stuff to the AdditionalValues Collection of the ModelMetadata as well as setting the TemplateHint. I can read this data from the object template but not from my custom template.
#foreach (var property in ViewData.ModelMetadata.Properties.Where(x => x.ShowForEdit))
{
if (!string.IsNullOrEmpty(property.TemplateHint))
{
#Html.Editor(property.PropertyName, property.TemplateHint)
}
else
{
#Html.Editor(property.PropertyName)
}
}
But please note that if you don't rely on the established conventions for resolving templates for complex collection types (a.k.a ~/Views/Shared/EditorTemplates/NameOfTheTypeOfCollectionElements.cshtml) and have used an UIHint on your collection property:
[UIHint("FooBar")]
public IEnumerable<FooViewModel> Foos { get; set; }
then the ~/Views/Shared/EditorTemplates/FooBar.cshtml editor template must be strongly typed to IEnumerable<FooViewModel> and not FooViewModel. So be careful, if this is your case, it's up to you to loop inside this custom template if you want to get to individual items of the collection. It will no longer be ASP.NET MVC that will automatically loop for you and invoke the editor template for each element.
UPDATE:
Still can't repro your issue.
Custom attribute:
public class ACustomAttribute : Attribute, IMetadataAware
{
private readonly string _templateHint;
public ACustomAttribute(string templateHint)
{
_templateHint = templateHint;
}
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.AdditionalValues["foo"] = "bar";
metadata.TemplateHint = _templateHint;
}
}
Model:
public class MyViewModel
{
[ACustom("Something")]
public IEnumerable<int> Foos { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel
{
Foos = Enumerable.Range(1, 5)
};
return View(model);
}
}
View (~/Views/Home/Index.cshtml):
#model MyViewModel
#using (Html.BeginForm())
{
#Html.EditorForModel()
}
Editor template for the object type (~/Views/Shared/EditorTemplates/Object.cshtml):
#foreach (var property in ViewData.ModelMetadata.Properties.Where(x => x.ShowForEdit))
{
if (!string.IsNullOrEmpty(property.TemplateHint))
{
#Html.Editor(property.PropertyName, property.TemplateHint)
}
else
{
#Html.Editor(property.PropertyName)
}
}
Custom editor template (~/Views/Shared/EditorTemplates/Something.cshtml):
#model IEnumerable<int>
<h3>
#ViewData.ModelMetadata.AdditionalValues["foo"]
</h3>
#foreach (var item in Model)
{
<div>
#item
</div>
}
Result:
So as you can see the additional metadata we added is shown in the template.
I'm new to MVC3 and I can't figure out how to use checkboxes in MVC.
I have a bunch of text in my view like
text1
text2
text3
text4
text5
submitbutton
This text is not related to any model its just plain text. I would like to place a checkbox for each item and a link it to the controller so that when a user selects some of the checkbox values and clicks on the submit button my controller picks up which items have been selected.
I tried using #html.checkbox("text"+ index) and tried the controller to be
[HttpPost]
public ActionResult controller(List<string> list)
{
}
But that doesn't pick up the list of selected items. Can you tell me what i'm doing wrong or another way to do it?
What i would do in this situation is to make those items to be a property of my ViewModel.
public class MyViewModel
{
public bool text1 { set;get}
public bool text2 { set;get;}
public bool SomeMeaningFullName { set;get;}
// Other properties for the view
}
and in my Get Action method i will return this ViewModel to my View
public ActionResult Edit()
{
MyViewModel objVM=new MyViewModel();
return View(objVM);
}
and in my View
#model MyViewModel
#using (Html.BeginForm("Edit","yourcontroller"))
{
#Html.LabelFor(Model.text1)
#Html.CheckBoxFor(Model.text1)
#Html.LabelFor(Model.text2)
#Html.CheckBoxFor(Model.text2)
<input type="submit" value="Save" />
}
Now this property value will be available in your post action method
[HttpPost]
public ActionResult Edit(MyViewModel objVM)
{
//Here you can access the properties of objVM and do whatever
}
Create a ViewModel with all of your values. Populate the ViewModel and send it to the view. When something is checked, you'll know what's what on the post.
public class MyModelViewModel
{
public List<CheckBoxes> CheckBoxList {get; set;}
// etc
}
public class CheckBoxes
{
public string Text {get; set;}
public bool Checked {get; set;}
}
[HttpPost]
public ActionResult controller(MyModelViewModel model)
{
foreach(var item in model.CheckBoxList)
{
if(item.Checked)
{
// do something with item.Text
}
}
}
Basically ViewModels are your friend. You want to have a separate ViewModel for each View, and it's what gets passed back and forth between the Controller and the View. You can then do your data parsing either in the controller, or (preferably) in a service layer.
Additional Reference:
Should ViewModels be used in every single View using MVC?