martes, 20 de diciembre de 2011

ClientResourceManager la pieza olvidada en ASP.NET MVC

Muchas veces, cuando se comienza a escribir una aplicación desde cero, es frecuente incluir un archivo con los scripts que se van a ejecutar en el cliente (aka javascript). Resulta que cuando la aplicación comienza a crecer, este archivo se vuelve inmanejable, más aún cuando otros desarrolladores se incorporan al proyecto, aún así tiene una “ventaja” y es que en ese archivo está todo, no hay que buscar nada en otro lugar, y lo fundamental, con que se haga referencia a este archivo es suficiente para que la interfaz funcione bien.

Con esto muchos pensarán que es mejor tenerlo todo en un solo archivo, sin embargo, para mantener el código lo más “higiénico” se recomienda escribir pequeños módulos que sean fácilmente actualizables. Esto trae consigo problemas que antes no se tenían como puede ser la dependencia entre los diferentes módulos. Por ejemplo, si una página requiere una funcionalidad implementada en un determinado módulo (archivo js), y a su vez, dicho módulo depende de otro módulo (otro archivo js), entonces es necesario referenciar ambos módulos en la página (escribir 2 secciones <script>). Peor aún, muchas veces se tiende a escribir una pequeña funcionalidad de la página en una especie de componente (UserControl, Include, Partial o como le quiera llamar) que requiere de la presencia de algún script, en ese caso se debe estar atento e incluir las referencias que necesita en la página que está usando el componente.

Para evitar lo anterior surge el ClientResourceManager. La responsabilidad de este componente es administrar los recursos que se descargan al cliente, fundamentalmente archivos JavaScript y CSS. En la siguiente figura se muestra un diagrama de clases con la estructura básica de nuestro componente.

ResourceManager
La clase ResourceDefinition servirá para almacenar instancias de los recursos que necesita la aplicación. Cada objeto de tipo ResourceDefinition deberá tener un nombre que lo identifique (propiedad Name). Entre sus propiedades más importantes están la URL del recurso, una lista con los nombres de los recursos de los que depende (propiedad Dependecies) y una propiedad que indica de que tipo es el recurso (propiedad Type, puede ser cualquier cadena, ej: script, css, etc. )

Por su parte, la clase ResourceManager se encarga de gestionar  los recursos, los métodos más importantes son: RegisterResource encargado de almacenar las referencias a los recursos, GetResourcesOfType retorna una lista con los recursos de un determinado tipo y GetResourceByName que se encarga de buscar un determinado recurso según su nombre, el código de esta clase se muestra a continuación:
   1: public class ResourceManager : IResourceManager
   2: {
   3:     private readonly IDictionary<string, ResourceDefinition> resources = new Dictionary<string, ResourceDefinition>(StringComparer.OrdinalIgnoreCase);
   5:     public ResourceDefinition RegisterResource(string resourceType, string resourceName)
   6:     {
   7:         ResourceDefinition rd;
   8:         if (!resources.TryGetValue(resourceName, out rd))
   9:         {
  10:             rd = new ResourceDefinition(resourceName, resourceType);
  11:             resources.Add(resourceName, rd);
  12:         }
  13:         return rd;
  14:     }
  15:  
  16:     public IEnumerable<ResourceDefinition> GetResourcesOfType(string resourceType)
  17:     {
  18:         return resources.Values.Where(r => r.Type.Equals(resourceType, StringComparison.OrdinalIgnoreCase));
  19:     }
  20:  
  21:     public ResourceDefinition GetResourceByName(string resourceName)
  22:     {
  23:         ResourceDefinition rd;
  24:         resources.TryGetValue(resourceName, out rd);
  25:         return rd;
  26:     }
  27:         
  28:     public ResourceDefinition RegisterScript(string name)
  29:     {
  30:         return RegisterResource("script", name);
  31:     }
  32:  
  33:     public ResourceDefinition RegisterStylesheet(string name)
  34:     {
  35:         return RegisterResource("link", name);
  36:     }
  37: }
Cuando la aplicación se ejecuta por primera vez es necesario cargar los recursos. En el siguiente fragmento de código se muestra esto:
   1: public class UiResourceBootstrapperTask : IBootstrapperTask
   2: {
   3:     private readonly IResourceManager resourceManager;
   4:  
   5:     public UiResourceBootstrapperTask(IResourceManager resourceManager)
   6:     {
   7:         this.resourceManager = resourceManager;
   8:     }
   9:     
  10:     public void Execute()
  11:     {
  12:         resourceManager.RegisterStylesheet(Styles.Default)
  13:             .SetUrl("Default.css");
  14:         
  15:         resourceManager.RegisterStylesheet(Styles.JqueryUi)
  16:             .SetUrl("themes/custom-theme/jquery-ui-1.8.16.custom.css")
  17:             .SetVersion("1.8.16");
  18:  
  19:         resourceManager.RegisterScript(Scripts.JQuery)
  20:             .SetUrl("jquery-1.6.2.min.js")
  21:             .SetVersion("1.6.2");
  22:  
  23:         resourceManager.RegisterScript(Scripts.JQueryUI)
  24:             .SetUrl("jquery-ui-1.8.14.min.js")
  25:             .SetVersion("1.8.14")
  26:             .SetDependencies(Scripts.JQuery, Styles.JqueryUi);
  27:         
  28:         resourceManager.RegisterScript(Scripts.JQueryValidate)
  29:             .SetUrl("jquery.validate.1.8.1.min.js")
  30:             .SetVersion("1.8.1")
  31:             .SetDependencies(Scripts.JQuery);
  32:  
  33:         resourceManager.RegisterScript(Scripts.JQueryValidateUnobtrusive)
  34:             .SetUrl("jquery.validate.unobtrusive.min.js")
  35:             .SetDependencies(Scripts.JQuery, Scripts.JQueryValidate);
  36:  
  37:         resourceManager.RegisterScript(Scripts.Popups)
  38:             .SetUrl("jquery.tools.min.js") 
  39:             .SetVersion("1.2.6-dev")
  40:             .SetDependencies(Scripts.JQuery);
  41:  
  42:         resourceManager.RegisterScript(Scripts.Tooltips)
  43:             .SetUrl("jquery.tools.min.js")
  44:             .SetVersion("1.2.6-dev")
  45:             .SetDependencies(Scripts.JQuery);
  46:  
  47:         resourceManager.RegisterScript(Scripts.Tiger)
  48:             .SetUrl("tiger.js")
  49:             .SetDependencies(Scripts.JQuery, Scripts.JQueryUI);
  50:  
  51:         resourceManager.RegisterScript(Scripts.TigerLocalization)
  52:             .SetUrl("tiger.localization.js")
  53:             .SetDependencies(Scripts.JQuery, Scripts.Tiger);
  54:  
  55:         resourceManager.RegisterScript(Scripts.TigerDialogs)
  56:             .SetUrl("tiger.dialogs.js")
  57:             .SetDependencies(Scripts.JQuery, Scripts.Popups, Scripts.Tiger, Scripts.TigerLocalization);
  58:     }
  59: }
Del código anterior resaltar que se cuenta con una clase llamada Scripts que no es más que una clase estática con los nombres de los scripts de la aplicación (lo mismo sucede con la clase CSS pero para los estilos), esto es totalmente opcional pero resulta útil. Si se quiere se puede usar alguna herramienta o T4 como es el caso del T4MVC que genera automáticamente los nombres de los Scripts y CSS.

Una vez que se han cargado los recursos nos queda lo fundamental, poder usar estos recursos desde nuestras vistas, controladores, etc. Con este propósito surge la clase RequireResources la cual administra los recursos que se van a descargar en cada petición, la implementación de esta clase es la siguiente:
   1: public class RequiereResources : IRequiereResources
   2: {
   3:     private readonly IResourceManager resourceManager;
   4:  
   5:     private readonly IDictionary<string, ResourceDefinition> loadedResocurces = new Dictionary<string, ResourceDefinition>(StringComparer.OrdinalIgnoreCase);
   6:  
   7:     public RequireSettings Settings { get; set; }
   8:  
   9:     public RequiereResources(IResourceManager resourceManager)
  10:     {
  11:         this.resourceManager = resourceManager;
  12:         Settings = new RequireSettings();
  13:         Settings.DebugMode = System.Web.HttpContext.Current.IsDebuggingEnabled;
  14:         Settings.Culture = System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
  15:     }
  16:  
  17:     public void Requiere(string resourceName)
  18:     {
  19:         if (loadedResocurces.ContainsKey(resourceName))
  20:             return;
  21:  
  22:         var resource = resourceManager.GetResourceByName(resourceName);
  23:         if (resource != null)
  24:         {
  25:             if (resource.Dependencies != null)
  26:             {
  27:                 foreach (var d in resource.Dependencies)
  28:                 {
  29:                     Requiere(d);
  30:                 }
  31:             }
  32:  
  33:             loadedResocurces.Add(resource.Name, resource);
  34:         }
  35:     }
  36:  
  37:     public IEnumerable<string> GetDistinctUrlOfType(string resourceType)
  38:     {
  39:         ISet<string> result = new HashSet<string>();
  40:         
  41:         foreach (var resource in loadedResocurces.Values)
  42:         {
  43:             if (resource.Type.Equals(resourceType, StringComparison.OrdinalIgnoreCase))
  44:             {
  45:                 var url = resource.ResolveUrl(Settings);
  46:                 if (!string.IsNullOrEmpty(url))
  47:                 {
  48:                     if (!result.Contains(url))
  49:                     {
  50:                         result.Add(url);
  51:                     }
  52:                 }
  53:             }
  54:         }
  55:         
  56:         return result;
  57:     }
  58: }
El método Requiere se llama cada vez que se necesita algún recurso. Este método lo primero que hace es verificar si ya el recurso ha sido requerido por algún otro componente, si es así no hay nada que hacer, en caso contrario es necesario cargar todas las dependencias de dicho recurso (nótese que es un método recursivo) además del recurso en sí. Por su parte el método GetDistinctUrlOfType se encarga de retornar una lista con las URL de los recursos que han solicitado los distintos componentes, este método asegura que una URL solo se retorna una vez, esto último es muy importante pues cuando la aplicación se va a desplegar lo más seguro es que los scripts se agrupen de forma lógica en un archivo único con el objetivo de reducir su tamaño así como el número de llamadas al servidor (en este caso se debe crear una clase UiResourceBootstrapperDeploy e indicar en la configuración del contenedor de DI  que es esta la clase que se va a usar en vez de la clase UiResourceBootstrapper).

Es hora de mezclarlo todo. En este caso, para que se pueda usar la administración de recursos desde las vistas (dejo al lector la posibilidad de otros escenarios como pueden ser controladores, helpers, etc.) se crea una clase de la cual deberán heredar todas las demás vistas de la aplicación, su código se muestra a continuación:
   1: public abstract class RazorWebViewPage<TModel> : System.Web.Mvc.WebViewPage<TModel>
   2: {
   3:     public IRequiereResources RequiereResources { get; private set; }
   4:  
   5:     public IScriptManager ScriptManager { get; private set; }
   6:  
   7:     public JqueryHelper Jquery { get; private set; }
   8:  
   9:     public override void InitHelpers()
  10:     {
  11:         base.InitHelpers();
  12:  
  13:         RequiereResources = ServiceLocator.Current.GetInstance<IRequiereResources>();
  14:         ScriptManager = ServiceLocator.Current.GetInstance<IScriptManager>();
  15:     }
  16:  
  17:     public void Requiere(string resourceName)
  18:     {
  19:         RequiereResources.Requiere(resourceName); 
  20:     }
  21:  
  22:     public IEnumerable<string> RequieredStylesheets
  23:     {
  24:         get { return RequiereResources.GetDistinctUrlOfType("link"); }
  25:     }
  26:  
  27:     public IEnumerable<string> RequieredScripts
  28:     {
  29:         get { return RequiereResources.GetDistinctUrlOfType("script"); }
  30:     }
  31: }
Para que todas las vistas de la aplicación hereden de esta clase es necesario modificar el archivo web.config de las vistas quedando de la siguiente forma:
   1: <pages pageBaseType="App.Core.Web.Mvc.ViewEngines.RazorWebViewPage">
La implementación de la clase anterior es bastante sencilla, si se es observador se puede notar que los métodos RequiredStylesheets y RequieredScripts retornan un listado con las URL de los CSS y JavaScript respectivamente, sin embargo, necesitamos transformar esto en código HTML, con este objetivo se construye un Helper que se muestra en el siguiente código:
   1: public static class ResourceHelper
   2: {
   3:     public static MvcHtmlString RenderAsScripts(this IEnumerable<string> urls)
   4:     {
   5:         StringBuilder sb = new StringBuilder();
   6:  
   7:         foreach (var url in urls)
   8:         {
   9:             TagBuilder tag = new TagBuilder("script");
  10:             tag.MergeAttributes(new Dictionary<string, string>{ 
  11:                 {"src", url.StartsWith("http:") ? url : "/scripts/" + url},
  12:                 {"type", "text/javascript"} 
  13:             });
  14:             sb.Append(tag.ToString(TagRenderMode.Normal));
  15:         }
  16:  
  17:         return new MvcHtmlString(sb.ToString());
  18:     }
  19:  
  20:     public static MvcHtmlString RenderAsStylesheets(this IEnumerable<string> urls)
  21:     {
  22:         StringBuilder sb = new StringBuilder();
  23:  
  24:         foreach (var url in urls)
  25:         {
  26:             TagBuilder tag = new TagBuilder("link");
  27:             tag.MergeAttributes(new Dictionary<string, string>{ 
  28:                 {"href", url.StartsWith("http:") ? url : "/styles/" + url},
  29:                 {"rel", "stylesheet"}, 
  30:                 {"type", "text/css"} 
  31:             });
  32:             tag.MergeAttribute("type", "text/javascript");
  33:             sb.Append(tag.ToString(TagRenderMode.Normal));
  34:         }
  35:  
  36:         return new MvcHtmlString(sb.ToString());
  37:     }
  38: }
Los métodos son similares. Ambos aceptan una lista de strings (que deben ser las URL de los recursos que se han cargado) y con esto “dibuja” las etiquetas link o script según el caso. 

Y ya está, todo listo, que comience finalmente el espectáculo. Este es el código de una posible MasterPage (o layout como se quiera) :
   1: @model ViewModel
   2: @{
   3:     Requiere(Styles.Default);
   4:     Requiere(Scripts.Tooltips);
   5:     Requiere(Scripts.Tiger);
   6:     Requiere(Scripts.TigerLocalization);
   7: }
   8: <!DOCTYPE html> 
   9: <html>
  10: <head>
  11:     <title>@Html.Raw(ViewBag.Title)</title>
  12:     @RequieredStylesheets.RenderAsStylesheets()
  13: </head>
  14: <body>
  15:     <div class="section-main">
  16:         HTML Code....
  17:     </div>
  18:  
  19:     @RequieredScripts.RenderAsScripts()
  20:     <script type="text/javascript">
  21:         @Html.Raw(ScriptManager.GetScripts())
  22:     </script>
  23:     @RenderSection("scripts", false)
  24: </body>
  25: </html>
Primero se define que la vista requiere de ciertos scripts y estilos. Luego en la sección Head se renderizan (@RequieredStylesheets.RenderAsStylesheets) los distintos estilos que se han cargado y al final se renderizan los scripts (@RequieresScripts.RenderAsScripts) . Las distintas páginas o componentes irán añadiendo más scripts y estilos pero es en la página maestra donde los mismos se deben de renderizar.

Y esto es todo, ahora podemos separar los archivos de código javascript (o css) y a la vez poder usarlos de manera más cómoda, otra cosa es que lo hagamos ;)

No hay comentarios:

Publicar un comentario