TRULY Understanding Dynamic Controls (Part 2)

Part 1: Dynamic vs. Static
Part 2: Creating Dynamic Controls

PART 2

Creating Dynamic Controls

Creating a dynamic control isn't just about "newing" one up. There are several different types of dynamic controls. There are also several different ways that dynamic controls can manifest themselves in your page's control tree, sometimes when you don't even realize it. As promised, understanding this will not only increase your understanding of dynamic controls, but of ASP.NET in general.

A Dynamic Control can be a:
  1. Server Control
  2. User Control
  3. Parsed Control

But no matter what kind of control it is (dynamic or otherwise), they all share the same base class: System.Web.UI.Control. All built-in controls, custom server controls, and user controls share this base class. Even the System.Web.UI.Page class derives from it.

This section will focus very specifically on how dynamic controls are created. So the following code samples aren't really complete. Just creating a control dynamically isn't enough to get it participating in the page life cycle because it must be added to the control tree. But I like focusing in one thing at a time. Fully understanding something makes it easier to understand something that builds on it, because you can make assumptions and focus on the new functionality.

 

Dynamically creating Server Controls

TextBox tb = new TextBox();

There's not much to them. All the built-in controls are Server Controls, and so are any controls you create yourself that inherit from Control, WebControl, CompositeControl, etc.

 

Dynamically creating User Controls

First lets create a hypothetical user control:

<MyUserControl.ascx>
<%@ Control Language="C#" AutoEventWireup="true"
CodeFile="MyUserControl.ascx.cs" Inherits="MyUserControl" %>
Your name: <asp:TextBox ID="txtName" runat="server" Text="Enter your name" />

A user control is not unlike a regular ASPX webform in that the ASCX contains markup that declares "static controls", and inherits from a code-behind class, and that code-behind class itself may or may not load dynamic controls.

Assuming this ASCX file were placed into the root of the application (denoted by "~/"), we could dynamically load it like this:

Control c = this.LoadControl("~/MyUserControl.ascx");

You might wonder why there's a difference in loading server controls and user controls. Why can't we create a user control just like a server control? This user control has a code-behind class "MyUserControl", so why can't we do this:

MyUserControl c = new MyUserControl(); // no!

I invented a new code sample CSS style just to illustrate this. Blue means good. Red means bad. You definitely can't create a user control like this.

In Part 1 remember we examined how the page parser converts your markup into a class that inherits from your code-behind class? And remember that the generated code creates the controls, adds them to the control tree, and assigns them to your code-behind's control variables that are declared as protected? User Controls work the same way.

By creating the code-behind class directly you have effectively by-passed this whole process. The result will be an instance of your control with no control tree. Your control definitely won't work, and will probably result in a NullReferenceException the first time your code actually tries to use any of the controls (because, well, they're null).

That is why the "LoadControl" method exists, and that is why the actual ASCX file must stick around. LoadControl retrieves and instantiates the type of the auto-generated code created from your markup.

The path given to LoadControl must be the virtual path to the ASCX file. Physical paths aren't allowed. The virtual path can take on one of three forms. Lets assume the application is running under virtual directory name "Truly" (in other words, http://myserver/truly represents the root of the application). The three forms are:

Control c;
c = this.LoadControl("MyUserControl.ascx"); // relative
c = this.LoadControl("./MyUserControl.ascx"); // relative
c = this.LoadControl("~/MyUserControl.ascx"); // app relative
c = this.LoadControl("/Truly/MyUserControl.ascx"); // root relative

Whatever the case, the virtual path must map inside the current application. No loading user controls that reside in other applications. So a root-relative path better map back into the current application ("Truly"), or you will get an error.

Relative paths are relative to the current page, or the current user control. That is important to know, because if you write a user control that loads another user control dynamically, you should know the difference between these:

Control c;
c = this.LoadControl("AnotherUserControl");
c = this.Page.LoadControl("AnotherUserControl");

The path is relative to the control you call the method on. The first line loads a control that exists in the same directory as the current user control. The second line loads a control that exists in the same directory as the current page, and the two don't have to be the same. Don't assume your user control is going to live in a particular location. It might be moved around. Really you have the same problem with Page.LoadControl, because you don't really know where in the directory structure the page loading your control will be.

It's usually best to keep user controls in one location, or at least segregated into multiple "silos" that logically separate them. That way you can always use a relative path like in the first line, yet you still have the freedom to move the controls around, just as long as you move them together. Pages that reference the controls will just have to know where they are, at least relatively.

You could also consider making the path to it configurable (via a property), use a web.config setting (less favorable), or use an app-relative path (ie, "~/UserControls/MyUserControl.ascx") and make the location known and unchangeable. 

 

Dynamically creating Parsed Controls

The ParseControl method is even more dynamic. It takes a string of markup and converts it into a control:

Control c;
c = this.ParseControl("Enter your name: <asp:TextBox id='txtFirstName' runat='server'/>");

The string can contain virtually anything you can put on a page (disclaimer: I say virtually, because while I don't know of a limitation, there may be one. If anyone reading this knows of a limitation let me know). All of the controls contained within will be put together into a single control, where it's control tree contains the parsed controls.

Parsed controls are interesting, but use them very carefully. Parsed controls carry a performance burden, because the string must be re-parsed every time. Normally the parsed result of pages or user controls is cached.

Here's a dramatic demonstration of the performance hit. Here we create the same user control over and over again (1 million times):

start = DateTime.Now;
for(int i = 0; i < 1000000; i++) {
    c = this.LoadControl("UserControls/MyUserControl.ascx");
}
end = DateTime.Now;
Response.Write((end - start).TotalSeconds);

And here we parse the content of the user control as a string over and over again:

start = DateTime.Now;
for(int i = 0; i < 1000000; i++) {
    c = this.ParseControl(
"Enter your name: <asp:TextBox id='txtFirstName' runat='server'/>");
}
end = DateTime.Now;
Response.Write((end - start).TotalSeconds);

And here are the results:

ParseControl took 253 seconds, loading the user control only took 19 seconds. That's a huge difference. Running ParseControl 1 million times in 253 seconds is still not a major performance issue (you're likely to have much more significant bottle necks in your application), but your mileage may vary, and you can't deny the performance results. Use it very sparingly. And definitely don't parse a string that came from a potentially malicious user! You would be enabling them to create any control that is available in the application, and they could use that to attack your site.

Any virtual paths you use inside the string follow the same rules as in the user control example. Relative paths are relative to the location of the control (or page) which you call ParseControl on. So there's that same subtle but important difference between the UserControl.ParseControl method and the Page.ParseControl method.

 

Dynamic Controls and Templates

There's another sneaky way that dynamic controls can be created. You have likely used templates without even realizing it. This topic is critical because if you understand how templates are used in databound controls like the Repeater, DataGrid, GridView, etc, then you will better recognize times where you may be approaching a problem with dynamic controls when you really don't have to. Later on there will be a section all about those situations.

Templates decrease the need for you to create dynamic controls manually because they let you declaratively define a "template" by which controls will be created dynamically for you. The Repeater control for example lets you define an ItemTemplate and optionally an AlternatingItemTemplate. Within the template you declaratively define controls:

<asp:Repeater ID="rpt1" runat="server">
    <ItemTemplate>
        Your name: <asp:TextBox ID="txtName" runat="server" Text="Enter your name" />
    </ItemTemplate>
</asp:Repeater>

The template contains a TextBox server control. Do you think the page that this repeater sits on has this TextBox, just like in our simple UserControl above? Do you think you can access this TextBox like this?

protected override void OnLoad(EventArgs e) {
    this.txtName.Text = "foo"; // no!
    base.OnLoad(e);
}

No no no. Remember -- red means bad. The TextBox you declared does not exist on the page, at all. There is no "build control" method generated for it, and it will not be assigned to any protected class variables you define (as described in Part 1).

Think about it for a minute. It doesn't even make sense for the TextBox to be accessible from the page like a regular statically declared control. The Repeater is going to be creating this TextBox once for every DataItem that is data bound to it. So there will be many of these on the page -- anywhere between 0 and N of them in fact. So if there were a page-level reference to it, which one would it point to? It doesn't make any sense.

When you declare a regular static controls, you are telling the framework "here is a control I'd like you to create and add to the page's control tree." When you define a template, you are saying "here is a control tree that I would like added to the page's control tree when the template is used." In the case of a repeater, the template is used once for every data item. But Templates don't have to be used that way (for example, the System.Web.UI.WebControls.Login control's LayoutTemplate property is a Template that isn't repeated). It's up to the control's implementation how the template is utilized.

Instead, the markup within the Template is parsed into an object that implements the System.Web.UI.ITemplate interface. Let's take a look at what code is generated for the static repeater in the example above:

private global::System.Web.UI.WebControls.Repeater @__BuildControlrpt1() {
    global::System.Web.UI.WebControls.Repeater @__ctrl;
 
    #line 12 "C:/projects/Truly/MyPage.aspx"
    @__ctrl = new global::System.Web.UI.WebControls.Repeater();
 
    #line default
    #line hidden
    this.rpt1 = @__ctrl;
 
    #line 12 "C:/projects/Truly/MyPage.aspx"
    @__ctrl.ItemTemplate = new System.Web.UI.CompiledTemplateBuilder(
        new System.Web.UI.BuildTemplateMethod(this.@__BuildControl__control4));
 
    #line default
    #line hidden
 
    #line 12 "C:/projects/Truly/MyPage.aspx"
    @__ctrl.ID = "rpt1";
 
    #line default
    #line hidden
    return @__ctrl;
}

Pay particular attention to the middle of this method. The repeater control has a property named ItemTemplate (which happens to accept objects of type ITemplate). The parser's generated code is creating a new CompiledTemplateBuilder and passing in a BuildTemplateMethod Delegate to it's constructor. The delegate is pointed at a method defined on this page, which happens to be this "build control" method:

private void @__BuildControl__control4(System.Web.UI.Control @__ctrl) {
    System.Web.UI.IParserAccessor @__parser = 
          ((System.Web.UI.IParserAccessor)(@__ctrl));
 
    #line 12 "C:/projects/Truly/MyPage.aspx"
    @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("/r/n Your name: "));
 
    #line default
    #line hidden
    global::System.Web.UI.WebControls.TextBox @__ctrl1;
 
    #line 12 "C:/projects/Truly/MyPage.aspx"
    @__ctrl1 = this.@__BuildControl__control5();
 
    #line default
    #line hidden
 
    #line 12 "C:/projects/Truly/MyPage.aspx"
    @__parser.AddParsedSubObject(@__ctrl1);
 
    #line default
    #line hidden
 
    #line 12 "C:/projects/Truly/MyPage.aspx"
    @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("    /r/n   "));
 
    #line default
    #line hidden
}

This "build control" auto-generated method is responsible for building the control tree that we declared within the repeater's ItemTemplate. As you can see, it creates a literal control, then calls the TextBox's build control method to create it, and then creates another literal control (the literal controls represent the non-server-control markup before and after the TextBox).

But this method isn't actually called from anywhere. It's simply the target of a Delegate, which was passed to the CompiledTemplateBuilder. So we haven't figured out yet exactly how the controls get into the control tree.

That's because it's up to the control containing the template to do something with the template. The page parser has done it's job. Now it's up to the repeater to do something about it. And of course, we already know the repeater "uses" it once for each data item. To see exactly how it uses it, lets look at the ITemplate interface:

public interface ITemplate {
    void InstantiateIn(Control container);
}

That's the entire interface. Just one method. The idea is that you have this control tree template, and when you need to instantiate it -- when you need to create an actual control tree based on the template (not unlike the relationship between a Class and an Instance of a Class) -- you call InstantiateIn(). The control tree is then created and added to the container you give it.

So we can summarize Repeater's use of templates as the following:

  1. Go to the next data item.
  2. Determine the appropriate item template for this data item (one of: ItemTemplate, AlternatingItemTemplate, HeaderTemplate, FooterTemplate).
  3. Create a container control (repeater uses a RepeaterItem).
  4. Call the selected template's InstantiateIn method, passing the container.
  5. Add the container to the repeater's control collection.
  6. Goto step 1 if there's any data items left.

In addition to this logic, the Repeater has an optional SeparatorTemplate, which it calls InstantiateIn() on between each data item.

Remember the "build control" method that the Delegate pointed to? Calling InstantiateIn() on the template is going to call that method. So the method is executed once for each data item, and thus that txtName TextBox we declared in the ItemTemplate markup is going to be created multiple times, in a dynamic manner.

So as you can see, Templates are powerful. And it also demonstrates that the framework itself is very much involved in the creation of dynamic controls. You can also create templates programmatically instead of statically by implementing the ITemplate interface yourself, but that's for a different blog entry.

All this and I still haven't gotten to the heart of the matter. The next part will examine how and when dynamic controls are added to the control tree. By "when" I mean when during the page lifecycle. That is so important because "when" can affect how the dynamic control participates in the lifecycle, and depending on the type of control and what features of it you are relying on, doing it at the wrong time will break it...

 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值