Creating a custom ViewEngine for the ASP.NET MVC framework

Edit on GitHub

Have you ever seen a presentation of ScottGu about the ASP.NET MVC framework? There is one particular slide that keeps coming back, stating that every step in the ASP.NET MVC life cycle is pluggable. Let's find out if replacing one of these components is actually easy by creating a custom ViewEngine and corresponding view.

 ASP.NET MVC life cycle

Some background

After a route has been determined by the route handler, a Controller is fired up. This Controller sets ViewData, which is afterwards passed into the ViewEngine. In short, the ViewEngine processes the view and provides the view with ViewData from the Controller. Here's the base class:

[code:c#]

public abstract class ViewEngineBase {
     public abstract void RenderView(ViewContext viewContext);
}

[/code]

By default, the ASP.NET MVC framework has a ViewEngine named WebFormsViewEngine. As the name implies, this WebFormsViewEngine is used to render a view which is created using ASP.NET web forms.

The MvcContrib project contains some other ViewEngine implementations like NVelocity, Brail, NHaml, XSLT, ...

What we are going to build...

Rendered viewIn this blog post, we'll build a custom ViewEngine which will render a page like you see on the right from a view with the following syntax:

[code:c#]

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>Custom ViewEngine Demo</title>
</head>
<body>
    <h1>{$ViewData.Title}</h1>
    <p>{$ViewData.Message}</p>
    <p>The following fruit is part of a string array: {$ViewData.FruitStrings[1]}</p>
    <p>The following fruit is part of an object array: {$ViewData.FruitObjects[1].Name}</p>
    <p>Here's an undefined variable: {$UNDEFINED}</p>
</body>
</html>

[/code]

What do we need?

First of all, download the current ASP.NET MVC framework from CodePlex. After creating a new ASP.NET MVC web site, tweak some stuff:

  • Remove /Views/*.*
  • Remove /Content/*.* (unless you want to keep the default CSS files)
  • Add a folder /Code

In order to create a ViewEngine, we will have to do the following:

  • Create a default IControllerFactory which sets the ViewEngine we will create on each Controller
  • Edit Global.asax.cs and register the default controller factory
  • Create a ViewLocator (this one will map a controller + action to a specific file name that contains the view to be rendered)
  • Create a ViewEngine (the actual purpose of this blog post)

Let's do some coding!

1. Creating and registering the IControllerFactory implementation

Of course, ASP.NET MVC has a default factory which creates a Controller instance for each incoming request. This factory takes care of dependency injection, including Controller initialization and the assignment of a ViewEngine. Since this is a good point of entry to plug our own ViewEngine in, we'll create an inherited version of the DefaultControllerFactory:

[code:c#]

public class SimpleControllerFactory : DefaultControllerFactory
{
    protected override IController CreateController(RequestContext requestContext, string controllerName)
    {
        Controller controller = (Controller)base.CreateController(requestContext, controllerName);
        controller.ViewEngine = new SimpleViewEngine(); // <-- will be implemented later in this post
        return controller;
    }
}

[/code]

In order to make this SimpleControllerFactory the default factory, edit the Global.asax.cs file and add the following line of code in the Application_Start event:

[code:c#]

ControllerBuilder.Current.SetControllerFactory(typeof(SimpleControllerFactory));

[/code]

2. Create a ViewLocator

In order for the ViewEngine we'll build to find the correct view for each controller + action combination, we'll have to implement a ViewLocator too:

[code:c#] 

public class SimpleViewLocator : ViewLocator
{
    public SimpleViewLocator()
    {
        base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.htm",
                                                  "~/Views/{1}/{0}.html",
                                                  "~/Views/Shared/{0}.htm",
                                                  "~/Views/Shared/{0}.html"
        };
        base.MasterLocationFormats = new string[] { "" };
    }
}

[/code]

We are actually providing the possible application paths where a view can be stored.

3. Create a ViewEngine

The moment you have been waiting for! The IViewEngine interface requires the following class structure:

[code:c#]

public class SimpleViewEngine : IViewEngine
{
    // ...
    // Private member: IViewLocator _viewLocator = null;
    // Public property: IViewLocator ViewLocator
    // ...

    #region IViewEngine Members

    public void RenderView(ViewContext viewContext)
    {
        string viewLocation = ViewLocator.GetViewLocation(viewContext, viewContext.ViewName);
        if (string.IsNullOrEmpty(viewLocation))
        {
            throw new InvalidOperationException(string.Format("View {0} could not be found.", viewContext.ViewName));
        }

        string viewPath = viewContext.HttpContext.Request.MapPath(viewLocation);
        string viewTemplate = File.ReadAllText(viewPath);

        IRenderer renderer = new PrintRenderer();
        viewTemplate = renderer.Render(viewTemplate, viewContext);

        viewContext.HttpContext.Response.Write(viewTemplate);
    }

    #endregion
}

[/code]

Note that we first locate the view using the ViewLocator, map it to a real path on the server and then render contents directly to the HTTP response. The PrintRenderer class maps {$....} strings in the view to a real variable from ViewData. If you want to see the implementation, please check the download of this example.

Conclusion

Conclusions Replacing the default ViewEngine with a custom made version is actually quite easy! The most difficult part in creating your own ViewEngine implementation will probably be the parsing of your view. Fortunately, there are some examples around which may be a good source of inspiration (see MvcContrib).

If someone wants to use the code snippets I posted to create their own PHP Smarty, please let me know! Smarty is actually quite handy, and might also be useful in ASP.NET MVC.

And yes, it has been a lot of reading, but I did not forget. Download the example code from this blog post: CustomViewEngine.zip (213.17 kb)

kick it on DotNetKicks.com

This is an imported post. It was imported from my old blog using an automated tool and may contain formatting errors and/or broken images.

Leave a Comment

avatar

9 responses

  1. Avatar for Roger
    Roger May 31st, 2008

    I'm curious, would it be possible to store views in a database or somewhere other than the Views folder? I know that routes always look to the Views folder, but if we can change the View Engine, etc, could we not make the framework look to a database for the view?

  2. Avatar for maartenba
    maartenba June 2nd, 2008

    That is perfectly possible. Make sure you create your own viewlocator on top of the WebFormsViewEngine, for example, and you can store your ASP.NET webforms-based views in a database for example.

  3. Avatar for Andrey
    Andrey June 30th, 2008

    It's not posiible to store views in database,because ViewLocator simple return file name in file system,but you can take a look at VirtualPathProvider.That's very weak extensibility point.

  4. Avatar for Scott Hanselman
    Scott Hanselman July 7th, 2008

    Audrey, actually there's nothing that INSISTS that you use ViewLocator. It's just there as a helper. You can ignore it and pull the View page from whereever. That said, you are correct that VirtualPathProvider was actually intended for this purpose. Here's a DbVirtualPathProvider: http://www.dotnetbips.com/a...

  5. Avatar for umbyersw
    umbyersw September 25th, 2008

    Hi, I like your post and tried to follow it, however the property:

    controller.ViewEngine

    was not available when I tried to implement the SimpleControllerFactory. I'm using ASP.NET MVC preview 5. Did I miss something?

    Thanks,
    Wes

  6. Avatar for maartenba
    maartenba September 25th, 2008

    There have been some changes in preview 5, that's correct.

    All ViewEngine implementations for the ASP.NET MVC framework implement the IViewEngine interface:
    public interface IViewEngine
    {
    ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName);
    ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName);
    }

    The only responsibility an IViewEngine implementation has, is finding a view or partial view in the application. When a view has not been found, the implementation should return a list of searched locations. If a view has been found, a ViewEngineResult is returned.
    When a view is required to render, each registered IViewEngine is consulted (in order of registration) until the ASP.NET it finds one that returns a view that can be rendered. If no view can be found, an exception will be thrown.

    A ViewEngine could look like this:

    using System.Web.Mvc;

    namespace CustomViewEngine.Core
    {
    public class SimpleViewEngine : VirtualPathProviderViewEngine
    {
    #region Constructor

    public SimpleViewEngine() : base()
    {
    base.MasterLocationFormats = new string[] { "" };

    base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.htm",
    "~/Views/{1}/{0}.html",
    "~/Views/Shared/{0}.htm",
    "~/Views/Shared/{0}.html"
    };

    base.PartialViewLocationFormats = ViewLocationFormats;
    }

    #endregion

    #region VirtualPathProviderViewEngine Members

    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
    return new SimpleView(partialPath);
    }

    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
    return new SimpleView(viewPath);
    }

    #endregion
    }
    }

    The View itself should implement IView which has one method on which you can render:

    void Render(ViewContext viewContext, TextWriter writer)

  7. Avatar for mazellee
    mazellee October 10th, 2008

    so if controller.ViewEngine isn't available in MVC preview 5. Any other way to find out what ViewEngine a particular type of controller is using?

    Thanks!
    mazel

  8. Avatar for maartenba
    maartenba October 10th, 2008

    Actually, this is no longer possible. MVC is all about separation of concerns: the controller does not have to know which ViewEngine is used. It just knows a name and that specific data (the model) is required.

  9. Avatar for Anon
    Anon February 16th, 2009

    For those interested, here's a great example of implementing a custom Asp.Net MVC view engine using the popular StringTemplate engine.

    http://websitelogic.net/art...