Modelos de vista dinámicos en asp.net mvc

Muy buenas!

Imagina que tienes la siguiente entidad de un modelo de datos de Entity Framework:

   1: public class Product

   2: {

   3:     public int Id {get; set;}

   4:     [Required]

   5:     public string Name { get; set; }

   6:     public string ShortDescription { get; set; }

   7:     public string LongDescription { get; set; }

   8:     public string Remmarks { get; set; }

   9:     public string LegalNotices { get; set; }

  10:     public string TermsOfUse { get; set; }

  11:     [Column(TypeName="Image")]

  12:     public byte[] Icon { get; set; }

  13:     [Column(TypeName = "Image")]

  14:     public byte[] Screenshot { get; set; }

  15:     [Column(TypeName = "Image")]

  16:     public byte[] ScreenshotHD { get; set; }

  17:     public decimal Price { get; set; }

  18: }

Y que tienes un controlador que recoge los productos que tengas en la BBDD y los pasa a una vista:

   1: public ActionResult Index()

   2: {

   3:     IEnumerable<Product> products = null;

   4:     using (var ctx = new MyContext())

   5:     {

   6:         products = ctx.Products.ToList();

   7:     }

   8:  

   9:     return View(products);

  10: }

Donde la vista se limita a mostrar usar tan solo un par de propiedades de la clase Product:

   1: <h2>Produtcs list</h2>

   2:  

   3: <ul>

   4:     @foreach (var item in Model)

   5:     {

   6:         <li>@item.Name - @Html.ActionLink("View", "Details", new {id=@item.Id})</li>

   7:     }

   8: </ul>

Probablemente habrás visto código muy parecido en muchos blogs o ejemplos. Dicho código funciona correctamente, pero… ¿cual es el problema?

Pues bien, en el caso de entidades grandes, como puede ser esta Product (fíjate que hay 3 campos de imágen, y un par de campos de texto que podrían ser muy largos), el problema es que la consulta que está generando Entity Framework es:

SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[ShortDescription] AS [ShortDescription], [Extent1].[LongDescription] AS [LongDescription], [Extent1].[Remmarks] AS [Remmarks], [Extent1].[LegalNotices] AS [LegalNotices], [Extent1].[TermsOfUse] AS [TermsOfUse], [Extent1].[Icon] AS [Icon], [Extent1].[Screenshot] AS [Screenshot], [Extent1].[ScreenshotHD] AS [ScreenshotHD], [Extent1].[Price] AS [Price]FROM [dbo].[Products] AS [Extent1]

Es decir, estamos trasladando todos los campos desde la base de datos hacia el servidor web para tan solo usar un par de campos en la vissta (Name e Id).

Esto, en el caso de que seleccionemos muchas entidades grandes puede ser un problema. Por supuesto, la solución es muy sencilla, decirle a Entity Framework que tan solo seleccione los campos que deseemos:

   1: products = ctx.Products.Select(p => new { p.Id, p.Name}).ToList();

Con esto la query generada contiene tan solo los campos deseados:

SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name]FROM [dbo].[Products] AS [Extent1]

El tema está ahora en como pasamos esta información a la vista. Anteriormente la vista tenia la declaración @model IEnumerable<Product> pero ahora esto ya no nos sirve y ASP.NET MVC se quejará (con razón) de que el tipo del modelo de la vista no es el que le pasamos, y nos dará un error.

Quitar la directiva @model de la vista no funciona y recibirás un error parecido al siguiente ‘object’ does not contain a definition for ‘Name’. Si quitamos la directiva @model, el modelo de la vista pasa a ser de tipo Object lo que no nos sirve porque ni Name ni Id están definidas en Object.

Una solución es generar un ViewModel, es decir una clase que contenga los datos que la vista requiere:

   1: public class ProductListViewModel

   2: {

   3:     public int Id { get; set; }

   4:     public string Name { get; set; }

   5: }

Luego en EF debemos crear una colección de este objeto, en lugar del objeto anónimo:

   1: var products = ctx.Products.Select(p => new ProductListViewModel 

   2: { 

   3:     Id = p.Id, 

   4:     Name = p.Name 

   5: }).ToList();

