asp.net mvc - visual - Cómo enlazar la propiedad del modelo de vista con un nombre diferente
install asp net mvc visual studio 2017 (2)
En realidad, hay una manera de hacerlo.
En los metadatos de enlace de ASP.NET recopilados por TypeDescriptor
, no por reflexión directamente. Para ser más valioso, se utiliza AssociatedMetadataTypeTypeDescriptionProvider
, que, a su vez, simplemente llama a TypeDescriptor.GetProvider
con nuestro tipo de modelo como parámetro:
public AssociatedMetadataTypeTypeDescriptionProvider(Type type)
: base(TypeDescriptor.GetProvider(type))
{
}
Por lo tanto, todo lo que necesitamos es configurar nuestro TypeDescriptionProvider
personalizado para nuestro modelo.
Vamos a implementar nuestro proveedor personalizado. En primer lugar, vamos a definir el atributo para el nombre de la propiedad personalizada:
[AttributeUsage(AttributeTargets.Property)]
public class CustomBindingNameAttribute : Attribute
{
public CustomBindingNameAttribute(string propertyName)
{
this.PropertyName = propertyName;
}
public string PropertyName { get; private set; }
}
Si ya tiene un atributo con el nombre deseado, puede reutilizarlo. El atributo definido anteriormente es solo un ejemplo. Prefiero usar JsonPropertyAttribute
porque en la mayoría de los casos, trabajo con json y la biblioteca de Newtonsoft y quiero definir un nombre personalizado solo una vez.
El siguiente paso es definir el descriptor de tipo personalizado. No implementaremos la lógica del descriptor de tipo completo y no usaremos la implementación predeterminada. Solamente se anulará el acceso a la propiedad:
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);
}
}
También se debe implementar un descriptor de propiedades personalizado. Nuevamente, todo, excepto el nombre de la propiedad, será manejado por el descriptor predeterminado. Tenga en cuenta, NameHashCode
por alguna razón es una propiedad separada. Como se cambió el nombre, también se debe cambiar el código hash:
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; }
}
}
Finalmente, necesitamos nuestro TypeDescriptionProvider
personalizado y una forma de vincularlo a nuestro tipo de modelo. De forma predeterminada, TypeDescriptionProviderAttribute
está diseñado para realizar ese enlace. Pero en este caso no podremos obtener el proveedor predeterminado que queremos usar internamente. En la mayoría de los casos, el proveedor predeterminado será ReflectTypeDescriptionProvider
. Pero esto no está garantizado y este proveedor es inaccesible debido a su nivel de protección, es internal
. Pero todavía queremos recurrir al proveedor predeterminado.
TypeDescriptor
también permite agregar explícitamente un proveedor para nuestro tipo a través del método AddProvider
. Eso lo usaremos. Pero primero, definamos nuestro propio proveedor personalizado:
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));
}
}
El último paso es vincular a nuestro proveedor con nuestros tipos de modelos. Podemos implementarlo de la manera que queramos. Por ejemplo, definamos alguna clase simple, como:
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);
}
}
}
Y o bien invocar ese código a través de la activación web:
[assembly: PreApplicationStartMethod(typeof(TypeDescriptorsConfig), "InitializeCustomTypeDescriptorProvider")]
O simplemente llámelo en el método Application_Start
:
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
TypeDescriptorsConfig.InitializeCustomTypeDescriptorProvider();
// rest of init code ...
}
}
Pero este no es el final de la historia. :(
Considere el siguiente modelo:
public class TestModel
{
[CustomBindingName("actual_name")]
[DisplayName("Yay!")]
public string TestProperty { get; set; }
}
Si intentamos escribir en la vista .cshtml
algo como:
@model Some.Namespace.TestModel
@Html.DisplayNameFor(x => x.TestProperty) @* fail *@
Obtendremos ArgumentException
:
Se produjo una excepción del tipo ''System.ArgumentException'' en System.Web.Mvc.dll pero no se manejó en el código de usuario
Información adicional: No se pudo encontrar la propiedad Some.Namespace.TestModel.TestProperty.
Eso se debe a que todos los ayudantes invocan el método ModelMetadata.FromLambdaExpression
. Y este método toma la expresión que proporcionamos ( x => x.TestProperty
) y toma el nombre del miembro directamente de la información del miembro y no tiene idea de ninguno de nuestros atributos, metadatos (a quién le importa, ¿eh?) :
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 - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ...
}
Para x => x.TestProperty
(donde x
es TestModel
) este método devolverá TestProperty
, no actual_name
, pero los metadatos del modelo contienen la propiedad actual_name
, no tienen TestProperty
. Es por eso que the property could not be found
error arrojado.
Esto es un fallo de diseño .
Sin embargo, a pesar de este pequeño inconveniente, hay varias soluciones, como:
La forma más fácil es acceder a nuestros miembros por sus nombres redefinidos:
@model Some.Namespace.TestModel @Html.DisplayName("actual_name") @* this will render "Yay!" *@
Esto no está bien. No tenemos ninguna inteligencia y, a medida que cambie nuestro modelo, no tendremos ningún error de compilación. En cualquier cambio, cualquier cosa puede romperse y no hay una manera fácil de detectar eso.
Otra forma es un poco más compleja: podemos crear nuestra propia versión de los ayudantes y prohibir que alguien llame a los ayudantes predeterminados o
ModelMetadata.FromLambdaExpression
para clases de modelos con propiedades renombradas.Finalmente, se preferiría la combinación de los dos anteriores: escriba su propio análogo para obtener el nombre de la propiedad con soporte de redefinición, luego transfiéralo al asistente predeterminado. Algo como esto:
@model Some.Namespace.TestModel @Html.DisplayName(Html.For(x => x.TestProperty))
Compilación en tiempo y soporte inteligente, sin necesidad de dedicar mucho tiempo a un conjunto completo de ayudantes. ¡Lucro!
También todo lo descrito anteriormente funciona como un amuleto para atar modelos. Durante el proceso de enlace del modelo, el enlace predeterminado también utiliza metadatos, recopilados por TypeDescriptor
.
Pero supongo que vincular datos json es el mejor caso de uso. Usted sabe, una gran cantidad de software y estándares web utilizan la convención de nomenclatura de lowercase_separated_by_underscores
separables. Desafortunadamente esta no es la convención usual para C #. Tener clases con miembros nombrados en diferentes convenciones se ve feo y puede terminar en problemas. Especialmente cuando tienes herramientas que se quejan cada vez de nombrar una infracción.
El archivador de modelo predeterminado de ASP.NET MVC no obliga a json a modelar de la misma manera que ocurre cuando se llama al método JsonConverter.DeserializeObject de JsonConverter.DeserializeObject
. En su lugar, json analizó en el diccionario. Por ejemplo:
{
complex: {
text: "blabla",
value: 12.34
},
num: 1
}
será traducido al siguiente diccionario:
{ "complex.text", "blabla" }
{ "complex.value", "12.34" }
{ "num", "1" }
Y luego estos valores junto con otros valores de cadena de consulta, datos de ruta, etc., recopilados por diferentes implementaciones de IValueProvider
, serán utilizados por el cuaderno predeterminado para enlazar un modelo con ayuda de metadatos, recopilados por TypeDescriptor
.
Así que llegamos a un círculo completo desde la creación del modelo, la representación, el enlace y el uso.
¿Hay una manera de hacer una reflexión para una propiedad de modelo de vista como un elemento con diferentes nombres y valores de identificación en el lado html?
Esa es la pregunta principal de lo que quiero lograr. Así que la introducción básica para la pregunta es como:
1- Tengo un modelo de vista (como ejemplo) que se creó para una operación de filtro en el lado de vista.
public class FilterViewModel
{
public string FilterParameter { get; set; }
}
2- Tengo una acción de controlador que se crea para OBTENER valores de formulario (aquí está el filtro)
public ActionResult Index(FilterViewModel filter)
{
return View();
}
3- Tengo una opinión de que un usuario puede filtrar algunos datos y enviar parámetros a través de una cadena de consulta sobre el envío del formulario.
@using (Html.BeginForm("Index", "Demo", FormMethod.Get))
{
@Html.LabelFor(model => model.FilterParameter)
@Html.EditorFor(model => model.FilterParameter)
<input type="submit" value="Do Filter" />
}
4- Y lo que quiero ver en la salida renderizada es
<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- Y como solución quiero modificar mi modelo de vista de esta manera:
public class FilterViewModel
{
[BindParameter("fp")]
[BindParameter("filter")] // this one extra alias
[BindParameter("param")] //this one extra alias
public string FilterParameter { get; set; }
}
Así que la pregunta básica es sobre BindAttribute pero el uso de propiedades de tipo complejo. Pero también si hay una forma integrada de hacer esto es mucho mejor. Pros incorporados:
1- El uso con TextBoxFor, EditorFor, LabelFor y otros ayudantes de modelos de vista fuertemente tipificados pueden entenderse y comunicarse mejor entre sí.
2- Soporte de enrutamiento de url
3- Sin marco por diseño de problemas:
En general, recomendamos que las personas no escriban carpetas de modelos personalizadas porque son difíciles de entender y rara vez se necesitan. El tema que estoy discutiendo en esta publicación podría ser uno de esos casos en los que está justificado.
Y también después de algunas investigaciones encontré estos útiles trabajos:
Propiedad de modelo vinculante con nombre diferente
Actualización de un solo paso del primer enlace.
Resultado: pero ninguno de ellos me da la solución exacta a mis problemas. Estoy buscando una solución fuertemente tipada para este problema. Por supuesto, si usted sabe otra manera de ir, por favor comparta.
Actualizar
Las razones subyacentes por las que quiero hacer esto son básicamente:
1- Cada vez que quiero cambiar el nombre del control html, tengo que cambiar PropertyName en el momento de la compilación. (Hay una diferencia al cambiar el nombre de una propiedad entre cambiar una cadena en el código)
2- Quiero ocultar (camuflar) los nombres de propiedades reales de los usuarios finales. La mayoría de las veces, los nombres de las propiedades del modelo Ver son iguales a los nombres de las propiedades de los objetos de entidad asignados (Por razones de legibilidad del desarrollador)
3- No quiero eliminar la legibilidad para el desarrollador. Piense en un montón de propiedades con como 2-3 caracteres de largo y con significados de mo.
4- Hay muchos modelos de vista escritos. Por lo tanto, cambiar sus nombres llevará más tiempo que esta solución.
5- Esta será una mejor solución (en mi punto de vista) que otras que se describen en otras preguntas hasta ahora.
La respuesta corta es NO y la respuesta larga todavía NO. No hay un ayudante incorporado, atributo, cuaderno de modelos, lo que sea (Nada fuera de la caja).
Pero lo que hice en la respuesta anterior (lo borré) fue una solución horrible que me di cuenta ayer. Voy a ponerlo en github para quien todavía quiera ver (tal vez resuelva el problema de alguien) (¡No lo sugiero también!)
Ahora lo busqué de nuevo y no pude encontrar nada útil. Si está utilizando una herramienta similar a AutoMapper o ValueInjecter para asignar sus objetos de ViewModel a objetos de negocios y si quiere ofuscar también los parámetros del modelo de vista, probablemente tenga algún problema. Por supuesto que puedes hacerlo, pero los ayudantes html fuertemente tipados no te ayudarán mucho. Incluso no estoy hablando de si otros desarrolladores tomaron Branch y trabajaron sobre modelos de vista comunes.
Por suerte, mi proyecto (4 personas que trabajan en él y su uso comercial para) no es tan grande por ahora, ¡así que decidí cambiar los nombres de las propiedades de View Model! (Todavía queda mucho trabajo por hacer. ¡Cientos de modelos de vista para ofuscar sus propiedades!) ¡Gracias Asp.Net MVC!
Hay algunas maneras en los enlaces que he dado en cuestión. Pero también si aún desea usar el atributo BindAlias, solo puedo sugerirle que use los siguientes métodos de extensión. Al menos no tiene que escribir la misma cadena de alias que escribe en el atributo BindAlias.
Aquí está:
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;
}
Cuando quieras usarlo:
[ModelBinder(typeof(AliasModelBinder))]
public class FilterViewModel
{
[BindAlias("someText")]
public string FilterParameter { get; set; }
}
En html:
@* at least you dont write "someText" here again *@
@Html.Editor(Html.AliasNameFor(model => model.FilterParameter))
@Html.ValidationMessage(Html.AliasNameFor(model => model.FilterParameter))
Así que estoy dejando esta respuesta aquí de esta manera. Esto incluso no es una respuesta (y no hay una respuesta para MVC 5), pero a la persona que busca el mismo problema en Google le puede resultar útil esta experiencia.
Y aquí está el repositorio de github: https://github.com/yusufuzun/so-view-model-bind-20869735