Use custom ASP.NET MVC IValueProvider, without setting it globally? - asp.net-mvc

I want to be able to grab keys/values from a cookie and use that to bind a model.
Rather than building a custom ModelBinder, I believe that the DefaultModelBinder works well out of the box, and the best way to choose where the values come from would be to set the IValueProvider that it uses.
To do this I don't want to create a custom ValueProviderFactory and bind it globally, because I only want this ValueProvider to be used in a specific action method.
I've built an attribute that does this:
/// <summary>
/// Replaces the current value provider with the specified value provider
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class SetValueProviderAttribute : ActionFilterAttribute
{
public SetValueProviderAttribute(Type valueProviderType)
{
if (valueProviderType.GetInterface(typeof(IValueProvider).Name) == null)
throw new ArgumentException("Type " + valueProviderType + " must implement interface IValueProvider.", "valueProviderType");
_ValueProviderType = valueProviderType;
}
private Type _ValueProviderType;
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
IValueProvider valueProviderToAdd = GetValueProviderToAdd();
filterContext.Controller.ValueProvider = valueProviderToAdd;
}
private IValueProvider GetValueProviderToAdd()
{
return (IValueProvider)Activator.CreateInstance(_ValueProviderType);
}
}
Unfortunately, the ModelBinder and its IValueProvider are set BEFORE OnActionExecuting (why?????). Has anyone else figured out a way to inject a custom IValueProvider into the DefaultModelBinder without using the ValueProviderFactory?

You should still use a ValueProviderFactory in this case.
The method that you have to implement on your ValueProviderFactory has this signature:
IValueProvider GetValueProvider(ControllerContext controllerContext)
Within your implementation of that method you can inspect the controller context, and if the incoming request is for the controller/action that you want to leverage cookies on, return some CustomCookieValueProvider.
If you don't want to leverage cookies for the request, just return null and the framework will filter that out of from the list of Value Providers.
As a bonus, you might not want to hard code the logic for when to use the CustomCookieValueProvider into the ValueProviderFactory. You could, perhaps, leverage DataTokens to match when to use cookies with given routes. So add a route like this:
routes.MapRoute("SomeRoute","{controller}/{action}").DataTokens.Add("UseCookies", true);
Notice the DataTokens.Add() call in there, now inside you GetValueProvider method you could do something like this:
if (controllerContext.RouteData.DataTokens.ContainsKey("UseCookies"))
{
return new CustomCookieValueProvider(controllerContext.RequestContext.HttpContext.Request.Cookies);
}
return null;

Here is an alternative that lets you specify IValueProviders as attributes against an actions parameters.
This makes the IValueProviders transient and not Global.
public interface IControllerContextAware
{
ControllerContext ControllerContext { get; set; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
public class ValueProviderAttribute : CustomModelBinderAttribute
{
public Type[] ValueProviders { get; private set; }
public ValueProviderAttribute(params Type[] valueProviders)
{
if (valueProviders == null)
{
throw new ArgumentNullException("valueProviders");
}
foreach (var valueProvider in valueProviders.Where(valueProvider => !typeof(IValueProvider).IsAssignableFrom(valueProvider)))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "The valueProvider {0} must be of type {1}", valueProvider.FullName, typeof(IValueProvider)), "valueProviders");
}
ValueProviders = valueProviders;
}
public override IModelBinder GetBinder()
{
return new ValueProviderModelBinder
{
ValueProviderTypes = ValueProviders.ToList(),
CreateValueProvider = OnCreateValueProvider
};
}
protected virtual IValueProvider OnCreateValueProvider(Type valueProviderType, ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueProvider = (IValueProvider)Activator.CreateInstance(valueProviderType);
if (valueProvider is IControllerContextAware)
{
(valueProvider as IControllerContextAware).ControllerContext = controllerContext;
}
return valueProvider;
}
private class ValueProviderModelBinder : DefaultModelBinder
{
public IList<Type> ValueProviderTypes { get; set; }
public Func<Type, ControllerContext, ModelBindingContext, IValueProvider> CreateValueProvider { get; set; }
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueProviders = from type in ValueProviderTypes
select CreateValueProvider(type, controllerContext, bindingContext);
bindingContext.ValueProvider = new ValueProviderCollection(valueProviders.Concat((Collection<IValueProvider>)bindingContext.ValueProvider).ToList());
return base.BindModel(controllerContext, bindingContext);
}
}
}
This is basically the code form the ModelBinderAttribute, but with a few tweaks.
It isn't sealed and so you can alter the way in which the IValueProviders are created if need be.
Here is a simple example which looks in another field, possibly a hidden or encrypted field, and takes the data and puts it into another property.
Here is the model, which has no knowledge of the IValueProvider, but does know about the hidden field.
public class SomeModel
{
[Required]
public string MyString { get; set; }
[Required]
public string MyOtherString { get; set; }
[Required]
public string Data { get; set; }
}
THen we have the IValueProvider, in this case, my provider knows explicitly about my model, but this doesn't have to be the case.
public class MyValueProvider : IValueProvider, IControllerContextAware
{
public ControllerContext ControllerContext { get; set; }
public bool ContainsPrefix(string prefix)
{
var containsPrefix = prefix == "MyString" && ControllerContext.HttpContext.Request.Params.AllKeys.Any(key => key == "Data");
return containsPrefix;
}
public ValueProviderResult GetValue(string key)
{
if (key == "MyString")
{
var data = ControllerContext.RequestContext.HttpContext.Request.Params["Data"];
var myString = data.Split(':')[1];
return new ValueProviderResult(myString, myString, CultureInfo.CurrentCulture);
}
return null;
}
}
and then the action that ties all this together:
[HttpGet]
public ActionResult Test()
{
return View(new SomeModel());
}
[HttpPost]
public ActionResult Test([ValueProvider(typeof(MyValueProvider))]SomeModel model)
{
return View(model);
}

Figured out how to do this. First, create a custom model binder that takes a value provider type in the constructor - but inherits from default modelbinder. This allows you to use standard model binding with a custom value provider:
/// <summary>
/// Uses default model binding, but sets the value provider it uses
/// </summary>
public class SetValueProviderDefaultModelBinder : DefaultModelBinder
{
private Type _ValueProviderType;
public SetValueProviderDefaultModelBinder(Type valueProviderType)
{
if (valueProviderType.GetInterface(typeof(IValueProvider).Name) == null)
throw new ArgumentException("Type " + valueProviderType + " must implement interface IValueProvider.", "valueProviderType");
_ValueProviderType = valueProviderType;
}
/// <summary>
/// Before binding the model, set the IValueProvider it uses
/// </summary>
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
bindingContext.ValueProvider = GetValueProvider();
return base.BindModel(controllerContext, bindingContext);
}
private IValueProvider GetValueProvider()
{
return (IValueProvider)Activator.CreateInstance(_ValueProviderType);
}
}
Then we create a model binding attribute that will inject the value provider type in the custom model binder created above, and use that as the model binder:
/// <summary>
/// On the default model binder, replaces the current value provider with the specified value provider. Cannot use custom model binder with this.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public class SetValueProviderAttribute : CustomModelBinderAttribute
{
// Originally, this was an action filter, that OnActionExecuting, set the controller's IValueProvider, expecting it to be picked up by the default model binder
// when binding the model. Unfortunately, OnActionExecuting occurs AFTER the IValueProvider is set on the DefaultModelBinder. The only way around this is
// to create a custom model binder that inherits from DefaultModelBinder, and in its BindModel method set the ValueProvider and then do the standard model binding.
public SetValueProviderAttribute(Type valueProviderType)
{
if (valueProviderType.GetInterface(typeof(IValueProvider).Name) == null)
throw new ArgumentException("Type " + valueProviderType + " must implement interface IValueProvider.", "valueProviderType");
_ValueProviderType = valueProviderType;
}
private Type _ValueProviderType;
public override IModelBinder GetBinder()
{
var modelBinder = new SetValueProviderDefaultModelBinder(_ValueProviderType);
return modelBinder;
}
}

Related

How to pass interface parameter to RouteValueDictionary()?

In an ASP.NET MVC app, I'm passing an interface instance as a parameter. In the code snippet below, myinterface is the interface instance.
return RedirectToAction( "Main", new RouteValueDictionary(
new { controller = controllerName, action = "Main", Id = Id, someInterface = myinterface } ) );
At the recipient side, the action looks like:
public ActionResult Index(Int Id, ISomeInterface someInterface) {...}
I get the following runtime exception:
Cannot create an instance of an interface
Is there some way to do this?
I dont know what your reasons are. I am assuming they are valid. MVC is not going to provide implementation for your interface. You will have to override the default model binding behavior like below and provide the concrete type (it can come from your IOC container):
public class MyBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext
, ModelBindingContext bindingContext, Type modelType)
{
if (bindingContext.ModelType.Name == "ISomeInterface")
return new SomeType();
//You can get the concrete implementation from your IOC container
return base.CreateModel(controllerContext, bindingContext, modelType);
}
}
public interface ISomeInterface
{
string Val { get; set; }
}
public class SomeType : ISomeInterface
{
public string Val { get; set; }
}
Then in your Application Start will look something like below:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
ModelBinders.Binders.DefaultBinder = new MyBinder();
//Followed by other stuff
}
}
Here's working actions
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
var routeValueDictionary = new RouteValueDictionary()
{
{"id",1},
{"Val","test"}
};
return RedirectToAction("Abouts", "Home", routeValueDictionary);
}
public ActionResult Abouts(int id, ISomeInterface testInterface)
{
ViewBag.Message = "Your application description page.";
return View();
}

Making model binding work for a model with no default constructor

I’ve been trying to figure a way to have a model-binding go on with a model with a constructor with arguments.
the action:
[HttpPost]
public ActionResult Create(Company company, HttpPostedFileBase logo)
{
company.LogoFileName = SaveCompanyLogoImage(logo);
var newCompany = _companyProvider.Create(company);
return View("Index",newCompany);
}
and the model
public Company(CustomProfile customProfile)
{
DateCreated = DateTime.Now;
CustomProfile = customProfile;
}
I've done my research and seems I need to mess around with my ninjectControllerfactory:
public class NinjectControllerFactory : DefaultControllerFactory
{
private readonly IKernel ninjectKernel;
public NinjectControllerFactory()
{
ninjectKernel = new StandardKernel();
AddBindings();
}
protected override IController GetControllerInstance(RequestContext requestContext,
Type controllerType)
{
return controllerType == null
? null
: (IController) ninjectKernel.Get(controllerType);
}
private void AddBindings()
{
ninjectKernel.Bind<IAuthProvider>().To<FormsAuthProvider>();
ninjectKernel.Bind<IMembershipProvider>().To<MembershipProvider>();
ninjectKernel.Bind<ICustomProfileProvider>().To<CustomProfileProvider>();
ninjectKernel.Bind<ICompanyProvider>().To<CompanyProvider>();
}
}
I also feel I need to modify my model binder but I'm not clear on the way forward:
public class CustomProfileModelBinder : IModelBinder
{
private const string sessionKey = "CustomProfile";
#region IModelBinder Members
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
// get the Cart from the session
var customProfile = (CustomProfile) controllerContext.HttpContext.Session[sessionKey];
// create the Cart if there wasn't one in the session data
if (customProfile == null)
{
customProfile = new CustomProfile("default name");
controllerContext.HttpContext.Session[sessionKey] = customProfile;
}
// return the cart
return customProfile;
}
#endregion
}
Hope this explains my issue, I'm sorry if its a rather long winded question!
Thanks for any assistance
In this case it seems that the parameter you need to create (CustomProfile) must be taken from the session. You could then use a specific model binder for the Company model that derives from the default model binder, changing only the way it creates an instance of the Company class (it will then populate the properties in the same way as the default one):
public class CompanyModelBinder: DefaultModelBinder
{
private const string sessionKey = "CustomProfile";
protected override object CreateModel(ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type modelType)
{
if(modelType == typeOf(Company))
{
var customProfile = (CustomProfile) controllerContext.HttpContext.Session[sessionKey];
// create the Cart if there wasn't one in the session data
if (customProfile == null)
{
customProfile = new CustomProfile("default name");
controllerContext.HttpContext.Session[sessionKey] = customProfile;
}
return new Company(customProfile);
}
else
{
//just in case this gets registered for any other type
return base.CreateModel(controllerContext, bindingContext, modelType)
}
}
}
You will register this binder only for the Company type by adding this to the global.asax Application_Start method:
ModelBinders.Binders.Add(typeOf(Company), CompanyModelBinder);
Another option could be to create a dependency-aware model binder using the Ninject dependencies by inheriting from the DefaultModelBinder (As you are using Ninject, it knows how to build instances of concrete types without the need of registering them).
However you would need to configure a custom method that builds the CustomProfile in Ninject, which I believe you could do using the ToMethod().
For this you would extract you would extract your configuration of your Ninject kernel outside the controller factory:
public static class NinjectBootStrapper{
public static IKernel GetKernel()
{
IKernel ninjectKernel = new StandardKernel();
AddBindings(ninjectKernel);
}
private void AddBindings(IKernel ninjectKernel)
{
ninjectKernel.Bind<IAuthProvider>().To<FormsAuthProvider>();
ninjectKernel.Bind<IMembershipProvider>().To<MembershipProvider>();
ninjectKernel.Bind<ICustomProfileProvider>().To<CustomProfileProvider>();
ninjectKernel.Bind<ICompanyProvider>().To<CompanyProvider>();
ninjectKernel.Bind<CustomProfile>().ToMethod(context => /*try to get here the current session and the custom profile, or build a new instance */ );
}
}
public class NinjectControllerFactory : DefaultControllerFactory
{
private readonly IKernel ninjectKernel;
public NinjectControllerFactory(IKernel kernel)
{
ninjectKernel = kernel;
}
protected override IController GetControllerInstance(RequestContext requestContext,
Type controllerType)
{
return controllerType == null
? null
: (IController) ninjectKernel.Get(controllerType);
}
}
In that case you would create this model binder:
public class NinjectModelBinder: DefaultModelBinder
{
private readonly IKernel ninjectKernel;
public NinjectModelBinder(IKernel kernel)
{
ninjectKernel = kernel;
}
protected override object CreateModel(ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type modelType)
{
return ninjectKernel.Get(modelType) ?? base.CreateModel(controllerContext, bindingContext, modelType)
}
}
And you would update the global.asax as:
IKernel kernel = NinjectBootStrapper.GetKernel();
ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory(kernel));
ModelBinders.Binders.DefaultBinder = new NinjectModelBinder(kernel);

