Thursday, October 16, 2008

Creating Easy ASP.NET Custom Web Controls

public void Warning()
{
/*
WARNING: This is my first post so I apologize in advance for any strange formatting errors you may experience and also my inability to write complete sentences or sometimes just keep on going and never acutally end the sentence making for a very long sentence that just goes on and on and on and on, and on... ;-)
*/
throw new BadWriterException("Keep your day job!");
}
Overview
In this quick article I am going to show you a method I use to quickly make "Custom Web Controls" without having to deal with manually building the layout declaritvely in code or by hardcoding the markup directly into the control.  
This will essentially be accomplished by saving the control markup as a seperate file and marking the file as an embedded resource within the web control project.
Sound interesintg, then read on...
Let's Get Started
Many times when I am working on an ASP.NET site I realize that one of my "UserControls" I have created in my web project would be extremely useful in my toolkit and warrants conversion into a "Custom Web Control".
"Custom Web Controls" have several advantages over "User Controls" such as: easier/reliable method of sharing across multiple projects/solutions and developers/people, and the ability to add more "Designer" features, to name a few...

After spending hours of tweaking the nuances of the visual portion of my "UserControl" I don't want to have to go back to square one and spend alot of time converting my XHTML markup to a declaritive control tree in code.  I also don't want to have to spend alot of time trying to get my markup to hard-code into the control which also makes it more difficult to manage.
The solution I have created is to put the markup into a seperate ".txt" file and save it as an "Embedded Resource" in my custom web control project and then parse the control text dynamically at runtime.

So enough typing, lets look at some pictures!
In the following pictures I will be demonstrating a simple templated container control with a "Header" and "Body".  I am generally buidling business applications and I use this type of control all the time to group sections of a page into logical sections.
The final result looks like this:

Example 1:
And of course you can change the style sheets and customize however you want:
Example 2:
And here is what the usage looks like in an .ASPX or .ASCX:
Building the Control
To create this control you will need to create a custom webcontrol project and then create (2) files:
  • One for the Control itself
  • and one more for the Control markup; which is just a .txt file
Make to sure to right-click the Control markup file, choose properties, and sets its "Build Action" to "Embedded Resource"
When you're done it should something like this:
And here is the control markup that gets placed into the SectionHeaderControl.txt file:

<asp:panel runat="server" CssClass="sectionHeaderContainer">
<asp:panel runat="server" CssClass="sectionHeaderTitleContainer">
<span class="sectionHeaderTitleContainerCell"><asp:Label runat="server" ID="lblTitle" CssClass="sectionHeaderTitleText" /></span>
</asp:panel>
<asp:panel runat="server" CssClass="sectionHeaderContentContainer">
<asp:PlaceHolder runat="server" ID="plContent" />
</asp:panel>
</asp:panel>

The Code
Just like in most CustomControls the magic happens in the "CreateChildControls()" method. Here is the entire section of code in the "CreateChildControls()" method:
protected override void CreateChildControls()
{
string controlText = CSUtility.GetEmbeddedResource("CreateSoftwareUtils.Web.Controls.SectionHeaderControlText.txt");
ControlParser parser = new ControlParser();
Control parsedMasterControl = parser.Parse(controlText);
parsedMasterControl.ID = "masterControl";
this.Controls.Add(parsedMasterControl);

_contentControls = parsedMasterControl.FindControl("plContent") as PlaceHolder;
Label lblTitle = parsedMasterControl.FindControl("lblTitle") as Label;

lblTitle.Text = Title;
base.CreateChildControls();
}
}
So, as you can see, there isn't much to it!
I have wrapped a couple of generic things into some utlity classes so I don't have to keep typing the same stuff over and over again; also just in-case I change how I am accomplishing those particular tasks it will be an easy change across all the controls. 
The first is CSUtility.GetEmbeddedResource():
This method is responsible for getting the control text from the embedded resource and is pretty simple...

public static string GetEmbeddedResource(string resourceName)
{
Assembly assembly = Assembly.GetExecutingAssembly();
Stream resourceStream = assembly.GetManifestResourceStream(resourceName);
StreamReader reader = new StreamReader(resourceStream);
string resourceText = reader.ReadToEnd();
return resourceText;
}

