Creating a custom View Engine for ASP.NET MVC leveraging Text Template (T4) engine for rendering the view


ANOOP MADHUSUDANAN

Vote on HN

image

This post explains how to create a View Engine for ASP.NET MVC, leveraging the Text Template (T4) infrastructure already out there for rendering the view based using a custom T4 template host.

Clarification: Here, I’m not using T4 for design time code generation. We are using T4 toolkit to render the views during runtime.

[+] Download Related Source Code

For me, the most beautiful aspect of ASP.NET MVC is it’s extensibility – they way you can ‘stretch’ the framework, to make it suitable for your own needs. I highly recommend you to read this article from Code Climber’s blog - 13 ASP.NET MVC Extensibility Points you have to know

In this post, we’ll explore the following concepts.

  • ViewEngines in ASP.NET MVC
  • Creating a custom ViewEngine for ASP.NET MVC
  • Supporting multiple View Types (our view engine will support both aspx/ascx files and tt files)
  • Partial rendering between view types (you can render a tt view from an aspx view)

Preface About View Engines

This is a quick recap on how the View Engine is invoked with in the ASP.NET MVC Framework. Let us start from how a controller is created and how an action is called. I’m going the easy way - the route handling system in ASP.NET MVC invokes a DefaultControllerFactory by default, which is responsible to choose the correct controller class and an action for a given request. For example, consider the URL http://Portal.com/Customer/Get/3 - As you know, by default, MVC will expect a CustomerController class with a Get Action inside the same, like

   public class CustomerController : Controller
    {
        public ActionResult Get(int id)
        {

           //Get the customer with ID from repository, place it in ViewData
            ViewData["Customer"] = rep.GetCustomerWithId(id);

            //View method returns a Viewresult
            return View();
        }
    }

imageThe controller will invoke the correct action method, which returns an ActionResult object. In the above example, you may see that we are returning a ViewResult. In ASP.NET MVC, there are various action results, including ViewResult, ContentResult etc.

Wow, There we are. If the controller action is returning a ViewResult, the action method can use the ViewData structure (see the above example) to fill it with some values, to pass the same to ViewResult. ViewResult can locate and render a particular view template using ViewData. It does so by invoking WebFormViewEngine.

The default View engine available with ASP.NET MVC is WebFormViewEngine, which creates a WebFormView to render your aspx and ascx files. The View Engine normally passes the path information of the file to render, along with the view context information to the view.

The view file name and path is normally detected based on convention – normally in the path – /Views/{ControllerName}/{ActionName} – i.e, If your Controller class name is CustomerController and action/method name is Get, the default view engine will expect a file in the location /Views/Customer/Get.aspx

Using Text Template Toolkit To Render A View

The beauty of ASP.NET MVC is in it’s extensibility. All interaction points above can be customized the way you like. As of now, we are only interested to see how to create a custom View Engine, which can create a T4View that knows how to render a text template (tt) file, leveraging the Text Templating infrastructure.

Normally, as you are aware, Text Templates (T4) are used with in Visual Studio for activities like Code Generation. When I was going through this exercise of explaining how to create a custom view engine for ASP.NET MVC, I thought it’ll be an interesting exercise to leverage T4 toolkit for the task.

But we’ve got a problem there – We can’t use the Microsoft.VisualStudio.TextTemplating libraries, because I don't believe that T4 can be legally redistributed without Visual Studio. So, I’ve decided to relay on the Mono equivalent T4 implementation, Mono.TextTemplating (included in the download)

Now, let us get in to the actual task. These are the steps we should do to create our view engine.

  • A ViewEngine implementation
    • We’ll create a view engine by implementing the IViewEngine interface. There is an abstract class VirtualPathProviderViewEngine that already implements IViewEngine interface and provides some extra functionality for path detection of view files.  VirtualPathProviderViewEngine  has two methods we are concerned about – CreateView and CreatePartialView from where we should return a custom View, that has the path information to the template file (*.tt) to render.
  • A View
    • We’ll create a view, by implementing the IView interface. IView has a Render method that we are interested about. We’ll read and render the tt file, leveraging the TT engine
  • A T4 Host
    • We’ll also create a custom T4 host, by implementing the ITextTemplatingEngineHost interface, for self hosting the template transformation process.

Once we’ve the above pieces, we need to register our custom view engine with ASP.NET MVC. That’s pretty simple, and we’ll see that soon.

The View Engine Implementation

imageMay be it is time to have a look in to the actual view engine implementation. A slight variation to what we discussed earlier. Instead of creating a ViewEngine from scratch, we are going to inherit our ViewEngine from VirtualPathProviderViewEngine that’s already there in MVC framework – so that all the file path logic will be taken care automatically. VirtualPathProviderViewEngine provides some extra functionality so that we can specify the location formats to look for our view files (in this case, *.tt files), when ever the View Engine is invoked.

Alright. Let us be a bit more creative here. What about creating a View Engine that can handle the aspx and ascx files *along with* the text template files? Creating such a composite view engine is pretty simple – So, this is what our view engine should do.

  • First, the view engine should look for a View file that ends with *.view.tt
    • If that file exists, create and return a T4View that’ll render our tt file
    • If not, look for a View file that ends with *.ascx or *.aspx
      • If exists, create and return a WebFormView that knows how to render the aspx/ascx files.

Enough blabbering. Here we go, the code of our CompositeViewEngine. All the implementations are in MvcT4ViewEngine.Lib project, so you may download the related source code from the above link to have a look at the same side by side.

    /// <summary>
    /// A composite view engine to help plugging view engines
    /// </summary>
    public class CompositeViewEngine : VirtualPathProviderViewEngine
    {
        // Ctor - Let us set all location formats
        public CompositeViewEngine()
        {
            base.MasterLocationFormats = new string[] { "~/Views/{1}/{0}.master", 
                                                        "~/Views/Shared/{0}.master" };
            base.AreaMasterLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.master", 
                                                         "~/Areas/{2}/Views/Shared/{0}.master" };
            base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.view.tt", 
                                                      "~/Views/{1}/{0}.aspx", "~/Views/{1}/{0}.ascx",  
                                                      "~/Views/Shared/{0}.view.tt", 
                                                      "~/Views/Shared/{0}.aspx", "~/Views/Shared/{0}.ascx" };
            base.AreaViewLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.view.tt", 
                                                       "~/Areas/{2}/Views/{1}/{0}.aspx", "~/Areas/{2}/Views/{1}/{0}.ascx",
                                                       "~/Areas/{2}/Views/Shared/{0}.view.tt", 
                                                       "~/Areas/{2}/Views/Shared/{0}.aspx", "~/Areas/{2}/Views/Shared/{0}.ascx" };
            base.PartialViewLocationFormats = base.ViewLocationFormats;
            base.AreaPartialViewLocationFormats = base.AreaViewLocationFormats;
        }

        /// <summary>
        /// Handle the creation of a partial view
        /// </summary>
        protected override IView CreatePartialView
                 (ControllerContext controllerContext, string partialPath)
        {
            if (partialPath.EndsWith(".view.tt"))
                return new T4View(partialPath);
            else
                return new WebFormView(partialPath, null);
        }

        /// <summary>
        /// Handle the creation of a view
        /// </summary>
        protected override IView CreateView
               (ControllerContext controllerContext, string viewPath, string masterPath)
        {
            if (viewPath.EndsWith(".view.tt") && String.IsNullOrEmpty(masterPath))
            {
                return new T4View(viewPath);
            }
            else if (viewPath.EndsWith(".view.tt") && !String.IsNullOrEmpty(masterPath))
            {
                return new T4View(viewPath,masterPath);
            }
            else
                return new WebFormView(viewPath, masterPath);
        }

        /// <summary>
        /// Check if the file exists
        /// </summary>
        protected override bool FileExists
                (ControllerContext controllerContext, string virtualPath)
        {
            return base.FileExists(controllerContext, virtualPath);
        }

       
    }

That looks pretty simple, right? Have a look at the constructor, and you’ll find that we are specifying the path format constraints to detect our T4 view files as well (*.view.tt), along with the aspx and ascx path formats. And you may also find that in the CreateView and CreatePartialView, we are creating and returning a WebFormView, in case the *.vew.tt files are not found. CreatePartialView will be invoked when a partial rendering is requested – e.g. when the user calls a RenderPartial method in a view.

Now, the interesting aspect there is, you can render a text template view from an aspx view, using the Ht

The View Implementation

The T4View implementation is also very simple. We are just invoking our T4 host, to render the tt files. Have a look at the Render method below. You may also note that we are passing the ViewContext to the host, so that we can access the view context later in our text template files, via the host variable.

     /// <summary>
    /// A view based on T4
    /// </summary>
    public class T4View : IView
    {

        #region IView Members

        private string viewName = string.Empty;
        private string masterName = string.Empty;

        public T4View(string ttViewName)
        {
            viewName = ttViewName;
        }
        public T4View(string ttViewName, string masterttName)
        {
            viewName = ttViewName;
            masterName = masterttName;
        }

        /// <summary>
        /// Render our tt file
        /// </summary>
        /// <param name="viewContext"></param>
        /// <param name="writer"></param>
        public void Render(ViewContext viewContext, System.IO.TextWriter writer)
        {
            string filePath = viewContext.HttpContext.Server.MapPath(viewName);
            string masterPath=string.Empty;


            if (!string.IsNullOrEmpty(masterName)) 
            {
                masterPath=viewContext.HttpContext.Server.MapPath(masterName);
            }

            var thost = new T4TemplateHost();
            thost["ViewContext"] = viewContext;


            string data = string.Empty;
            var results = thost.ProcessTemplate(filePath, masterPath, out data);
            if (results.HasErrors)
            {
                writer.WriteLine("<h1>errors found</h1>");
            }
            foreach (var res in results)
            {
                writer.WriteLine("Error - " + (res as CompilerError).ToString());
            }
            writer.Write(data);

        }

        #endregion
    }

About the Template Host

You may see that in the Render method, we are creating an instance of our T4 template host, and requesting the template host to process our our *.view.tt file. You may read more about creating a custom template host here, though I’m not detailing that much. How ever, if you are so curios, here is the ProcessTemplate method in our custom T4 host.

        /// <summary>
        /// Process the input template
        /// </summary>
        /// <returns></returns>
        public CompilerErrorCollection ProcessTemplate
		          (string templateFileName, string masterFileName, out string data)
        {

            if (!File.Exists(templateFileName))
            {
                throw new FileNotFoundException("The file cannot be found");
            }

            var engine = new TemplatingEngine();
            TemplateFile = templateFileName;

            //Read the text template.
            string input = File.ReadAllText(templateFileName);


            if (!string.IsNullOrEmpty(masterFileName))
            {
                input = File.ReadAllText(masterFileName).Replace("<!--[Content]-->",input);
            }

            //Transform the text template.
            data = engine.ProcessTemplate(input, this);
            return Errors;
        }

Registering our View Engine

The last piece of the puzzle would be to register our custom View Engine, so that the framework will use our View Engine instead of the default one. Let us create a new ASP.NET MVC Project. Now in the Global.asax.cs file of our MVC application (See MvcT4ViewEngine.Demo project in the downloaded source code), we need to specify our CompositeViewEngine as the default view engine, in the Application_Start.

    protected void Application_Start()
        {
            ViewEngines.Engines.Clear();
            ViewEngines.Engines.Add(new CompositeViewEngine()); 
            RegisterRoutes(RouteTable.Routes);
        }

And there we go.

 

The Results

First of all, let us add a new GetMessage method to the HomeController ASP.NET MVC project. Our GetMessage action in the Home controller simply returns a view after storing someting in ViewData, like

   public ActionResult GetMessage()
        {
            ViewData["MessageForT4"] = "Welcome to ASP.NET MVC Views using T4";
            return View();
        }
Now, add a new view, named GetMessage.view.tt in the Views/Home  folder as shown below. And you can access the ViewData like this.

image

Now, run the application, and navigate to the path /Home/GetMessage and you should be able to see the above view getting rendered. If you are wondering what the GetViewData method does, it fetches the ViewData context we set to the host earlier, in the above Render method.

image

 

More interestingly, you may also try partially rendering a T4 view from an aspx file. You can use Html.RenderPartial("YourView"); from your aspx view, to render the YourView.view.tt file – See how I’m rendering IndexPart.view.tt from the Index.aspx view, in the attached example.

Conclusion

The intent of this article is just to explore how to create custom view engines for ASP.NET MVC. The example view engine we put together is very elementary as of now, but I’ld like to evolve that towards something useful, so that finally it can be a part of MvcContrib. For this, several performance features like caching of compiled views needs to be implemented. That is for later.

Recommending you to follow me on twitter – Also, read my previous posts – A duck typed view model in ASP.NET MVC or Understanding Managed Extensibility Framework and Lazy<T> – Happy Coding!!

Shout it
© 2012. All Rights Reserved. Amazedsaint.com