Implementing an XSLT View Engine for ASP.NET MVC

Introduction

ASP.NET MVC 3 is a highly extensible framework that allows us to replace most of the built-in parts in the processing pipeline with custom implementations. As default it comes with two standard view engines: Razor and Web-Forms. The interesting thing is the framework provides extensions that allow us to register even our own custom view engines.

In some web applications the business model is available as XML and developers prefer to use XSLTs for rendering data as HTML. In those cases instead of using the built-in view engines it would be a nice choice to use a XSLT engine that renders the data as HTML. There are couples of XSLT view engines already available: one at MVCContrib and the other at NuGet. In my current project I need a mini XSLT view engine so instead of using the existing ones I implemented one myself.

The XSLT view engine we are going to see here is a simple one and for real scenarios it is worth checking out the existing ones before inventing another.

View Engines and Views

In ASP.NET MVC the logic of finding the views is separated out from the logic of rendering. The View Engines are responsible for finding the views. The Views are responsible for rendering the HTML. One of the powerful feature in ASP.NET MVC is it is not attached to single view engine / view concept. Multiple view engines can be registered to return multiple types of views. You can have a MVC application that returns views using Razor view engine or Web-Forms view engine or both. You can register any no. of view engines in your application.

All the registered view engines can be accessed anywhere through the static class ViewEngines.

public static class ViewEngines
{
	public static ViewEngineCollection Engines { get; }
}

The ViewEngines class contains a single property Engines that contains all the registered view engines. Accessing the Engines property is not thread-safe so adding/removing view engines has to be done in the Application_Start event of the Global.asax.cs.

The order in which the view engines are registered is important. The one that is registered first has the highest priority than the rest. We can easily understand this by creating a view folder that contains both .cshtml and .aspx files with the same name. If a request comes to the particular controller the .aspx view will come into action instead of .cshtml. This is because as default the Web-Forms view engine is registered prior to the Razor view engine. We can change this by wiping out the existing view engine collection and adding the view engines again in reverse order.

IViewEngine and IView

IViewEngine

Directly or indirectly all the view engines should implement this interface.

public interface IViewEngine
{
	ViewEngineResult FindPartialView(ControllerContext controllerContext, 
		string partialViewName, bool useCache);

	ViewEngineResult FindView(ControllerContext controllerContext, 
		string viewName, string masterName, bool useCache);

	void ReleaseView(ControllerContext controllerContext, IView view);
}

The interface contains three methods, two of them are used to find views and one is used to release view. The Find methods are used to search and return views and the Release method is used to perform any clean-up if necessary. The ControllerContext is passed to all the methods which contain details about the controller, HttpContext, RouteData etc.

VirtualPathProviderViewEngine

For our XSLT view engine we can directly implement the IViewEngine but luckily some built-in classes already implements this interface and takes away most of the burden from our shoulders. Like the built-in view engines, our XSLT view engine is also a file-based one, that means in both the Razor and WebForms, the view that is going to render the HTML exist as a template file in a physical folder. In our XSLT view engine also the view is nothing but an XSLT file having extension as ".xsl" or ".xslt".

The logic behind searching the folders and finding the view is pretty same for the built-in view engines and our XSLT view engine. Fortunately the abstract class VirtualPathProviderViewEngine implements the IViewEngine which does most of the job required for the file-based view engines. Both the built-in view engines directly or indirectly derive from this abstract class and of course we are going to inherit this class for our view engine.

public abstract class VirtualPathProviderViewEngine : IViewEngine
{          
	public string[] PartialViewLocationFormats { get; set; }

	public string[] ViewLocationFormats { get; set; }

	// other properties

	protected abstract IView CreatePartialView(ControllerContext controllerContext, string partialPath);

	protected abstract IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath);

	public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);

	public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);

	public virtual void ReleaseView(ControllerContext controllerContext, IView view);

	// other methods
}

The VirtualPathProviderViewEngine implements IViewEngine as virtual methods so we can override them in our classes if required. Most importantly the abstract class has two abstract methods CreatePartialView and CreateView which we must implement. If you notice those methods, they return instances of type IView.

IView

public interface IView
{
	void Render(ViewContext viewContext, TextWriter writer);
}

This interface contains only a single method Render where we put all our logic to generate HTML.

XsltViewEngine and XsltView

XsltViewEngine

Lets implement our view engine class XSLTViewEngine by deriving from VirtualPathProviderViewEngine. Below is the implementation of our view engine.

public class XsltViewEngine : VirtualPathProviderViewEngine
{
	public XsltViewEngine()
	{
		ViewLocationFormats = new[] 
			{ 
				"~/XSLTs/{1}/{0}.xsl", "~/XSLTs/Shared/{0}.xsl", 
				"~/XSLTs/{1}/{0}.xslt", "~/XSLTs/Shared/{0}.xslt" 
			};
		PartialViewLocationFormats = ViewLocationFormats;
	}

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

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

In the constructor we are setting the physical locations where the engine has to search the "xsl" or ".xslt" files. In the Create methods we are instantiating and returning the XsltView which is of type IView.

XsltView

Here is the implementation of our XsltView.

public class XsltView : IView
{
	private readonly string _path;

	public XsltView(string path)
	{
		_path = path;
	}

	public void Render(ViewContext viewContext, TextWriter writer)
	{
		var xsltFile = viewContext.HttpContext.Server.MapPath(_path);
		var xmlData = viewContext.ViewData["data"] != null 
			? ((XElement)viewContext.ViewData["data"]).ToString() 
			: "";

		var xmlTree = XDocument.Parse(xmlData);
		var xslt = new XslCompiledTransform();

		xslt.Load(xsltFile);
		xslt.Transform(xmlTree.CreateReader(), null, writer);
	}
}

In the Render method we are converting the data that is passed in ViewData dictionary to HTML using the built-in XslCompiledTransform class and our xslt file.

So our custom view engine and view are ready and the only thing that is pending is registering the engine.

Registering the XsltViewEngine

protected void Application_Start()
{
	ViewEngines.Engines.Add(new XsltViewEngine());

	AreaRegistration.RegisterAllAreas();

	RegisterGlobalFilters(GlobalFilters.Filters);
	RegisterRoutes(RouteTable.Routes);
}

If you don’t want to use any view engines other than the custom view engine then you have to remove the registered view engines by calling ViewEngines.Engines.Clear() before the registration.

Download Source

blog comments powered by Disqus