Использовать собственный ASP.NET MVC IValueProvider, не устанавливая его глобально?
Я хочу иметь возможность захватывать ключи/значения из файла cookie и использовать это для привязки модели.
Вместо того, чтобы создавать пользовательский ModelBinder, я считаю, что DefaultModelBinder отлично работает из коробки, и лучший способ выбрать, откуда взялись значения, - это установить IValueProvider, который он использует.
Для этого я не хочу создавать собственный ValueProviderFactory и связывать его глобально, потому что я хочу, чтобы этот ValueProvider использовался в определенном методе действий.
Я создал атрибут, который делает это:
/// <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);
}
}
К сожалению, ModelBinder и его IValueProvider устанавливаются перед OnActionExecuting (почему?????). Кто-нибудь еще выяснил способ ввода пользовательского IValueProvider в DefaultModelBinder без использования ValueProviderFactory?
Ответы
Ответ 1
Выяснилось, как это сделать. Во-первых, создайте настраиваемое связующее устройство, которое принимает тип поставщика значений в конструкторе, но наследует от шаблона по умолчанию. Это позволяет использовать стандартную привязку модели к пользовательскому поставщику значений:
/// <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);
}
}
Затем мы создаем атрибут привязки модели, который будет вставлять тип поставщика значений в созданное выше настраиваемое связующее устройство, и использовать его в качестве связующего для модели:
/// <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 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;
}
}
Ответ 2
В этом случае вы должны использовать ValueProviderFactory
.
Метод, который вы должны реализовать на ValueProviderFactory
, имеет следующую подпись:
IValueProvider GetValueProvider(ControllerContext controllerContext)
В рамках реализации этого метода вы можете проверить контекст контроллера, и если входящий запрос предназначен для контроллера/действия, для которого вы хотите использовать файлы cookie, верните несколько CustomCookieValueProvider
.
Если вы не хотите использовать файлы cookie для запроса, просто верните null
, и инфраструктура будет фильтровать это из списка поставщиков Value.
В качестве бонуса вы можете не захотеть жестко закодировать логику использования CustomCookieValueProvider
в ValueProviderFactory
. Вы могли бы, возможно, использовать DataTokens
для соответствия, когда использовать файлы cookie с заданными маршрутами. Поэтому добавьте такой маршрут:
routes.MapRoute("SomeRoute","{controller}/{action}").DataTokens.Add("UseCookies", true);
Обратите внимание на вызов DataTokens.Add()
там, теперь внутри вас GetValueProvider
вы можете сделать что-то вроде этого:
if (controllerContext.RouteData.DataTokens.ContainsKey("UseCookies"))
{
return new CustomCookieValueProvider(controllerContext.RequestContext.HttpContext.Request.Cookies);
}
return null;
Ответ 3
Вот альтернатива, которая позволяет вам указывать IValueProviders как атрибуты по параметрам действий.
Это приводит к тому, что IValueProviders являются переходными, а не глобальными.
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);
}
}
}
Это в основном код формы ModelBinderAttribute, но с несколькими настройками.
Он не запечатан и поэтому вы можете изменить способ создания IValueProviders, если это необходимо.
Вот простой пример, который выглядит в другом поле, возможно, в скрытом или зашифрованном поле, и берет данные и помещает их в другое свойство.
Вот модель, которая не знает IValueProvider, но знает о скрытом поле.
public class SomeModel
{
[Required]
public string MyString { get; set; }
[Required]
public string MyOtherString { get; set; }
[Required]
public string Data { get; set; }
}
У нас есть IValueProvider, в этом случае мой провайдер явно знает о моей модели, но этого не должно быть.
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;
}
}
а затем действие, которое связывает все это вместе:
[HttpGet]
public ActionResult Test()
{
return View(new SomeModel());
}
[HttpPost]
public ActionResult Test([ValueProvider(typeof(MyValueProvider))]SomeModel model)
{
return View(model);
}