I've just started doing some small projects with ASP.NET MVC and the Razor View Engine. Coming from many years of Web Forms programming, there a number of things I really like about it but also some really confusing things. The HtmlHelpers seem nice for small examples but seem to break down when you want to configure a lot parameters or some of the non-standard properties.
First Name: @Html.TextBox("firstname")<br />
Last Name: @Html.TextBox("lastname", Request["lastname"])<br />
Town: @Html.TextBox("town", Request["town"], new Dictionary<string, object>(){{ "class", "special" }})<br />
The text box for firstname is the simplest example but the parameter list can start to get really long or you have to resort to passing a dictionary of attributes. The dictionary is particularly troubling to me because the keys are the attributes you want to set and you lose Intellisense and compile time checking.
I've seen a number of posts decrying the lack of (or benefit of) server controls in MVC and while I can appreciate doing away with Viewstate and the various postbacks they were a nice way to encapsulate a bunch of properties and custom html generation. So looking at ways to accomplish this without adding a ton of HtmlHelper extensions I found the
fluent pattern used by the
Telerik and
MvcContrib libraries. This seemed like a good compromise to wrapping the properties and complex html code.
So I set off to try and create a simple Image class just to understand how this might be applied to larger and more complex objects. Well something that should have been a lot simpler took me a day and a half to have a working example so hopefully this blog post might help someone avoid my same traps.
I wanted these controls to be reusable across projects so I created a solution with a C# class library. To test out the rendering I created a new web site by selecting "Add -> New Web Site..." and choosing "ASP.NET Web Site (Razor)" from the dialog (this would lead to one set of issues below). This creates a weird default web site with some basic account management features. I say weird because there are no 'Controllers' or 'Views' directories and everything is done inline on the pages. But anyway all I needed was some place to try out my code.
I started with the standard HtmlHelper extension example to verify that everything was working between the class library and the web site.
In the class library I added references to:
- System.Web
- System.Web.Mvc (3.0)
- System.Web.WebPages
using System.Web.Mvc;
namespace FluentTest {
public static class ImageHelper {
public static string RenderImage(this HtmlHelper html, string imagePath) {
TagBuilder img = new TagBuilder("img");
img.MergeAttribute("src", imagePath);
return img.ToString(TagRenderMode.SelfClosing);
}
}
}
In the web site I added a reference to the class library and in default.cshtml I added:
@using FluentTest;
@Html.RenderImage("Resources/images/example.jpg")
Now I ran into my first issue. Trying to compile the code I got the following errors:
- Instance argument: cannot convert from 'System.Web.WebPages.Html.HtmlHelper' to 'System.Web.Mvc.HtmlHelper'
- 'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'RenderImage' and the best extension method overload 'FluentTest.ImageHelper.RenderImage(System.Web.Mvc.HtmlHelper, string)' has some invalid arguments
I changed the HtmlHelper parameter to System.Web.WebPages.Html.HtmlHelper and the code compiled but when I viewed the output it was encoded as plain text:
<img src="Resources/images/example.jpg" />
After some more Googling I changed the return type from string to MvcHtmlString, added a reference to System.Web.Mvc in the web site and finally the images was correctly displayed.
Now I wanted to turn this into a class that I could use method chaining to set the properties. Both the Telerik and the MvcContrib libraries use the fluent pattern and are open source. Unfortunately, they are such large frameworks that make use of things like dependency injection, multiple base classes, factories and interfaces that it is hard to track where to start. This
blog post ended up being a great starting point for a simple fluent example but I knew I wanted to follow the Telerik model where the only thing returned from the HtmlHelper extension was the object instance to start the chain. So I modified the code as follows:
namespace FluentTest {
public static class ImageHelper {
public static Image Image(this System.Web.WebPages.Html.HtmlHelper htmlHelper) {
return new Image();
}
}
public class Image {
private string imagePath;
public Image ImagePath(string path) {
imagePath = path;
return this;
}
public override string ToString() {
TagBuilder builder = new TagBuilder("img");
builder.MergeAttribute("src", imagePath);
return builder.ToString(TagRenderMode.SelfClosing);
}
}
}
In the web page I changed the RenderImage call to:
@Html.Image().ImagePath("Resources/images/example.jpg")
This worked great in the sense that now I had an object I could use to set a bunch of properties (with Intellisense) and encapsulate the html rendering. The problem was the output was back to being encoded:
<img src="Resources/images/example.jpg" />
The Razor engine was automatically calling ToString() on my object which as we all know returns a string. And the Razor engine automatically encodes all strings to prevent cross-site scripting. That's well and good but I'm writing an Html control and want Html outputted. Lots of Googling and reading StackOverflow answers that all say return a MvcHtmlString or HtmlString but no examples of how to get the Razor engine to call something other than ToString() on my object.
Finally I stumbled across this
StackOverflow question. It wasn't directly about my issue but I could see that he was trying to do same thing I was. The light bulb went off when I saw that he had implemented the IHtmlString interface on his class along with the required ToHtmlString() method. I knew this had to be it so I modified the Image class and added the IHtmlString interface and
voilĂ my image rendered.
public class Image : IHtmlString {
public string ToHtmlString() {
return ToString();
}
}