Udostępnij za pośrednictwem


Take your MVC User Controls to the next level

Note: this is based on ASP.NET MVC 2 RC, and will not work on earlier builds.

 

The quick pitch: make your User Controls as cool as built-in render helpers!

The goal of this post is to show you how to change the way MVC user controls are called from something like this:

 <%= Html.Partial("~/Views/Shared/gravatar.ascx", new { Email = "foo@bar.com", Size = 80 }) %>

To something that looks just like a built-in render helper (like Html.TextBox(…)):

 <%= Html.Gravatar("foo@bar.com", 80) %> 

 

The current model for User Controls in MVC

If you have used ASP.NET MVC, you probably know that you can use User Controls (.ascx files) to provide partial rendering.

For example, the default MVC app has a Site.Master and a LogOnUserControl.ascx under Views/Shared, and the Site.Master contains:

 <div id="logindisplay">
    <% Html.RenderPartial("LogOnUserControl"); %>
</div>

As a slightly nicer alternative, you can change the RenderPartial call to:

 <%= Html.Partial("LogOnUserControl") %>

 

What’s wrong with this pattern

While this pattern certainly works, there are some issues with it.  Let’s look at a few:

Hard coded paths with no intellisense

The first is that you need to refer to them by their path.  In the example above, the UC is in the same folder as the master, so you can use a relative path, but in the general case, you would have to refer to it as “~/Views/Shared/LogOnUserControl.ascx”, which gets ugly.  If you use T4MVC (plugging my other baby if you don’t mind!), it’ll get rid of the literal string and give you intellisense, but in essence it still works the same way: the View engine gets a literal string and tries to go from there.

Inconsistent with HTML render helpers

Another issue is that even though the UC is basically an HTML render helper, it doesn’t look like one when you call it.  Ideally, you really want to call it the same way you call ‘built in’ helpers.  e.g. since you can call Html.TextBox(), you should be able to call Html.LogOnUserControl().  In comparison, the RenderPartial call looks painfully complex.

Difficult to pass parameters

Suppose your UC needs to receive parameters from the caller.  With Partial/RenderPartial, you have to pass those through a model object, which is painful.  You can either rely on untyped data, or create a custom ViewModel type to encapsulate the data you need.  In either case, it looks nothing like a call to a render helper like Html.TextBox(…), which takes ‘natural’ parameters.

 

How we can make this better

The tools at our disposal

There are many little known gems in ASP.NET that allow us to put together a very nice solution that solves all those issues.

The first little known fact is that Web Applications can have an App_Code folder, where all its files get compiled into a single assembly that all pages reference.  Of course, the App_Code folder is very well known in Web Sites, but the fact that they can also be used in Web Application Projects (WAPs) is often overlooked.

The second little known gem is that user controls can go in App_Code!   Come on, admit it, you didn’t know that.  No one knows that! :)

The third one is that you can associate a control builder for an entire User Control (or page), and not just for controls within the page.

And finally, the fourth gem is the ControlBuilder.ProcessGeneratedCode method, which I blogged about a while back.  This method lets us modify the code generated for the User Control (or page), which opens up some very powerful possibilities.

What the end result looks like

Instead of giving you all the technical details here (the full sample is attached), let’s look at what the end result looks like.  In order to use it, you just need to add a reference to the MvcUserControlHtmlHelpers assembly in the sample (I know, lame name).

Then you can just move your User Control into App_Code, and make a small change to it.  e.g. in the example above, you would change the directive from

 <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>

to

 <%@ Control Language="C#" Inherits="MvcUserControlHtmlHelpers.UserControlHtmlHelper" ClassName="LogOnUserControl" %>

So basically you change the base type and give it a specific class name.

One you have that, the real fun begins, as you can change Site.master to just have

 <div id="logindisplay">
    <%= Html.LogOnUserControl() %>
</div> 

You now have full intellisense, and the call looks just like a built-in helper.  Yeah!

 

So what about parameters?