How do I pass value to MVC3 master page ( _layout)?

I have a custom modelbinder, its check the authentication cookie and return the value.
public class UserDataModelBinder<T> : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.RequestContext.HttpContext.Request.IsAuthenticated)
{
var cookie =
controllerContext.RequestContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
if (cookie == null)
return null;
var decrypted = FormsAuthentication.Decrypt(cookie.Value);
if (!string.IsNullOrWhiteSpace(decrypted.UserData))
return JsonSerializer.DeserializeFromString<T>(decrypted.UserData);
}
return null;
}
}
if I need to use it, I just need to pass it to the action. everything works.
public ActionResult Index(UserData userData)
{
AccountLoginWidgetVM model = new AccountLoginWidgetVM();
if (null != userData)
model.UserData = userData;
return View(userData);
}
However, I want to use it in my master page, because once user login, i want to display their info on the top on every page. I tried a few things, coudln't get it work
#Html.RenderPartial("LoginPartial", ???model here??)
We did it as follows:
Defined separate viewmodel for masterpages.
public class MasterPageViewModel
{
public Guid CurrentUserId { get; set; }
public string CurrentUserFullName { get; set; }
}
Added injection filter and filter provider.
public class MasterPageViewModelInjectorFilterProvider: IFilterProvider
{
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
return new [] {new Filter(new MasterPageViewModelInjectorFilter(), FilterScope.Action, null), };
}
private class MasterPageViewModelInjectorFilter: IResultFilter
{
public void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult == null)
return;
if (viewResult.ViewBag.MasterPageViewModel != null)
return;
//setup model whichever way you want
var viewModel = new MasterPageViewModel();
//inject model into ViewBag
viewResult.ViewBag.MasterPageViewModel = viewModel;
}
public void OnResultExecuted(ResultExecutedContext filterContext)
{
}
}
}
Configure filter provider:
//in Application_Start
FilterProviders.Providers.Add(new MasterPageViewModelInjectorFilterProvider());
Use in master:
ViewBag.MasterPageViewModel
This way you have fine uncoupled architecture. Of course you can combine it with Dependency Injection (we do, but I left it out for clarity) and configure your action filter for every action whichever way you want.
In this case you can use ViewBag.
public ActionResult Index(UserData userData)
{
AccountLoginWidgetVM model = new AccountLoginWidgetVM();
if (null != userData)
model.UserData = userData;
ViewBag.UserData = userData;
return View(userData);
}
#Html.RenderPartial("LoginPartial", ViewBag.UserData)
You have to make sure that userData is not null. If it'll be null the passed model will be default model of the view.

