This is part 1 of a 4-part series discussing a concept that I've long called "Custom Controls Everywhere" and how ASP.NET MVC affects it. This article will explain why I advocate the pattern and how ASP.NET MVC takes the capability away.
What does Custom Controls Everywhere mean?
When I start my explanation of Custom Controls Everywhere, many listeners think I'm off my rocker. At the least, they think I'm creating a bunch of extra work for nothing. But once those listeners hear me out, as with some of my other ideas, the critics come to realize the benefits. I preface the definition of Custom Controls Everywhere this way because I don't want listeners to stop paying attention before reaching the climax of the story. There's an important take-away here.
To cut to the chase: Custom Controls Everywhere means that you don't use ASP.NET controls out of the box. Don't use the <asp:TextBox> control; don't use the <asp:DropDownList> control; and don't use <asp:Button> or <asp:Hyperlink>. With even more emphasis, never use the <asp:DataGrid> or <asp:GridView> controls! Use your own custom controls, everywhere. There are a few exceptions to the rule, which we'll talk about another time, but the idea is to virtually always inherit from the ASP.NET controls, deriving your own controls, and use those. You end up with <myApp:TextBox>, <myApp:DropDownList>, <myApp:Button>, <myApp:Hyperlink>, <myApp:DataGrid> and <myApp:GridView>.
Why on Earth would you do that!?
The answer to that question is multifold. Back in .NET 1.0 and 1.1, providing a consistent presentation was the first reason. ASP.NET 2.0 introduced themes and skins, which was excellent, but when I used themes for the first time, I realized that I still needed Custom Controls Everywhere because of a second reason, behavior. Themes allow a consistent presentation to be configured application-wide, but they don't allow you to change behavior of controls. For instance, the MaxLength property is not themeable.
If you define the following skin:
1: <asp:TextBox runat="server"
2: SkinId="EmailAddress"
3: Columns="25"
4: MaxLength="80" />
And you attempt to utilize this SkinId:
1: <asp:TextBox runat="server"
2: ID="txtEmailAddress"
3: SkinId="EmailAddress" />
You get the following error message: The 'MaxLength' property of a control type System.Web.UI.WebControls.TextBox cannot be applied through a control skin.
Behavior ends up being one of the most important reasons for using Custom Controls Everywhere. MaxLength is a rudimentary example, but it illustrates simply how themes and skins fall short.
My project (DASL) was faced with a more involved behavior-related feature request after we already had 500 screens in place. We needed to make all auto-post-back drop-downs retain focus after causing the post-back. Users were very frustrated that changing the selection on drop-downs would cause them to lose their place in the tab order, so we needed to implement this feature application-wide, and in short order. Since we were using a custom (derived) DropDownList control ubiquitously, I was able to implement this new behavior into the control and meet the new requirement in an afternoon, without having to change any of the screens' code. This, on its own, justified having created Custom Controls Everywhere for DASL.
Some other notable reasons for Custom Controls Everywhere
I've also found that custom controls afford better cross-browser support capabilities, possibility for security integration, validation hooks, and abstractions from external resources.
One way that cross-browser support is improved includes changing the way a disabled controls are rendered. Several of my ASP.NET projects had to support Netscape 4, which had no concept of disabled controls. My custom TextBox, when disabled, actually renders as static text in a <span> that looks like a disabled TextBox.
I've implemented security into controls by having <myApp:Button Access="Save"> or even <myApp:SaveButton>, where these controls automatically hook into the application security to ensure that the user has save permission for the given page. If the user doesn't have the permission, the button is hidden. The developer has no code for this other than using the correct control.
Validation is dramatically simplified when you can use a tag like <myApp:TextBox Format="EmailAddress" Required-Enabled="True" FieldName="Email Address">. Enough said there.
As for external resources, this can get a little trickier. But if you wrap the Peter Blum DateTextBox in your own <myApp:DateTextBox>, you can use all of the goodness that comes with Peter's Date Package, but you can also add additional behavior. For example, you can add quick selections for dates that are known to be important to the user. In the case of DASL, a school system, dates like the end of the current semester make sense. And if you need to, you can set it up so that you could yank Peter's Date Package out in the future, and replace it with another control.
But isn't it a lot of extra work to create the controls?
Gosh no. It's virtually no work at all. All you have to do is create empty classes that inherit from the ASP.NET controls, and use those instead of the stock controls. Then you can add code when you need to, like the auto-focus auto-post-back drop-down list feature.
Here's an example of how to start using Custom Controls Everywhere:
1: public class TextBox : System.Web.UI.WebControls.TextBox
2: {
3: }
Then you can just register the namespace of your custom controls in the web.config file, and it's available everywhere. That's it. Done; on to the next control.
1: <pages>
2: <controls>
3: <add tagPrefix="myApp"
4: namespace="CustomControlsEverywhere.Controls"
5: assembly="CustomControlsEverywhere" />
6: ...
7: </controls>
8: </pages>
Okay, I'll buy in, but why won't it work in ASP.NET MVC?
One of the first things I tried to do with ASP.NET MVC was to create a custom TextBox control, and slap it into a view. But, that results in an error:
Control 'ctl00_MainContentPlaceHolder_EmailAddress' of type 'TextBox' must be placed inside a form tag with runat=server.
This is by design, and I expected to get this error. MVC is not designed for use with forms set to runat=server. Server forms are all about post-backs, and MVC is the polar opposite of post-backs. While you could probably find a way to marry the two together, it would be ugly and pointless. The result is that you cannot use some of the System.Web.UI.WebControls in MVC. Form controls and validators will throw the above error, which is why the MVC demos out there show the old-school usage of <input type="text">, and no validation in the view.
I thought I heard Scott Guthrie mention that putting a server form in an MVC view would throw an exception, but that isn't the case (right now). Instead, it appears to work. You can even put TextBox controls and validators onto the form. But, the controller will not be able to retrieve the values from these controls, because of the Name attribute that will be emitted to the control. The Name attribute will respect the naming container, to where it's unpredictable what the name of the control will be. In summary, you cannot use these server controls. Say bye-bye to all of the custom controls that you've already put in place. And forget about inheriting from the ASP.NET TextBox for use within MVC. No more Custom Controls Everywhere.
What about the UI Helpers?
Microsoft has given us some extensions to provide UI helpers. These are pretty snazzy. Rob Conery has written a bunch about them--check em out. Essentially, these are helper methods that emit HTML for you, so that you don't have to write all of those silly <input> tags. Instead, you use code like this: (credit to Rob Conery on this)
1: Name:<br />
2: <%=Html.TextBox(“txtName”,20) %><br />
3: Name, with maxlength:<br />
4: <%=Html.TextBox(“txtName”,“My Value”,20,20) %><br />
There is a lot of work that has gone into these UI helpers, and they have some great advantages. But, and you knew there was a but, they don't lend themselves to a Custom Controls Everywhere approach. All of the UI Helpers are created as extension methods to the HtmlHelper class, so you can't create a derived HtmlHelper class that overrides these methods. And even if you could, you'd have to override every overload that is offered. So we have all of this goodness, and we cannot use it with the Custom Controls Everywhere pattern.
Where does that leave us?
When I think of losing my self-validating controls, I am sad. The loss of Custom Controls Everywhere really makes me feel like ASP.NET MVC is a huge step backward. I've refused to throw in the towel though; so in the next article of this series, I'll demonstrate a way to bring Custom Controls Everywhere back to life within MVC. Then part 3 of the series will discuss how the MVC framework could be changed to promote the concept. Part 4 will extend on that, showing how validation could be baked in too.
Please stay tuned and provide any thoughts or feedback that you have.