Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Embedded UserControls: Revisited

4.83/5 (19 votes)
7 Jan 20076 min read 1   1.4K  
Embedding UserControls in .NET assemblies is easy with VS2005 SP1

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.

Image 1

To start from scratch, create a new ASP.NET Web Application project. Notice the difference here - we choose New > Project.

Image 2

Image 3

Since we're creating an assembly, remove Default.aspx and web.config. This will leave you with an empty project.

Image 4

Create VirtualPathProvider.cs or copy it from the sample project.

C#
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.

C#
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.

Image 5

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.

Image 6

Run Default.aspx and be amazed. Your user control loads flawlessly, directly from the ControlLibrary assembly!

Image 7

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.

Image 8

FeedbackControl.ascx

ASP.NET
<%@ 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

C#
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.

Image 9

Feedback.aspx

ASP.NET
<%@ 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>
            &nbsp;</div>
    </form>
</body>
</html>

Feedback.aspx.cs

C#
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.

Image 10

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.

C#
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.

Image 11

FeedbackCustomControl.aspx.cs

C#
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!

Image 12

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here