Подтвердить что ты не робот

Как связать свойство модели представления с другим именем

Есть ли способ сделать отражение для свойства модели представления как элемента с разными именами и идентификаторами на стороне html.

Это основной вопрос того, чего я хочу достичь. Итак, базовое введение для вопроса таково:

1- У меня есть модель представления (в качестве примера), созданная для операции фильтра со стороны вида.

public class FilterViewModel
{
    public string FilterParameter { get; set; }
}

2- У меня есть действие контроллера, которое создается для значений формы GETting (здесь это фильтр)

public ActionResult Index(FilterViewModel filter)
{
return View();
}

3- У меня есть представление, что пользователь может фильтровать некоторые данные и отправлять параметры через querystring через форму submit.

@using (Html.BeginForm("Index", "Demo", FormMethod.Get))
{    
    @Html.LabelFor(model => model.FilterParameter)
    @Html.EditorFor(model => model.FilterParameter)
    <input type="submit" value="Do Filter" />
}

4- И то, что я хочу видеть в визуализированном представлении представления,

<form action="/Demo" method="get">
    <label for="fp">FilterParameter</label>
    <input id="fp" name="fp" type="text" />
    <input type="submit" value="Do Filter" />
</form>

5- И в качестве решения я хочу изменить свою модель представления следующим образом:

public class FilterViewModel
{
    [BindParameter("fp")]
    [BindParameter("filter")] // this one extra alias
    [BindParameter("param")] //this one extra alias
    public string FilterParameter { get; set; }
}

Итак, основной вопрос касается BindAttribute, но использование свойств сложного типа. Но также, если есть встроенный способ сделать это намного лучше. Встроенные профи:

1- Использование с TextBoxFor, EditorFor, LabelFor и другими сильно типизированными помощниками модели просмотра может лучше понимать и обмениваться друг с другом.

2- Поддержка маршрутизации по URL-адресам

3 Без создания проблем:

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

Ссылка цитаты

А также после некоторых исследований я нашел эти полезные работы:

Свойство привязки модели с другим именем

Одноэтапное обновление первой ссылки

Вот несколько справочных материалов

Результат: Но ни один из них не дает мне своих проблем точное решение. Я ищу строго типизированное решение этой проблемы. Конечно, если вы знаете любой другой способ, пожалуйста, поделитесь.


Обновление

Основные причины, по которым я хочу сделать это, в основном:

1- Каждый раз, когда я хочу изменить имя элемента управления html, мне нужно изменить PropertyName во время компиляции. (Существует разница Изменение имени свойства между изменением строки в коде)

2- Я хочу скрыть (камуфляж) имена реальных объектов от конечных пользователей. В большинстве случаев имена свойств Model Model аналогичны именам объектов Entity Objects. (Для удобства чтения)

3- Я не хочу удалять читаемость для разработчика. Подумайте о множестве свойств с длиной в 2-3 символа и с смыслами mo.

4- Написано множество моделей взглядов. Поэтому изменение их имен займет больше времени, чем это решение.

5- Это будет лучшее решение (в моем POV), чем другие, которые до сих пор описаны в других вопросах.

4b9b3361

Ответ 1

Короткий ответ - НЕТ, а длинный ответ - НЕТ. Нет встроенного помощника, атрибута, привязки к модели, что бы это ни было (ничего из коробки).

Но то, что я делал перед ответом (я удалил его), было ужасным решением, которое я понял вчера. Я собираюсь поместить его в github для тех, кто все еще хочет видеть (может быть, он решает какую-то проблему) (я тоже не предлагаю!)

Теперь я снова искал его, и я не мог найти ничего полезного. Если вы используете что-то вроде AutoMapper или ValueInjecter, как инструмент для сопоставления объектов ViewModel с объектами Business, и если вы хотите также запутать параметры View Model, возможно, у вас есть некоторые проблемы. Конечно, вы можете это сделать, но сильно типизированные html-помощники не помогут вам. Я даже не говорю о том, что если другие разработчики берут филиал и работают над общими типами представлений.

К счастью, мой проект (4 человека, работающие над ним, и его коммерческое использование) не так уж велик, поэтому я решил изменить имена свойств Model Model! (Это еще много работы. Сотни моделей взглядов, чтобы обмануть их свойства!!!) Спасибо Asp.Net MVC!

В ссылках, которые я поставил под вопрос, есть некоторые способы. Но также, если вы все еще хотите использовать атрибут BindAlias, я могу предложить вам использовать следующие методы расширения. По крайней мере, вам не нужно писать одну и ту же строку псевдонимов, которую вы пишете в атрибуте BindAlias.

Вот он:

public static string AliasNameFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression)
{
    var memberExpression = ExpressionHelpers.GetMemberExpression(expression);
    if (memberExpression == null)
        throw new InvalidOperationException("Expression must be a member expression");
    var aliasAttr = memberExpression.Member.GetAttribute<BindAliasAttribute>();
    if (aliasAttr != null)
    {
        return MvcHtmlString.Create(aliasAttr.Alias).ToHtmlString();
    }
    return htmlHelper.NameFor(expression).ToHtmlString();
}

public static string AliasIdFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression)
{
    var memberExpression = ExpressionHelpers.GetMemberExpression(expression);
    if (memberExpression == null)
        throw new InvalidOperationException("Expression must be a member expression");
    var aliasAttr = memberExpression.Member.GetAttribute<BindAliasAttribute>();
    if (aliasAttr != null)
    {
        return MvcHtmlString.Create(TagBuilder.CreateSanitizedId(aliasAttr.Alias)).ToHtmlString();
    }
    return htmlHelper.IdFor(expression).ToHtmlString();
}



public static T GetAttribute<T>(this ICustomAttributeProvider provider)
    where T : Attribute
{
    var attributes = provider.GetCustomAttributes(typeof(T), true);
    return attributes.Length > 0 ? attributes[0] as T : null;
}

public static MemberExpression GetMemberExpression<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression)
{
    MemberExpression memberExpression;
    if (expression.Body is UnaryExpression)
    {
        var unaryExpression = (UnaryExpression)expression.Body;
        memberExpression = (MemberExpression)unaryExpression.Operand;
    }
    else
    {
        memberExpression = (MemberExpression)expression.Body;
    }
    return memberExpression;
}

Если вы хотите его использовать:

[ModelBinder(typeof(AliasModelBinder))]
public class FilterViewModel
{
    [BindAlias("someText")]
    public string FilterParameter { get; set; }
}

В html:

@* at least you dont write "someText" here again *@
@Html.Editor(Html.AliasNameFor(model => model.FilterParameter))
@Html.ValidationMessage(Html.AliasNameFor(model => model.FilterParameter))

Итак, я оставляю этот ответ вот так. Это даже не ответ (и ответа на MVC 5 нет), но поиск в google по той же проблеме может найти полезный этот опыт.

И вот репозиторий github: https://github.com/yusufuzun/so-view-model-bind-20869735

Ответ 2

Собственно, есть способ сделать это.

В метаданных привязки ASP.NET, собранных TypeDescriptor, а не отражением напрямую. Чтобы быть более ценным, используется AssociatedMetadataTypeTypeDescriptionProvider, который, в свою очередь, просто вызывает TypeDescriptor.GetProvider с нашим типом модели в качестве параметра:

public AssociatedMetadataTypeTypeDescriptionProvider(Type type)
  : base(TypeDescriptor.GetProvider(type))
{
}

Итак, все, что нам нужно, это установить нашу TypeDescriptionProvider для нашей модели.

Позвольте реализовать наш пользовательский поставщик. Прежде всего, пусть определить атрибут для имени настраиваемого свойства:

[AttributeUsage(AttributeTargets.Property)]
public class CustomBindingNameAttribute : Attribute
{
    public CustomBindingNameAttribute(string propertyName)
    {
        this.PropertyName = propertyName;
    }

    public string PropertyName { get; private set; }
}

Если у вас уже есть атрибут с желаемым именем, вы можете его повторно использовать. Атрибут, определенный выше, является просто примером. Я предпочитаю использовать JsonProeprtyAttribute, потому что в большинстве случаев я работаю с библиотекой json и Newtonsoft и хочу определить настраиваемое имя только один раз.

Следующим шагом будет определение дескриптора пользовательского типа. Мы не будем реализовывать логику дескриптора целочисленного типа и использовать реализацию по умолчанию. Только переопределение свойств будет отменено:

public class MyTypeDescription : CustomTypeDescriptor
{
    public MyTypeDescription(ICustomTypeDescriptor parent)
        : base(parent)
    {
    }

    public override PropertyDescriptorCollection GetProperties()
    {
        return Wrap(base.GetProperties());
    }

    public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        return Wrap(base.GetProperties(attributes));
    }

    private static PropertyDescriptorCollection Wrap(PropertyDescriptorCollection src)
    {
        var wrapped = src.Cast<PropertyDescriptor>()
                         .Select(pd => (PropertyDescriptor)new MyPropertyDescriptor(pd))
                         .ToArray();

        return new PropertyDescriptorCollection(wrapped);
    }
}

Также необходимо реализовать настраиваемый дескриптор свойств. Опять же, все, кроме имени свойства, будет обрабатываться по умолчанию дескриптором. Примечание. NameHashCode по какой-то причине является отдельным свойством. Поскольку имя изменено, необходимо также изменить его хеш-код:

public class MyPropertyDescriptor : PropertyDescriptor
{
    private readonly PropertyDescriptor _descr;
    private readonly string _name;

    public MyPropertyDescriptor(PropertyDescriptor descr)
        : base(descr)
    {
        this._descr = descr;

        var customBindingName = this._descr.Attributes[typeof(CustomBindingNameAttribute)] as CustomBindingNameAttribute;
        this._name = customBindingName != null ? customBindingName.PropertyName : this._descr.Name;
    }

    public override string Name
    {
        get { return this._name; }
    }

    protected override int NameHashCode
    {
        get { return this.Name.GetHashCode(); }
    }

    public override bool CanResetValue(object component)
    {
        return this._descr.CanResetValue(component);
    }

    public override object GetValue(object component)
    {
        return this._descr.GetValue(component);
    }

    public override void ResetValue(object component)
    {
        this._descr.ResetValue(component);
    }

    public override void SetValue(object component, object value)
    {
        this._descr.SetValue(component, value);
    }

    public override bool ShouldSerializeValue(object component)
    {
        return this._descr.ShouldSerializeValue(component);
    }

    public override Type ComponentType
    {
        get { return this._descr.ComponentType; }
    }

    public override bool IsReadOnly
    {
        get { return this._descr.IsReadOnly; }
    }

    public override Type PropertyType
    {
        get { return this._descr.PropertyType; }
    }
}

Наконец, нам нужен наш пользовательский TypeDescriptionProvider и способ привязать его к нашему типу модели. По умолчанию TypeDescriptionProviderAttribute предназначен для выполнения этой привязки. Но в этом случае мы не сможем получить поставщика по умолчанию, который мы хотим использовать внутри страны. В большинстве случаев поставщик по умолчанию будет ReflectTypeDescriptionProvider. Но это не гарантируется, и этот провайдер недоступен из-за его уровня защиты - он internal. Но мы все еще хотим отступить к поставщику по умолчанию.

TypeDescriptor также позволяют явно добавлять поставщика для нашего типа с помощью метода AddProvider. Это то, что мы будем использовать. Но, во-первых, давайте определим наш собственный поставщик:

public class MyTypeDescriptionProvider : TypeDescriptionProvider
{
    private readonly TypeDescriptionProvider _defaultProvider;

    public MyTypeDescriptionProvider(TypeDescriptionProvider defaultProvider)
    {
        this._defaultProvider = defaultProvider;
    }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
    {
        return new MyTypeDescription(this._defaultProvider.GetTypeDescriptor(objectType, instance));
    }
}

Последний шаг - привязать нашего провайдера к нашим типам моделей. Мы можем реализовать его любым способом. Например, пусть определит некоторый простой класс, например:

public static class TypeDescriptorsConfig
{
    public static void InitializeCustomTypeDescriptorProvider()
    {
        // Assume, this code and all models are in one assembly
        var types = Assembly.GetExecutingAssembly().GetTypes()
                            .Where(t => t.GetProperties().Any(p => p.IsDefined(typeof(CustomBindingNameAttribute))));

        foreach (var type in types)
        {
            var defaultProvider = TypeDescriptor.GetProvider(type);
            TypeDescriptor.AddProvider(new MyTypeDescriptionProvider(defaultProvider), type);
        }
    }
}

И либо вызовите этот код с помощью веб-активации:

[assembly: PreApplicationStartMethod(typeof(TypeDescriptorsConfig), "InitializeCustomTypeDescriptorProvider")]

Или просто вызовите его в методе Application_Start:

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        TypeDescriptorsConfig.InitializeCustomTypeDescriptorProvider();

        // rest of init code ...
    }
}

Но это еще не конец истории.: (

Рассмотрим следующую модель:

public class TestModel
{
    [CustomBindingName("actual_name")]
    [DisplayName("Yay!")]
    public string TestProperty { get; set; }
}

Если мы попытаемся записать в .cshtml представление что-то вроде:

@model Some.Namespace.TestModel
@Html.DisplayNameFor(x => x.TestProperty) @* fail *@

Мы получим ArgumentException:

Исключение типа "System.ArgumentException" произошло в System.Web.Mvc.dll, но не было обработано в коде пользователя

Дополнительная информация: Не удалось найти свойство Some.Namespace.TestModel.TestProperty.

Это потому, что все помощники скоро или поздно вызывают метод ModelMetadata.FromLambdaExpression. И этот метод принимает выражение, которое мы предоставили (x => x.TestProperty), и принимает имя члена непосредственно из информации о членах и не имеет понятия о каких-либо наших атрибутах, метаданных (кому это важно, да?):

internal static ModelMetadata FromLambdaExpression<TParameter, TValue>(/* ... */)
{
    // ...

        case ExpressionType.MemberAccess:
            MemberExpression memberExpression = (MemberExpression) expression.Body;
            propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : (string) null;
            //                                  I want to cry here - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    // ...
}

Для x => x.TestProperty (где x is TestModel) этот метод вернет TestProperty, а не actual_name, но метаданные модели содержит свойство actual_name, не имеют TestProperty. Вот почему ошибка the property could not be found.

Это ошибка проектирования.

Однако, несмотря на это небольшое неудобство, существует несколько обходных решений, таких как:

  • Самый простой способ - получить доступ к нашим членам по их переопределенным именам:

    @model Some.Namespace.TestModel
    @Html.DisplayName("actual_name") @* this will render "Yay!" *@
    

    Это не хорошо. Никакой intellisense вообще и как наше изменение модели у нас не будет никаких ошибок компиляции. При любом изменении ничего нельзя сломать, и нет простого способа обнаружить это.

  • Другой способ немного сложнее - мы можем создать собственную версию этих помощников и запретить кому-либо обращаться к помощникам по умолчанию или ModelMetadata.FromLambdaExpression для классов модели с переименованными свойствами.

  • Наконец, было бы предпочтительным сочетание предыдущих двух: написать собственный аналог, чтобы получить имя свойства с поддержкой переопределения, а затем передать это в помощник по умолчанию. Что-то вроде этого:

    @model Some.Namespace.TestModel
    @Html.DisplayName(Html.For(x => x.TestProperty)) 
    

    Поддержка времени компиляции и intellisense и не нужно тратить много времени на полный набор помощников. Прибыль!

Также все описанное выше работает как шарм для привязки модели. Во время процесса привязки модели по умолчанию связующее вещество также использует метаданные, собранные с помощью TypeDescriptor.

Но я думаю, что привязка json-данных является наилучшим вариантом использования. Вы знаете, многие веб-программы и стандарты используют соглашение об именовании lowercase_separated_by_underscores. К сожалению, это не обычное соглашение для С#. Наличие классов с членами, названными в разных конвенциях, выглядит уродливым и может закончиться неприятностями. Особенно, когда у вас есть инструменты, которые каждый раз ныли о нарушении именования.

Связывание модели ASP.NET MVC по умолчанию не привязывает json к модели так же, как это происходит при вызове метода newtonsoft JsonConverter.DeserializeObject. Вместо этого json разбирается в словаре. Например:

{
    complex: {
        text: "blabla",
        value: 12.34
    },
    num: 1
}

будет переведен в следующий словарь:

{ "complex.text", "blabla" }
{ "complex.value", "12.34" }
{ "num", "1" }

И позже эти значения вместе с другими значениями из строки запроса, данных маршрута и т.д., собранных различными реализациями IValueProvider, будут использовать связующее по умолчанию для привязки модели с помощью метаданных, собранных с помощью TypeDescriptor.

Итак, мы получили полный круг от создания модели, рендеринга, привязки ее и использования.