Imagine um cenário onde seu aplicativo web é acessado por várias empresas e você quer que cada empresa possa personalizar o website com uma skin diferente. ASP.NET MVC 2 permite que você crie temas com pouco esforço.

Vamos lembrar alguns conceitos e aí partimos para o código. MVC é dividido em 3 responsabilidades Model, View e Controller. O ponto chave para conseguirmos usar temas no MVC é atuar em cima das Views.

O código a seguir irá fazer o seguinte:

  1. Identificar pela Url qual tema está sendo requisitado.
  2. Definir o diretório onde as Views deverão ser procuradas para a requisição atual.
  3. Prosseguir normalmente com o ciclo da requisição.

Configurar estrutura de pastas

O template ASP.NET MVC 2 Web Application separa em 3 pastas os modelos, as views e os controladores. Nós precisamos fazer alguns pequenos ajustes na estrutura de pastas para termos os nossos temas separadinhos uns dos outros. Vamos ao passo a passo das mudanças:

  1. Criar uma pasta na raiz chamada Themes.
  2. Criar uma subpasta chamada Default.
  3. Mover as pastas Content, Scripts e Views para dentro de default.
  4. Editar todas as referências ao arquivo Site.Master.
    Use MasterPageFile="../Shared/Site.Master”
  5. Crie dentro da pastas temas, duas pastas uma “Black” e outra “Orange”. Copie todo o conteúdo da pasta Default em cada uma dessas pastas.
  6. Edite o arquivo background do arquivo “\Themes\Black\Content\Site.css” para algo assim: “background-color: black;”
  7. Edite o arquivo background do arquivo “\Themes\Orange\Content\Site.css” para algo assim: “background-color: orange;”

Seus arquivos devem estar parecendo com isto:

image

Identificar o tema requisitado

Precisamos identificar qual tema o usuário quer exibir, nós temos várias formas de fazer isso. Vamos a algumas sugestões:

  1. O usuário autenticado pode definir no seu perfil qual tema quer usar.
  2. Capturamos na URL uma identificação de um tema.

Para o exemplo desse artigo ser o mais simples possível, deixo em aberto a forma como você irá decidir qual tema exibir. Use o seguinte código e alterne manualmente qual tema será usado:

public class WebFormThemeViewEngine : WebFormViewEngine
{

    public WebFormThemeViewEngine()
    {

        //
        // Define os caminhos possíveis as Masterpages
        //
        base.MasterLocationFormats = new string[] {
                "~/Themes/{2}/Views/{1}/{0}.master", 
                "~/Themes/{2}/Views/Shared/{0}.master",
                "~/Themes/Default/Views/{1}/{0}.master", 
                "~/Themes/Default/Views/Shared/{0}.master"
            };


        //
        // Define os caminhos possíveis as Views
        //
        base.ViewLocationFormats = new string[] { 
                "~/Themes/{2}/Views/{1}/{0}.aspx", 
                "~/Themes/{2}/Views/{1}/{0}.ascx", 
                "~/Themes/{2}/Views/Shared/{0}.aspx", 
                "~/Themes/{2}/Views/Shared/{0}.ascx",
                "~/Themes/Default/Views/{1}/{0}.aspx", 
                "~/Themes/Default/Views/{1}/{0}.ascx", 
                "~/Themes/Default/Views/Shared/{0}.aspx", 
                "~/Themes/Default/Views/Shared/{0}.ascx"
            };

        //
        // Define os caminhos possíveis as PartialViews
        //
        base.PartialViewLocationFormats = new string[] {
                "~/Themes/{2}/Views/{1}/{0}.aspx",
                "~/Themes/{2}/Views/{1}/{0}.ascx",
                "~/Themes/{2}/Views/Shared/{0}.aspx",
                "~/Themes/{2}/Views/Shared/{0}.ascx",
                "~/Themes/Default/Views/{1}/{0}.aspx",
                "~/Themes/Default/Views/{1}/{0}.ascx",
                "~/Themes/Default/Views/Shared/{0}.aspx",
                "~/Themes/Default/Views/Shared/{0}.ascx"
            };

    }

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

    public override ViewEngineResult FindView(
        ControllerContext controllerContext,
        string viewName, string masterName, bool useCache)
    {
        string[] searchedLocationsViewPath;
        string[] searchedLocationsMasterPath;

        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }

        if (string.IsNullOrEmpty(viewName))
        {
            throw new ArgumentException("viewName must be specified.", "viewName");
        }

        string themeName = Application.GetTheme(HttpContext.Current.Request.Url.Host);

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

        string viewPath = this.GetPath(controllerContext,
            this.ViewLocationFormats, "ViewLocationFormats",
                viewName, themeName, requiredString, "View",
                useCache, out searchedLocationsViewPath);

        string masterPath = this.GetPath(controllerContext,
            this.MasterLocationFormats, "MasterLocationFormats",
                masterName, themeName, requiredString, "Master",
                useCache, out searchedLocationsMasterPath);
        try
        {

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

        }
        catch (Exception)
        {
            throw new Exception(string.Format("There is an error with View '{0}' or MastePage '{1}' or Master '{2}'.",
                viewPath, masterPath, masterName));
        }

        return new ViewEngineResult(searchedLocationsViewPath.Union(searchedLocationsMasterPath));
    }

    public override ViewEngineResult FindPartialView(
        ControllerContext controllerContext,
        string partialViewName, bool useCache)
    {
        string[] searchedLocations;

        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }

        if (string.IsNullOrEmpty(partialViewName))
        {
            throw new ArgumentException("partialViewName must be specified.", "partialViewName");
        }

        string themeName = Application.GetTheme(HttpContext.Current.Request.Url.Host);

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

        string partialViewPath = this.GetPath(controllerContext,
            this.PartialViewLocationFormats,
                "PartialViewLocationFormats", partialViewName,
                themeName, requiredString, "Partial", useCache,
                out searchedLocations);

        if (string.IsNullOrEmpty(partialViewPath))
        {
            return new ViewEngineResult(searchedLocations);
        }

        return new ViewEngineResult(this.CreatePartialView(controllerContext,
            partialViewPath), this);
    }

    private string GetPath(ControllerContext controllerContext,
        string[] locations, string locationsPropertyName,
        string name, string themeName, string controllerName,
        string cacheKeyPrefix, bool useCache, out string[] searchedLocations)
    {
        searchedLocations = null;

        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 = this.CreateCacheKey(
            cacheKeyPrefix, name, flag ? string.Empty : controllerName, themeName);

        if (useCache)
        {

            string viewLocation = this.ViewLocationCache.GetViewLocation(controllerContext.HttpContext, key);

            if (viewLocation != null)
            {
                return viewLocation;
            }

        }

        if (!flag)
        {
            return this.GetPathFromGeneralName(controllerContext,
                locations, name, controllerName, themeName,
                key, ref searchedLocations);
        }

        return this.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[] { base.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 (this.FileExists(controllerContext, str2))
            {
                searchedLocations = null;
                virtualPath = str2;
                this.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 (!this.FileExists(controllerContext, name))
        {
            virtualPath = string.Empty;
            searchedLocations = new string[] { name };
        }

        this.ViewLocationCache.InsertViewLocation(
            controllerContext.HttpContext, cacheKey, virtualPath);
        return virtualPath;
    }
}

Agora que temos uma ViewEngine precisamos sobrescrever o funcionamento padrão de busca por views do framework. Você precisa criar o seguinte código no Global.asax.

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
    //    
    // Substituímos o ViewEngine padrão pelo nosso ViewEngine personalizado    
    //    
    System.Web.Mvc.ViewEngines.Engines.Clear();    
    System.Web.Mvc.ViewEngines.Engines.Add(new ViewEngines.WebFormThemeViewEngine());
}

Oh! Yes Baby!

image

image

image

Eu base-ei o meu código no artigo do Chris Pietschmann vale a pena dar uma lida no post dele sobre o assunto e entender mais alguns detalhes dessa implementação tão simples de temas em MVC.

Download

Quer ver o aplicativo funcionando? Baixe a solução.

Escrevam suas dúvidas e sugestões nos comentários.