Simplifying html generation in code using Razor templates

Html Generation

In ASP.NET MVC, we can construct html from code using the TagBuilder class. The built-in html helpers uses the TagBuilder class to generate textboxes, checkboxes and other html stuff. Generating html from code is not flexible because every time we need to alter the html we have to go for recompilation.

We all know that, the built-in ValidationSummary html helper displays the validation errors as an unordered list but in some cases we need to customize the way in which the errors are being displayed, say as a table instead of list. The ValidationSummary method creates the list inside the code and we can't customize it. All we could do is create our own custom helper to display the errors as a table.

It would be nice if we could pass the html structure or template that controls the way in which the validation errors are being displayed to the user to the helper from the view and that's what the subject of this post.

Templated Razor Delegates

Razor delegates are an excellent ways to create templates that could be easily passed to a method from views. Phil Haack has thrown some initial thoughts on this subject right here.

Razor delegates are nothing but they are templates build by C# and html.

A simple Razor delegate looks like this,

Func<dynamic, HelperResult> variable = @@item;

Listing 1. A simple Razor delegate

Let's see the right-hand side first. The markup starts with the "@" character and ends with ";". The @item parameter represents the passed argument to the delegate/template.

The markup represents a Func delegate that is represented in the left-hand side. The first parameter of the delegate represents the type of the parameter passed to the template and the second parameter represents the type returned by the Razor markup, which is HelperResult.

Before diving deep into templates I would like to tell more about HelperResult.

HelperResult

HelperResult is the type returned from declarative html helpers. Not only that, we can return HelperResult from code as well. The HelperResult class implements IHtmlString, which represents a html-encoded string that should not be encoded again.

Let's see a simple html helper that returns the server time as HelperResult.

public static HelperResult ServerTime(this HtmlHelper htmlHelper)
{
	return new HelperResult
	(
		writer => 
		{
			writer.Write(String.Format("{0}", DateTime.Now.ToShortTimeString()));
		}
	);						
}

Listing 2. Html helper that returns server time as HelperResult

The HelperResult class has a constructor that takes Action<TextWriter> as parameter. In the above helper we are writing the server time to the writer in the action that is passed to the HelperResult's constructor.

What happens if the helper just returns a plain string instead of HelperResult?

public static string ServerTime(this HtmlHelper htmlHelper)
{
	return String.Format("<i>{0}</i>", DateTime.Now.ToShortTimeString());
}

Listing 3. Html helper that returns server time as string

The Razor engine html encodes the output and this is how the time is displayed to the user.

Complex Razor Delegates

In the listing 1. we saw a simple Razor template. We can also create complex templates that spans multiple lines as shown in the below listing.

Func<Product[], HelperResult> tableTpl = @<table>
	@foreach(var p in @item)
	{
		<tr>
			<td>@p.Name</td>
			<td>@p.Price $</td>
		</tr>
	}				
</table>;

Listing 4. Complex Razor delegate

One important thing to note down is in the above template is I've strongly typed the input parameter type of Func delegate to Product[]. The advantage I'm going to get is intellisense.

We can pass only one argument to the Razor template and it can be accessed through the @item parameter.

Razor delegates as templates

Let's see how we can pass the Razor delegate as template to the html helper (that we saw in listing 2). The advantage of passing a template to the helper is we can display the server time in different styles.
Here is our modified helper.

public static HelperResult ServerTime(this HtmlHelper htmlHelper, 
	Func<string, HelperResult> template)
{
	return template(DateTime.Now.ToShortTimeString()); 
}

Listing 5. Html helper that accepts Razor delegate as template

The parameter Func<Name, HelperResult> represents our template. Inside the method, all we doing is calling the template passing the server time as input parameter. The template returns HelperResult as output which is returned from the method.

Here are some of the examples of how we can pass different templates to the helper to display the time in different styles.

@Html.ServerTime(@@item)

@Html.ServerTime(@@item)

Listing 6. Passing templates to html helper

Displaying validation errors using templates

As you know, the built-in ValidationSummary helper displays both the property and model validation errors. The ValidationSummary helper displays the errors as an unordered list(<ul>).
As default the ValidationSummary displays both the property and model errors but we can filter out the property errors by passing true to excludePropertyErrors.

The following is the typical html generated by the ValidationSummary helper.

	
  • Duplicate Order.
  • Invalid Product Code.

Listing 7. Html rendered by ValidationSummary

The default implementation of the ValidationSummary is sufficient in most of the cases but it creates some inconvenience when we try to display only the model errors. The issue is even though there are no model errors it still renders the below html and this may creates some CSS issues.

	

Listing 8. Html rendered by ValidationSummary when there are no model errors

Custom ValidationSummary helper

In this section we are going to create a simple html helper that displays only the model validation errors in any format that is decided by the passed template. Let's call our custom helper as ModelValidationSummary and the implementation is shown below.

public static HelperResult ModelValidationSummary(this HtmlHelper htmlHelper, Func<string[], HelperResult> tpl)
{
	ModelState ms;
	htmlHelper.ViewData.ModelState.TryGetValue(htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix, out ms);

	if (ms != null && ms.Errors != null && ms.Errors.Count > 0)
		return tpl(ms.Errors.Select(p => p.ErrorMessage).ToArray());

	return null;
}

Listing 9. Custom ModelValidationSummary that takes a delegate/template as input parameter

The code is very simple! We are reading the model errors from the ModelState and passing it to the template as a string array.

Here are some examples of how we can pass different templates to our custom ModelValidationSummary helper to display the model validation errors in different formats.

@Html.ModelValidationSummary(@<ol class="val-container">
	@foreach(var error in @item)
	{
		<li>@error</li>
	}
	</ol>)

Listing 10. Passing template that displays validation errors as an ordered list

@Html.ModelValidationSummary(@<table>
	@foreach(var error in @item)
	{
		<tr>
			<td class="error-icon"></td>
			<td class="error">@error</td>
		</tr>
	}
</table>);

Listing 11. Passing template that displays validation errors in a table

Creating global templates in application

Mostly applications prefers to display the validation errors in a common format throughout the pages. In those cases we don't like to create the template in every view and pass to our ModelValidationSummary helper instead we would like to store it in a global variable that could be easily accessible anywhere from views.

One way we can create global templates is by creating a view in App_Code folder and writing the templates as declarative html helpers. Unfortunately, storing templates as public static variables in functions won't work and we have to rely on declarative helpers.

Let's create a view in the App_Code folder with the name Templates.cshtml that contains the global validation template as a html helper.

@helper ValidationTemplate(string[] errors)
{
	<ol class="val-container">
	@foreach(var error in errors)
	{
		<li>@error</li>
	}
	</ol>
}

Listing 12. Creating global templates as declarative html helpers

Now from the views we can do something like this,

@Html.ModelValidationSummary(Templates.ValidationTemplate)

Listing 13. Calling ModelValidationSummary by passing html helper as template

That's cool! We can pass the html helper just like a delegate to our ModelValidationSummary method and the result is same!

Accessing templates from code

How about accessing global templates in the ModelValidationSummary helper? Let's modify the ModelValidationSummary helper such that if we don't passes any template it uses the default (global) one else uses the passed one. The problem is we can’t just directly access the declarative html helper from code, fortunately, using the David Eboo's RazorGenerator extension tool we can easily create re-usable classes from views.

RazorGenerator

RazorGenerator is an extension tool for Visual Studio and we can install it by going to Tools > Extension Manager and search for RazorGenerator.

RazorGenerator extension

Installing RazorGenerator extension

After installing RazorGenerator we have to restart Visual Studio. The next step is we have to create class for the Templates.cshtml file. For that, we have to go to the properties of the file and set Build Action to None and Custom Tool to RazorGenerator.

Setting Custom Tool for Razor file

Setting Custom Tool for Razor file

On saving, Templates.generated.cs file will be created that contains the equivalent C# class for the declarative html helper ValidationTemplate we saw in listing 12. To access the generated class from code we have to set the Build Action to Compile for the Templates.generated.cs file.

Now we can access the declarative html helper ValidationTemplate from the code and this is our modified ModelValidationSummary helper.

public static HelperResult ModelValidationSummary(this HtmlHelper htmlHelper, Func<string[], HelperResult> tpl = null)
{
	ModelState ms;
	htmlHelper.ViewData.ModelState.TryGetValue(htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix, out ms);

	if (ms != null && ms.Errors != null && ms.Errors.Count > 0)
	{
		var errors = ms.Errors.Select(p => p.ErrorMessage).ToArray();

		if (tpl == null)
			return App_Code.Templates.ValidationTemplate(errors);

		return tpl(errors);
	}

	return null;
}

Listing 14. Modified ModelValidationSummary helper with default template

Hope you enjoyed this article. Glad to hear comments from you.

Download

blog comments powered by Disqus