【问题标题】:How to bind view model property with different name如何绑定具有不同名称的视图模型属性
【发布时间】:2014-01-19 02:54:16
【问题描述】:

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

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

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

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

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

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

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- 我想在渲染视图输出中看到的是

<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- 没有设计问题的框架:

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

Link of quote

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

Binding model property with different name

One step upgrade of first link

Here some informative guide

结果:但他们都没有给我我的问题确切的解决方案。我正在为这个问题寻找一个强类型的解决方案。当然,如果您知道其他方法,请分享。


更新

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

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

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

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

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

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

【问题讨论】:

  • 实现这一目标的最简单方法是在视图模型中使用Fp 作为属性名称,而不是FilterParameter。毕竟,这就是视图模型的用途:表达视图要求。无法修改标准助手。如果您编写自定义属性并装饰您的视图模型属性,则必须编写理解它们的自定义模型绑定器,并且您还必须编写自定义 HTML 助手。
  • 由于某种原因,更改属性名称的工作量太大 :) 是的 viewmodel 是因为这个原因,但这不是我的问题的缺点,是吗?
  • 如果您使用的是视图模型,则不应出现这种情况,因为视图模型正是为此目的而设计的。相信我,如果更改视图名称属性的工作量太大,那么在这里实现您所要求的就像重写整个 ASP.NET MVC 框架 :-) 所以由您决定您喜欢哪种方式。是的,我知道,工作量太大了,因为您没有使用视图模型,而是简单地将这个域业务模型传递给您的视图。坏,坏,坏。下次你就知道了。
  • 我检查了 mvc 输入扩展代码,我知道我无法更改它们,但你说我也无法为此做任何事情。
  • 嗯,你可能想要,但要实现它并不容易。此外,如果您使用的是视图模型,只需重命名您的视图模型属性即可获得所需的结果。

标签: asp.net-mvc razor data-annotations model-binding asp.net-mvc-5


【解决方案1】:

简短的答案是“否”,而长的答案仍然是“否”。没有内置的帮助器、属性、模型绑定器,不管它是什么(没有开箱即用)。

但是我在回答之前所做的(我删除了它)是我昨天意识到的一个糟糕的解决方案。我要把它放在github上给那些还想看的人(也许它解决了某人的问题)(我也不建议这样做!)

现在我再次搜索它,但找不到任何有用的东西。如果您使用 AutoMapper 或 ValueInjecter 之类的工具将 ViewModel 对象映射到 Business 对象,并且还想混淆 View Model 参数,那么您可能遇到了一些麻烦。当然你可以做到,但是强类型的 html 助手不会帮助你很多。我什至没有谈论其他开发人员是否采用分支并处理通用视图模型。

幸运的是,我的项目(4 个人参与其中,并用于商业用途)目前还没有那么大,所以我决定更改 View 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

【讨论】:

  • 这看起来很有趣。你还有原始来源吗?看起来您的仓库不再存在。我意识到您已经在上面发布了一些源代码,但是作为 MVC 的新手,我希望看到它全部到位并正常工作。 IOW 一个完整的工作样本。我正在尝试解决this problem,看起来我可以这样做。你同意吗?
【解决方案2】:

其实是有办法的。

TypeDescriptor 收集的 ASP.NET 绑定元数据中,而不是直接通过反射。更珍贵的是,使用了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; }
}

如果您已经拥有具有所需名称的属性,则可以重复使用它。上面定义的属性只是一个例子。我更喜欢使用JsonPropertyAttribute,因为在大多数情况下,我使用 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.Web.Mvc.dll 中出现“System.ArgumentException”类型的异常,但未在用户代码中处理

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

那是因为所有助手迟早都会调用ModelMetadata.FromLambdaExpression 方法。这种方法采用我们提供的表达式 (x =&gt; 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 =&gt; x.TestProperty(其中xTestModel),此方法将返回TestProperty,而不是actual_name,但模型元数据包含actual_name 属性,没有TestProperty。这就是引发the property could not be found 错误的原因。

这是一个设计失败

不过,尽管有一些不便,但仍有一些解决方法,例如:

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

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

    这不好。根本没有智能感知,随着我们的模型改变,我们不会有任何编译错误。在任何更改中,任何东西都可能被破坏,并且没有简单的方法可以检测到。

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

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

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

    编译时间和智能感知支持,无需花费大量时间来获取完整的帮助程序。利润!

此外,上述所有内容都可以作为模型绑定的魅力。在模型绑定过程中,默认绑定器还使用由TypeDescriptor 收集的元数据。

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

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

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

将被翻译成以下字典:

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

稍后这些值以及来自查询字符串、路由数据等的其他值,由IValueProvider 的不同实现收集,默认情况下将使用绑定器在元数据的帮助下绑定模型,由TypeDescriptor 收集.

所以我们从创建模型、渲染、绑定回来并使用它开始了一个完整的循环。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-26
    • 1970-01-01
    • 1970-01-01
    • 2012-10-15
    • 2016-08-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多