Article of the day at ASP.NET (august 26th, 2010)
FULL SOURCE CODE IS AVAILABLE AT THE BOTTOM OF THIS ARTICLE.
Converting a UserControl (ascx markup-based control) to a CustomControl (redistributable-dll based control) is a well known thread among ASP.NET developers.
There is an old MSDN article about pre-compiling a web project, publishing it and using the generated Asp_Web_MyUserControl.ascx_xxxxx.dll assembly to port the UserControl in other projects.
This seems to me (and many other developers) much closer to a hack than to a solution.
In these days I came upon an interesting article in timgittos blog. The author embeds the ascx markup file, loads it at runtime, then manually (by code) re-binds each control from the markup to newly created variables.
Well, this is much closer to what I was looking for!
I tested it and it seems working fine. There is still one drawback though: manually rebinding forces you to write customized code for each embedded UserControl. This breaks portability.
Here is my try to get rid of this limitation.
Let's start creating an EmbeddedUserControl class which will encapsulate timgittos' logic to create a control from embedded ascx markup.
public abstract class EmbeddedUserControl : UserControl
{
private string _markupName;
public EmbeddedUserControl(string markupName)
{
_markupName = markupName;
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// loads markup from embedded resource
string content = String.Empty;
Stream stream = Assembly.GetAssembly(GetType()).GetManifestResourceStream(GetType(), _markupName);
using (StreamReader reader = new StreamReader(stream))
{
content = reader.ReadToEnd();
}
Control userControl = Page.ParseControl(content);
Controls.Add(userControl);
}
}
This is almost the same code we have seen in timgittos' blog.
Now we can create an .ascx file like the following:
<asp:Label runat="server" ID="MyLabel" Text="Test" />
<asp:Button runat="server" ID="TestButton" Text="Test" />
We can save it as "MyControl.ascx". Remember to set it's Build Action to Embedded Resource in the Solution Explorer.
Now it's time to create its "code behind". We can call it MyControl.ascx.cs. It will be as simple as the following inherited class:
public class MyControl : EmbeddedUserControl
{
public Label MyLabel;
public Button MyButton;
public MyControl()
: base("MyControl.ascx")
{
}
}
Well, now we can create a WebSite, reference our DLL and use the following test page:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register TagPrefix="Test" Namespace="MyControl" Assembly="MyControl" %>
<!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></title>
</head>
<body>
<form id="form1" runat="server">
<Test:MyControl ID="MyControl" runat="server" />
</form>
</body>
</html>
If everything is working, you should see our ascx markup is loaded, showing a "Test" label and a "Test" button.
This is ok, but nothing new if compared to timgittos' code.
Now, what if in the Default.aspx.cs code-behind we try to set the label's text like this:
protected void Page_Load(object sender, EventArgs e)
{
MyControl.MyLabel.Text = "Hello!";
}
We will get a NullReferenceException beacuse MyLabel is never instanciated.
Here comes the interesting part.
I have implemented an attribute we can assing to MyControl's fields to identify Controls we have defined in the ascx markup:
[AttributeUsage(AttributeTargets.Field, AllowMultiple=false)]
public class MarkupControlAttribute : Attribute
{
public string ID;
public MarkupControlAttribute()
{
}
public MarkupControlAttribute(string id)
{
ID = id;
}
}
And another attribute to handle event-binding:
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class MarkupControlEventAttribute : Attribute
{
public string EventName;
public string EventHandler;
public MarkupControlEventAttribute(string eventName, string eventHandler)
{
EventName = eventName;
EventHandler = eventHandler;
}
}
Now, let's update MyControl:
public class MyControl : EmbeddedUserControl
{
[MarkupControl()]
public Label MyLabel;
[MarkupControl()]
[MarkupControlEvent("Click", "onClick")]
public Button MyButton;
public MyControl()
: base("MyControl.ascx")
{
}
private void onClick(object sender, EventArgs e)
{
MyButton.Text = "Hello!";
}
}
And add binding logic in the EmbeddedUserControl base class:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// loads markup from embedded resource
string content = String.Empty;
Stream stream = Assembly.GetAssembly(GetType()).GetManifestResourceStream(GetType(), _markupName);
using (StreamReader reader = new StreamReader(stream))
{
content = reader.ReadToEnd();
}
Control userControl = Page.ParseControl(content);
Controls.Add(userControl);
// binds child controls and events
bindControls(userControl);
}
private void bindControls(Control control)
{
Type type = GetType();
foreach (FieldInfo fieldInfo in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
foreach (MarkupControlAttribute attribute in fieldInfo.GetCustomAttributes(typeof(MarkupControlAttribute), true))
{
string id = string.IsNullOrEmpty(attribute.ID) ? fieldInfo.Name : attribute.ID;
Control childControl = control.FindControl(id);
fieldInfo.SetValue(this, childControl);
foreach (MarkupControlEventAttribute eventAttribute in fieldInfo.GetCustomAttributes(typeof(MarkupControlEventAttribute), true))
{
string eventName = eventAttribute.EventName;
string eventHandler = eventAttribute.EventHandler;
EventInfo eventInfo = childControl.GetType().GetEvent(eventName);
Delegate eventDelegate = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, GetType().GetMethod(eventHandler,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
eventInfo.AddEventHandler(childControl, eventDelegate);
}
}
}
}
Here I have added the bindControls method, which is called right after the ascx markup is loaded from the embedded resources.
- bindControls iterates all control's fields, looking for those marked with the MarkupControl attribute.
- It then binds the ascx markup control to the corresponding field using FindControl.
- It finally looks for MarkupControlEvent Attribute, creates a delegate and assigns the event handler using reflection.
This is it! Feedbacks and comments are welcome!
Here is full source code for you to test it:
CodeGolem.UserControlDLL.zip (27.58 kb)
Hope you find this useful in your projects!
Happy ASCX-embedding!