Gestione dei temi con ASP.NET MVC

Nella prima versione di ASP.NET MVC non è presente la possibilità di gestire i temi nelle applicazioni: ciò significa che, per tutti i siti sviluppati con questo Framework che necessitino di una forte customizzazione dell’interfaccia web, è necessario realizzare un’implementazione custom in grado di far ciò.

Com’è possibile apprendere dalla roadmap della versione 2.0 di ASP.NET MVC qui, che sarà inglobata in ASP.NET, avrà incluso un sistema per la gestione dei temi; in attesa del rilascio, una possibile soluzione per affrontare il problema è quella di sostituire il WebFormViewEngine con uno realizzato ad hoc, in grado di cambiare la View in base al tema scelto.

Sebbene sia possibile realizzare forti customizzazioni dell’interfaccia cambiando semplicemente il foglio di stile della pagina, a volte questo può non bastare in quanto si necessita di un particolare markup di output, tipo HTML Strict, Transitional, ecc;
per rendersi meglio conto della situazione basta pensare ad un dispositivo mobile, la cui visualizzazione è differente da quella di browser su un normale computer, e alle molte interazioni che da una parte avvengono tramite javascript, dall’altra devono avvenire senza.

L’immagine seguente mostra la nuova struttura per le Views e i Contents di un’applicazione che fa uso del Framework ASP.NET MVC:

 

Temi

Per far sì che l’applicazione cambi il percorso da cui leggere la View, è necessario realizzare un nuovo WebFormViewEngine; nell’esempio, come quello mostrato di seguito, si chiamerà WebFormThemeViewEngine:

public class WebFormThemeViewEngine : WebFormViewEngine
{
    private static readonly string[] masterLocationFormats = new[] { "~/Themes/{2}/Views/{1}/{0}.master", "~/Themes/{2}/Views/Shared/{0}.master" };
    private static readonly string[] viewLocationFormats = new[] { "~/Themes/{2}/Views/{1}/{0}.aspx", "~/Themes/{2}/Views/{1}/{0}.ascx", "~/Themes/{2}/Views/Shared/{0}.aspx", "~/Themes/{2}/Views/Shared/{0}.ascx" };

    public WebFormThemeViewEngine()
    {
        MasterLocationFormats = masterLocationFormats;
        ViewLocationFormats = viewLocationFormats;
        PartialViewLocationFormats = viewLocationFormats;
    }

    protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
    {
        try
        {
            return File.Exists(controllerContext.HttpContext.Server.MapPath(virtualPath));
        }
        catch (HttpException exception)
        {
            if (exception.GetHttpCode() != 0x194)
            {
                throw;
            }
            return false;
        }
        catch
        {
            return false;
        }
    }

    /// <summary>
    /// Finds the view.
    /// </summary>
    /// <param name="controllerContext">The controller context.</param>
    /// <param name="viewName">Name of the view.</param>
    /// <param name="masterName">Name of the master.</param>
    /// <param name="useCache">if set to <c>true</c> [use cache].</param>
    /// <returns>The page view.</returns>
    /// <exception cref="ArgumentNullException"><c>controllerContext</c> is null.</exception>
    /// <exception cref="ArgumentException">viewName must be specified.</exception>
    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        string[] strArray;
        string[] strArray2;

        if (controllerContext == null)
            throw new ArgumentNullException("controllerContext");
        
        if (string.IsNullOrEmpty(viewName))
            throw new ArgumentException("viewName must be specified.", "viewName");

        string themeName = GetThemeToUse(controllerContext);

        string requiredString = controllerContext.RouteData.GetRequiredString("controller");

        string viewPath = GetPath(controllerContext, ViewLocationFormats, viewName, themeName, requiredString, "View", useCache, out strArray);
        string masterPath = GetPath(controllerContext, MasterLocationFormats, masterName, themeName, requiredString, "Master", useCache, out strArray2);

        if (!string.IsNullOrEmpty(viewPath) && (!string.IsNullOrEmpty(masterPath) || string.IsNullOrEmpty(masterName)))
            return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
            
        return new ViewEngineResult(strArray.Union(strArray2));
    }

    /// <summary>
    /// Finds the partial view.
    /// </summary>
    /// <param name="controllerContext">The controller context.</param>
    /// <param name="partialViewName">Partial name of the view.</param>
    /// <param name="useCache">if set to <c>true</c> [use cache].</param>
    /// <returns>The partial view.</returns>
    /// <exception cref="ArgumentNullException"><c>controllerContext</c> is null.</exception>
    /// <exception cref="ArgumentException">partialViewName must be specified.</exception>
    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
    {
        string[] strArray;
        if (controllerContext == null)
            throw new ArgumentNullException("controllerContext");
            
        if (string.IsNullOrEmpty(partialViewName))
            throw new ArgumentException("partialViewName must be specified.", "partialViewName");

        string themeName = GetThemeToUse(controllerContext);

        string requiredString = controllerContext.RouteData.GetRequiredString("controller");
        string partialViewPath = GetPath(controllerContext, PartialViewLocationFormats, partialViewName, themeName, requiredString, "Partial", useCache, out strArray);
        if (string.IsNullOrEmpty(partialViewPath))
        {
            return new ViewEngineResult(strArray);
        }
        return new ViewEngineResult(CreatePartialView(controllerContext, partialViewPath), this);
    }

    private static string GetThemeToUse(ControllerContext controllerContext)
    {
        return controllerContext.HttpContext.Items["theme"] as string ?? "Default";
    }

    /// <summary>
    /// Gets the path.
    /// </summary>
    /// <param name="controllerContext">The controller context.</param>
    /// <param name="locations">The locations.</param>
    /// <param name="name">The name.</param>
    /// <param name="themeName">Name of the theme.</param>
    /// <param name="controllerName">Name of the controller.</param>
    /// <param name="cacheKeyPrefix">The cache key prefix.</param>
    /// <param name="useCache">if set to <c>true</c> [use cache].</param>
    /// <param name="searchedLocations">The searched locations.</param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException">locations must not be null or emtpy.</exception>
    private string GetPath(ControllerContext controllerContext, string[] locations, string name, string themeName, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations)
    {
        searchedLocations = new string[0];
        if (string.IsNullOrEmpty(name))
            return string.Empty;
            
        if ((locations == null) || (locations.Length == 0))
            throw new InvalidOperationException("locations must not be null or emtpy.");

        bool flag = IsSpecificPath(name);
        string key = CreateCacheKey(cacheKeyPrefix, name, flag ? string.Empty : controllerName, themeName);
        
        if (useCache)
        {
            string viewLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, key);
            
            if (viewLocation != null)
                return viewLocation;
        }
        
        if (!flag)
            return GetPathFromGeneralName(controllerContext, locations, name, controllerName, themeName, key, ref searchedLocations);
        
        return GetPathFromSpecificName(controllerContext, name, key, ref searchedLocations);
    }

    private static bool IsSpecificPath(string name)
    {
        char ch = name[0];
        
        if (ch != '~')
            return (ch == '/');
        
        return true;
    }

    private string CreateCacheKey(string prefix, string name, string controllerName, string themeName)
    {
        return string.Format(CultureInfo.InvariantCulture, ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}", new object[] {GetType().AssemblyQualifiedName, prefix, name, controllerName, themeName});
    }

    private string GetPathFromGeneralName(ControllerContext controllerContext, string[] locations, string name, string controllerName, string themeName, string cacheKey, ref string[] searchedLocations)
    {
        string virtualPath = string.Empty;
        searchedLocations = new string[locations.Length];
        for (int i = 0; i < locations.Length; i++)
        {
            string str2 = string.Format(CultureInfo.InvariantCulture, locations[i], new object[] {name, controllerName, themeName});

            if (FileExists(controllerContext, str2))
            {
                searchedLocations = new string[0];
                virtualPath = str2;
                ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
                return virtualPath;
            }
            searchedLocations[i] = str2;
        }
        return virtualPath;
    }

    private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations)
    {
        string virtualPath = name;
        if (!FileExists(controllerContext, name))
        {
            virtualPath = string.Empty;
            searchedLocations = new[] {name};
        }
        ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
        return virtualPath;
    }
}

Come si può notare, basta replicare i metodi delle classi WebFormViewEngine e VirtualPathProviderViewEngine, andando a cambiare i percorsi tramite le 3 variabili impostate dal costruttore dell’engine.
È importante notare il metodo che restituisce il tema corrente, GetThemeToUse, che sarà utilizzato per andare a costruire il path da dove leggere le Views e i Contents.
Nell’esempio mostrato è stata utilizzata la collection Items dell’HttpContext in modo da poter cambiare anche solo provvisoriamente il tema da visualizzare (http://localhost:1659/?theme=yellow), come mostrato dallo snippet seguente:

public abstract class ControllerBase : Controller
{
    private string currentTheme;

    protected override void Initialize(RequestContext requestContext)
    {
        //Andrebbe recuperato dal profilo
        string defaultTheme = "Default";

        string previewTheme = requestContext.HttpContext.Request.QueryString["theme"];

        requestContext.HttpContext.Items["theme"] = !string.IsNullOrEmpty(previewTheme) ? previewTheme : defaultTheme;

        currentTheme = (string)requestContext.HttpContext.Items["theme"];

        base.Initialize(requestContext);

        ViewData["ThemeName"] = currentTheme;
    }
}

Questa funzione può risultare particolamente comoda quando si ha la necessità di vedere un’anteprima di una skin con i propri contenuti.

Per concludere ed utilizzare la nuova skin è necessario registrare il nuovo WebFormViewEngine nel file Global.asax, come mostrato di seguito:

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);

    // Sostituisco il Default WebFormViewEngine con il nostro WebFormThemeViewEngine
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new WebFormThemeViewEngine());
}

Ciauz

 

Download Esempio qui

 


Comments