ASP.NET MVC (4) - Bind properties in a certain order - asp.net-mvc

Is there a way to force binding of properties A and B before C?
There's Order property in the System.ComponentModel.DataAnnotations.DisplayAttribute class, but does it affect binding order?
What i'm trying to achieve is
page.Path = page.Parent.Path + "/" + page.Slug
in a custom ModelBinder

Why not implement the Page property as:
public string Path{
get { return string.Format("{0}/{1}", Parent.Path, Slug); }
}
?

I would have initially recommended Sams answer as it would have not involved any binding of the Path property at all. You mentioned that you could concatenate the values using a Path property as this would cause lazy loading to occur. I imagine therefore you are using your domain models to display information to the view. I would therefore recommend using view models to display only the information required in the view (then use Sams answer to retrieve the path) and then map the view model to the domain model using a tool (i.e. AutoMapper).
However, if you continue to use your existing model in the view and you cannot use the other values in the model, you can set the path property to the values provided by the form value provider in a custom model binder after the other binding has occurred (assuming no validation is to be performed on the path property).
So lets assume you have the following view:
#using (Html.BeginForm())
{
<p>Parent Path: #Html.EditorFor(m => m.ParentPath)</p>
<p>Slug: #Html.EditorFor(m => m.Slug)</p>
<input type="submit" value="submit" />
}
And the following view model (or domain model as the case may be):
public class IndexViewModel
{
public string ParentPath { get; set; }
public string Slug { get; set; }
public string Path { get; set; }
}
You can then specify the following model binder:
public class IndexViewModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//Note: Model binding of the other values will have already occurred when this method is called.
string parentPath = bindingContext.ValueProvider.GetValue("ParentPath").AttemptedValue;
string slug = bindingContext.ValueProvider.GetValue("Slug").AttemptedValue;
if (!string.IsNullOrEmpty(parentPath) && !string.IsNullOrEmpty(slug))
{
IndexViewModel model = (IndexViewModel)bindingContext.Model;
model.Path = bindingContext.ValueProvider.GetValue("ParentPath").AttemptedValue + "/" + bindingContext.ValueProvider.GetValue("Slug").AttemptedValue;
}
}
}
And finally specify that this model binder is to be used by using the following attribute on the view model:
[ModelBinder(typeof(IndexViewModelBinder))]

Related

How to persist an object in view model on post back [duplicate]

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!

ASP.NET mvc 3 model binding with list of strings

I am currently using model binding and ASP.NET MVC 3 and .NET 4.0.
View Model Class:
public class BasicViewModel
{
[Display(Name = #"Names")]
[Required(ErrorMessage = #"Names is required")]
[DisplayFormat(ConvertEmptyStringToNull = true)]
List<string> Names { get; set; }
[Display(Name = #"Email")]
[Required(ErrorMessage = #"Email is required")]
string Email { get; set; }
}
Controller
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult NameEmail( BasicViewModel basicModel)
{
// some manipulation of data
}
View in cshtml file (razor view engine)
// model declared here using #model BasivViewModel
// only required part shown labels part of code removed
#Html.TextBoxFor(model => model.Names)
...
#Html.TextBoxFor(model => model.Email)
...
The model binding provided by ASP.NET MVC binds the string Email to null if it is empty but binds the List Names to empty string (""). I want it to be null. I made the binding work using JavaScript by parsing the values of form fields on click of submit button. But i want the asp.net model binding to do this. Furthermore, it would be great if there is some field in Data Annotations class like Required for this functionality. I tried this Null Display Text Property and refer to the remarks section. Is there a solution or is this how it is implemented?. I am not sure whether i have understood this part of model binding correctly.
By default, if the field, representing an array, is in the html, the controller will receive an array of length 0. However, to make the array null, you can define a custom ModelBinder.
public class MyModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(List<string>))
{
HttpRequestBase request = controllerContext.HttpContext.Request;
// Check to see if any of the elements the array is not empty and
// returns null if they all are.
return request.Form.GetValues("Names").Any(x => !string.IsNullOrWhiteSpace(x)) ?
base.BindModel(controllerContext, bindingContext) :
null;
//You can also remove empty element from the array as well, by using
// a where clause
}
return base.BindModel(controllerContext, bindingContext);
}
}
Alternatively, you can also implement IModelBinder instead of DefaultModelBinder.
The next step is to register the custom binder in your Application_Start function in the Global.asax.cs file.
ModelBinders.Binders.Add(typeof(List<string>), new MyModelBinder());
This basically tells the mvc engine to use the MyModelBinder whenever the field is List<string>
To know more about modelbinder, goolge "MVC custom model binding". Let me know you go :)

ASP.NET Web API Model Binding

I'm using Web API within ASP .NET MVC 4 RC, and I have a method that takes a complex object with nullable DateTime properties. I want the values of the input to be read from the query string, so I have something like this:
public class MyCriteria
{
public int? ID { get; set; }
public DateTime? Date { get; set; }
}
[HttpGet]
public IEnumerable<MyResult> Search([FromUri]MyCriteria criteria)
{
// Do stuff here.
}
This works well if I pass a standard date format in the query string such as 01/15/2012:
http://mysite/Search?ID=1&Date=01/15/2012
However, I want to specify a custom format for the DateTime (maybe MMddyyyy)... for example:
http://mysite/Search?ID=1&Date=01152012
Edit:
I've tried to apply a custom model binder, but I haven't had any luck applying it to only DateTime objects. The ModelBinderProvider I've tried looks something like this:
public class DateTimeModelBinderProvider : ModelBinderProvider
{
public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(DateTime) || bindingContext.ModelType == typeof(DateTime?))
{
return new DateTimeModelBinder();
}
return null;
}
}
// In the Global.asax
GlobalConfiguration.Configuration.Services.Add(typeof(ModelBinderProvider), new DateTimeModelBinderProvider());
The new model binder provider is created, but GetBinder is only called once (for the complex model parameter, but not for each property within the model). This makes sense, but I would like to find a way to make it to use my DateTimeModelBinder for DateTime properties, while using the default binding for non-DateTime properties. Is there a way to override the default ModelBinder and specify how each property is bound?
Thanks!!!
Consider setting your view-model's Date property to type string
Then either write a utility function to handle the mapping between the viewmodel type and the domain-model type:
public static MyCriteria MapMyCriteriaViewModelToDomain(MyCriteriaViewModel model){
var date = Convert.ToDateTime(model.Date.Substring(0,2) + "/" model.Date.Substring(2,2) + "/" model.Date.Substring(4,2));
return new MyCriteria
{
ID = model.ID,
Date = date
};
}
or use a tool like AutoMapper, like this:
in Global.asax
//if passed as MMDDYYYY:
Mapper.CreateMap<MyCriteriaViewModel, MyCriteria>().
.ForMember(
dest => dest.Date,
opt => opt.MapFrom(src => Convert.ToDateTime(src.Date.Substring(0,2) + "/" src.Date.Substring(2,2) + "/" src.Date.Substring(4,2)))
);
and in the controller:
public ActionResult MyAction(MyCriteriaViewModel model)
{
var myCriteria = Mapper.Map<MyCriteriaViewModel, MyCriteria>(model);
// etc.
}
From this example it might not seem that AutoMapper is providing any added value. It's value comes when you are configuring several or many mappings with objects that generally have more properties than this example. CreateMap will automatically map properties with the same name and type, so it saves lots of typing and it's much DRYer.

implementing model binder to bind a certain data format

i want to model bind this this data that is sent from the client
tag[15-d] : Little Owl
tag[19-a] : Merlin
name : value
into IEnumrable<AutoCompleteItem>
public class AutoCompleteItem
{
public string Key { get; set; }
public string Value { get; set; }
}
for example
Key = 15-d
Value = Little Owl
i don't know how to implement my own model binder in this scenario , any solution ?
Here is a model binder that I did for you and does what you want. It by no means complete (no validation, no error checking etc), but it can kick start you. One thing I particularly dislike is that the ModelBinder directly accesses the form collection instead of using the ValueProvider of the context, but the latter doesn't let you get all bindable values.
public class AutoCompleteItemModelBinder : IModelBinder
{
// Normally we would use bindingContext.ValueProvider here, but it doesn't let us
// do pattern matching.
public object BindModel (ControllerContext controllerContext, ModelBindingContext bindingContext)
{
string pattern = #"tag\[(?<Key>.*)\]";
if (!String.IsNullOrWhiteSpace (bindingContext.ModelName))
pattern = bindingContext.ModelName + "." + pattern;
IEnumerable<string> matchedInputNames =
controllerContext.HttpContext.Request.Form.AllKeys.Where(inputName => Regex.IsMatch(inputName, pattern, RegexOptions.IgnoreCase));
return matchedInputNames.Select (inputName =>
new AutoCompleteItem {
Value = controllerContext.HttpContext.Request.Form[inputName],
Key = Regex.Match(inputName, pattern).Groups["Key"].Value
}).ToList();
}
}
Here is a sample action that uses it:
[HttpPost]
public void TestModelBinder ([ModelBinder(typeof(AutoCompleteItemModelBinder))]
IList<AutoCompleteItem> items)
{
}
And a sample view. Note the "items." prefix - it's the Model Name (you can drop it depending on how you submit this list of items:
#using (Html.BeginForm ("TestModelBinder", "Home")) {
<input type="text" name="items.tag[15-d]" value="Little Owl" />
<input type="text" name="items.tag[19-a]" value="Merlin" />
<input type="submit" value="Submit" />
}
If you have questions - add a comment and I will expand this answer.
You should just be able to name your fields key[0], value[0] (1,2,3 etc) and it should bind automatically since these are just strings. If you need to customize this for some reason - still name your fields key[0] value[0] (then 1,2,3 etc) and do exactly as specified here:
ASP.NET MVC - Custom model binder able to process arrays

DefaultModelBinder Problem with nested levels + other binders

I have what I would think is a somewhat normal situation where I need to bind form posts to an "order" model. This model has a few levels of information to it:
Order.Billing.FirstName
Order.Billing.Address.City
Order.Billing.Address.Country
Using the DefaultModelBinder, if I POST a form to an action that takes this Order model as the param, the following fields JustWork(TM):
<%=Html.TextBox("Billing.FirstName")%>
<%=Html.TextBox("Billing.Address.City")%>
This field does not:
<%=Html.TextBox("Billing.Address.Country")%>
The wrinkle I have is with the country property. In our case, Address.Country returns a Country class instance (ISO2/3/Name/Code logic). It is not a string. Not surprise that it doesn't work by default.
My first thought was to create a CountryModelBinder (inherit DefaultModelBinder) and ModelBinders.Binders.Add it to the type of Country. When I do that, CountryModelBinder never gets called in the scenerio above.
My second thought was to create an AddressModelBinder (inherit DefaultModelBinder) and bind it to our Address type. While that does get called, the SetProperty call for "Country" has an empty value, even though the form has posted a field called "Billing.Address.Country".
After some tinkering, it appears that the model binding behavior only calls CreateModel when the model is the top level class the action wants, and all other binders have their BindPropery/SetProperty called for child properties.
In other words, if I create model binders for Order, OrderAddress(Billing), Address, and Country. For the action that takes an order, only OrderModelBinder.CreateModel is called. ORderAddress and Address.BindProperty/SetProperty are called for some things, and sometimes SetProperty value argument is empty when it was clearly posted in a name that matches the other field property mappings.
It's easy enough to just add code to OrderModelBinder to pull Billing.Address.Country out of Request.Form. But I have multiple models that use Address and having all of them do that seems broken.
What am I missing here? Is there a way to have the CountryModelBinder actually get called in this case? I would think that the CountryModelBinder should get called when Billing.Address.Country is mapped to the Country property of the Address binder.
I've tried doing what you've done here, appearntly on MVC3 it does indeed work if I provide a model binder for that type.
This is just a proof of concept to show that it DOES WORK, and shouldn't be seen as even close to production level code:
Models:
public class SimpleModel
{
public string Value { get; set; }
public int Other { get; set; }
}
public class ComplexModel
{
public SimpleModel Complexity {get;set;}
public string StrVal { get; set; }
}
some binder:
public class MBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if ( bindingContext.ModelType == typeof(SimpleModel))
{
var simpleModel= new SimpleModel();
simpleModel.Other = 1;
simpleModel.Value = controllerContext.HttpContext.Request.Form["Complexity"];
return cm;
}
return null;
}
}
in global asax:
ModelBinders.Binders.Add(typeof (SimpleModel), new MBinder());
code in View:
#model ComplexModel
#using ( Html.BeginForm() )
{
<fieldset>
#Html.LabelFor(x => x.Complexity)
#Html.TextBoxFor(x => x.Complexity)
</fieldset>
<fieldset>
#Html.LabelFor(x => x.StrVal)
<br />
#Html.EditorFor(x => x.StrVal)
</fieldset>
<input type="submit" />
}
Controller:
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(ComplexModel model)
{
return RedirectToAction("Index");
}
BTW in MVC 3 a better option would be to use the IModelBinderProvider interface, but I just wanted to show something that would work.

Resources