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!

¡Hello World Katana!

Buenas! En este post vamos a ver como empezar a trabajar con Katana. En un post anterior hablé un poco de Katana y mis fantasías (más o menos húmedas) de lo que podría ser un un futuro.

Antes que nada hagamos un repaso rápido:

  1. OWIN: Open Web Interface for .NET. Especificación que define un estándard para comunicar servidores web y aplicaciones web (en tecnología .NET).
  2. Katana: Implementación de Microsoft de la especificación OWIN.

¿Cuál es la ventaja principal de hacer que una aplicación web sea compatible con OWIN? Pues simplemente que desacoplas esta aplicación web del servidor usado. Cualquier servidor (que sea compatible con OWIN) podrá hospedar tu aplicación. Esto abre la puerta a tener aplicaciones web self-hosted.

Empezando con Owin y Visual Studio 2012

En este primer post vamos a realizar la aplicación más posible sencilla (un hello world, originalidad a tope).

Para empezar abre VS2012 (o VS2013 si lo tienes, para este post da igual) y crea una aplicación de consola. Luego añade con NuGet los siguientes paquetes:

  1. Microsoft.Owin.Hosting
  2. Microsoft.Owin.Host.HttpListener
  3. Microsoft.Owin.Diagnostics
  4. Owin.Extensions

Actualmente están en prerelase así que incluye el flag –IncludePreRelase  cuando lances el comando Install-Package desde la consola de NuGet.

Una vez tengos estos paquetes instalados, ya podemos desarrollar nuestra aplicación. Lo primero que necesitamos es una clase que ponga en marcha nuestra aplicación:

class Program

{

    public static void Main(string[] args)

    {

        var uri = "http://localhost:8080/";

 

        using (WebApp.Start<Startup>(uri))

        {

            Console.WriteLine("Started");

            Console.ReadKey();

            Console.WriteLine("Stopping");

        }

    }

}

Usamos la clase WebApp del paquete Microsoft.Owin.Hosting para poner en marcha nuestra aplicación. El parámetro genérico (Startup) es el nombre de una clase que será la que configurará nuestra aplicación.

Veamos el código:

public class Startup

{

    public void Configuration(IAppBuilder app)

    {

        app.UseHandlerAsync((req, res) =>

        {

            res.ContentType = "text/plain";

            return res.WriteAsync("Hello Katana. You reached " + req.Uri);

        });

 

    }

}

El método Configuration se invoca automáticamente y se recibe un parámetro IAppBuilder. Dicha interfaz estaba definida en la specificación de OWIN (http://owin.org/spec/owin-0.12.0.html#_2.10._IAppBuilder) pero desapareció en la versión final.

Katana usa esta interfaz para permitir a la aplicación web configurar el pipeline de procesamiento de peticiones. De momento nuestra aplicación es muy simple: Por cada petición, construirá una respuesta con el texto “Hello Katana. You reached “ seguido de la URL navegada.

Si ejecutamos el proyecto, y abrimos un navegador y nos vamos a localhost:8080, vemos que nuestra aplicación ya está en marcha:

image

Fíjate que nuestra aplicación es un ejecutable. No hay servidor web, ni cassini, ni IIS Express, ni IIS, ni nada 🙂

Agregando un módulo

OWIN se define de forma totalmente modular. Por un lado tenemos un Host (en este caso es nuestro ejecutable a través del objeto WebApp del paquete Microsoft.Owin.Host), varios módulos y finalmente la aplicación en si.

Los módulos implementan un “delegado” que se conoce como AppFunc (aunque no hay ningún delegado real con este nombre). AppFunc es realmente Func<IDictionary<string, object>, Task>, es decir recibir un IDictionary<string, object> y devolver un Task.

La idea es que un módulo recibe un diccionario (claves cadenas, valores objects) que es el entorno del servidor y devuelve una Task que es el código que este módulo debe ejecutar.

Los módulos están encadenados y cada módulo debe llamar al siguiente. El aspecto genérico de un módulo queda así:

using AppFunc = Func<IDictionary<string, object>, Task>;

public class OwinConsoleLog

{

    private readonly AppFunc _next;

    public OwinConsoleLog(AppFunc next)

    {

        _next = next;

    }

    public Task Invoke(IDictionary<string, object> environment)

    {

        Console.WriteLine("Path requested: {0}", environment["owin.RequestPath"]);

        return _next(environment);

    }

}

El módulo define el método Invoke y recibe como parámetro el diccionario que contiene el entorno del servidor. Luego llama al siguiente módulo y le pasa el entorno. Fíjate que la clase OwinConsoleLog no implementa ninguna interfaz ni nada, pero debe tener un método llamado Invoke que sea conforme al “delegado” AppFunc (es decir que devuelva un Task y reciba un diccionario).

Para añadir el módulo simplemente llamamos al método Use de IAppBuilder pasándole el Type de nuestro módulo:

public void Configuration(IAppBuilder app)

{

    app.Use(typeof (OwinConsoleLog));

    app.UseHandlerAsync((req, res) =>

    {

        res.ContentType = "text/plain";

        return res.WriteAsync("Hello Katana. You reached " + req.Uri);

    });

}

Si ahora ejecutas el proyecto y navegas a localhost:8080 verás como se imprimen las distintas peticiones recibidas:

image

¡Listos! Hemos creado nuestra primera aplicación web, compatible con OWIN y auto hospedada 🙂

En sucesivos posts iremos desgranando más cosillas sobre OWIN y Katana…

Saludos!