Customizing and Extending the ASP.NET MVC Framework

From ThemesWiki

Jump to: navigation, search
Customizing and Extending the ASP.NET MVC Framework
Official Page
Project Documentation
Download
Source Book
200px-184719754X.jpg
ISBN 978-1-847197-54-2
Publisher Packt Publishing
Author(s) Maarten Balliauw

One of the driving goals for the ASP.NET MVC framework has been to create a flexible framework in which every component can be extended or replaced by a custom solution, whether developed by you or obtained from a third-party vendor. This tutorial describes how you can customize and extend the ASP.NET MVC framework: from creating a control and creating a custom ActionResult to creating your own view engine.

You will learn the following in this tutorial:

  • How to extend the ASP.NET MVC framework
  • How to create a control, or a so-called partial view
  • More about filter attributes and how to create one
  • How to create a custom ActionResult that displays an image containing text based on a controller's action method
  • How to create your own ViewEngine and IView, supporting simple HTML markup that contains entries from the ViewData dictionary

Contents

[edit] Creating a control

When building applications, you probably also build controls. Controls are re-usable components that contain functionality that can be re-used in different locations. In ASP.NET Webforms, a control is much like an ASP.NET web page. You can add existing web server controls and markup to a custom control and define properties and methods for it. When, for example, a button on the control is clicked, the page is posted back to the server that performs the actions required by the control.

The ASP.NET MVC framework does not support ViewState and postbacks, and therefore, cannot handle events that occur in the control. In ASP.NET MVC, controls are mainly re-usable portions of a view, called partial views, which can be used to display static HTML and generated content, based on ViewData received from a controller. In this topic, we will create a control to display employee details. We will start by creating a new ASP.NET MVC application using File | New | Project in Visual Studio, and selecting ASP.NET MVC Application under Visual C# - Web. First of all, we will create a new Employee class inside the Models folder. The code for this Employee class is:

public class Employee
 {
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string Email { get; set; }
  public string Department { get; set; }
 }

On the home page of our web application, we will list all of our employees. In order to do this, modify the Index action method of the HomeController to pass a list of employees to the view in the ViewData dictionary. Here's an example that creates a list of two employees and passes it to the view:

 public ActionResult Index()
 {
  ViewData["Title"] = "Home Page";
  ViewData["Message"] = "Our employees welcome you to our site!";
  List<Employee> employees = new List<Employee>
  {
  new Employee{
  FirstName = "Maarten",
  LastName = "Balliauw",
  Email = "maarten@maartenballiauw.be",
  Department = "Development"
  },
  new Employee{
  FirstName = "John",
  LastName = "Kimble",
  Email = "john@example.com",
  Department = "Development"
  }
  };
  return View(employees);
 }

The corresponding view, Index.aspx in the Views | Home folder of our ASP.NET MVC application, should be modified to accept a List<Employee> as a model. To do this, edit the code behind the Index.aspx.cs file and modify its contents as follows:

 using System.Collections.Generic;
 using System.Web.Mvc;
 using ControlExample.Models;
 namespace ControlExample.Views.Home
 {
  public partial class Index : ViewPage<List<Employee>>
  {
  }
 }

In the Index.aspx view, we can now use this list of employees. Because we will display details of more than one employee somewhere else in our ASP.NET MVC web application, let's make this a partial view.

Right-click the Views | Shared folder, click on Add | New Item and select the MVC View User Control item template under Visual C# | Web | MVC. Name the partial view, DisplayEmployee.ascx.

The ASP.NET MVC framework provides the flexibility to use a strong-typed version of the ViewUserControl class, just as the ViewPage class does. The key difference between ViewUserControl and ViewUserControl<T> is that with the latter, the type of view data is explicitly passed in, wherease the non-generic version will contain only a dictionary of objects. Because the DisplayEmployee.aspx partial view will be used to render items of the type Employee, we can modify the DisplayEmployee.ascx code behind the file DisplayEmployee.ascx.cs and make it strong-typed:

 using ControlExample.Models;
 namespace ControlExample.Views.Shared
 {
  public partial class DisplayEmployee : System.Web.Mvc.ViewUserControl<Employee>
  {
  }
 }

In the view markup of our partial view, the model can now be easily referenced. Just as with a regular ViewPage, the ViewUserControl will have a ViewData property containing a Model property of the type Employee. Add the following code to DisplayEmployee.ascx:

 <%@ Control Language="C#" AutoEventWireup="true" 
 CodeBehind="DisplayEmployee.ascx.cs" 
 Inheits="ControlExample.Views.Shared.DisplayEmployee" %>
 <%=Html.Encode(Model.LastName)%>, 
 <%=Html.Encode(Model.FirstName)%><br />
 <em><%=Html.Encode(Model.Department)%></em>

The control can now be used on any view or control in the application. In the Views | Home | Index.aspx view, use the Model property (which is a List<Employee>) and render the control that we have just created for each employee:

 <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
 AutoEventWireup="true"  CodeBehind="Index.aspx.cs" 
 Inherits="ControlExample.Views.Home.Index" %>
<asp:Content ID="indexContent" 
 ContentPlaceHolderID="MainContent" 
 runat="server">
  <h2><%= Html.Encode(ViewData["Message"]) %></h2>
  <p>Here are our employees:</p>
  <ul>
  <% foreach (var employee inModel) { %>
  <li> <% Html.RenderPartial("DisplayEmployee", employee); %> </li>
  <% } %>
  </ul>
 </asp:Content>

In case the control's ViewData type is equal to the view page's ViewData type, another method of rendering can also be used. This method is similar to ASP.NET Webforms controls, and allows you to specify a control as a tag. Optionally, a ViewDataKey can be specified. The control will then fetch its data from the ViewData dictionary entry having this key.

 <uc1:EmployeeDetails ID="EmployeeDetails1" runat="server" ViewDataKey="...." />

For example, if the ViewData contains a key emp that is filled with an Employee instance, the user control could be rendered using the following markup: <uc1:EmployeeDetails ID="EmployeeDetails1" runat="server" ViewDataKey="emp" />

After running the ASP.NET MVC web application, the result will appear as shown in the following screenshot:

[edit] Creating a filter attribute

An action filter is an attribute that can be applied to a controller class or an action method. Whenever the controller or action method is called, the action filter will be triggered both before and after the execution. Typically, action filters are used for solving problems that can occur in more than one class the so called cross-cutting concerns. A typical cross-cutting concern is output caching or authentication both can be required for more than one action method. More information about action filters can be found in Tutorial 4, Components in the ASP.NET MVC Framework.

One cross-cutting concern of an action method might be logging. For example, one wants to log when an action was called, and whether its result was executed, in a log file as shown here:

 [2008-09-02 - 03:03:13] Controller: Home; Action: Index; Action executing...
 [2008-09-02 - 03:03:13] Controller: Home; Action: Index; Action executed.
 [2008-09-02 - 03:03:13] Controller: Home; Action: Index; Result executing...
 [2008-09-02 - 03:03:15] Controller: Home; Action: Index; Result executed.
 [2008-09-02 - 03:04:42] Controller: Account; Action: Login; Action executing...
 [2008-09-02 - 03:04:42] Controller: Account; Action: Login; Action executed.
 [2008-09-02 - 03:04:42] Controller: Account; Action: Login; Result executing...
 [2008-09-02 - 03:04:43] Controller: Account; Action: Login; Result executed.
 [2008-09-02 - 03:04:44] Controller: Home; Action: About; Action executing...
 [2008-09-02 - 03:04:44] Controller: Home; Action: About; Action executed.
 [2008-09-02 - 03:04:44] Controller: Home; Action: About; Result executing...
 [2008-09-02 - 03:04:44] Controller: Home; Action: About; Result executed.

To achieve this, a filter attribute can be created by implementing the IActionFilter and IResultFilter interfaces, and optionally overloading the FilterAttribute class. An action filter allows you to run code before and after an action method is called, but before the result of the action method is executed. A result filter is very similar to an action filter except that it is executed before and after the result is returned from the action that has been executed.

The IActionFilter interface defines two methods:

  • OnActionExecuting Runs before the action method is executed.
  • OnActionExecuted Runs after the action method is executed, but before the ActionResult is executed.

The IResultFilter interface defines two methods:

  • OnResultExecuting Runs before the action method's action result is executed
  • OnResultExecuted Runs after the action method's action result is executed.

Let's create a class called LoggingAttribute which implements these two interfaces, and will run whenever an action is executing or an action result is executing. The example in this topic is based on an ASP.NET MVC web application, which can be found in the sample code for this tutorial (ActionFilterExample).

First of all, let's define the class and apply the AttributeUsage attribute to it. This attribute tells the compiler that the LoggingAttribute we are creating can only be applied to classes and methods. The LoggingAttribute class implements IActionFilter and IResultFilter. We also add a property named LogName, which will hold the path to our log file.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
 public class LoggingAttribute : FilterAttribute, IActionFilter, IResultFilter
 {
  #region Properties
   public string LogName { get; set; }
  #endregion
 }

Because we will be logging some things, let's also add a method that writes a log message to the file referenced by the LogName property. This method will simply open the file, append a new line to it, and close it again.

 private void LogMessage(string controller, string action, string message)
 {
  if (!string.IsNullOrEmpty(LogName))
  {
  TextWriter writer = new StreamWriter(LogName, true);
  writer.WriteLine("[{0}] Controller: {1}; Action: {2}; {3}",
  DateTime.Now.ToString("yyyy-MM-dd - hh:mm:ss"), controller, action, message);
  writer.Close();
  }
 }

Now, it is time to implement the IActionFilter and IResultFilter interfaces. For the IActionFilter, we'll add the OnActionExecuting and OnActionExecuted methods. For IResultFilter, we'll add the OnResultExecuting and OnResultExecuted methods. All of these methods will use the LogMessage method that we've just created and pass in some information for logging to the file.

 public void OnActionExecuting(ActionExecutingContext filterContext)
 {
  LogMessage(
  filterContext.RouteData.Values["controller"].ToString(),
  filterContext.RouteData.Values["action"].ToString(),
  "Action executing..."
  );
 }
  public void OnActionExecuted(ActionExecutedContext filterContext)
 {
  LogMessage(
  filterContext.RouteData.Values["controller"].ToString(),
  filterContext.RouteData.Values["action"].ToString(),
  "Action executed."
  );
 }
  public void OnResultExecuting(ResultExecutingContext filterContext)
 {
  LogMessage(
  filterContext.RouteData.Values["controller"].ToString(),
  filterContext.RouteData.Values["action"].ToString(),
  "Result executing..."
  );
 }
  public void OnResultExecuted(ResultExecutedContext filterContext)
 {
  LogMessage(
  filterContext.RouteData.Values["controller"].ToString(),
  filterContext.RouteData.Values["action"].ToString(),
  "Result executed.".
  );
 }

The source of information that is being logged originates in the parameter passed to the On methods. This is always an object containing information about the current execution context, such as the controller that is executing, the view that is being rendered, and HTTP request data.

Here's the full LoggingAttribute class that we have been creating:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
 public class LoggingAttribute : FilterAttribute, IActionFilter, IResultFilter
 {
  #region Properties
  public string LogName { get; set; }
  #endregion
  #region Helper methods
  private void LogMessage(string controller, string action, string message)
  {
  if (!string.IsNullOrEmpty(LogName))
  {
  TextWriter writer = new StreamWriter(LogName, true);
  writer.WriteLine("[{0}] Controller: {1}; Action: {2}; {3}",
  DateTime.Now.ToString("yyyy-MM-dd - hh:mm:ss"), controller, action, message);
  writer.Close();
  }
  }
  #endregion
  #region IActionFilter Members
  public void OnActionExecuting(ActionExecutingContext filterContext)
  {
  LogMessage(
  filterContext.RouteData.Values["controller"].ToString(),
  filterContext.RouteData.Values["action"].ToString(),
  "Action executing..."
  );
  }
  public void OnActionExecuted(ActionExecutedContext filterContext)
  {
  LogMessage(
  filterContext.RouteData.Values["controller"].ToString(),
  filterContext.RouteData.Values["action"].ToString(),
  "Action executed."
  );
  }
  #endregion
  #region IResultFilter Members
  public void OnResultExecuting(ResultExecutingContext filterContext)
  {
  LogMessage(
  filterContext.RouteData.Values["controller"].ToString(),
  filterContext.RouteData.Values["action"].ToString(),
  "Result executing..."
  );
  }
  public void OnResultExecuted(ResultExecutedContext filterContext)
  {
  LogMessage(
  filterContext.RouteData.Values["controller"].ToString(),
  filterContext.RouteData.Values["action"].ToString(),
  "Result executed."
  );
  }
  #endregion}

The filter can now be applied to any controller, which in turn will apply the filter to all of the action methods, or to individual action methods:

[Logging(LogName = "C:\\temp\\ApplicationLog.log")]
 public class HomeController : Controller
 {
  // ...
  [Logging(LogName = "C:\\temp\\ActionLog.log")]
  public ActionResult SomeAction() {
  // ...
  }
 }

Now, when the SomeAction action method of the HomeController is called, a log entry will be created when the view is being rendered.

[edit] Creating a custom ActionResult

The ASP.NET MVC framework's action method implements the concept of returning an ActionResult instance, which will typically render a specific view, or redirect the user to a different location on the web site. An ActionResult that renders a view is returned as a RenderViewResult . The ExecuteResult() method is called in order to render specific contents to the HTTP response stream.

An ActionResult can take any form, as we have seen in Tutorial 4, as long as it has something to do with the HTTP response stream. For example, you can create a FileDownloadResult that streams a file on the HTTP response stream, or a PermanentRedirectResult that renders HTTP status code 302.

One of the problems that many web designers face is the fact that a user's web browser may or may not have the specific fonts needed to display the contents. This may be a problem, say, if the web designer wants to style a title element in some exotic font face. Because the ASP.NET MVC framework has a modular architecture, a custom ActionResult class can easily be created to achieve this goal. This custom ActionResult will render a JPEG image based on input text, which can be used to display a page title. This ActionResult class will be named ImageResult.

The example in this topic is based on an ASP.NET MVC web application that can be found in the sample code for this tutorial (CustomActionResultExample). The Code folder contains the ImageResult that we will build. The new ImageResult class will inherit the abstract class ActionResult and implement its ExecuteResult method. This method basically performs communication over the HTTP response stream. It accepts an Image object as a property as well as an ImageFormat. This means that a custom image can easily be rendered to the HTTP response stream, whether as JPEG, BMP, PNG, or GIF.

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Web.Mvc;

namespace CustomActionResultExample.Code
{
 public class ImageResult : ActionResult
 {
 public ImageResult() { }

 public Image Image { get; set; }
 public ImageFormat ImageFormat { get; set; }

 public override void ExecuteResult(ControllerContext context)
 {
 // verify properties
 if (Image == null)
 {
 throw new ArgumentNullException("Image");
 }
 if (ImageFormat == null)
 {
 throw new ArgumentNullException("ImageFormat");
 }

 // output
 context.HttpContext.Response.Clear();

 if (ImageFormat.Equals(ImageFormat.Bmp)) context.HttpContext.Response.ContentType = "image/bmp";
 if (ImageFormat.Equals(ImageFormat.Gif)) context.HttpContext.Response.ContentType = "image/gif";
 if (ImageFormat.Equals(ImageFormat.Icon)) 

context.HttpContext.Response.ContentType = "image/vnd.microsoft.icon";

 if (ImageFormat.Equals(ImageFormat.Jpeg)) context.HttpContext.Response.ContentType = "image/jpeg";
 if (ImageFormat.Equals(ImageFormat.Png)) context.HttpContext.Response.ContentType = "image/png";
 if (ImageFormat.Equals(ImageFormat.Tiff)) context.HttpContext.Response.ContentType = "image/tiff";
 if (ImageFormat.Equals(ImageFormat.Wmf)) context.HttpContext.Response.ContentType = "image/wmf";

 Image.Save(context.HttpContext.Response.OutputStream, ImageFormat);
 }
 }
}

The ImageResult class defines two properties: Image and ImageFormat. These properties can be set in the ImageResult constructor. When the ImageResult is executed in the ExecuteResult method, the in-memory image is rendered to the HTTP response stream in the specified image format.

Rendering the page title image will be done by a PageTitleController which has one action method, ShowTitle, that builds a bitmap image and returns it as an ImageResult.

using System.Drawing;
using System.Drawing.Imaging;
using System.Web.Mvc;
using CustomActionResultExample.Code;

namespace CustomActionResultExample.Controllers
{
 public class PageTitleController : Controller
 {
 public ActionResult ShowTitle(string pageTitle, int width, int height)
 {
 // Create bitmap
 Bitmap bmp = new Bitmap(width, height);
 Graphics g = Graphics.FromImage(bmp);

 g.FillRectangle(Brushes.White, 0, 0, width, height);

 // Render light gray background
 g.DrawString(pageTitle, new Font("Lucida Handwriting", height / 2),
 Brushes.LightGray, new PointF(2, 2));

 // Render black text on top
 g.DrawString(pageTitle, new Font("Lucida Handwriting", height / 2),
 Brushes.Black, new PointF(0, 0));

 // Return ImageResult
 return new ImageResult { Image = bmp, ImageFormat = ImageFormat.Jpeg };
 }
 }
}

The ShowTitle action method creates a new in-memory bitmap and renders a text and text shadow on a white background. This bitmap is then passed into a new ImageResult instance, which will render the image to the HTTP response stream.

As an extra feature, this image-rendered page title can be added to the web page in an easy manner.

<%=Html.PageTitle("Welcome to ASP.NET MVC!", 400, 40)%>

This HtmlHelper extension method will render an HTML image tag such as <img src="/PageTitle/ShowTitle?pageTitle=Welcome%20to%20ASP.NET%20MVC!&width=400&height=40" width="400" height="40" alt="Welcome to ASP.NET MVC!" />. It is implemented as follows:

 using System.Web.Mvc;
 using CustomActionResultExample.Controllers;
 using Microsoft.Web.Mvc;
  namespace CustomActionResultExample.Code
 {
  public static class PageTitleHelper
  {
  public static string PageTitle(this HtmlHelper helper,
  string pageTitle, int width, int height)
  {
  string url = LinkBuilder.BuildUrlFromExpression 
 <PageTitleController>(helper.ViewContext. RequestContext, 
 helper.RouteCollection,
  c => c.ShowTitle(pageTitle, width, height));
  return string.Format("<img src=\"{0}\" width=\"{1}\" 
  height=\"{2}\" alt=\"{3}\" />", url, width, height, pageTitle);
  }
  }
 }
We have used a new namespace here (Microsoft.Web.Mvc) to make use of the LinkBuilder class. This namespace contains several classes that may be included in the ASP.NET MVC framework in future, but are currently not considered to be stable by the ASP.NET MVC development team. The MVC features can be downloaded from the official CodePlex site at http://www.codeplex.com/aspnet.

If all of the classes are in place, we can now add an image-based title in our view in an easy, intuitive manner. The following code will replace the default home page by an enhanced version using our newly-created ImageResult. The HtmlHelper class now has a new method, PageTitle, which we can use to create a title image of a specified width and height.

 <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
  <%=Html.PageTitle(ViewData["Message"].ToString(), 400, 40)%>
  <p>
  To learn more about ASP.NET MVC visit 
 <a href="http://asp.net/mvc" 
 title="ASP.NET MVC Website">http://asp.net/mvc</a>.
  </p>
 </asp:Content>

After running the application, the index page will look like the screenshot presented earlier in this topic.

[edit] Creating a ViewEngine

A ViewEngine maps view names to actual files on the web server and instantiates a View if one is found. By default, views are located in the Views | ControllerName project folder, or in the Views | Shared folder. There are some custom ViewEngine implementations available on the Internet (NHaml, Spark, and so on; you will find links to these in Appendix C of this tutorial); we will be building a custom ViewEngine and View implementation.

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 to find a view or a partial view in the application. If 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 be rendered, each registered IViewEngine is consulted (in the order in which they were registered) until the ASP.NET MVC framework finds one that returns a view that can be rendered.

The WebFormsViewEngine (ASP.NET MVC's default) searches the following virtual paths for views or partial views:

  • ~/Views/<controllerName>/<viewName>.aspx
  • ~/Views/<controllerName>/<viewName>.ascx
  • ~/Views/Shared/<viewName>.aspx
  • ~/Views/Shared/<viewName>.ascx

Master pages are searched for in the following virtual paths:

  • ~/Views/<controllerName>/<masterName>.master
  • ~/Views/Shared/<masterName>.master

In order to create a custom IViewEngine, the tone can overload the base class, VirtualPathProviderViewEngine, instead of implementing the IViewEngine interface. The VirtualPathProviderViewEngine class provides the base functionality for searching a view on a file system.

The example in this topic is based on an ASP.NET MVC web application which can be found in the sample code for this tutorial (CustomViewEngine).

Let's start creating a SimpleViewEngine by overloading the VirtualPathProviderViewEngine. In the constructor, set the paths where the master and view pages can be found. The SimpleViewEngine will search for views and partial views in the same locations that the WebFormsViewEngine does, except that it searches for .htm or .html files. Master page support is not available; hence the empty path is passed in the constructor.

Next, override the two methods: CreatePartialView() and CreateView(). These methods are used to instantiate a view based on the path defined in this SimpleViewEngine. CreatePartialView() and CreateView() return a SimpleView, which is our own view implementation, on which we'll focus right away.

 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
  }
 }