The next is the "ControlParser" object.  Those of you who have used the "ParseControl()" method before probably did this on the "Page" object.  Unfortunatly the Page object is not available all the time, and so when one of the properties calls "EnsureChildControls()" we get a object null error.
The ControlParser class inherits from the TemplateControl abstract class (same as Page) and is also quite simple.  Setting the .AppRelativeVirtualPath is what allows things to work correctly and not receive "virtualPath" errors:
Here is entire code base of that control:
/// <summary>
/// Allows parsing of controls without an HttpContext or Page object
/// Normally ParseControl would be called from the Page object but thats
/// not possible when the Page object is not in scope/available
/// </summary>
public class ControlParser : TemplateControl, INamingContainer
{
public Control Parse(string controlText)
{
this.AppRelativeVirtualPath = "/";

return this.ParseControl(controlText);
}
}
Ok, now that you know the secret parts of the control lets go back and talk about the CreateChildControls() method, here it is again so you don't have to scroll up:

protected override void CreateChildControls()
{
string controlText = CSUtility.GetEmbeddedResource("CreateSoftwareUtils.Web.Controls.SectionHeaderControlText.txt");
ControlParser parser = new ControlParser();
Control parsedMasterControl = parser.Parse(controlText);
parsedMasterControl.ID = "masterControl";
this.Controls.Add(parsedMasterControl);

_contentControls = parsedMasterControl.FindControl("plContent") as PlaceHolder;
Label lblTitle = parsedMasterControl.FindControl("lblTitle") as Label;

lblTitle.Text = Title;
base.CreateChildControls();
}
What's happening?
Basically heres what happens:
  • (1): We get the control markup from the embedded resource
  • (2,3):  Use ParserControl to create a new Control from the control markup
  • (4): Set the ID for fun
  • (5): Add the Control we just created dynamically to the Control Tree
  • (6,7): Get references to controls within the dynamic control so we can do stuff with them
  • (8): Set the title
This, to me, is a lot easier to look at than 300 lines of declartive C# code that creates a big long control tree!
Full Source:
Here is the complete source of the control:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;


namespace CreateSoftwareUtils.Web.Controls
{
public class SectionHeader : WebControl, INamingContainer
{
private string _title;
[
Browsable(true),
PersistenceMode(PersistenceMode.Attribute)]
public string Title
{
get
{
return _title;
}
set
{
_title = value;
}
}

private PlaceHolder _contentControls;
[PersistenceMode(PersistenceMode.InnerProperty)]
public PlaceHolder ContentControls
{
get
{
EnsureChildControls();
return _contentControls;
}
}

protected override void CreateChildControls()
{
string controlText = CSUtility.GetEmbeddedResource("CreateSoftwareUtils.Web.Controls.SectionHeaderControlText.txt");
ControlParser parser = new ControlParser();
Control parsedMasterControl = parser.Parse(controlText);
parsedMasterControl.ID = "masterControl";
this.Controls.Add(parsedMasterControl);

_contentControls = parsedMasterControl.FindControl("plContent") as PlaceHolder;
Label lblTitle = parsedMasterControl.FindControl("lblTitle") as Label;

lblTitle.Text = Title;
base.CreateChildControls();
}
}
}
Conclusion
Now you may be wondering why I didn't use ITemplate's and TemplateContainers for creating this templated control, here's why:
Anything contained within an ITemplate is not accessible from within the page unless you use FindControl() on the parent to locate what you need.  I find this to be very annoying and adds alot of ugly code in the back end to get references and you don't have intellisense available.
[NOTE: it also possible to use ITemplate instead of PlaceHolder by setting the TemplateInstance.Single attribute on the property so that the internal Template controls would be accesible outside the container.  This is new in 2.0 and later]
By exposing a PlaceHolder as the Template instead of ITemplate inside the custom control, you still get to have Intellisence, normal control references, and everything just works as expected on the consuming .ASPX/.ASCX page
One last thing, since I didn't create any "Designer" for this Control it of course doesn't render/pukes in the "Design" view of Visual Studio.  If you end up writing a designer for this control let me know and I will post it here for all to enjoy, including me! ;-)
Let me know what you think of this article, the more responses/followers/comments I receive the more encouraged I will be to write another.  Unless of course the responses/comments are all "You Suck!" ;-)
Thanks,
Joshua Mason
Senior Software Engineer

No comments: