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);
}