Ahora colocando la directiva @model IEnumerable<ProductListViewModel> en nuestra vista todo funciona correctamente.

El problema ahora viene si tenemos muchas vistas que listen distintos campos Product (p. ej. una con Name e Id, otra con Name, Id, Icon y ShortDescription, otra con Name, LongDescription y Remmarks, etc…). Si sigues esta técnica te vas a encontrar con muchos view models distintos, lo cual puede ser tedioso de mantener.

ASP.NET MVC tiene el concepto de tipo de modelo dinámico. Para ello en la vista puedes poner @model dynamic.

Pero… no es oro todo lo que reluce. Si cambias la directiva a @model dynamic y vuelves al código donde usábamos un objeto anónimo… Obtendrás de nuevo el error ‘object’ does not contain a definition for ‘Name’. Es decir, a pesar de que declaras el tipo de modelo como dinámico en la vista, parece que para ASP.NET MVC el modelo sigue siendo un object.

La razón es que cuando usas @model dynamic el objeto que pasas debe estar preparado para interaccionar con el DLR y los objetos anónimos no lo están. La solución pasa por usar ExpandoObject que si que lo está. Entonces el código del controlador pasa a:

   1: var products = ctx.Products.Select(p => new

   2: {

   3:     p.Id,

   4:     p.Name

   5: }).ToList();

   6:  

   7: List<dynamic> model = new List<dynamic>();

   8: foreach (var item in products)

   9: {

  10:     dynamic data = new ExpandoObject();

  11:     data.Id = item.Id;

  12:     data.Name = item.Name;

  13:     model.Add(data);

  14: }

  15:  

  16: return View(model);

Ahora usando @model dynamic todo funciona correctamente. Por supuesto parece que el remedio es peor que la enfermedad: ¡El código es bastante más pesado y tienes que realizar esta copia de “propiedades” del objeto anónimo al ExpandoObject!

Ahí es donde una característica de ExpandoObject y un método de extensión te pueden ayudar. La característica es que ExpandoObject implementa IDictionary<string,object> y agregar una propiedad a ExpandoObject es equivalente a agregar una entrada en el diccionario. Así podemos tener un par de métodos de extensión sencillos:

   1: static class ExpandoObjectExtensions

   2: {

   3:     public static dynamic CopyFrom(this ExpandoObject source, object data)

   4:     {

   5:         var dict = source as IDictionary<string, object>;

   6:         foreach (var property in data.GetType().GetProperties())

   7:         {

   8:             dict.Add(property.Name, property.GetValue(data, null));

   9:         }

  10:  

  11:         return source;

  12:     }

  13:  

  14:     public static IEnumerable<dynamic> Dynamize<T>(this IEnumerable<T> source)

  15:     {

  16:         foreach (var entry in source)

  17:         {

  18:             var expando = new ExpandoObject();

  19:             yield return expando.CopyFrom(entry);

  20:         }

  21:     }

  22: }

Y ahora nuestro código en el controlador queda simplemente como:

   1: var products = ctx.Products.Select(p => new

   2:                 {

   3:                     p.Id,

   4:                     p.Name

   5:                 }).ToList().Dynamize();

¡Y listos! Usando @model dynamic nuestra vista sigue funcionando y nos olvidamos de ir creando viewmodels y de copiar propiedades arriba y abajo.

Espero que os haya sido interesante! 😉

¡Un saludo!

