Asp.net mvc 3 - Custom model binding - asp.net-mvc

I have a model like this
public string Name { get; set; }
public IEnumerable<int> ProjectMembersId { get; set; }
The property Name should be bound using the standart binding code.
But the property ProjectMembersId should be bound using my custom code.
So I derived a class from the DefaultModelBinder and overrided the SetProperty method.
protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
{
if (propertyDescriptor.Name == "ProjectMembersId")
{
var list = new List<int>(5);
var form = controllerContext.HttpContext.Request.Form;
var names = form.AllKeys.Where(x => x.StartsWith("dhxGridObj"));
foreach (var name in names)
{
int i;
if (int.TryParse(form.Get(name), out i))
{
list.Add(i);
}
}
value = list;
}
base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
}
Bud the problem is the SetProperty method isn't called because the value provider doesn't contain an item called ProjectMembersId.
Maybe I'm overriding a wrong part of the defaultModelBinder. So what'd be the best way to go ?

Try it with BindProperty method:
public class CustomModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext,
ModelBindingContext bindingContext,
System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name == "ProjectMembersId")
{
var list = new List<int>(5);
var form = controllerContext.HttpContext.Request.Form;
var names = form.AllKeys.Where(x => x.StartsWith("dhxGridObj"));
foreach (var name in names)
{
int i;
if (int.TryParse(form.Get(name), out i))
{
list.Add(i);
}
}
SetProperty(controllerContext, bindingContext, propertyDescriptor, list);
}
else
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
}

Related

Controlling the order properties are filled by ASP.NET MVC IModelBinder

In my custom ASP.NET MVC ModelBinder I have to bind an object of type MyType:
public class MyType
{
public TypeEnum Type { get; set; }
public string Tag { get; set; } // To be set when Type == TypeEnum.Type123
}
In the pseudo-code above you can see that I want the property 'Tag' to be set only when 'Type' is Type123.
My custom ModelBinder lokks like that:
public class CustomModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext cc, ModelBindingContext mbc, PropertyDescriptor pd)
{
var propInfo = bindingContext.Model.GetType().GetProperty(propertyDescriptor.Name);
switch (propertyDescriptor.Name)
{
case "Type": // ....
var type = (TypeEnum)controllerContext.HttpContext.Request.Form["Type"].ToString();
propInfo.SetValue(bindingContext.Model, name, null);
break;
case "Tag": // ...
if (bindingContext.Model.Type == TypeEnum.Type123) { // Fill 'Tag' }
break;
}
}
The problem I have is that in my curstom ModelBinder I have no control on the order the properties are binded by ASP.NET MVC.
Do you know how can I specify the order the proerties are filled by ASP.NET MV?
You could try overriding the BindModel method:
public class MyTypeModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = (MyType)base.BindModel(controllerContext, bindingContext);
if (model.Type != TypeEnum.Type123)
{
model.Tag = null;
}
return model;
}
}
You can try this in your custom model binder:
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
var formCollection = new FormCollection(controllerContext.HttpContext.Request.Form);
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
Then extract what you need from the formCollection. Good luck.
You could override the GetModelProperties method and facilitate System.ComponentModel.PropertyDescriptorCollection.Sort(string[]) method to reorder the properties (note that this method has several overloads). In your case, this should get you the expected results:
protected override PropertyDescriptorCollection GetModelProperties(
ControllerContext controllerContext, ModelBindingContext bindingContext)
{
return base.GetModelProperties(controllerContext, bindingContext)
.Sort(new[] { "Type", "Tag" });
}

Custom model binding issue