Suppose you want to write a User Control that displays Gravatars.  You’d put in in App_Code, and write something like this:

 <%@ Control Language="C#" Inherits="MvcUserControlHtmlHelpers.UserControlHtmlHelper" ClassName="Gravatar" %>

<script runat="server">
    // Declare the paramaters that we need the caller the pass to us
    public string Email;
    public int Size;
</script>

<%
    // Build hash of the email address
    // Note: in spite of its name, this API is really just a general MD5 encoder
    string hash = FormsAuthentication.HashPasswordForStoringInConfigFile(Email.ToLower(), "MD5").ToLower();

    // Construct Gravatar URL
    string imageURL = String.Format("https://www.gravatar.com/avatar/{0}.jpg?s={1}&d=wavatar", hash, Size);
%>

<img src="<%= imageURL %>" alt="<%= Email %>" title="<%= Email %>" />

The key new concept here is that you declare the parameters that you want as public fields, and you just use them in your code.  Basically, think of this as declaring a method, but without a real method declaration.

And once you have that, things get really nice in your view, where you can now write

 <%= Html.Gravatar("foo@bar.com", 80) %>

or if you prefer, you can write

 <% Html.RenderGravatar("foo@bar.com", 80); %>

With the same distinction as between Partial() and RednerPartial().  Now for the fun, try hitting F12 (Go To Definition) on one of those methods.  You’ll get:

 public static class GravatarExtensions {
    public static string Gravatar(this HtmlHelper htmlHelper, string Email, int Size);
    public static void RenderGravatar(this HtmlHelper htmlHelper, string Email, int Size);
}

What happened in that the special base generated some extension methods to allow you to call the User Control exactly as you would call a ‘built-in’ helper like Html.TextBox.

 

How does it all work

To pull this magic, it uses a combination of the various things I outlined above.  I won’t go into the details since the full source is attached and has enough comments to make it clear what it’s doing.  The file you want to look at is UserControlHtmlHelper.cs.  I warn you, I had to use a scary trick to get around a CodeDom limitation.  Also, there are some edge cases that don’t work yet but could be made to work.  At this point, this is just a sample!

Enjoy, and let me know what you think about this pattern.

MvcUserControlHtmlHelpers.zip

