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.
Related
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 :)
Maybe the title is not so explicitly. Let me explain you my situation
I've got a get and post method in my controller. In the GET method, gets the entities from the database context
[HttpGet]
public ActionResult RecheckAssignment(short id)
{
var assignment = db.Assignments.Find(id);
Session["QuestionList"] = QuestionRepositoryManager.GetAllPossibleQuestionsFromJson(assignment.Content); // it's a list!
return View(Session["QuestionList"]);
}
Assignment entity contains as 10 properties. When I show this entities in the model, it shows uses all the properties, but when the user does post should get only two properties from it (Id string, Changed bool) in the POST METHOD.
I do not what to put inside of the method parameters.
[HttpPost]
public ActionResult RecheckAssignment(...)
{
return View();
}
I put everything in a session variable because later I must have to get the entities again, I guess this is a good option using Session but I'm not sure.
So, what should I have to write inside of the method to get only the Id and Changed properties to updated the entities.
When ASP.NET MVC maps a <form> back to the Action during a POST it will fill in what it can. Consider a class like this:
public class Car
{
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }
}
and now consider this form:
#using (Html.BeginForm("ActionName", "ControllerName", FormMethod.Post))
{
Html.TextBoxFor(m => m.Make)
}
and now consider this Action:
public ActionResult ActionName(Car model)
{
// the values of Car will look like this
model.Make // this will be what was in the text box
model.Model // this will be null
model.Year // this will be 0
}
and take note that null and 0 are the default values for those types. So, if I wanted to POST the property Model I need to get it in the form. I can do that with #Html.TextBoxFor, but what if I don't want the user to see it? Well, I can do that too:
Html.HiddenFor(m => m.Model);
and so now when the form is POSTed it will populate the Model with the value it was downloaded with. So, just make sure that all the properties you need are in the form in some way.
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))]
This question has been asked before in earlier versions of MVC. There is also this blog entry about a way to work around the problem. I'm wondering if MVC3 has introduced anything that might help, or if there are any other options.
In a nutshell. Here's the situation. I have an abstract base model, and 2 concrete subclasses. I have a strongly typed view that renders the models with EditorForModel(). Then I have custom templates to render each concrete type.
The problem comes at post time. If I make the post action method take the base class as the parameter, then MVC can't create an abstract version of it (which i would not want anyways, i'd want it to create the actual concrete type). If I create multiple post action methods that vary only by parameter signature, then MVC complains that it's ambiguous.
So as far as I can tell, I have a few choices on how to solve this proble. I don't like any of them for various reasons, but i will list them here:
Create a custom model binder as Darin suggests in the first post I linked to.
Create a discriminator attribute as the second post I linked to suggests.
Post to different action methods based on type
???
I don't like 1, because it is basically configuration that is hidden. Some other developer working on the code may not know about it and waste a lot of time trying to figure out why things break when changes things.
I don't like 2, because it seems kind of hacky. But, i'm leaning towards this approach.
I don't like 3, because that means violating DRY.
Any other suggestions?
Edit:
I decided to go with Darin's method, but made a slight change. I added this to my abstract model:
[HiddenInput(DisplayValue = false)]
public string ConcreteModelType { get { return this.GetType().ToString(); }}
Then a hidden automatically gets generated in my DisplayForModel(). The only thing you have to remember is that if you're not using DisplayForModel(), you'll have to add it yourself.
Since I obviously opt for option 1 (:-)) let me try to elaborate it a little more so that it is less breakable and avoid hardcoding concrete instances into the model binder. The idea is to pass the concrete type into a hidden field and use reflection to instantiate the concrete type.
Suppose that you have the following view models:
public abstract class BaseViewModel
{
public int Id { get; set; }
}
public class FooViewModel : BaseViewModel
{
public string Foo { get; set; }
}
the following controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new FooViewModel { Id = 1, Foo = "foo" };
return View(model);
}
[HttpPost]
public ActionResult Index(BaseViewModel model)
{
return View(model);
}
}
the corresponding Index view:
#model BaseViewModel
#using (Html.BeginForm())
{
#Html.Hidden("ModelType", Model.GetType())
#Html.EditorForModel()
<input type="submit" value="OK" />
}
and the ~/Views/Home/EditorTemplates/FooViewModel.cshtml editor template:
#model FooViewModel
#Html.EditorFor(x => x.Id)
#Html.EditorFor(x => x.Foo)
Now we could have the following custom model binder:
public class BaseViewModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
var type = Type.GetType(
(string)typeValue.ConvertTo(typeof(string)),
true
);
if (!typeof(BaseViewModel).IsAssignableFrom(type))
{
throw new InvalidOperationException("Bad Type");
}
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
}
The actual type is inferred from the value of the ModelType hidden field. It is not hardcoded, meaning that you could add other child types later without having to ever touch this model binder.
This same technique could be easily be applied to collections of base view models.
I have just thought of an intersting solution to this problem. Instead of using Parameter bsed model binding like this:
[HttpPost]
public ActionResult Index(MyModel model) {...}
I can instead use TryUpdateModel() to allow me to determine what kind of model to bind to in code. For example I do something like this:
[HttpPost]
public ActionResult Index() {...}
{
MyModel model;
if (ViewData.SomeData == Something) {
model = new MyDerivedModel();
} else {
model = new MyOtherDerivedModel();
}
TryUpdateModel(model);
if (Model.IsValid) {...}
return View(model);
}
This actually works a lot better anyways, because if i'm doing any processing, then I would have to cast the model to whatever it actually is anyways, or use is to to figure out the correct Map to call with AutoMapper.
I guess those of us who haven't been using MVC since day 1 forget about UpdateModel and TryUpdateModel, but it still has its uses.
It took me a good day to come up with an answer to a closely related problem - although I'm not sure it's precisely the same issue, I'm posting it here in case others are looking for a solution to the same exact problem.
In my case, I have an abstract base-type for a number of different view-model types. So in the main view-model, I have a property of an abstract base-type:
class View
{
public AbstractBaseItemView ItemView { get; set; }
}
I have a number of sub-types of AbstractBaseItemView, many of which define their own exclusive properties.
My problem is, the model-binder does not look at the type of object attached to View.ItemView, but instead looks only at the declared property-type, which is AbstractBaseItemView - and decides to bind only the properties defined in the abstract type, ignoring properties specific to the concrete type of AbstractBaseItemView that happens to be in use.
The work-around for this isn't pretty:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
// ...
public class ModelBinder : DefaultModelBinder
{
// ...
override protected ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType.IsAbstract && bindingContext.Model != null)
{
var concreteType = bindingContext.Model.GetType();
if (Nullable.GetUnderlyingType(concreteType) == null)
{
return new AssociatedMetadataTypeTypeDescriptionProvider(concreteType).GetTypeDescriptor(concreteType);
}
}
return base.GetTypeDescriptor(controllerContext, bindingContext);
}
// ...
}
Although this change feels hacky and is very "systemic", it seems to work - and does not, as far as I can figure, pose a considerable security-risk, since it does not tie into CreateModel() and thus does not allow you to post whatever and trick the model-binder into creating just any object.
It also works only when the declared property-type is an abstract type, e.g. an abstract class or an interface.
On a related note, it occurs to me that other implementations I've seen here that override CreateModel() probably will only work when you're posting entirely new objects - and will suffer from the same problem I ran into, when the declared property-type is of an abstract type. So you most likely won't be able to edit specific properties of concrete types on existing model objects, but only create new ones.
So in other words, you will probably need to integrate this work-around into your binder to also be able to properly edit objects that were added to the view-model prior to binding... Personally, I feel that's a safer approach, since I control what concrete type gets added - so the controller/action can, indirectly, specify the concrete type that may be bound, by simply populating the property with an empty instance.
Using Darin's method to discriminate your model types via a hidden field in your view, I would recommend that you use a custom RouteHandler to distinguish your model types, and direct each one to a uniquely named action on your controller. For example, if you have two concrete models, Foo and Bar, for your Create action in your controller, make a CreateFoo(Foo model) action and a CreateBar(Bar model) action. Then, make a custom RouteHandler, as follows:
public class MyRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var httpContext = requestContext.HttpContext;
var modelType = httpContext.Request.Form["ModelType"];
var routeData = requestContext.RouteData;
if (!String.IsNullOrEmpty(modelType))
{
var action = routeData.Values["action"];
routeData.Values["action"] = action + modelType;
}
var handler = new MvcHandler(requestContext);
return handler;
}
}
Then, in Global.asax.cs, change RegisterRoutes() as follows:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
routes.Add("Default", new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home",
action = "Index",
id = UrlParameter.Optional }),
new MyRouteHandler()));
}
Then, when a Create request comes in, if a ModelType is defined in the returned form, the RouteHandler will append the ModelType to the action name, allowing a unique action to be defined for each concrete model.
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