In my MVC 3 solution I want to have all Ids in querystring to be crypted. To decrypt URLs I inherited from DefaultModelBinder and overrided BindProperty method:
public class CryptedIdBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name.ToLower() == "id")
{
propertyDescriptor.SetValue(bindingContext.Model, CryptoHelper.Decrypt(controllerContext.HttpContext.Request.Form["id"]));
return;
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
return;
}
After that I set new DefaultBinder in global.asax on Application_Start:
System.Web.Mvc.ModelBinders.Binders.DefaultBinder = new CryptedIdBinder();
I didn't inherit from IModelBinder because I want to change binding logic only for id fields in solution.
The issue is that BindProperty method is never called. What am I doning wrong?
PS. In order to be sure that I call at least BindModel method I added a peace of this code inside my custom binder, and it was hit by the debugger:
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
return base.BindModel(controllerContext, bindingContext);
}
If your models don't have Id properties of course the BindProperty won't be called. Because it called on the model properties. If I understood your question what you need is to transform each Id named query string parameter. In this case you need a custom value provider instead of a modelbinder. This is good article about the value providers. And it's quite easy to write one:
public class MyValueProviderFacotry : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
return new MyValueProvider(controllerContext);
}
}
public class MyValueProvider : IValueProvider
{
private ControllerContext controllerContext;
public MyValueProvider(ControllerContext controllerContext)
{
this.controllerContext = controllerContext;
}
public bool ContainsPrefix(string prefix)
{
return true;
}
public ValueProviderResult GetValue(string key)
{
if (key.ToLower() == "id")
{
var originalValue = controllerContext.HttpContext.Request.QueryString[key];
var transformedValue = CryptoHelper.Decrypt(orignalValue );
var result = new ValueProviderResult(transformedValue,originalValue,CultureInfo.CurrentCulture);
return result;
}
return null;
}
}
In global.asax:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ValueProviderFactories.Factories.Insert(4, new MyValueProviderFacotry()); //Its need to be inserted before the QueryStringValueProviderFactory
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}

asp.net MVC 1.0 and 2.0 currency model binding

I would like to create model binding functionality so a user can enter ',' '.' etc for currency values which bind to a double value of my ViewModel.
I was able to do this in MVC 1.0 by creating a custom model binder, however since upgrading to MVC 2.0 this functionality no longer works.
Does anyone have any ideas or better solutions for performing this functionality? A better solution would be to use some data annotation or custom attribute.
public class MyViewModel
{
public double MyCurrencyValue { get; set; }
}
A preferred solution would be something like this...
public class MyViewModel
{
[CurrencyAttribute]
public double MyCurrencyValue { get; set; }
}
Below is my solution for model binding in MVC 1.0.
public class MyCustomModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = null;
ValueProviderResult valueResult;
bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out valueResult);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueResult);
if (bindingContext.ModelType == typeof(double))
{
string modelName = bindingContext.ModelName;
string attemptedValue = bindingContext.ValueProvider[modelName].AttemptedValue;
string wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
string alternateSeperator = (wantedSeperator == "," ? "." : ",");
try
{
result = double.Parse(attemptedValue, NumberStyles.Any);
}
catch (FormatException e)
{
bindingContext.ModelState.AddModelError(modelName, e);
}
}
else
{
result = base.BindModel(controllerContext, bindingContext);
}
return result;
}
}
You might try something among the lines:
// Just a marker attribute
public class CurrencyAttribute : Attribute
{
}
public class MyViewModel
{
[Currency]
public double MyCurrencyValue { get; set; }
}
public class CurrencyBinder : DefaultModelBinder
{
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
var currencyAttribute = propertyDescriptor.Attributes[typeof(CurrencyAttribute)];
// Check if the property has the marker attribute
if (currencyAttribute != null)
{
// TODO: improve this to handle prefixes:
var attemptedValue = bindingContext.ValueProvider
.GetValue(propertyDescriptor.Name).AttemptedValue;
return SomeMagicMethodThatParsesTheAttemptedValue(attemtedValue);
}
return base.GetPropertyValue(
controllerContext,
bindingContext, propertyDescriptor,
propertyBinder
);
}
}
public class HomeController: Controller
{
[HttpPost]
public ActionResult Index([ModelBinder(typeof(CurrencyBinder))] MyViewModel model)
{
return View();
}
}
UPDATE:
Here's an improvement of the binder (see TODO section in previous code):
if (!string.IsNullOrEmpty(bindingContext.ModelName))
{
var attemptedValue = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName).AttemptedValue;
return SomeMagicMethodThatParsesTheAttemptedValue(attemtedValue);
}
In order to handle collections you will need to register the binder in Application_Start as you will no longer be able to decorate the list with the ModelBinderAttribute:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(MyViewModel), new CurrencyBinder());
}
And then your action could look like this:
[HttpPost]
public ActionResult Index(IList<MyViewModel> model)
{
return View();
}
Summarizing the important part:
bindingContext.ValueProvider.GetValue(bindingContext.ModelName)
A further improvement step of this binder would be to handle validation (AddModelError/SetModelValue)

Is there a way to have the DefaultModelBinder ignore empty items when binding to a List<Enum>

I have a scenario where I'd like to change the behavior of the DefaultModelBinder in how it binds to a List of enums.
I have an enum...
public enum MyEnum { FirstVal, SecondVal, ThirdVal }
and a class for a model...
public class MyModel
{
public List<MyEnum> MyEnums { get; set; }
}
and the POST body is...
MyEnums=&MyEnums=ThirdVal
Currently, after model binding, the MyEnums property will contain...
[0] = FirstVal
[1] = ThirdVal
Is there was a way to tell the model binder to ignore the empty value in the posted data so that MyEnums property could look like the following?
[0] = ThirdVal
You could write a custom model binder for MyModel:
public class MyModelModelBinder : DefaultModelBinder
{
protected override void SetProperty(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
object value)
{
if (value is ICollection<MyEnum>)
{
var myEnums = controllerContext.HttpContext.Request[propertyDescriptor.Name];
if (!string.IsNullOrEmpty(myEnums))
{
var tokens = myEnums.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries);
value = tokens.Select(x => (MyEnum)Enum.Parse(typeof(MyEnum), x)).ToList();
}
}
base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
}
}
which is registered in Application_Start:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(MyModel), new MyModelModelBinder());
}
UPDATE:
As requested in the comments section here's how to make the previous binder more generic:
protected override void SetProperty(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
object value)
{
var collection = value as IList;
if (collection != null && collection.GetType().IsGenericType)
{
var genericArgument = collection
.GetType()
.GetGenericArguments()
.Where(t => t.IsEnum)
.FirstOrDefault();
if (genericArgument != null)
{
collection.Clear();
var enumValues = controllerContext.HttpContext
.Request[propertyDescriptor.Name];
var tokens = enumValues.Split(
new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var token in tokens)
{
collection.Add(Enum.Parse(genericArgument, token));
}
}
}
base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
}

How to filter form data with custom model binder

I have a bunch of forms where currency values are entered and I want them to be able to enter "$1,234.56". By default, the model binders won't parse that into a decimal.
What I am thinking of doing is creating a custom model binder the inherits DefaultModelBinder, override the BindProperty method, check if the property descriptor type is decimal and if it is, just strip out the $ and , from the values.
Is this the best approach?
Code:
public class CustomModelBinder : DefaultModelBinder
{
protected override void BindProperty( ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor )
{
if( propertyDescriptor.PropertyType == typeof( decimal ) || propertyDescriptor.PropertyType == typeof( decimal? ) )
{
var newValue = Regex.Replace( bindingContext.ValueProvider[propertyDescriptor.Name].AttemptedValue, #"[$,]", "", RegexOptions.Compiled );
bindingContext.ValueProvider[propertyDescriptor.Name] = new ValueProviderResult( newValue, newValue, bindingContext.ValueProvider[propertyDescriptor.Name].Culture );
}
base.BindProperty( controllerContext, bindingContext, propertyDescriptor );
}
}
Update
This is what I ended up doing:
public class CustomModelBinder : DataAnnotationsModelBinder
{
protected override void BindProperty( ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor )
{
if( propertyDescriptor.PropertyType == typeof( decimal ) || propertyDescriptor.PropertyType == typeof( decimal? ) )
{
decimal newValue;
decimal.TryParse( bindingContext.ValueProvider[propertyDescriptor.Name].AttemptedValue, NumberStyles.Currency, null, out newValue );
bindingContext.ValueProvider[propertyDescriptor.Name] = new ValueProviderResult( newValue, newValue.ToString(), bindingContext.ValueProvider[propertyDescriptor.Name].Culture );
}
base.BindProperty( controllerContext, bindingContext, propertyDescriptor );
}
}
It's reasonable to do it in the binder. However, I think that Decimal.Parse with the currency format provider or number style (see the docs) would be more reliable than stripping the "$" and calling base. For starters, it would handle non-US currency, which might be an issue for you some day.
In MVC3 you can just register a custom modelbinder that implements the IModelBinder interface specifically for decimal types and then tell it to handle currency or decimal by using the ModelMetaData.DataTypeName property on the bindingContext.
I've modified the sample provided by Phil Haack in his article to demonstrate how it could be done:
public class DecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
decimal actualValue = 0;
try
{
if(bindingContext.ModelMetadata.DataTypeName == DataType.Currency.ToString())
decimal.TryParse(valueResult.AttemptedValue, NumberStyles.Currency, null, out actualValue);
else
actualValue = Convert.ToDecimal(valueResult.AttemptedValue,CultureInfo.CurrentCulture);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
You could create your own ValidationAttribute which checks if value has correct format. Then you could look if property is decorated with this attribute and bind it in proper way. Attribute doesn't need to be ValidationAttibute, but it seems like good idea.

Resources