Comments

  • Anonymous
    January 12, 2010
    Very cool! Can you generate those as named parameters for ASP.NET 4? That'd be pretty cool. :)

  • Anonymous
    January 13, 2010
    Awesome! Great way to move devs to the helper way of dealing with sub-views!

  • Anonymous
    January 13, 2010
    This looks really great. What about strongly-typed views, aka ViewModels? Does that pattern make sense in this context?

  • Anonymous
    January 13, 2010
    Ninja + black magic. Love it, thank you.

  • Anonymous
    January 13, 2010
    @Haacked: absolutely, with 4.0 generating named params with defaults makes improved things further.  I actually started that way, but then chose to make something that works on 3.5.  I suppose it could 'light up' on 4.0 and do that.

  • Anonymous
    January 13, 2010
    @Matt: the way I look at it, this pattern as an alternate strongly-typed way of passing data to sub-view.  With a ViewModel, you create a special type instance, and pass it to the view.  Here, you basically create the equivalent of a ViewModel type by declaring the public fields that you want to receive.  The end result is similar, but it avoids the extra ViewModel type and makes the calling code cleaner. That being said, I'm sure there are some scenario where this pattern is not best.  I think it works well for reusable componentized UI, like the Gravatar example.

  • Anonymous
    January 13, 2010
    As a refinement, you might consider some namespacing, based on folder hierarchy in App_Code. So one might call: <%= Html.People.Profile(myPerson) %> <%= Html.Account.RegistrationForm() %>

  • Anonymous
    January 13, 2010
    very interesting... this really turns some things on its side.  I am currently doing this with partials and hand written HtmlHelpers..

  • Anonymous
    January 13, 2010
    @Matt: yes that would make sense. One silly issue is that there is no easy way to get the path to the ascx while processing it in the ControlBuilder. Hopefully we can fix that in .net 4 by exposing a ControlBuilder.VirtualPath property (it actually exists, but is internal).

  • Anonymous
    January 13, 2010
    The comment has been removed

  • Anonymous
    January 14, 2010
    @thorn: interesting idea.  I'll think about the best way to do it, and I'll try to blog it if I find a good solution.

  • Anonymous
    January 14, 2010
    @thorn: one thing you could do is override AppendLiteralString() in the control builder, so you can preprocess the string at parse time before the page is compiled.

  • Anonymous
    January 14, 2010
    Very interesting.  Lots of possibilities for other enhancements using the above concepts!

  • Anonymous
    January 16, 2010
    @thorn: Have a look at  http://omari-o.blogspot.com/2009/09/aspnet-white-space-cleaning-with-no.html

  • Anonymous
    January 24, 2010
    Omari, your blog is awesome! Maybe you should promote it better. David, did you check OmariO's solution? I think possibilities of the compile-time stage are underestimated by the community and by Microsoft itself. Maybe it is not to late to add more support for this stage in ASP.NET 4? For example, Omari had to use reflection, because there isn't 'official' way to do some stuff. I thinks it can be turned into the excellent unique advantage of ASP.NET. Don't you think so?

  • Anonymous
    January 25, 2010
    The comment has been removed

  • Anonymous
    October 23, 2010
    Great example, but for start people, make sure include UserControlHtmlHelper.cs and change name space then it would work

  • Anonymous
    October 23, 2010
    Great example, but for people who just start MVC, make sure include UserControlHtmlHelper.cs and change name space then it would work

  • Anonymous
    November 28, 2010
    It's seems a great solution, but I'm quite new to ASP.NET and I don't know how to include this in my own projects. Can someone explain to me howto do this? Thanks in advance!

  • Anonymous
    November 28, 2010
    @Dirk: please try the solution that is attached to the post for an easy way to get started.

  • Anonymous
    November 30, 2010
    Off course.. why didn't I think of that? Thanks for the tip, it works now :-)  

  • Anonymous
    January 25, 2011
    The comment has been removed

  • Anonymous
    January 25, 2011
    @jimseld Yes, the regex is not quite right.  Someone else had iterated on it and came up with the following:        private static Regex publicFieldRegex = new Regex(@"publics+(?<Type>w+(.w+)(((s?<s?w+(.w+)s?(,s?w+(.w+))>|s?[]))?))s+(?<Name>w+)s*(;|=)"); It handles many more cases with generics and arrays.

  • Anonymous
    March 07, 2011
    Hi, I have two UserControls. One include the other, and both have declared the same parameter. How can I pass the first usercontrol parametes to the second one? Thanks - Dominik.

  • Anonymous
    March 08, 2011
    @Dominik: are you using the technique described here, or you're asking generally about User Controls? With the techique here, I would think you should be able to just call the inner UC from the outer UC.

  • Anonymous
    March 09, 2011
    Hi again, I'm using your model, but I don't know how to pass the parameter of the first user control to the second one. Thanks - Dominik.

  • Anonymous
    March 10, 2011
    @Dominik: I tried and it worked fine. I would need more details on exactly what you are trying and wht you're seeing. Please post a StackOverflow question and I'll try to help there, as blog post comments dont' work well to write code.

  • Anonymous
    April 01, 2011
    My only wish is that it could support overloads...  Now that would be fantastic!

  • Anonymous
    October 31, 2012
    Nice Demo David.

  • Anonymous
    January 15, 2013
    Is there any way to share one user control between two different ASP.NET MVC apps?

  • Anonymous
    December 16, 2013
    @raulhmacias: Create a new MVC project that has the default directory structure, but contains nothing more than the user controls. Open user control's properties and set the Build Action to Embedded Resource. Then build the project and use gacutil to put it in GAC. In your two MVC apps' project, create a virtualpathprovider and register it in the global asax (HostingEnvironment.RegisterVirtualPathProvider). Then you can use the path like "~gac/MyControl.ascx"... You will need to google a lot more about it, but this is how I did it.