Actually, there is a way to do this.
The ASP.NET binding metadata compiled by TypeDescriptor , and not reflection directly. To be more valuable, an AssociatedMetadataTypeTypeDescriptionProvider used, which in turn simply calls TypeDescriptor.GetProvider with our model type as a parameter:
public AssociatedMetadataTypeTypeDescriptionProvider(Type type) : base(TypeDescriptor.GetProvider(type)) { }
So, all we need is to set our TypeDescriptionProvider to our model.
Let us implement our custom provider. First of all, let's define an attribute for the name of a custom property:
[AttributeUsage(AttributeTargets.Property)] public class CustomBindingNameAttribute : Attribute { public CustomBindingNameAttribute(string propertyName) { this.PropertyName = propertyName; } public string PropertyName { get; private set; } }
If you already have an attribute with the desired name, you can reuse it. The attribute defined above is just an example. I prefer to use JsonProeprtyAttribute , because in most cases I work with the json and Newtonsoft library and I want to define a custom name only once.
The next step is to define a custom type descriptor. We will not implement the integer descriptor logic and use the default implementation. Only property overrides will be undone:
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); } }
You must also implement a custom property descriptor. Again, everything except the property name will be handled by default with a descriptor. Note. NameHashCode for some reason a separate property. Since the name is changed, you must also change its hash code:
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 a way to bind it to our model type. By default, TypeDescriptionProviderAttribute designed to perform this binding. But in this case, we will not be able to get the default provider that we want to use domestically. In most cases, the default provider will be ReflectTypeDescriptionProvider . But this is not guaranteed, and this provider is not available due to its level of protection - it is internal . But we still want to backtrack to the default provider.
TypeDescriptor also allows you to explicitly add a provider for our type using the AddProvider method. This is what we will use. But first, let's define our own provider:
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. For example, let it define some simple class, for example:
public static class TypeDescriptorsConfig { public static void InitializeCustomTypeDescriptorProvider() {
And either call this code using web activation:
[assembly: PreApplicationStartMethod(typeof(TypeDescriptorsConfig), "InitializeCustomTypeDescriptorProvider")]
Or just call it in the Application_Start method:
public class MvcApplication : HttpApplication { protected void Application_Start() { TypeDescriptorsConfig.InitializeCustomTypeDescriptorProvider();
But this is not the end of the story .: (
Consider the following model:
public class TestModel { [CustomBindingName("actual_name")] [DisplayName("Yay!")] public string TestProperty { get; set; } }
If we try to write in the .cshtml view something like:
@model Some.Namespace.TestModel @Html.DisplayNameFor(x => x.TestProperty) @* fail *@
We will get an ArgumentException :
An exception of type "System.ArgumentException" occurred in System.Web.Mvc.dll, but was not processed in the user code
Additional information: Could not find the Some.Namespace.TestModel.TestProperty property.
This is because all helpers will sooner or later call the ModelMetadata.FromLambdaExpression method. And this method takes the expression that we provided ( x => x.TestProperty ), and takes the name of the member directly from the member information and has no idea about any of our attributes, metadata (who cares, right?):
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 the model metadata contains the actual_name property, they do not have TestProperty . This is why the property could not be found error.
This is a design mistake.
However, despite this slight inconvenience, there are several workarounds, such as:
The easiest way is to access our members by their 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 compilation errors. With any change, nothing can be broken, and there is no easy way to detect it.
Another way is a bit more complicated - we can create our own version of these helpers and prevent anyone from accessing the default ModelMetadata.FromLambdaExpression or ModelMetadata.FromLambdaExpression for model classes with renamed properties.
Finally, it would be preferable to combine the previous two: write your own analogue to get the name of the property with override support, and then pass this to the default helper. Something like that:
@model Some.Namespace.TestModel @Html.DisplayName(Html.For(x => x.TestProperty))
Compile time and intellisense support and no need to spend a lot of time on a full set of helpers Profit!
Also, everything described above works like a charm for model binding. During the default model binding process, the binder also uses metadata collected using TypeDescriptor .
But I think json data binding is the best use case. You know, many web programs and standards use the lowercase_separated_by_underscores naming lowercase_separated_by_underscores . Unfortunately, this is not an ordinary convention for C #. Having classes with members named in different conventions looks ugly and can end up in trouble. Especially when you have tools that are whining about naming violations every time.
Binding an ASP.NET MVC model by default does not bind json to the model in the same way as when calling the newtonsoft JsonConverter.DeserializeObject method. Instead, json parses the dictionary. For example:
{ complex: { text: "blabla", value: 12.34 }, num: 1 }
will be translated into the following dictionary:
{ "complex.text", "blabla" } { "complex.value", "12.34" } { "num", "1" }
And later, these values, together with other values from the query string, route data, etc., collected by various IValueProvider implementations, will use the default binder to bind the model using metadata collected using TypeDescriptor .
So, we got a full circle from creating a model, rendering, binding it and using.