IView is the interface that is used for defining a view. A view is responsible for rendering itself to a TextWriter instance, which will probably be the HTTP response stream. Let's create our SimpleView class. First of all, implement the IView interface. This interface defines the Render() method on which we'll focus later. Also, add a constructor that can be used by the SimpleViewEngine that we created earlier, and a ViewPath property, for the sake of convenience when debugging.

 using System;
 using System.Collections;
 using System.IO;
 using System.Reflection;
 using System.Text.RegularExpressions;
 using System.Web.Mvc;
 namespace CustomViewEngine.Core
 {
  public class SimpleView : IView
  {
  #region Private fields
   private string viewPath;
   #endregion
   #region Constructor
   public SimpleView(string viewPath)
  {
  this.viewPath = viewPath;
  }
   #endregion
   #region Public properties
   public string ViewPath {
  get { return this.viewPath; }
  }
   #endregion
   #region IView Members
   public virtual void Render(ViewContext viewContext, TextWriter writer)
  {
  // ...
  }
   #endregion
  }
 }

Next, let's implement the Render() method. This is passed the ViewContext and TextWriter instances. The first instance contains the ViewData that we are receiving from the controller, along with some other properties. The latter will probably be the HTTP response stream. In our implementation of Render(), the view source code is evaluated using a regular expression, which will look for things such as {$ViewData.Message}, and which we will map to ViewData["Message"] later on.

 public virtual void Render(ViewContext viewContext, TextWriter writer)
 {
  string viewTemplate = File.ReadAllText(
  viewContext.HttpContext.Request.MapPath(this.viewPath)
  );
  Regex templatePattern = new Regex(@"({\$\w+((\.|\[)\w+\]?)*})",
  RegexOptions.Multiline);
  MatchEvaluator replaceCallback = new MatchEvaluator(m =>
  SimpleView.Resolve(m.Value, viewContext.ViewData). ToString());
  viewTemplate = templatePattern.Replace(viewTemplate,
  replaceCallback);
  writer.Write(viewTemplate);
 }

Note that the MatchEvaluator will call a method named Resolve() for each match that is found in the view markup. We will not go deeper into the Resolve() method, but it basically replaces things such as {$ViewData.Message} with more meaningful data found in ViewData["Message"]. The Resolve() method can be found in the following complete code for SimpleView:

 using System;
 using System.Collections;
 using System.IO;
 using System.Reflection;
 using System.Text.RegularExpressions;
 using System.Web.Mvc;
 namespace CustomViewEngine.Core
 {
  public class SimpleView : IView
  {
  #region Private fields
 
  private string viewPath;
 
  #endregion
 
  #region Constructor
 
  public SimpleView(string viewPath)
  {
  this.viewPath = viewPath;
  }
 
  #endregion
 
  #region Public properties
 
  public string ViewPath {
  get { return this.viewPath; }
  }
 
  #endregion
 
  #region IView Members
  public virtual void Render(ViewContext viewContext, TextWriter writer)
  {
  string viewTemplate = File.ReadAllText(viewContext. HttpContext.Request.MapPath (this.viewPath));
 
  Regex templatePattern = new Regex(@"({\$\w+((\.|\[)\w+\]?)*})" , RegexOptions.Multiline);
  MatchEvaluator replaceCallback = new MatchEvaluator(m => SimpleView.Resolve(m.Value, viewContext.ViewData). ToString());
  viewTemplate = templatePattern.Replace (viewTemplate, replaceCallback);
 
  writer.Write(viewTemplate);
  }
 
  #endregion
 
  #region Helper methods
 
  public static object Resolve(string sourceString, object sourceObject)
  {
  // Setup regular expressions engine
  Regex templatePattern = new Regex(@"(\$\w+((\.|\[)\w+\]?)*)", RegexOptions.Multiline);
 
  object resolvedObject = null;
  MatchEvaluator replaceCallback = new MatchEvaluator(
  delegate(Match m)
  {
  // Split expression
  string[] expressions = m.Value.Replace("{", "")
  .Replace("$", "")
  .Replace("}", "")
  .Replace("[", ".")
  .Replace("]", "")
  .Split('.');
 
  // Loop expressions
  object lastObject = sourceObject;
  string expression = "";
  for (int i = 1; i < expressions.Length; i++)
  {
  expression = expressions[i];
 
  if (lastObject != null)
  {
  if (lastObject is IDictionary<string, object>)
  {
  lastObject = ((IDictionary<string, object>)lastObject)[expression];
  }
  else if (lastObject is IDictionary)
  {
  lastObject = ((IDictionary)lastObject) [expression];
  }
  else if (lastObject is Array)
  {
  lastObject = ((Array)lastObject).GetValue (int.Parse(expression));
  }
  else
  {
  try
  {
  lastObject = lastObject.GetType(). 
  InvokeMember(expression, BindingFlags.Instance | BindingFlags.Public | 
  BindingFlags.GetField | BindingFlags.GetProperty, null, lastObject, null);
  }
  catch (MissingMethodException)
  {
  lastObject = string.Format("Undefined: {0}", m.Value);
  }
  }
  }
  }
 
  if (lastObject != null)
  {
  resolvedObject = lastObject;
  } else {
  resolvedObject = string.Format("Undefined: {0}", sourceString);
  }
  return resolvedObject.ToString();
  }
  );
 
  // Fire up replacement engine!
  templatePattern.Replace(sourceString, replaceCallback);
  return resolvedObject;
  }
   #endregion
  }
 }

