I would like to do complex validation on my form that contains a list of objects.
My form contains a list of, let's say, MyObjects. MyObject consists of a double amount and a MyDate which is just a wrapper around DateTime.
public class MyObject
{
public MyDate Date { get; set; } //MyDate is wrapper around DateTime
public double Price { get; set; }
}
The form...
<input type="text" name="myList[0].Date" value="05/11/2009" />
<input type="text" name="myList[0].Price" value="100,000,000" />
<input type="text" name="myList[1].Date" value="05/11/2009" />
<input type="text" name="myList[1].Price" value="2.23" />
Here is my Action
public ActionResult Index(IList<MyObject> myList)
{
//stuff
}
I want to allow the user to enter in 100,000,000 for a Price and for the custom model binder to strip the ',' so it can convert to a double. Likewise, I need to convert the 05/11/2009 to a MyDate object. I thought about creating a MyObjectModelBinder but dont know what to do from there.
ModelBinders.Binders[typeof(MyObject)] = new MyObjectModelBinder();
Any help appreciated.
Here's a sample implementation of a custom model binder:
public class MyObjectModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// call the base method and let it bind whatever properties it can
var myObject = (MyObject)base.BindModel(controllerContext, bindingContext);
var prefix = bindingContext.ModelName;
if (bindingContext.ValueProvider.ContainsKey(prefix + ".Price"))
{
string priceStr = bindingContext.ValueProvider[prefix + ".Price"].AttemptedValue;
// priceStr = 100,000,000 or whatever the user entered
// TODO: Perform transformations on priceStr so that parsing works
// Note: Be carefull with cultures
double price;
if (double.TryParse(priceStr, out price))
{
myObject.Price = price;
}
}
if (bindingContext.ValueProvider.ContainsKey(prefix + ".Date"))
{
string dateStr = bindingContext.ValueProvider[prefix + ".Date"].AttemptedValue;
myObject.Date = new MyDate();
// TODO: Perform transformations on dateStr and set the values
// of myObject.Date properties
}
return myObject;
}
}
You're definitely going down the right path. When I did this, I made an intermediate view model that took Price as a string, because of the commas. I then converted from the view model (or presentation model) to a controller model. The controller model had a very simple constructor that accepted a view model and could Convert.ToDecimal("12,345,678.90") the price value.
Related
I'm using ASP.NET MVC 4 for an internal web application and I have a desire to bind HTML input fields to a custom object rather than string.
In the HTML I have input fields that will look like the following:
<input type="hidden" name="First" value="1;Simple" />
<input type="hidden" name="First" value="2;Sample" />
<input type="hidden" name="Second" value="1;Over" />
<input type="hidden" name="Third" value="22;Complex" />
<input type="hidden" name="Third" value="17;Whosit" />
This will happily bind to ViewModel properties like:
public string[] First { get; set; }
public string[] Second { get; set; }
public string[] Third { get; set; }
Each string is a delimited string of key+value that I'd love to have automatically parsed into a concrete object (I have one already defined.) Ideally I'd want it to bind exactly as above but using my object that would know how to split the delimited string into the proper properties.
I can't figure out how to get MVC to bind to a custom object. I've used constructors and implicit operator definitions but I can't get it to work with anything but string datatype.
I know I could get this to work if I pre-split the values into pairs in the HTML but I'm using a JavaScript library that doesn't give this ability. For instance I know repeating {name}.Label and {name}.Value would work to bind to the string properties on my complex object but this is prohibitive and a non-starter.
I have gotten this to work with a custom object to handle File Uploads but I suspect that worked only because it inherited from the same base object. I can't do this here since string is a sealed type and can't be extended.
My last resort is to find the default model binder code and reflect that to figure out how it's assigning the values to see if it teaches me anything that I can override. I'd prefer not to go the route of a custom binder I'd have to write myself and if it comes down to it I'll just have duplicate ViewModel fields and convert them myself but I'd really love to avoid this if there's already a capability for the model binder to do this for me.
Here is what you can do. Let's say your MyThing class is something like this:
public class MyThing
{
public int Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
return string.Format("{0};{1}", this.Id, this.Name);
}
}
Then, you can create a custom model binder for it like below:
public class MyModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
if (valueResult != null && !string.IsNullOrEmpty(valueResult.AttemptedValue))
{
if(valueResult.AttemptedValue.Contains(';'))
{
try
{
var attemptedValue = valueResult.AttemptedValue.Split(';');
int id = int.Parse(attemptedValue.First());
string name = attemptedValue.Last();
actualValue = new MyThing { Id = id, Name = name };
}
catch(Exception e)
{
modelState.Errors.Add(e);
}
}
else
{
modelState.Errors.Add("Invalid value.");
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
}
return actualValue;
}
}
You'll need to register your ModelBinder in Application_Start event of Global.asax like this:
ModelBinders.Binders.Add(typeof(MyThing), new MyModelBinder());
The question didn't get a single bite so I looked at the default model binder to see what was happening under the covers. There are a number of stages it goes through to see if a value can be converted to the ViewModel type but most of them are inaccessible to me. I did find a segment of code that fell back to using a type converter which I'd never used before.
Using this MSDN Type Converter how-to, I made a simple converter and decorated my class with the appropriate attribute and it just worked. I'm not sure what the performance implications are but it really simplifies my ViewModel code.
This example below is working for me. Keep in mind I'm only converting from the simple string type used by the DefaultModelBinder so it doesn't look like it's doing much but it solves my need and taught me a new feature of the framework.
public class MyThingConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context,
Type sourceType)
{
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
if (value is string)
return new MyThing((string)value);
return base.ConvertFrom(context, culture, value);
}
}
[TypeConverter(typeof(MyThingConverter))]
public class MyThing
{
public MyThing(string combinedValue)
{
//Split combinedValue into whatever properties I need
...
}
public override string ToString()
{
return string.Format("{0};{1}", prop1, prop2);
}
...
}
And that's it. So far it's working as expected.
I have a base view model with an Id property of type object (so I can have it be an int or a Guid) like so:
public abstract class BaseViewModel
{
public virtual object Id { get; set; }
}
And the view models thus derive from this
public class UserViewModel : BaseViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
My HTML then is rendered as:
<input id="Id" name="Id" type="hidden" value="240" />
<input id="FirstName" name="FirstName" type="text" value="John" />
<input id="LastName " name="LastName " type="text" value="Smith" />
And when submitted to the MVC action:
[HttpPost]
public ActionResult EditUser(UserViewModel model)
{
...code omitted...
}
The values for the model properties are:
Id: string[0] = "240"
FirstName: string = "John"
LastName: string = "Smith"
My question is, why am I getting a one item string array as the value for Id, rather than just a string? And is there a way to change this behavior? It causes problems when I try to parse it into the expected type.
I ended up solving this with a custom model binder that handles the "Id" object property as a special case:
public class CustomModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
// apply the default model binding first to leverage the build in mapping logic
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
// since "Id" is a special property on BaseViewModel of type object,
// we need to figure out what it should be and parse it appropriately
if (propertyDescriptor.Name == "Id" && propertyDescriptor.PropertyType == typeof(object))
{
// get the value that the default binder applied
var defaultValue = propertyDescriptor.GetValue(bindingContext.Model);
// this should be a one element string array
if (defaultValue is string[])
{
var defaultArray = defaultValue as string[];
// extract the first element of the array (the actual value of "Id")
var propertyString = defaultArray[0];
object value = propertyString;
// try to convert the ID value to an integer (the most common scenario)
int intResult;
if (int.TryParse(propertyString, out intResult))
{
value = intResult;
}
else
{
// try to convert the ID value to an Guid
Guid guidResult;
if (Guid.TryParse(propertyString, out guidResult)) value = guidResult;
}
// set the model value
propertyDescriptor.SetValue(bindingContext.Model, value);
}
}
}
}
The issue is with typing your id property as object -- not sure how the default binding is supposed to work here, but since an object is potentially anything -- like a complex object with multiple properties itself -- perhaps it attempts to dump all of the properties it finds there into an array?
If the Id is not always going to be an integer, I'd suggest typing this as string, since the model-binding mechanism should have no problem mapping virtually anything sent over HTTP as string, so:
public abstract class BaseViewModel
{
public virtual string Id { get; set; }
}
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))]
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
Let's say that you have a Model that looks kind of like this:
public class MyClass {
public string Name { get; set; }
public DateTime MyDate { get; set; }
}
The default edit template that Visual Studio gives you is a plain textbox for the MyDate property. This is all fine and good, but let's say that you need to split that up into it's Month/Day/Year components, and your form looks like:
<label for="MyDate">Date:</label>
<%= Html.TextBox("MyDate-Month", Model.MyDate.Month) %>
<%= Html.TextBox("MyDate-Day", Model.MyDate.Day) %>
<%= Html.TextBox("MyDate-Year", Model.MyDate.Year) %>
When this is submitted, a call to UpdateModel won't work, since there isn't a definition for MyDate-Month. Is there a way to add a custom binder to the project to handle situations like this, or if the HTML inputs are named differently (for whatever reasons)?
One workaround I've found is to use JavaScript to inject a hidden input into the form before submission that concatenates the fields and is named properly, but that feels wrong.
I would suggest you a custom model binder:
using System;
using System.Globalization;
using System.Web.Mvc;
public class MyClassBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var model = (MyClass)base.CreateModel(controllerContext, bindingContext, modelType);
var day = bindingContext.ValueProvider["MyDate-Day"];
var month = bindingContext.ValueProvider["MyDate-Month"];
var year = bindingContext.ValueProvider["MyDate-Year"];
var dateStr = string.Format("{0}/{1}/{2}", month.AttemptedValue, day.AttemptedValue, year.AttemptedValue);
DateTime date;
if (DateTime.TryParseExact(dateStr, "MM/dd/yyyy", null, DateTimeStyles.None, out date))
{
model.MyDate = date;
}
else
{
bindingContext.ModelState.AddModelError("MyDate", "MyDate has invalid format");
}
bindingContext.ModelState.SetModelValue("MyDate-Day", day);
bindingContext.ModelState.SetModelValue("MyDate-Month", month);
bindingContext.ModelState.SetModelValue("MyDate-Year", year);
return model;
}
}
This simplifies your controller action to:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult MyAction(MyClass myClass)
{
if (!ModelState.IsValid)
{
return View(myClass);
}
// Do something with myClass
return RedirectToAction("success");
}
And register the binder in Global.asax:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(MyClass), new MyClassBinder());
}
A simple way to handle this would be to get the values manually from the ValueProvider and construct the date server side, using UpdateModel with a white list that excludes these properties.
int month = int.Parse( this.ValueProvider["MyDate-Month"].AttemptedValue );
int day = ...
int year = ...
var model = db.Models.Where( m = > m.ID == id );
var whitelist = new string[] { "Name", "Company", ... };
UpdateModel( model, whitelist );
model.MyDate = new DateTime( year, month, day );
Of course, you'd need to add validation/error handling manually as well.