2010-02-15 19 views
14

Esta pregunta se inspiró en mi lucha con ASP.NET MVC, pero creo que se aplica a otras situaciones también.¿Cómo "SECAR" los atributos de C# en Modelos y Modelos de Vista?

Digamos que tengo un modelo generado por ORM-y dos ViewModels (uno para una vista "Detalles" y uno para un "editar" vista):

Modelo

public class FooModel // ORM generated 
{ 
    public int Id { get; set; } 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
    public string EmailAddress { get; set; } 
    public int Age { get; set; } 
    public int CategoryId { get; set; } 
} 

Display ViewModel

public class FooDisplayViewModel // use for "details" view 
{ 
    [DisplayName("ID Number")] 
    public int Id { get; set; } 

    [DisplayName("First Name")] 
    public string FirstName { get; set; } 

    [DisplayName("Last Name")] 
    public string LastName { get; set; } 

    [DisplayName("Email Address")] 
    [DataType("EmailAddress")] 
    public string EmailAddress { get; set; } 

    public int Age { get; set; } 

    [DisplayName("Category")] 
    public string CategoryName { get; set; } 
} 

Editar modelo de vista

public class FooEditViewModel // use for "edit" view 
{ 
    [DisplayName("First Name")] // not DRY 
    public string FirstName { get; set; } 

    [DisplayName("Last Name")] // not DRY 
    public string LastName { get; set; } 

    [DisplayName("Email Address")] // not DRY 
    [DataType("EmailAddress")] // not DRY 
    public string EmailAddress { get; set; } 

    public int Age { get; set; } 

    [DisplayName("Category")] // not DRY 
    public SelectList Categories { get; set; } 
} 

Tenga en cuenta que los atributos de los ViewModels no son en seco - una gran cantidad de información se repite. Ahora imagina este escenario multiplicado por 10 o 100, y puedes ver que puede volverse bastante tedioso y propenso a errores para garantizar la coherencia entre ViewModels (y, por lo tanto, entre Views).

¿Cómo puedo "SECAR" este código?

Antes de responder, "Sólo hay que poner todos los atributos de FooModel," lo he intentado, pero no funcionó porque tengo que mantener mis ViewModels "plana". En otras palabras, no puedo simplemente componer cada ViewModel con un modelo; necesito que mi ViewModel tenga solo las propiedades (y atributos) que debe consumir la vista, y la vista no puede excavar en sub-propiedades para obtener los valores.

actualización

respuesta de LukLed sugiere el uso de la herencia. Esto definitivamente reduce la cantidad de código no DRY, pero no lo elimina. Tenga en cuenta que, en mi ejemplo anterior, el atributo DisplayName para la propiedad Category debería escribirse dos veces porque el tipo de datos de la propiedad es diferente entre la visualización y la edición de ViewModels. Esto no va a ser un gran problema a pequeña escala, pero a medida que aumenta el tamaño y la complejidad de un proyecto (imagina muchas más propiedades, más atributos por propiedad, más vistas por modelo), aún existe la posibilidad de que "repitiéndose" una cantidad justa. Tal vez estoy tomando DRY demasiado lejos aquí, pero aún prefiero tener todos mis "nombres amistosos", tipos de datos, reglas de validación, etc. mecanografiados solo una vez.

Respuesta

7

Supongo que hará esto para aprovechar el HtmlHelpers EditorFor y DisplayFor y no quiere que la taquilla declare ceremoniosamente lo mismo 4000 veces a lo largo de la aplicación.

La manera más fácil de SECAR esto es implementar su propio ModelMetadataProvider.El ModelMetadataProvider es lo que está leyendo esos atributos y presentándolos a los helpers de plantilla. MVC2 ya proporciona una implementación de DataAnnotationsModelMetadataProvider para que las cosas funcionen, por lo que heredar de eso hace las cosas realmente fáciles.

Para empezar aquí es un ejemplo sencillo que rompe los nombres de propiedades aparte CamelCased en espacios, Nombre => Nombre:

public class ConventionModelMetadataProvider : DataAnnotationsModelMetadataProvider 
{ 
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) 
    { 
     var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); 

     HumanizePropertyNamesAsDisplayName(metadata); 

     if (metadata.DisplayName.ToUpper() == "ID") 
      metadata.DisplayName = "Id Number"; 

     return metadata; 
    } 

    private void HumanizePropertyNamesAsDisplayName(ModelMetadata metadata) 
    { 
     metadata.DisplayName = HumanizeCamel((metadata.DisplayName ?? metadata.PropertyName)); 
    } 

    public static string HumanizeCamel(string camelCasedString) 
    { 
     if (camelCasedString == null) 
      return ""; 

     StringBuilder sb = new StringBuilder(); 

     char last = char.MinValue; 
     foreach (char c in camelCasedString) 
     { 
      if (char.IsLower(last) && char.IsUpper(c)) 
      { 
       sb.Append(' '); 
      } 
      sb.Append(c); 
      last = c; 
     } 
     return sb.ToString(); 
    } 
} 

Entonces todo lo que tiene que hacer es registrarlo como la adición de su propia aduana ViewEngine o ControllerFactory interior de Global.asax de Aplicación de inicio:

ModelMetadataProviders.Current = new ConventionModelMetadataProvider(); 

Ahora sólo para que yo no estoy engañando este espectáculo es el modelo de vista que estoy usando para obtener la misma HtmlHelper * Por experiencia como modelo de vista de su decorado.. :

public class FooDisplayViewModel // use for "details" view 
    { 
     public int Id { get; set; } 

     public string FirstName { get; set; } 

     public string LastName { get; set; } 

     [DataType("EmailAddress")] 
     public string EmailAddress { get; set; } 

     public int Age { get; set; } 

     [DisplayName("Category")] 
     public string CategoryName { get; set; } 
    } 
+0

Gracias, jfar y +1. Sí, exactamente, estoy tratando de usar DisplayFor() y EditorFor() (aunque, incluso en los casos en que no puedo, aún me gustaría SECAR mis ViewModels). Su idea eliminaría la necesidad de atributos, lo que es de gran ayuda. Me pregunto, sin embargo, si también podría agregar una propiedad personalizada (y un atributo personalizado paralelo) que indique si se debe andamiar una propiedad particular para un modelo de vista particular. Esto me permitiría tener un ViewModel que maneje todas las vistas, lo que significa que nunca o casi nunca necesitaré repetir atributos. – devuxer

+0

Bueno, la única limitación son las propiedades predeterminadas de ModelMetadata. Si necesita agregar más información y crear un MyModelMetadata: ModelMetatdata, también tendrá que crear su propia página de vista personalizada con una propiedad MyModelMetadata personalizada O emitir el ViewData.ModelMetadata dentro de los archivos .aspx o .ascx que use. – jfar

7

Declarar BaseModel, heredar y añadir otras propiedades:

public class BaseFooViewModel 
{ 
    [DisplayName("First Name")] 
    public string FirstName { get; set; } 

    [DisplayName("Last Name")] 
    public string LastName { get; set; } 

    [DisplayName("Email Address")] 
    [DataType("EmailAddress")] 
    public string EmailAddress { get; set; } 
} 

public class FooDisplayViewModel : BaseFooViewModel 
{ 
    [DisplayName("ID Number")] 
    public int Id { get; set; } 
} 

public class FooEditViewModel : BaseFooViewModel 

EDITAR

Acerca de las categorías. ¿No debería editar el modelo de vista tener public string CategoryName { get; set; } y public List<string> Categories { get; set; } en lugar de SelectList? De esta forma puede colocar public string CategoryName { get; set; } en la clase base y mantener SECO. La vista de edición mejora la clase al agregar List<string>.

+0

He jugado con el uso de la herencia antes. Definitivamente te acerca, pero le falta flexibilidad. Un problema es que algunos atributos todavía necesitan ser repetidos, como el atributo 'DisplayName' para' Category' en mi ejemplo. Obviamente, esto no es un gran problema a pequeña escala, pero a gran escala, podría terminar con muchas entradas dobles o triples del mismo 'DisplayName' para diferentes ViewModels (y ese es solo uno de varios atributos que podría querer establecer para una propiedad dada). – devuxer

+0

@DanM: He añadido un comentario sobre la Categoría. – LukLed

+0

@LukLed, no estoy seguro de que te esté siguiendo. Estoy usando 'SelectList' porque, cuando la vista se scaffolded, se creará una' DropDownList' con la lista correcta y el elemento seleccionado inicialmente. Incluso puedo conectar 'DataValueField' y' DataTextField' para los casos en que la lista es realmente una tabla de base de datos. Si solo uso una 'List ', no creo que mi vista tenga el scaffolded correcto. – devuxer

1

