如何绑定具有不同名称的视图模型属性 [英] How to bind view model property with different name

查看:43
本文介绍了如何绑定具有不同名称的视图模型属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有没有办法将视图模型属性作为在 html 端具有不同名称和 id 值的元素进行反射.

这是我想要实现的主要问题.所以这个问题的基本介绍是这样的:

1- 我有一个视图模型(作为示例),它为视图端的过滤器操作创建.

公共类FilterViewModel{公共字符串过滤器参数 { 获取;放;}}

2- 我有一个控制器动作,它是为获取表单值而创建的(这里是过滤器)

public ActionResult Index(FilterViewModel filter){返回视图();}

3- 我认为用户可以过滤一些数据并通过表单提交通过查询字符串发送参数.

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

4- 我想在渲染视图输出中看到的是

<label for="fp">FilterParameter</label><input id="fp" name="fp" type="text"/><input type="submit" value="Do Filter"/></表单>

5- 作为一种解决方案,我想像这样修改我的视图模型:

公共类FilterViewModel{[绑定参数(fp")][BindParameter("filter")]//这一个额外的别名[BindParameter("param")]//这个额外的别名公共字符串过滤器参数 { 获取;放;}}

所以基本问题是关于 BindAttribute 的,而是复杂类型属性的使用.但如果有一种内置的方式来做到这一点会更好.内置优点:

1- 与 TextBoxFor、EditorFor、LabelFor 和其他强类型视图模型助手一起使用可以更好地相互理解和交流.

2- Url 路由支持

3- 设计问题没有框架:

<块引用>

一般来说,我们建议人们不要编写自定义模型绑定器因为它们很难做对,而且很少被需要.这我在这篇文章中讨论的问题可能是其中一种情况这是有保证的.

引用链接

而且经过一些研究,我发现了这些有用的作品:

绑定模型属性不同的名字

一个第一个链接的步骤升级

这里有一些信息指南

结果:但他们都没有给我我的问题的确切解决方案.我正在寻找针对此问题的强类型解决方案.当然,如果你知道任何其他方式去,请分享.

<小时>

更新

我想这样做的根本原因基本上是:

1- 每次我想更改 html 控件名称时,我都必须在编译时更改 PropertyName.(在代码中更改字符串与更改属性名称是有区别的)

2- 我想对最终用户隐藏(伪装)不动产名称.大多数情况下,视图模型属性名称与映射的实体对象属性名称相同.(出于开发者可读性的考虑)

3- 我不想删除开发人员的可读性.想想很多像 2-3 个字符长且带有 mo 含义的属性.

4- 编写了很多视图模型.因此,更改他们的名字将比这个解决方案花费更多的时间.

5- 这将是比其他问题中描述的其他问题更好的解决方案(在我的 POV 中).

解决方案

其实是有办法的.

在由 TypeDescriptor,不是直接通过反射.更珍贵的是,AssociatedMetadataTypeTypeDescriptionProvider 被使用,它反过来简单地调用 TypeDescriptor.GetProvider 并将我们的模型类型作为参数:

public AssociatedMetadataTypeTypeDescriptionProvider(类型类型):基础(TypeDescriptor.GetProvider(类型)){}

所以,我们需要做的就是设置我们的自定义 TypeDescriptionProvider 用于我们的模型.

让我们实现我们的自定义提供程序.首先,让我们为自定义属性名称定义属性:

[AttributeUsage(AttributeTargets.Property)]公共类 CustomBindingNameAttribute : 属性{公共自定义绑定名称属性(字符串属性名称){this.PropertyName = propertyName;}公共字符串 PropertyName { 获取;私人订制;}}

如果您已经拥有所需名称的属性,则可以重用它.上面定义的属性只是一个例子.我更喜欢使用 JsonPropertyAttribute 因为在大多数情况下,我使用 json 和 Newtonsoft 的库,并且只想定义一次自定义名称.

下一步是定义自定义类型描述符.我们不会实现整个类型描述符逻辑并使用默认实现.只有属性访问将被覆盖:

公共类 MyTypeDescription : CustomTypeDescriptor{公共 MyTypeDescription(ICustomTypeDescriptor 父级):基地(父母){}公共覆盖 PropertyDescriptorCollection GetProperties(){return Wrap(base.GetProperties());}公共覆盖 PropertyDescriptorCollection GetProperties(Attribute[] 属性){return Wrap(base.GetProperties(attributes));}私有静态 PropertyDescriptorCollection Wrap(PropertyDescriptorCollection src){varwrapped = src.Cast().Select(pd => (PropertyDescriptor)new MyPropertyDescriptor(pd)).ToArray();返回新的 PropertyDescriptorCollection(wrapped);}}

还需要实现自定义属性描述符.同样,除了属性名称之外的所有内容都将由默认描述符处理.注意,NameHashCode 出于某种原因是一个单独的属性.由于名称已更改,因此它的哈希码也需要更改:

公共类 MyPropertyDescriptor : PropertyDescriptor{私有只读 PropertyDescriptor _descr;私有只读字符串_name;公共 MyPropertyDescriptor(PropertyDescriptor descr):基础(描述){this._descr = descr;var customBindingName = this._descr.Attributes[typeof(CustomBindingNameAttribute)] as CustomBindingNameAttribute;this._name = customBindingName != null ?customBindingName.PropertyName : this._descr.Name;}公共覆盖字符串名称{得到 { 返回 this._name;}}protected override int NameHashCode{得到 { 返回 this.Name.GetHashCode();}}public override bool CanResetValue(对象组件){返回 this._descr.CanResetValue(component);}公共覆盖对象 GetValue(对象组件){返回 this._descr.GetValue(component);}公共覆盖无效重置值(对象组件){this._descr.ResetValue(组件);}公共覆盖无效 SetValue(对象组件,对象值){this._descr.SetValue(component, value);}public override bool ShouldSerializeValue(对象组件){返回 this._descr.ShouldSerializeValue(component);}公共覆盖类型 ComponentType{得到 { 返回 this._descr.ComponentType;}}公共覆盖 bool IsReadOnly{得到 { 返回 this._descr.IsReadOnly;}}公共覆盖类型 PropertyType{得到 { 返回 this._descr.PropertyType;}}}

最后,我们需要我们的自定义 TypeDescriptionProvider 以及将其绑定到我们的模型类型的方法.默认情况下,TypeDescriptionProviderAttribute 旨在执行该绑定.但是在这种情况下,我们将无法获得想要在内部使用的默认提供程序.在大多数情况下,默认提供程序将是 ReflectTypeDescriptionProvider.但这不能保证,并且由于它的保护级别,此提供程序不可访问 - 它是 internal.但我们仍然希望回退到默认提供程序.

TypeDescriptor 还允许通过 AddProvider 方法.那就是我们将使用的.但首先,让我们定义我们的自定义提供程序本身:

公共类 MyTypeDescriptionProvider : TypeDescriptionProvider{私有只读 TypeDescriptionProvider _defaultProvider;公共 MyTypeDescriptionProvider(TypeDescriptionProvider defaultProvider){this._defaultProvider = defaultProvider;}公共覆盖 ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance){返回新的 MyTypeDescription(this._defaultProvider.GetTypeDescriptor(objectType, instance));}}

最后一步是将我们的提供者绑定到我们的模型类型.我们可以以任何我们想要的方式实现它.比如我们定义一些简单的类,比如:

公共静态类 TypeDescriptorsConfig{public static void InitializeCustomTypeDescriptorProvider(){//假设,这段代码和所有模型都在一个程序集中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);}}}

或者通过网络激活调用该代码:

[程序集:PreApplicationStartMethod(typeof(TypeDescriptorsConfig), "InitializeCustomTypeDescriptorProvider")]

或者简单地在 Application_Start 方法中调用它:

公共类 MvcApplication : HttpApplication{protected void Application_Start(){TypeDescriptorsConfig.InitializeCustomTypeDescriptorProvider();//初始化代码的其余部分...}}

但这并不是故事的结局.:(

考虑以下模型:

公共类TestModel{[CustomBindingName("actual_name")][显示名称(耶!")]公共字符串 TestProperty { 获取;放;}}

如果我们尝试在 .cshtml 中编写类似的内容:

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

我们会得到ArgumentException:

<块引用>

System.Web.Mvc.dll 中出现类型为System.ArgumentException"的异常,但未在用户代码中处理

附加信息:找不到属性 Some.Namespace.TestModel.TestProperty.

那是因为所有帮助程序迟早都会调用 ModelMetadata.FromLambdaExpression 方法.这个方法采用我们提供的表达式 (x => x.TestProperty) 并直接从成员信息中获取成员名称,并且不知道我们的任何属性、元数据 (谁在乎,呵呵?):

内部静态 ModelMetadata FromLambdaExpression(/* ... */){//...case ExpressionType.MemberAccess:MemberExpression memberExpression = (MemberExpression) expression.Body;propertyName = memberExpression.Member 是 PropertyInfo 吗?memberExpression.Member.Name : (string) null;//想哭到这里-^^^^^^^^^^^^^^^^^^^^^^^^^^^//...}

对于 x =>x.TestProperty(其中xTestModel)这个方法将返回TestProperty,而不是actual_name,但模型元数据包含 actual_name 属性,没有 TestProperty.这就是为什么 找不到属性 错误抛出的原因.

这是一个设计失败.

然而,尽管有一点不便,但仍有几种解决方法,例如:

  1. 最简单的方法是通过重新定义的名称访问我们的成员:

    @model Some.Namespace.TestModel@Html.DisplayName("actual_name") @* 这将呈现Yay!"*@

    这不好.根本没有智能感知,随着我们的模型变化,我们将不会有任何编译错误.任何更改都可能被破坏,而且没有简单的方法可以检测到这一点.

  2. 另一种方法稍微复杂一些 - 我们可以创建我们自己版本的帮助器,并禁止任何人调用默认帮助器或 ModelMetadata.FromLambdaExpression 为具有重命名属性的模型类调用.p>

  3. 最后,最好结合前两者:编写自己的类似物以获取具有重定义支持的属性名称,然后将其传递给默认帮助程序.像这样:

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

    编译时间和智能感知支持,无需花费大量时间来完成一组助手.利润!

此外,上述所有内容都像模型绑定的魅力一样.在模型绑定过程中,默认绑定器也使用元数据,由 TypeDescriptor 收集.

但我想绑定 json 数据是最好的用例.您知道,许多 Web 软件和标准使用 lowercase_separated_by_underscores 命名约定.不幸的是,这不是 C# 的通常约定.具有以不同约定命名的成员的类看起来很丑陋,最终可能会遇到麻烦.尤其是当您拥有每次都抱怨命名违规的工具时.

ASP.NET MVC 默认模型绑定器不会像调用 newtonsoft 的 JsonConverter.DeserializeObject 方法那样将 json 绑定到模型.相反,json 解析为字典.例如:

<代码>{复杂的: {文字:blabla",值:12.34},数量:1}

将被翻译成以下词典:

{ "complex.text", "blabla" }{复杂值",12.34"}{数量",1"}

然后这些值以及来自查询字符串、路由数据等的其他值,由 IValueProvider,将被默认绑定器用于在元数据的帮助下绑定模型,收集通过 TypeDescriptor.

所以我们从创建模型、渲染、绑定和使用它整个循环了.

Is there a way to make a reflection for a view model property as an element with different name and id values on the html side.

That is the main question of what I want to achieve. So the basic introduction for the question is like:

1- I have a view model (as an example) which created for a filter operation in view side.

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

2- I have a controller action which is created for GETting form values(here it is filter)

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

3- I have a view that a user can filter on some data and sends parameters via querystring over form 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- And what I want to see in rendered view output is

<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- And as a solution I want to modify my view model like this:

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

So the basic question is about BindAttribute but the usage of complex type properties. But also if there is a built in way of doing this is much better. Built-in pros:

1- Usage with TextBoxFor, EditorFor, LabelFor and other strongly typed view model helpers can understand and communicate better with each other.

2- Url routing support

3- No framework by desing problems :

In general, we recommend folks don’t write custom model binders because they’re difficult to get right and they’re rarely needed. The issue I’m discussing in this post might be one of those cases where it’s warranted.

Link of quote

And also after some research I found these useful works:

Binding model property with different name

One step upgrade of first link

Here some informative guide

Result: But none of them give me my problems exact solution. I am looking for a strongly typed solution for this problem. Of course if you know any other way to go, please share.


Update

The underlying reasons why I want to do this are basically:

1- Everytime I want to change the html control name then I have to change PropertyName at compile time. (There is a difference Changing a property name between changing a string in code)

2- I want to hide (camouflage) real property names from end users. Most of times View Model property names same as mapped Entity Objects property names. (For developer readability reasons)

3- I don't want to remove the readability for developer. Think about lots of properties with like 2-3 character long and with mo meanings.

4- There are lots of view models written. So changing their names are going to take more time than this solution.

5- This is going to be better solution (in my POV) than others which are described in other questions until now.

解决方案

Actually, there is a way to do it.

In ASP.NET binding metadata gathered by TypeDescriptor, not by reflection directly. To be more precious, AssociatedMetadataTypeTypeDescriptionProvider is used, which, in turn, simply calls TypeDescriptor.GetProvider with our model type as parameter:

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

So, everything we need is to set our custom TypeDescriptionProvider for our model.

Let's implement our custom provider. First of all, let's define attribute for custom property name:

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

    public string PropertyName { get; private set; }
}

If you already have attribute with desired name, you can reuse it. Attribute defined above is just an example. I prefer to use JsonPropertyAttribute because in most cases I work with json and Newtonsoft's library and want to define custom name only once.

The next step is to define custom type descriptor. We will not implement whole type descriptor logic and use default implementation. Only property accessing will be overridden:

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

Also custom property descriptor need to be implemented. Again, everything except property name will be handled by default descriptor. Note, NameHashCode for some reason is a separate property. As name changed, so it's hash code need to be changed too:

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

Finally, we need our custom TypeDescriptionProvider and way to bind it to our model type. By default, TypeDescriptionProviderAttribute is designed to perform that binding. But in this case we will not able to get default provider that we want to use internally. In most cases, default provider will be ReflectTypeDescriptionProvider. But this is not guaranteed and this provider is inaccessible due to it's protection level - it's internal. But we do still want to fallback to default provider.

TypeDescriptor also allow to explicitly add provider for our type via AddProvider method. That what we will use. But firstly, let's define our custom provider itself:

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

The last step is to bind our provider to our model types. We can implement it in any way we want. For example, let's define some simple class, such as:

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

And either invoke that code via web activation:

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

Or simply call it in Application_Start method:

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

        // rest of init code ...
    }
}

But this is not the end of the story. :(

Consider following model:

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

If we try to write in .cshtml view something like:

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

We will get ArgumentException:

An exception of type 'System.ArgumentException' occurred in System.Web.Mvc.dll but was not handled in user code

Additional information: The property Some.Namespace.TestModel.TestProperty could not be found.

That because all helpers soon or later invoke ModelMetadata.FromLambdaExpression method. And this method take expression we provided (x => x.TestProperty) and takes member name directly from member info and have no clue about any of our attributes, metadata (who cares, huh?):

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 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    // ...
}

For x => x.TestProperty (where x is TestModel) this method will return TestProperty, not actual_name, but model metadata contains actual_name property, have no TestProperty. That is why the property could not be found error thrown.

This is a design failure.

However despite this little inconvenience there are several workarounds, such as:

  1. The easiest way is to access our members by theirs redefined names:

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

    This is not good. No intellisense at all and as our model change we will have no any compilation errors. On any change anything can be broken and there is no easy way to detect that.

  2. Another way is a bit more complex - we can create our own version of that helpers and forbid anybody from calling default helpers or ModelMetadata.FromLambdaExpression for model classes with renamed properties.

  3. Finally, combination of previous two would be preferred: write own analogue to get property name with redefinition support, then pass that into default helper. Something like this:

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

    Compilation-time and intellisense support and no need to spend a lot of time for complete set of helpers. Profit!

Also everything described above work like a charm for model binding. During model binding process default binder also use metadata, gathered by TypeDescriptor.

But I guess binding json data is the best use case. You know, lots of web software and standards use lowercase_separated_by_underscores naming convention. Unfortunately this is not usual convention for C#. Having classes with members named in different convention looks ugly and can end up in troubles. Especially when you have tools that whining every time about naming violation.

ASP.NET MVC default model binder does not bind json to model the same way as it happens when you call newtonsoft's JsonConverter.DeserializeObject method. Instead, json parsed into dictionary. For example:

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

will be translated into following dictionary:

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

And later these values along with others values from query string, route data and so on, collected by different implementations of IValueProvider, will be used by default binder to bind a model with help of metadata, gathered by TypeDescriptor.

So we came full circle from creating model, rendering, binding it back and use it.

这篇关于如何绑定具有不同名称的视图模型属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