Introduction
Visual Studio 2005 Service Pack 1 restores support for ASP.NET Web Projects, making it once again possible to compile ASPX and ASCX pages into a single assembly. Combined with design-time WYSIWYG support and clever use of resource embedding, it is possible to leverage the ASP.NET UserControl
technology to create web control libraries that are as flexible and robust as first-class custom controls.
Background
The initial release of Visual Studio 2005 dropped support for the "web project", much to the dismay of web developers. In response, a Web Application Project option became available to restore this functionality, but you had to go get it and install it. With the Service Pack 1 release, this functionality was restored and now we're going to take it for a spin!
Prerequisites
Project Setup
To demonstrate the power of embedded controls, we need to create two projects:
- A control library
- A test web site
You can use the project stub attached to this project, or if you prefer, you can set up your own project from scratch. When you finish, your solution layout should have one web project for testing and one web project that will contain your embedded controls.
To start from scratch, create a new ASP.NET Web Application project. Notice the difference here - we choose New > Project.
Since we're creating an assembly, remove Default.aspx and web.config. This will leave you with an empty project.
Create VirtualPathProvider.cs or copy it from the sample project.
using System.Web.Hosting;
using System.Web.Caching;
using System.Collections;
using System;
using System.IO;
using System.Web;
using System.Reflection;
namespace ControlLibrary
{
public class AssemblyResourceProvider : VirtualPathProvider
{
string mResourcePrefix;
public AssemblyResourceProvider()
:this("EmbeddedWebResource")
{
}
public AssemblyResourceProvider(string prefix)
{
mResourcePrefix = prefix;
}
private bool IsAppResourcePath(string virtualPath)
{
String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return checkPath.StartsWith("~/"+mResourcePrefix+"/",
StringComparison.InvariantCultureIgnoreCase);
}
public override bool FileExists(string virtualPath)
{
return (IsAppResourcePath(virtualPath) ||
base.FileExists(virtualPath));
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsAppResourcePath(virtualPath))
return new AssemblyResourceVirtualFile(virtualPath);
else
return base.GetFile(virtualPath);
}
public override CacheDependency
GetCacheDependency(string virtualPath,
IEnumerable virtualPathDependencies,
DateTime utcStart)
{
if (IsAppResourcePath(virtualPath))
return null;
else
return base.GetCacheDependency(virtualPath,
virtualPathDependencies, utcStart);
}
}
class AssemblyResourceVirtualFile : VirtualFile
{
string path;
public AssemblyResourceVirtualFile(string virtualPath)
: base(virtualPath)
{
path = VirtualPathUtility.ToAppRelative(virtualPath);
}
public override Stream Open()
{
string[] parts = path.Split('/');
string assemblyName = parts[2];
string resourceName = parts[3];
assemblyName = Path.Combine(HttpRuntime.BinDirectory,
assemblyName);
Assembly assembly = Assembly.LoadFile(assemblyName);
if (assembly == null) throw new Exception("Failed to load " +
assemblyName);
Stream s = assembly.GetManifestResourceStream(resourceName);
if (s == null) throw new Exception("Failed to load " +
resourceName);
return s;
}
}
}
Points of interest
- Note the constructor takes a configurable prefix. This differs from the original code.
- Note the exceptions thrown if the assembly or resource name is not found. This differs from the original code.
Create or copy the EmbeddedUserControlLoader.cs class. This is the one that gives us VS2005 toolbox support.
using System.Web.UI.WebControls;
using System.Web.UI;
using System;
using System.ComponentModel;
using System.Web;
namespace ControlLibrary
{
[ToolboxData("<{0}:EmbeddedUserControlLoader runat=server>
</{0}:EmbeddedUserControlLoader>")]
public class EmbeddedUserControlLoader : WebControl
{
# region VirtualPathProvider setup code
static EmbeddedUserControlLoader()
{
if (!IsDesignMode)
{
System.Web.Hosting.HostingEnvironment.
RegisterVirtualPathProvider(
new AssemblyResourceProvider(ResourcePrefix));
}
}
static bool IsDesignMode
{
get
{
return HttpContext.Current == null;
}
}
static string mResourcePrefix = "EmbeddedWebResource";
public static string ResourcePrefix
{
get
{
return mResourcePrefix;
}
set
{
mResourcePrefix = value;
}
}
#endregion
#region Toolbox properties
private string mAssemblyName = "";
[Bindable(true)]
[Category("Behavior")]
[Localizable(true)]
public string AssemblyName
{
get { return mAssemblyName; }
set { mAssemblyName = value; }
}
private string mControlNamespace = "";
[Bindable(true)]
[Category("Behavior")]
[Localizable(true)]
public string ControlNamespace
{
get { return mControlNamespace; }
set { mControlNamespace = value; }
}
private string mControlClassName = "";
[Bindable(true)]
[Category("Behavior")]
[Localizable(true)]
public string ControlClassName
{
get { return mControlClassName; }
set { mControlClassName = value; }
}
#endregion
#region Private members
string Path
{
get
{
return String.Format("/{0}/{1}.dll/{2}.{3}.ascx",
ResourcePrefix, AssemblyName, ControlNamespace,
ControlClassName);
}
}
Control c;
protected override void OnInit(EventArgs e)
{
c = Page.LoadControl(Path);
Controls.Add(c);
base.OnInit(e);
}
protected override void RenderContents(HtmlTextWriter output)
{
if (IsDesignMode)
{
output.Write(Path);
return;
}
base.RenderContents(output);
}
#endregion
#region Helper members to access UserControl properties
public void SetControlProperty(string propName, object value)
{
c.GetType().GetProperty(propName).SetValue(c, value, null);
}
public object GetControlProperty(string propName)
{
return c.GetType().GetProperty(propName).GetValue(c, null);
}
#endregion
}
}
Points of interest
- Static method
Registering a virtual path has a special sequence tied to it. Microsoft recommends registering all virtual paths "before any page parsing or compilation is performed by the Web application" and that registering them later will not affect pages that are compiled and cached.
In web portal settings, you do not always have access to Application_Start()
. Therefore, I prefer an alternative that initializes the provider within the user code. In my testing I didn't have any problem registering in the static
constructor of EmbeddedUserControlLoader
because that executes before any instance of the class executes, therefore the virtual path is configured in time for OnInit
to utilize it. For more information, read about VirtualPathProvider on MSDN.
- Static prefix property
Your control library must implement a unique prefix for the virtual paths to work. This property allows you to configure the prefix at runtime.
Get
/Set
methods
This class allows you to get
and set
properties on the underlying control. More on that later.
- Toolbox design-time properties
You can specify the assembly name, namespace, and control name to load at design-time. More on that later.
As a final step, create your test web site. This can be either a new ASP.NET Web Project or a traditional VS2005 Website project. Keep the default.aspx and web.config, and be sure to add a reference to your ControlLibrary
assembly.
Creating and Embedding Your First UserControl
With our solution stub in hand, we are ready to begin! Let's create a Hello World UserControl
to demonstrate.
Create a new UserControl
(named HelloControl
) in your ControlLibrary
project. Put some text in the UserControl
.
Then - and this is the key step - set the Build Action
on the ASCX file to Embedded Resource
. That's it! You are done. Compile. Your ASCX file is now embedded in ControlLibrary.dll.
Now turn to your test site where you have included a reference to ControlLibrary
. Open Default.aspx and drag an EmbeddedUserControlLoader
control from the VS2005 toolbox to your page. You can check HelloControl.ascx.cs in ControlLibrary
for the appropriate values for AssemblyName
, ControlClassName
, and ControlNamespace
. As you change them, the control's design-time representation will display the path to the embedded resource that it intends to load.
Run Default.aspx and be amazed. Your user control loads flawlessly, directly from the ControlLibrary
assembly!
Now that you've done something basic, let's take a look at a few more complicated scenarios.
Advanced Technique: Nested Controls
Your embedded UserControls
can be nested. This works so well that I'll leave it up to you. Just drop EmbeddedUserControlLoader
objects in the UserControls
you create in the ControlLibrary
project. You can nest your controls all day long.
Advanced Technique: Property Accessors
You can use EmbeddedUserControlLoader
to access properties of your UserControls
. This is worth understanding in detail because it is the only thing that separates your controls from feeling like genuine custom controls.
Let's make a feedback control. We want to collect some basic information and expose it through properties.
FeedbackControl.ascx
<%@ Control Language="C#" AutoEventWireup="true"
Codebehind="FeedbackControl.ascx.cs"
Inherits="ControlLibrary.FeedbackControl" %>
<h1>
Your Feedback is <span style="font-size: 5pt">not</span> Valuable</h1>
Name:
<asp:TextBox ID="txtName" runat="server"></asp:TextBox><br />
Subject:
<asp:TextBox ID="txtSubject" runat="server"></asp:TextBox><br />
Message:<br />
<asp:TextBox ID="txtMessage" runat="server" Height="256px"
TextMode="MultiLine" Width="353px"></asp:TextBox>
FeedbackControl.ascx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
namespace ControlLibrary
{
public partial class FeedbackControl : System.Web.UI.UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
}
public string Name
{
get
{
return this.txtName.Text;
}
}
public string Subject
{
get
{
return this.txtSubject.Text;
}
}
public string Message
{
get
{
return this.txtMessage.Text;
}
}
}
}
Now, let's make a feedback form that collects the information. Rather than emailing it, we will just display it to the user at postback.
Feedback.aspx
<%@ Page Language="C#" AutoEventWireup="true"
Codebehind="Feedback.aspx.cs" Inherits="DemoSite.Feedback" %>
<%@ Register Assembly="ControlLibrary" Namespace="ControlLibrary"
TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<cc1:EmbeddedUserControlLoader ID="EmbeddedUserControlLoader1"
runat="server" AssemblyName="ControlLibrary"
ControlClassName="FeedbackControl"
ControlNamespace="ControlLibrary">
</cc1:EmbeddedUserControlLoader>
<br />
<br />
<asp:Button ID="Button1" runat="server" OnClick="Button1_Click"
Text="Submit Feedback" /><br />
<br />
<asp:Panel ID="Panel1" runat="server" Height="244px"
Visible="False" Width="383px">
Name:
<asp:Label ID="lblName" runat="server" Text="Label">
</asp:Label><br />
Subject:
<asp:Label ID="lblSubject" runat="server" Text="Label">
</asp:Label><br />
Message:<br />
<asp:Label ID="lblMessage" runat="server" Text="Label">
</asp:Label></asp:Panel>
</div>
</form>
</body>
</html>
Feedback.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
namespace DemoSite
{
public partial class Feedback : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void Button1_Click(object sender, EventArgs e)
{
this.Panel1.Visible = true;
this.lblName.Text =
this.EmbeddedUserControlLoader1.GetControlProperty("Name")
as string;
this.lblSubject.Text=
this.EmbeddedUserControlLoader1.GetControlProperty("Subject")
as string;
this.lblMessage.Text =
this.EmbeddedUserControlLoader1.GetControlProperty("Message")
as string;
}
}
}
And there you have it. You have accessed your properties almost as easily as you can do with custom controls.
Advanced Technique: Custom Control Wrappers
Ok, some of you may not be satisfied with EmbeddedUserControlLoader.GetPropertyValue()
and I can appreciate that. For some situations, such as if you are distributing your control commercially, you want full design-time support available in Visual Studio 2005.
Let's approach that by creating a light-weight, custom control wrapper for the feedback control. Create a custom control FeedbackControlWrapper.cs in the ControlLibrary
project.
using System.Web.UI;
namespace ControlLibrary
{
[ToolboxData("<{0}:FeedbackControlWrapper runat=server>
</{0}:FeedbackControlWrapper>")]
public class FeedbackControlWrapper : EmbeddedUserControlLoader
{
public FeedbackControlWrapper()
{
ControlClassName = "FeedbackControl";
AssemblyName = "ControlLibrary";
ControlNamespace = "ControlLibrary";
}
public string Name
{
get
{
return GetControlProperty("Name") as string;
}
}
public string Subject
{
get
{
return GetControlProperty("Subject") as string;
}
}
public string Message
{
get
{
return GetControlProperty("Message") as string;
}
}
}
}
Points of interest
- The wrapper is a genuine custom control that uses the same property accessor technique shown above in Feedback.aspx.cs, but hides it from the end user of your control. Notice that the
ToolboxData
control prefix is FeedbackControlWrapper
. It should match the class name and should be different than the UserControl
class name. (An alternate naming convention is to save the SomeControl
name pattern for the wrapper names and use a different pattern for the underlying UserControls
that nobody else will ever see). - Creating a palatable design-time rendering of your control is beyond the scope of this article, but
EmbeddedUserControlLoader
demonstrates what you would need to do to create a design-time rendering. In general, HttpContext
is not available and must be simulated. Any commercial control must deal with the issue of design-time rendering better than I have in this article! - You can add properties to the wrapper that will appear in the VS2005 property designer box. Search around for your favorite article demonstrating how to expose properties of custom controls at design-time.
The new custom control feedback page is nearly identical to the earlier example. However this time, we drop our custom control from the VS2005 toolbox and change our code-behind to something a little more familiar.
FeedbackCustomControl.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
namespace DemoSite
{
public partial class FeedbackCustomControl : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void Button1_Click(object sender, EventArgs e)
{
this.Panel1.Visible = true;
this.lblName.Text = this.FeedbackControlWrapper1.Name;
this.lblSubject.Text = this.FeedbackControlWrapper1.Subject;
this.lblMessage.Text = this.FeedbackControlWrapper1.Message;
}
}
}
Success, and a much more professional architecture!
Advanced Topic: Migrating VS2005 UserControls
Users who want to migrate UserControl
files should be aware of the difference between CodeBehind
and CodeFile
in the @Control
attribute. CodeBehind
is the correct attribute to use, but CodeFile
is used by all UserControls
created with VS2005 before SP1.
See further details at http://msdn2.microsoft.com/en-us/library/d19c0t4b.aspx
History
- December 17, 2006 - Initial draft
- January 7, 2007 - Improved through various user comments
Credits
This article was heavily inspired by the following sources:
About Benjamin Allfree
Benjamin Allfree runs Launchpoint Software Inc., a technology-agnostic company specializing in unparalleled custom software solutions. Benjamin spends most of his time researching architecture and new technology.