Como LukLed dijo que podría crear una clase base de la que derivan los modelos Ver y Editar, o también podría derivar un modelo de vista del otro. En muchas aplicaciones, el modelo Editar es básicamente el mismo que Ver más algunas cosas adicionales (como listas de selección), por lo que podría tener sentido derivar el modelo Editar del modelo Vista.

O, si le preocupa la "explosión de clase", puede usar el mismo modelo de vista para ambos y pasar el material adicional (como listas de selección) a través de ViewData. No recomiendo este enfoque porque creo que es confuso pasar un estado a través del Modelo y otro estado a través de ViewData, pero es una opción.

Otra opción sería abrazar los modelos por separado. Me refiero a mantener la lógica DRY, pero estoy menos preocupado por algunas propiedades redundantes en mis DTO (especialmente en proyectos que usan generación de código para generar el 90% de los modelos de vista para mí).

1

Lo primero que noté es que tienes 2 modelos de vista. Ver mi respuesta here para más detalles sobre esto.

Ya se mencionaron otras cosas que se mencionan en la mente (enfoque clásico para aplicar DRY - herencia y convenciones).


Supongo que era demasiado vago. Mi idea es crear modelos de vista por modelo de dominio y luego combinarlos en modelos de vista que son por vista específica. En su caso: =>

public class FooViewModel { 
    strange attributes everywhere tralalala 
    firstname,lastname,bar,fizz,buzz 
} 

public class FooDetailsViewModel { 
    public FooViewModel Foo {get;set;} 
    some additional bull**** if needed 
} 

public class FooEditViewModel { 
    public FooViewModel Foo {get;set;} 
    some additional bull**** if needed 
} 

Eso nos permite crear modelos de vista más complejos (que son por visión) también =>

public class ComplexViewModel { 
    public PaginationInfo Pagination {get;set;} 
    public FooViewModel Foo {get;set;} 
    public BarViewModel Bar {get;set;} 
    public HttpContext lol {get;set;} 
} 

le puede resultar útil this question mío.

hmm ... resulta que realmente sugerí crear 3 modelos de vista. De todos modos, ese fragmento de código tipo a refleja mi enfoque.

Otro consejo: me gustaría ir con el filtro & mecanismo de convención (por ejemplo) basado que llena los datos de vista con la lista de selección necesaria (mvc framework puede vincular automáticamente la lista de selección de viewData por nombre o algo así).

Y otro consejo: si utiliza AutoMapper para administrar su modelo de vista, tiene una buena característica, puede flatten object graph.Por lo tanto, puede crear el modelo de vista (que es por vista) que tiene directamente el modelo de los apoyos de vista (que es por modelo de dominio) sea cual sea la profundidad a la que desee ir (Haack said está bien).

+0

Gracias, Arnis. Parece que está diciendo que debería tener un modelo * third * view (por ejemplo, 'FooEditPostViewModel'). De hecho, he pensado en hacerlo varias veces, pero esto solo agrava los problemas que tengo con los atributos. Sin embargo, estoy intrigado por el Fluent MetadataProvider. Veremos eso. – devuxer

+0

@DanM actualizó un poco mi respuesta. –

+0

Arnis, gracias por los detalles adicionales. Una cosa que me interesa hacer es autoandamiar (usando 'DisplayFor()' y 'EditorFor()'). Ya sea que use composición o herencia, las cosas terminan en el orden incorrecto o aparecen con sangría cuando no deberían. – devuxer

0

Estos nombres para mostrar (los valores) tal vez podrían mostrarse en otra clase estática con muchos campos const. No le ahorraría tener muchas instancias de DisplayNameAttribute pero haría un cambio de nombre rápido y fácil de hacer. Obviamente, esto no es útil para otros meta atributos.

Si le digo a mi equipo que tendrían que crear un nuevo modelo para cada pequeña permutación de los mismos datos (y posteriormente escribir definiciones de Automapper para ellos) se rebelarían y lincharían. Prefiero modelar los metadatos que también fueron, en cierto grado, conocidos. Por ejemplo, hacer un atributo de propiedades requeridas solo tiene efecto en un escenario de "Agregar" (Modelo == nulo). Particularmente porque ni siquiera escribiría dos vistas para manejar agregar/editar. Tendría una vista para manejar ambos y si comenzara a tener diferentes clases de modelo me metería en problemas con mi declaración de clase principal ... el ... Bit de la página.

Cuestiones relacionadas