The Render() method is responsible for rendering the view. It loads the template from the file system, replaces some variables with data from the ViewData dictionary using a regular expression and regular expression callback, and renders the result to the provided TextWriter.

The view markup for this SimpleViewEngine can be found in /Views/<controller>/<action>.htm. The following code snippet is the view markup for Views | Home | Index.htm, used by the Index action method of the HomeController class.

  <html>
  <head>
  <title>{$ViewData.Title}</title>
  </head>
  <bod>
  <h1>{$ViewData.Message}</h1>
  </body>
 </html>

For the SimpleViewEngine to be consulted by the ASP.NET framework, it has to be registered. This has to be done once, preferably in the Application_Start event handler, which can be found in the Global.asax.cs file, and is called only the first time the application is started:

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

Once we remove the Index.aspx view created by the ASP.NET MVC Visual Studio Project Template, the Index.htm view we created earlier will be used by the Index action method of the HomeController class. On running the application, you will see the following screen:

[edit] Summary

In this tutorial, we learned how to extend the ASP.NET MVC framework. We created a control, which is also called a partial view. We also learned more about filter attributes, and also created one of our own.

We looked at how to create a custom ActionResult, which displays an image containing text based on a controller's action method.

Finally, we created our own ViewEngine and IView, which provided support for simple HTML markup containing entries from the ViewData dictionary.

[edit] Additional References

  • For instructions on Installing ASP.NET, click here
  • For instructions on Design patterns in ASP.NET 3.5, click here

[edit] Source

The source of this content is Chapter 6: Customizing and Extending the ASP.NET MVC Framework of ASP.NET MVC 1.0 Quickly by Maarten Balliauw (Packt Publishing, 2009).

Personal tools