Использовать пользовательский ASP.NET MVC IValueProvider, не устанавливая его глобально? - PullRequest
12 голосов
/ 29 сентября 2011

Я хочу иметь возможность получать ключи / значения из файла 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?

Ответы [ 3 ]

10 голосов
/ 07 октября 2011

В этом случае вы все равно должны использовать ValueProviderFactory.

Метод, который вы должны реализовать на ValueProviderFactory, имеет следующую подпись:

IValueProvider GetValueProvider(ControllerContext controllerContext)

В рамках реализации этого метода вы можете проверить контекст контроллера, и, если входящий запрос относится к контроллеру / действию, для которого вы хотите использовать куки, верните немного CustomCookieValueProvider.

Если вы не хотите использовать файлы cookie для запроса, просто верните null, и среда отфильтрует его из списка поставщиков значений.

В качестве бонуса вы, возможно, не захотите жестко кодировать логику того, когда использовать CustomCookieValueProvider в ValueProviderFactory. Возможно, вы могли бы использовать DataTokens, чтобы определить, когда использовать куки с заданными маршрутами. Поэтому добавьте маршрут, подобный этому:

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 голосов
/ 03 марта 2013

Вот альтернатива, которая позволяет вам указывать 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);
    }
3 голосов
/ 28 декабря 2011

Разобрался, как это сделать.Во-первых, создайте пользовательский механизм связывания модели, который принимает тип поставщика значений в конструкторе, но наследует от связующего элемента модели по умолчанию.Это позволяет использовать стандартную привязку модели с настраиваемым поставщиком значений:

/// <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'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;
    }
}
...