20 comentarios en “Modelos de vista dinámicos en asp.net mvc”

  1. Hola Eduard,

    Magnífico post, como siempre!!

    La solución parece perfecta, poco código, poco esfuerzo y se lleva maravillosamente bien con Linq y las proyecciones (de cualquier tipo, anónimas o no anónimas). Yo me pienso usar esta solución porque estoy empezando a cansarme de tanto ViewModel y tanto AutoMapper, al final para pintar una vista tardo más de lo que debería… pero ¿Esto no va en contra de todas las “buenas-prácticas” habidas y por haber? Es decir, siempre que he estudiado MVC (ya sea en curso, libro, etc.) siempre te dicen que “Hay que hacer ViewModel” que cojas AutoMapper y te hinches, que es bueno… que el patrón MVVM es tu amigo, que estás haciendo SoC y no se cuentas cosas más… pero la realidad (al menos en mi caso) es que llevo un tiempo algo cansado con esta forma de proceder, no me siento productivo… y ahora vienes tú y muestras esta forma elegante y cool de “prescindir” de MVVM 😉

    ¿Tú esta forma de pasar datos a la vista la utilizas o es simplemente con fines didácticos? Si la utilizas ¿Alternas ViewModelos puros con dynamic o vas con dynamic hasta el infinito y más allá?

    Esto es como prescindir de los repos, últimamente (y quizás porque mis aplicaciones no son mega-ultra-grandes de la muerte) me están dando ganas de soltar lastre y dejar caer muchos bultos que no sé yo si me están dando más de lo que me están quitando..
    .
    Por cierto, estoy esperando a Pedro por aquí, me encantaría saber también su opinión xD

    Lo dicho, magnífico post!

  2. Llevo un tiempo trabajando así con Orchard (construido encima de asp.net mvc) y es sin duda la mejor forma de proceder. En el caso de Orchard tienes Parts que arrastran un sin fin de propiedades que no usas, y normalmente necesitas ViewModel para editar, para exponer, para listar, etc.

    Una recomendación, aunque obvia, sería tener un método (y sólo uno) en el que se declaren las variables y que sólo se encargue de ésto, p.e:

    var whatever = new ExpandoObject();
    whatever.X = GetX();
    whatever.Y = GetY();

    Si es una colección, lo ideal es exponer otro método que se encargue de llenarlo (aunque quedé en un select de una línea).

    Veo perfecto el uso de dynamics en este tipo de escenarios, pero como dice el tío de Peter, “un gran poder conlleva una gran responsabilidad” y a no ser que nos pasemos testeando cada respiro, la mejor forma de usar dynamic es haciendo código limpio. 🙂

    Buen post!

  3. Buenas!
    Muchas gracias a todos por vuestros comentarios! 🙂

    Sergio, sobre lo que me preguntas: Yo ahora para listados tiendo a utilizar más el dynamic. La verdad, gano en productividad y si quito/añado un campo en el listado no tengo que ir modificando el viewmodel asociado.

    Sigo usando viewmodels en vistas más complejas que un mero listado y cuando la vista envía datos al servidor. 🙂

    Al final lo que presento en este post es, simplemente, otra técnica. Que tendrá sus usos en cada momento en función de las necesidades de cada proyecto y (porque no decirlo) de las preferencias de cada uno.

    Un abrazo!

  4. Buenas!

    No me gusta contestar por invocación, pero a este Sergio le tengo cariño y a ti también:)

    El post buenisimo, la solución muy buena pero….

    Vamos a centrarnos en esto http://stackoverflow.com/questions/5120317/dynamic-anonymous-type-in-razor-causes-runtimebinderexception

    Que a la postre muestra una solución parecida pero peor explicada y lo que más me gusta de todo es esta frase.

    “Anonymous types having internal properties is a poor .NET framework design decision, in my opinion.”

    Con lo cual coincido al 100% con el autor.

    Me parece una aberración el uso de internal.

    Y otra más. Quien hizo la compilación de asp.net mvc(razor) que no tuviese en cuenta esto.

    Si esto se hubiese tenido en cuenta y no se generaran dll’s por cada vista o carpeta dependiendo de versiones de MVC este problema no se produciría, puesto que esto funciona perfectamente en un controlador.

    var context = new Context();
    dynamic tipopequeño = context.ClaseLarga.Select(x => new { Id = x.Id, Name = x.Name });
    foreach (var item in tipopequeño)
    Console.Write(item.Id);
    return View();

    El problema es que el Id del tipo anonimo es Internal:).

    Dicho esto yo siempre que pueda también utilizaría esto.

    Gracias!!!!

  5. Gracias Eduard, yo también voy a explorar en el proyecto actual esta técnica, para listados utilizaré dynamic y para ViewModels de Request o ViewModels complicados tiraré por la vía tradicional. La verdad es que tu solución junto a la Pedro Hurtado (que ahora comentaré también en su blog) me han servido para ver otras formas de hacer las cosas y ha sido una experiencia de aprender un poco más de MVC que no tiene precio! 😉
    Gracias de nuevo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *