Engine MVC baseada em XSLT

Na série sobre produtividade, no post sobre a camada de interface, eu  falei um pouco sobre o uso de XSLT na transformação de arquivos XML para gerar HTML. Várias pessoas me pediram mais detalhes sobre a engine utilizada. Neste artigo, vou entrar em mais detalhes desta engine usando o MS-MVC e fazer um paralelo com a implementação Monorail. Este artigo é bem técnico e pressupõe que o MS-MVC seja bem conhecido, em especial na parte de criação de custom view engines.

No download do MS-MVC existe uma View Engine baseada em XSLT. Existem também outras iniciativas como a Chris Hampson. Estas não nos atenderam porque se baseiam em um arquivo XML que é transformado diretamente pelo XSLT. No nosso caso é necessário injetar, além do arquivo XML de definição, dados que serão utilizados também pelo XSLT pra construir a tela. Finalmente, as atividades realizadas pelo controller também interferem na página resultante. Por exemplo, se uma ação gera um erro, a página resultante deve ser um redirecionamento para a página de tratamento de erros. Estas razões fizeram com que a gente desenvolvesse a nossa própria ViewEngine.

Em linhas gerais, a nossa ViewEngine simplesmente obtém o XML de definição, o XML de dados (que chamamos de DataIsland) e aplica um XSLT para gerar um HTML resultante (ver o post sobre camada de interface para exemplos destes artefatos). Para fazer isto no MS-MVC, é necessário implementar duas classes: uma que implemente a interface IViewEngine e uma que implemente a IView. Além destas duas, a implementação de nossa engine usa um controller base, para que possamos interceptar alguns métodos (ver adiante).

A IViewEngine tem que implementar os métodos FindView, ReleaseView e o FindPartialView. No nosso caso, a única coisa importante é o FindView. Este método é responsável por identificar o arquivo de definição XML e passar o tipo de ação e dados de apoio para a ViewBase, conforme código a seguir:

private ViewEngineResult FindView(ControllerContext controllerContext) {
            
            var server = controllerContext.HttpContext.Server;
            const string extension = "html";
            var area = string.Empty;

            if (controllerContext.RouteData.Values.ContainsKey("area")) 
                area = controllerContext.RouteData.Values["area"] + "/";

            var controller = (BaseController)controllerContext.Controller;
            var controllerName = 
                controller.GetType().Name.Replace("Controller", "")
                   .ToLowerInvariant();
            var path = string.Empty;
            if (controller.OutputType == OutputType.Html || 
                controller.OutputType == OutputType.XmlView) {
                if (controller.ViewNameOrigin == 
                        BaseController.ViewNameOriginType.Controller 
                    && string.IsNullOrEmpty(controller.ViewName))
                    path += string.Format("~/views/{0}{1}.{2}", area, 
                            controllerName, extension);
                else
                    path += string.Format("~/views/{0}{1}/{2}.{3}", area, 
                        controllerName, controller.ViewName ?? 
                        controller.ActionName, extension);
                if (!File.Exists(server.MapPath(path)))
                    return new ViewEngineResult(new[] { path });
                path = server.MapPath(path);
            }

            var dataIsland = 
                  (controller.OutputType != OutputType.XmlAction) ? 
                                controller.GetDataIsland() : null;
            var view = new XsltView(controller.OutputType, 
                controllerContext.RouteData.Values["area"].ToString(), 
                path, controllerName, controller.ActionName,  
                dataIsland == null ? null : dataIsland.Root, 
                controller.ElementsToRender, controller.Message, 
                controller.RedirectUrl);          
            return new ViewEngineResult(view, this);
        }

 

Como pode ser visto acima, temos dois casos de arquivos de definição. Alguns que tem o próprio nome do controller, e alguns que tem o nome da action, situados em um folder com o nome do controller. A última parte deste método chama o construtor da XsltView, que implementa a IView.

A XsltView é que contém o código principal da ViwEngine. Ela é responsável por efetivamente fazer a transformação XSLT e depois fazer escrever o HTML gerado. A transformação no nosso caso é um pouco mais complexa do que simplesmente rodar o Transform() de uma classe XsltCompiledTransform do C#. A gente faz também uma série de manipulações no XML de origem, fazendo a junção dele com o XML do DataIsland, alterando paths relativos (por exemplo, substituindo string ‘~/’ pelo path físico da aplicação) e suportando áreas que possuem XSLTs diferentes (por exemplo, temos áreas que simplesmente injetam HTML final; temos outras que usam pequenos templates XSLTs para cada controle; e outras que simplesmente fazem referência a um XSLT externo). Esta transformação poderia ser tema de um artigo por si só, quem tiver interesse em saber mais sobre ela, envie-me um email direto.

O método Render da XsltView é mostrado a seguir. Como mencionei acima, dependendo da ação executda nós podemos ter vários resultados possíveis. Temos HTML simples e, para chamadas que foram feitas por AJAX, temos opção de mandar mensagens de alerta, trechos de HTML para serem substituídos ou simples comandos de Redirect. Para suportar isto, fizemos uma mensagem XML que é decodificada por um arquivo .js que é responsável por tomar a decisão correta do que fazer com base nos elementos enviados.

public void Render(ViewContext viewContext, TextWriter writer) {
  XElement result;
  if (outputType == OutputType.Html) {
      var ns = GetNamespaces(contents.Document);
      if (!String.IsNullOrEmpty(redirectUrl)) {
       writer.Write("<html><script language='javascript'>location.href='" 
           + redirectUrl + "';</script></html>");
       return;
      }
      else {
       result = contents.XPathSelectElement(HtmlElementPath, ns);
       writer.Write(@"<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 
 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'>");
      }
  }
  else {
      viewContext.HttpContext.Response.ContentType = "text/xml";
      result = new XElement("Result");                
      if (!string.IsNullOrEmpty(message))
        result.Add(new XElement("Message", message));       
        if (!String.IsNullOrEmpty(redirectUrl)) {
           result.Add(new XAttribute("status", 
                       hasView ? "redirect" : "ok"));
           result.Add(new XElement("Redirect", redirectUrl));
        }
        else {                    
           switch (outputType) {
             case OutputType.XmlAction:
                result.Add(new XAttribute("status",  "ok"));
                break;
             case OutputType.XmlMessage:
                result.Add(new XAttribute("status",  "message"));
                break;
             case OutputType.XmlDataOnly:
                result.Add(new XAttribute("status", "ok"));
                result.Add(contents);
                break;
             case OutputType.XmlView:
                result.Add(new XAttribute("status", "ok"));
                if (elementsToRender != null) {
                   foreach (var element in elementsToRender) {
                      var ns = GetNamespaces(contents.Document);
                      var idName = element;
                      if (element == "Form" || element == "List"
                            || element == "CRUDList") {
                         idName = element + "Contents";
                         result.Add(
                           new XAttribute("configurePage", "true"));
                      }
                      var dataContents =
                      contents.XPathSelectElement(
                      String.Format("//*[name()='div' and @id='{0}']",
                             idName), ns);
                      if (dataContents == null) continue;
                      var item = new XElement("Data");
                      item.Add(new XElement("Id", idName));
                      var html = new XElement("Contents");
                      html.Add(new XCData(dataContents.ToString()));
                      item.Add(html);
                      result.Add(item);
                   }
                }
                break;
          }
     }
   }
   writer.Write(result.ToString());
}

O último componente da engine é o BaseController. Este controller faz o override de dois métodos do MS-MVC, o primeiro é o OnActionExecuting e OnActionExecuted, mostrados a seguir.

protected override void OnActionExecuting(
                                     ActionExecutingContext context) {
    ActionName = 
             context.ActionDescriptor.ActionName.ToLowerInvariant();
    if (HasAttribute<XmlResultAttribute>(context.ActionDescriptor)) {
        OutputType = OutputType.XmlView;
        var viewName = GetViewName(context.ActionDescriptor);
        if (!string.IsNullOrEmpty(viewName)) ViewName = viewName;
    }
    else 
        if (HasAttribute<ActionResultAttribute>(
                                   context.ActionDescriptor)) {
            OutputType = OutputType.XmlAction;
        }
        else
            if (HasAttribute<DataOnlyResultAttribute>(
                                    context.ActionDescriptor))
                OutputType = OutputType.XmlDataOnly;
            else {
                if (HasAttribute <HtmlResultAttribute>(
                                       context.ActionDescriptor)) {
                    var viewName =
                             GetViewName(context.ActionDescriptor);
                    if (!string.IsNullOrEmpty(viewName)) 
                              ViewName = viewName;
                }
                OutputType = OutputType.Html;
            }
    base.OnActionExecuting(context);
}

protected override void OnActionExecuted(ActionExecutedContext context) {
    if (context.Exception != null && !context.ExceptionHandled) {
        if (OutputType != OutputType.Html) {
            ProcessException(context.Exception);
            context.ExceptionHandled = true;
        }
    }
    base.OnActionExecuted(context);
}

Como pode ser visto, antes da execução, através de atributos da action, é possível determinar qual o tipo de retorno desejado. E após a execução, caso tenha ocorrido alguma exceção, é feito um tratamento da mesma.

Acho que deu para ter uma idéia de como a nossa engine foi construída. No Monorail, tudo funciona exatamente da mesma forma, a única diferença são nomes diferentes para interfaces e métodos do controller. No Monorail ao invés da IViewEngine, temos que herdar da ViewEngineBase. Não há interface específica para a View pois quem faz o Render é a própria ViewEngineBase. E no controller, o método a sofrer override é o InvokeMethod. Se alguém desejar informações específicas sobre o Monorail ou sobre qualquer outra área desta engine, basta me mandar um email.

, , , , , ,

  1. Deixe um comentário

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: