Comparing MVC 3 Helpers: Using Extension Methods and Declarative Razor @helper Syntax
HTML Helpers provide a clean way to encapsulate view code so you can keep your views simple and markup focused. There are lots of built in HTML Helpers in the System.Web.Mvc.HtmlHelper class, but one of the best features is that you can easily create your own helpers. While you've been able to create your own helpers since MVC 1 using extension methods, the Razor view engine gives you a new option to create helpers using the @helper keyword.
Extension Method helpers
Previous versions of ASP.NET MVC allowed you to do that by writing extension methods on the HtmlHelper class, and that's still available in MVC 3. Helpers are just methods which return strings, so they're pretty easy to set up. Let's look at an example from the MVC Music Store - a helper which which truncates a string to make sure it's smaller than a specified length.
The extension method needs to be in a static class, and I generally like to create a separate folder for organization.
The code is standard extension method syntax - the first parameter uses the this keyword to indicate that it's extending an HtmlHelper, and the rest is just a simple method that returns a string. The method takes a string and a max length, chops the string if needed, and returns the result.
using System.Web.Mvc; namespace MvcMusicStore.Helpers { public static class HtmlHelpers { public static string Truncate(this HtmlHelper helper, string input, int length) { if (input.Length <= length) { return input; } else { return input.Substring(0, length) + "..."; } } } }
Note: Make sure you're extending the HtmlHelper defined in System.Web.Mvc and not System.Web.WebPages. The using statement is important.
Our views can make use of this by either importing the namespace with the @using keyword to the views or by adding the helper's namespace to the pages/namespaces element in the web.config.
We could use this in the default Home / Index.cshtml view as below:
@{ ViewBag.Title = "Home Page"; } @using RazorHelpers.Helpers <h2>@Html.Truncate(ViewBag.Message as string, 8)</h2>
Notice that we need to cast the ViewBag.Message as a string, or we'll get a compiler error when the page is rendered: CS1973: 'System.Web.Mvc.HtmlHelper
Alternative, if we wanted to add the namespace to the web.config in the Views folder, we could use the helper class in all our views without a @using statement. That would look like this:
<system.web.webPages.razor> <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Routing" /> <add namespace="RazorHelpers.Helpers" /> </namespaces> </pages> </system.web.webPages.razor>
Razor Declarative Helpers
Razor allows us to implement this using either an in-page code block or in another file, using a more declarative syntax using an @helper block.
Razor helpers inline
To implement this helper inline, we'd just add the @helper block in the page as shown below.
@{ ViewBag.Title = "Home Page"; } @helper TruncateString(string input, int length) { if (input.Length <= length) { @input } else { @input.Substring(0, length)<text>...</text> } } <h2>@Truncate(ViewBag.Message, 8)</h2>
The code is very similar, but there are a few differences:
- I've tightened it up a bit since it's able to take advantage of Razor syntax to directly write out the value rather than return a string
- We also don't need to bother with the this HtmlHelper business because it's not an extension method.
- We don't need to cast the input as a string, since the Razor engine can figure that out for us on the fly
- Since it's not an extension method, we directly call @Truncate() rather than @Html.Truncate()
Reusable helpers within your project: Use App_Code
You can move Razor helpers to another file within your project, but you'll need it to be in the App_Code folder. Right-click on the project and select "Add / New Folder" and call the folder "App_Code".
You'll see that the App_Code folder gets a special icon:
Now we can add a new Razor view to that folder using "Add / New Item / MVC 3 View Page (Razor)". You can name it what you want, but you'll be using the filename when you call the helper. I called mine CustomHelpers.cshtml.
Replace the generated code in the new CSHTML file with the @helper block we just used above, so the entire source of the new CustomHelpers.cshtml file will read as follows.
@helper TruncateString(string input, int length) { if (input.Length <= length) { @input } else { @input.Substring(0, length)<text>...</text> } }
In order to use that in a view, we call the helper using the name we gave the view page, like this:
@{ ViewBag.Title = "Home Page"; } <h2>@CustomHelpers.Truncate(ViewBag.Message, 8)</h2>
Notice that we don't need a @using statement to pull it in, we just call into the CustomHelpers class.
Turning Razor Helpers in reusable libraries
David Ebbo posted about how to turn your Razor helpers into libraries using the Single File Generator. This uses a custom build tool create a .cs or .vb file based on the view, which means you can move it wherever you'd like. Running that generator on the CustomHelpers.cshtml file would create CustomHelpers.cs, which could be used within my project, in a code library, etc.
Summary
Extension method based Custom HTML Helpers still have some benefits. They're code files, they can live in any directory, and since they're extension methods off the HtmlHelper class they're very discoverable - just type @Html. and see them in the list with all the other built in helpers.
Razor declarative helpers have some nice benefits, though:
- You can easily whip one up inline in a view, then upsize it to a separate files in App_Code, then to a reusable library. I like the low barrier to entry - it removes any thought from writing helpers whenever possible.
- The syntax is a little simpler - rather than thinking about writing an extension method, you just write some Razor code that writes out values and it all works.
- Razor helpers are a little more dynamic due to the later compile - e.g. I didn't need to cast the ViewBag.Message to a string.
- You don't need to worry about namespaces. That's one of the most common problems beginners run into in using the helpers in the MVC Music Store sample.