Serialize IList property on model when passed into Html.ActionLink

I'm trying to generate an Html.ActionLink with the following viewmodel:
public class SearchModel
{
public string KeyWords {get;set;}
public IList<string> Categories {get;set;}
}
To generate my link I use the following call:
#Html.ActionLink("Index", "Search", Model)
Where Model is an instance of the SearchModel
The link generated is something like this:
http://www.test.com/search/index?keywords=bla&categories=System.Collections.Generic.List
Because it obviously is only calling the ToString method on every property.
What I would like to see generate is this:
http://www.test.com/search/index?keywords=bla&categories=Cat1&categories=Cat2
Is there any way I can achieve this by using Html.ActionLink
In MVC 3 you're just out of luck because the route values are stored in a RouteValueDictionary that as the name implies uses a Dictionary internally which makes it not possible to have multiple values associated to a single key. The route values should probably be stored in a NameValueCollection to support the same behavior as the query string.
However, if you can impose some constraints on the categories names and you're able to support a query string in the format:
http://www.test.com/search/index?keywords=bla&categories=Cat1|Cat2
then you could theoretically plug it into Html.ActionLink since MVC uses TypeDescriptor which in turn is extensible at runtime. The following code is presented to demonstrate it's possible, but I would not recommend it to be used, at least without further refactoring.
Having said that, you would need to start by associating a custom type description provider:
[TypeDescriptionProvider(typeof(SearchModelTypeDescriptionProvider))]
public class SearchModel
{
public string KeyWords { get; set; }
public IList<string> Categories { get; set; }
}
The implementation for the provider and the custom descriptor that overrides the property descriptor for the Categories property:
class SearchModelTypeDescriptionProvider : TypeDescriptionProvider
{
public override ICustomTypeDescriptor GetTypeDescriptor(
Type objectType, object instance)
{
var searchModel = instance as SearchModel;
if (searchModel != null)
{
var properties = new List<PropertyDescriptor>();
properties.Add(TypeDescriptor.CreateProperty(
objectType, "KeyWords", typeof(string)));
properties.Add(new ListPropertyDescriptor("Categories"));
return new SearchModelTypeDescriptor(properties.ToArray());
}
return base.GetTypeDescriptor(objectType, instance);
}
}
class SearchModelTypeDescriptor : CustomTypeDescriptor
{
public SearchModelTypeDescriptor(PropertyDescriptor[] properties)
{
this.Properties = properties;
}
public PropertyDescriptor[] Properties { get; set; }
public override PropertyDescriptorCollection GetProperties()
{
return new PropertyDescriptorCollection(this.Properties);
}
}
Then we would need the custom property descriptor to be able to return a custom value in GetValue which is called internally by MVC:
class ListPropertyDescriptor : PropertyDescriptor
{
public ListPropertyDescriptor(string name)
: base(name, new Attribute[] { }) { }
public override bool CanResetValue(object component)
{
return false;
}
public override Type ComponentType
{
get { throw new NotImplementedException(); }
}
public override object GetValue(object component)
{
var property = component.GetType().GetProperty(this.Name);
var list = (IList<string>)property.GetValue(component, null);
return string.Join("|", list);
}
public override bool IsReadOnly { get { return false; } }
public override Type PropertyType
{
get { throw new NotImplementedException(); }
}
public override void ResetValue(object component) { }
public override void SetValue(object component, object value) { }
public override bool ShouldSerializeValue(object component)
{
throw new NotImplementedException();
}
}
And finally to prove that it works a sample application that mimics the MVC route values creation:
static void Main(string[] args)
{
var model = new SearchModel { KeyWords = "overengineering" };
model.Categories = new List<string> { "1", "2", "3" };
var properties = TypeDescriptor.GetProperties(model);
var dictionary = new Dictionary<string, object>();
foreach (PropertyDescriptor p in properties)
{
dictionary.Add(p.Name, p.GetValue(model));
}
// Prints: KeyWords, Categories
Console.WriteLine(string.Join(", ", dictionary.Keys));
// Prints: overengineering, 1|2|3
Console.WriteLine(string.Join(", ", dictionary.Values));
}
Damn, this is probably the longest answer I ever give here at SO.
with linq of course...
string.Join("", Model.Categories.Select(c=>"&categories="+c))

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)

Resources