其实是有办法的。
在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 => 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 是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!" *@
这不好。根本没有智能感知,随着我们的模型改变,我们不会有任何编译错误。在任何更改中,任何东西都可能被破坏,并且没有简单的方法可以检测到。
另一种方式稍微复杂一些 - 我们可以创建我们自己版本的帮助程序,并禁止任何人调用默认帮助程序或 ModelMetadata.FromLambdaExpression 用于具有重命名属性的模型类。
-
最后,最好将前两者结合起来:编写自己的类比来获取具有重定义支持的属性名称,然后将其传递给默认帮助程序。像这样的:
@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 收集.
所以我们从创建模型、渲染、绑定回来并使用它开始了一个完整的循环。