2008年3月24日星期一

深入理解 ASP.NET 动态控件 (Part 6 - 模板控件)

在之前的文章中,我极力推荐大家使用Repeater和MultiView这类TemplateControl,为什么呢?因为只有这样做,才算是符合MVP或MVC模式。(到底是MVP还是MVC,这视乎你选用什么呈现引擎了。)

虽然我们要动态创建控件,但实际上这部分控件仍然属于View的部分,我们应该尽量采用ASPX的声明性名义来描述这些控件,避免用C#代码来创建控件、设置属性并添加为子控件。就拿最简单的例子来说,创建一个LinkButton,通常我们都需要设置它的ID、Text、OnClick属性/事件,甚至还要设置OnCommand、CommandName、CommandArgument等属性/事件,那就是大概3到5个属性了,用ASPX来声明只需要1~2行代码,而用C#代码则需要写至少5行(把new和Add()也算上的话),由此可见在定义控件这类声明上ASPX比C#代码的可读性要高。

接下来,我们来研究一下TemplateControl是如何工作的,这自然要从如何编写一个TemplateControl讲起。

编写模板控件

在这里,我们假设要编写一个SimpleRepeater控件,自身不支持数据绑定,只有唯一一个名为ItemTemplate的模板,并且就按照Count属性指定的次数重复出现该模板。首先,让我们来定义这两个属性:
public ITemplate ItemTemplate { get; set; }
public int Count { get; set; }

因为ItemTemplate属性不是以键值对的形式在SimpleRepeater的声明中给出的,而是以内嵌一对标签的方式定义的,因此我们需要让解释器去把<ItemTemplate>...</ItemTemplate>中间的内容读取出来,并把解释结果作为ItemTemplate属性的值处理。这时候,我们就需要为SimpleRepeater类加上ParseChildrenAttribute,也就是这样子:
[ParseChildren(true)] public class SimpleRepeater {...}

最后,我们需要重载一下CreateChildControl()方法,把ItemTemplate的内容作为子控件添加到SimpleRepeater之内:
protected override void CreateChildControls()
{
if (ItemTemplate != null)
{
Controls.Clear();
for (int i = 0; i < Count; i++)
{
Control control = new Control();
ItemTemplate.InstantiateIn(control);
Controls.Add(control);
}
}
}

这段代码的用意应该是相当清晰的了,就是循环Count指定那么多次,每次循环创建一个空白的Control,用ItemTemplate.InstantiateIn()方法填充它,最后把它添加到SimpleRepeater.Controls里面,就那么简单。那么这个神秘的InstantiateIn()方法到底是干什么的呢?后面来解释。

编译模板控件

在之前的《深入理解 ASP.NET 动态控件 (Part 2 - 编译过程)》里面,我详细地解释了ASP.NET 2.0的编译模型。在《深入理解 ASP.NET 动态控件 (Part 5 - 编译实验)》中,我们又做了一个动手实验,亲眼看到了ASPX和C#代码是如何编译到一起的。现在让我们来看看当碰上模板控件时,代码会被如何编译吧。我们把上面编写的SimpleRepeater注册到页面上,前缀为ctrl,并且编写如下一段代码:
<ctrl:SimpleRepeater ID="SimpleRepeater1" Count="10" runat="server">
<ItemTemplate>
<asp:Button ID="Button1" Text="Button" runat="server" />
</ItemTemplate>
</ctrl:SimpleRepeater>

然后还是用aspnet_compiler编译一下,并且用Reflector打开编译出来的dll看看。我们可以看到构造SimpleRepeater实例是通过这样一个语句完成的:
return new SimpleRepeater { ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control1)), Count = 10 };

这个语句其实就是一个普通的new语句,并且给ItemTemplate和Count两个属性赋值了,唯一值得关注的就是ItemTemplate的值。ItemTemplate的类型是ITemplate,因此任何实现了ITemplate接口的类都可以复制给它,然而我们却从来没指定过到底要赋值什么类型的实例给它,因为这已经由ASP.NET帮我们想好了,假若我们不指定的话,那就是CompiledTemplateBuilder类型。还是熟悉的Builder模式,它只需要已委托的形式接受一个实例化ITemplate的函数,然后就能返回实例化好的ITemplate控件子树。可能你会问,既然我已经有实例化ITemplate的函数,干什么要先传给你CompiledTemplateBuilder,让你来调用一下,再把实例化好的给我?我自己实例化不好吗?在此,ASP.NET引擎的做法只是为了保持Builder模式的一致性,处处用Builder模式来分离逻辑而已。

那么这个用于实例化ITemplate的函数从哪来呢?在解释器进入到<ItemTemplate>...</ItemTemplate>内部时,它会继续层层构建Builder模式,就如同在整个页面内执行的一样。因此,整个<ItemTemplate>...</ItemTemplate>会被解释器转化为一个函数,它也是通过层层调用内部函数完成自身的控件子树的构建,传递给BuilderTemplateBuilder构造函数的委托正是指向此函数。

因此,模板控件里面的内容将如同模板外的内容一样,被无缝地解释和构建到一起来。

INamingContainer

如果你查看SimpleRepeater输出的HTML代码,你会发现里面有10个<input id="Button1" name="Button1" type="button" value="Button" />。我们都知道,重复的id是不符合标准的,因此我们需要通过INamingContainer把这个问题解决掉。因为重复的控件是ITemplate,所以应该对它加上INamingContainer,然而它的实例编译时自动使用了CompiledTemplateBuilder,我们如何把INamingContainer加上去呢?我们就只能把INamingContainer加到它的父控件上面去。此时,我们需要一个实现了INamingContainer的简单控件:
public class SimpleRepeaterItem : System.Web.UI.Control, System.Web.UI.INamingContainer {}

然后我们把SimpleRepeater.CreateChildControls()方法的这个语句:Control control = new Control(),替换为:SimpleRepeaterItem control = new SimpleRepeaterItem()。这样,ITemplate的容器就变成了一个具有INamingContainer接口的控件,这时候各个Button的客户端id就会自动加上其容器的id作为前缀,因为容器的服务器端ID是自动变好的,所以必然是各不相同的,这样就解决了Button客户端id相同的问题。

通过这个例子,我们了解到了编写模板控件时必须为模板的容器加上INamingContainer,因为模板内的控件ID命名是可能重复的,加上INamingContainer就可以避免它们的客户端id重复。

小结

这次的文章解释了为什么我们应该尽量使用模板控件来实现动态控件,并且也说明了如何编写自己的模板控件,以及模板控件最终是被如何编译为Builder模式的代码的。

没有评论:

发表评论