How to associate a property of a view model with a different name

Is there a way to reflect on the property of the view model as an element with different names and identifiers on the html side.

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

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

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

2- I have a controller action that is created for the values ​​of the GETting form (here it is a filter)

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

3- I have an idea that the user can filter some data and send parameters via querystring through the submit form.

 @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 the visualized representation of the view,

 <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 change my presentation model as follows:

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

So, the main question is about BindAttribute, but using properties of a complex type. But also, if there is a built-in way to do it much better. Built-in pros:

1- Use with TextBoxFor, EditorFor, LabelFor and other highly typed view model helpers can better understand and share with each other.

2- Support for URL routing

3 Without creating problems:

In general, we recommend that people do not write custom model bindings because it is difficult for them to get the right and they are rarely needed. the question that I am discussing in this post may be one of those cases where this is justified.

Link Quote

And also after some research, I found these useful works:

Binding property of a model with a different name

One-step update of the first link

Here are some references.

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




Update

The main reasons I want to do this are mainly:

1- Each time I want to change the name of the html control, I need to change the PropertyName at compile time. (There is a difference in changing the property name between changing the line in the code)

2- I want to hide (camouflage) the names of real objects from end users. In most cases, Model Model property names are similar to Entity Objects. (For readability)

3- I do not want to remove readability for the developer. Think of many properties with a length of 2-3 characters and with the meanings of mo.

4- Written by many models of views. Therefore, changing their names will take longer than this solution.

5- This will be a better solution (in my POV) than others that are still described in other issues.

+16
asp.net-mvc razor asp.net-mvc-5 model-binding data-annotations
Jan 01 '14 at 14:40
source share
2 answers

The short answer is NO, and the long answer is NO. There is no built-in helper, attribute, model binding, whatever it is (nothing out of the box).

But what I did before the answer (I deleted it) was a terrible decision that I realized yesterday. I am going to put it on github for those who still want to see (maybe it solves some kind of problem) (I do not suggest either!)

Now I was looking for him again, and I could not find anything useful. If you use something like AutoMapper or ValueInjecter as a tool to map ViewModel objects to Business objects, and if you want to confuse View Model parameters as well, you may have some problems. Of course you can do this, but strongly typed html helpers will not help you. I'm not even saying that if other developers take a branch and work on common types of views.

Fortunately, my project (4 people working on it and its commercial use) is not that big, so I decided to change the names of the Model Model properties! (This is still a lot of work. Hundreds of models of views to deceive their properties !!!) Thanks Asp.Net MVC!

There are some ways in the links that I questioned. But also, if you still want to use the BindAlias ​​attribute, I can suggest you use the following extension methods. At the very least, you do not need to write the same alias string that you write in the BindAlias ​​attribute.

There he is:

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

If you want to use it:

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

In html:

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

So, I leave this answer like this. This is not even an answer (and there is no answer to MVC 5), but a google search for the same problem may find this experience useful.

And here is the github repository: https://github.com/yusufuzun/so-view-model-bind-20869735

+6
Jan 03 '14 at 10:00
source share
— -

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() { // 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 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(); // rest of init code ... } } 

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.

+5
Jul 18 '16 at 14:33
source share



All Articles