精通页面-用户控件通信






4.86/5 (67投票s)
2004年11月15日
16分钟阅读

310400
精通页面-用户控件通信。
目录
引言
在上一篇《精通》系列文章(精通ASP.NET数据绑定)中,我们详细探讨了数据绑定——这是新闻组中被问及最多的主题之一。今天,我们将继续这个系列,回答另一个非常普遍的问题:如何最大化页面与其用户控件之间的通信。实际提出的问题通常不包含“最大化通信”这些词,而是更倾向于以下几种:
- 如何在用户控件中访问页面中的值?
- 如何在另一个用户控件中访问一个用户控件中的值?
- 如何在页面中访问用户控件中的值?
- 以及其他类似的问题。
本教程的目标不仅是回答这些问题,更重要的是建立一套理解的基础,从而真正让你成为页面-用户控件通信的大师。
理解基础知识
在回答上述问题之前,有两项基本概念需要理解。一如既往,这些基本概念不仅超出了本教程的范围,而且通过真正理解它们,你将走上精通ASP.NET的道路。这两个概念,以及因此对上述问题的回答,都涉及到面向对象原则。使用扎实的面向对象方法论是开发ASP.NET解决方案中反复出现的主题,但我们必须意识到,这可能会让一些程序员感到畏惧。然而,如果你已经读到这里,说明你愿意做的不只是复制粘贴一个答案。
页面是一个类
这可能已经是你已知的事情了,但你创建的每一个代码隐藏文件实际上都会被编译成一个类。然而,你很可能还没有真正利用好这个知识。在我们深入之前,先来看看代码隐藏文件的结构。
1: //C#
2: public class SamplePage : System.Web.UI.Page {
3: private string title;
4: public string Title{
5: get { return title; }
6: }
7: ...
8: }
1: 'VB.NET
2: Public Class SamplePage
3: Inherits System.Web.UI.Page
4: Private _title As String
5: Public ReadOnly Property Title() As String
6: Get
7: Return _title
8: End Get
9: End Property
10: ...
11: End Class
正如你所见,它和其他类一样——只是ASP.NET页面总是继承自System.Web.UI.Page
。实际上,这个类并没有什么特别之处,就像其他任何类一样。确实,ASP.NET页面与普通类相比行为略有不同,例如Visual Studio .NET会自动为你生成一些代码,称为*Web Form Designer generated code*,并且你通常使用OnInit
或Page_Load
事件来放置初始化代码——而不是构造函数。但这些是ASP.NET框架的区别;从你自己的角度来看,你应该像对待任何其他类一样对待页面。
那么这到底意味着什么呢?嗯,正如我们将要看到的,当我们开始查看具体答案时,System.Web.UI.Control
类(System.Web.UI.Page
和System.Web.UI.UserControl
都继承自它)暴露了一个Page
属性。这个Page
属性是对用户正在访问的当前页面实例的引用。对于实际的页面来说,这个引用几乎没有用处(因为它指向自身),但对于用户控件来说,如果使用得当,它可以非常有用。
继承
我最初写了很多关于继承是什么的内容。然而,从一开始,我就觉得成千上万的教程试图用几个基本示例和简化的解释来讲解核心的OO原则。虽然继承不是一个复杂的话题,但试图以一种不显得廉价的方式来教授它,这超出了我的写作能力。如果你真的对这个主题很陌生,请在Google上搜索C#继承。
与其深入讨论继承,不如简要介绍我们需要了解的内容。从上面的类结构中,我们可以清楚地看到我们的SamplePage
类继承自System.Web.UI.Page
(在更冗长的VB.NET示例中尤其可以看到)。这本质上意味着我们的SamplePage
类(至少)提供了System.Web.UI.Page
类提供的所有功能。这保证了SamplePage
的实例始终可以被安全地视为System.Web.UI.Page
(或它可能继承的任何类)的实例。当然,反过来并不总是成立;System.Web.UI.Page
的实例不一定是SamplePage
的实例。
真正重要的是要理解,我们的SamplePage
通过提供一个名为Title
的只读属性来扩展System.Web.UI.Page
的功能。然而,Title
属性只能从SamplePage
的实例访问,而不能从System.Web.UI.Page
访问。由于这是关键概念,让我们看一些例子。
1: //C#
2: public static void SampleFunction(System.Web.UI.Page page,
SamplePage samplePage) {
3: // IsPostBack property is a member of the Page class,
4: // which all instances of SamplePage inherit
5: bool pb1 = page.IsPostBack; //valid
6: bool pb2 = samplePage.IsPostBack; //valid
7:
8: // The ToString() method is a member
// of the Object class, which instances
9: //of both the Page and SamplePage classes inherit
10: string name1 = page.ToString(); //valid
11: string name2 = samplePage.ToString(); //valid
12:
13: //Title is specific to the SamplePage class, only it or classes
14: //which inherit from SamplePage have the Title property
15: string title1 = page.Title; //invalid, won't compile
16: string title2 = samplePage.Title; //valid
17: string title3 = ((SamplePage)page).Title;
//valid, but might give a run-time error
18: string title4 = null;
19: if (page is SamplePage){
20: title4 = ((SamplePage)page).Title;
21: }else{
22: title4 = "unknown";
23: }
24: }
1: 'VB.NET
2: Public Shared Sub SampleFunction(ByVal page As System.Web.UI.Page, _
ByVal samplePage As SamplePage)
3: 'IsPostBack property is a member of the Page class, which all instances
4: 'of SamplePage inherit
5: Dim pb1 As Boolean = page.IsPostBack 'valid
6: Dim pb2 As Boolean = samplePage.IsPostBack 'valid
7:
8: 'The ToString() method is a member of the Object class, which instances
9: 'of both the Page and SamplePage classes inherit
10: Dim name1 As String = page.ToString() 'valid
11: Dim name2 As String = samplePage.ToString() 'valid
12:
13: 'Title is specific to the SamplePage class, only it or classes
14: 'which inherit from SamplePage have the Title property
15: Dim title1 As String = page.Title 'invalid, won't compile
16: Dim title2 As String = samplePage.Title 'valid
17: Dim title3 As String = CType(page, SamplePage).Title
'valid, but might give a run-time error
18: Dim title4 As String = Nothing
19: If TypeOf page Is SamplePage Then
20: title4 = CType(page, SamplePage).Title
21: Else
22: title4 = "unknown"
23: End If
24: End Sub
前几个例子很简单。首先,我们看到我们的SamplePage
类如何从System.Web.UI.Page
继承IsPostBack
属性 [5,6]。然后我们看到SamplePage
和System.Web.UI.Page
都从System.Object
继承了ToString()
函数——.NET中的所有对象都继承自它。当我们玩转Title
属性时,事情会变得更有趣。首先,由于System.Web.UI.Page
类没有Title
属性,第一个例子是完全无效的,幸运的是它甚至不会编译 [15]。当然,由于我们的SamplePage
类定义了它,第二个例子是完全合理的 [16]。第三和第四个例子非常有趣。为了让我们的代码编译,我们可以简单地将page
实例强制转换为SamplePage
的类型,然后就可以访问Title
属性了 [17]。当然,如果page
实际上不是SamplePage
的实例,这会引发一个异常。第四个例子展示了一种更安全的方法:通过检查page
是否是SamplePage
的实例 [19],然后才进行强制转换 [20]。
为了结束这个(痛苦的)章节,需要理解的关键点是,当你创建一个新的ASPX页面时,页面本身就是一个类,它继承自System.Web.UI.Page
。如果你可以访问System.Web.UI.Page
的一个实例,并且你知道实际的类型(例如,SamplePage
),你就可以将其强制转换为该类型,然后访问它的功能——就像我们能够通过page
获得Title
一样。
基本通信
我们首先将讨论页面与其用户控件之间在所有方向上的基本通信策略。虽然本节本身可能会回答你的问题,但真正重要的内容将在下一节中讨论更高级的策略。对于基本通信,我们将使用一个带有两个用户控件的单个页面,并保持一切相对简单。我们将使用上面的示例页面以及这两个用户控件。
1: 'VB.NET - Results user control
2: Public Class Results
3: Inherits System.Web.UI.UserControl
4: Protected results As Repeater
5: Private info As DataTable
6:
7: Public Property Info() As DataTable
8: Get
9: Return info
10: End Get
11: Set
12: info = value
13: End Set
14: End Property
15:
16: Private Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
17: If Not Page.IsPostBack AndAlso Not (info Is Nothing) Then
18: results.DataSource = info
19: results.DataBind()
20: End If
21: End Sub
22: End Class
1: 'VB.NET - ResultsHeader user control
2: Public Class ResultHeader
3: Inherits System.Web.UI.UserControl
4: Private Const headerTemplate As String = "Page {1} of {2}"
5: Protected header As Literal
6: Private currentPage As Integer
7: Private recordsPerPage As Integer
8:
9: Public Property CurrentPage() As Integer
10: Get
11: Return currentPage
12: End Get
13: Set
14: currentPage = value
15: End Set
16: End Property
17:
18: Public Property RecordsPerPage() As Integer
19: Get
20: Return recordsPerPage
21: End Get
22: Set
23: recordsPerPage = value
24: End Set
25: End Property
26:
27: Private Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
28: header.Text = headerTemplate
29: header.Text = header.Text.Replace("{1}", currentPage.ToString())
30: header.Text = header.Text.Replace("{2}", recordsPerPage.ToString())
31: End Sub
32: End Class
从页面到用户控件
虽然从页面到用户控件的通信并不经常被问及(因为大多数人都知道如何做),但它仍然是开始的好地方。当在页面上放置用户控件时(例如,通过@Control
指令),对于简单类型,传递值非常直接。
1: <%@ Register TagPrefix="Result" TagName="Header" Src="ResultHeader.ascx" %>
2: <%@ Register TagPrefix="Result" TagName="Results" Src="Results.ascx" %>
3: <HTML>
4: <body>
5: <form id="Form1" method="post" runat="server">
6: <Result:Header id="rh" CurrentPage="1" RecordsPerPage="2" runat="server" />
7: <Result:Results id="rr" runat="server" />
8: </form>
9: </body>
10: </HTML>
我们可以看到,我们的ResultHeader
用户控件的CurrentPage
和RecordsPerPage
属性被像任何其他HTML属性一样设置了值 [5]。然而,由于Results
用户控件的Info
属性是一个更复杂的类型,因此必须通过代码设置。
1: protected Results rr;
2: private void Page_Load(object sender, EventArgs e) {
3: if (!Page.IsPostBack){
4: rr.Info = SomeBusinessLayer.GetAllResults();
5: }
6: }
在动态加载控件(通过Page.LoadControl
)时,重要的是要意识到返回的是System.Web.UI.Control
的一个实例——**而不是加载控件的实际类**。既然我们知道确切的类型,我们只需要先将其强制转换。
1: //C#
2: Control c = Page.LoadControl("Results.ascx");
3: c.Info = SomeBusinessLayer.GetAllResults();
//not valid, Info isn't a member of Control
4:
5: Results r = (Results)Page.LoadControl("Results.ascx");
6: r.Info = SomeBusinessLayer.GetAllResults(); //valid
1: 'VB.NET
2: dim c as Control = Page.LoadControl("Results.ascx")
3: c.Info = SomeBusinessLayer.GetAllResults()
'not valid, Info isn't a member of Control
4:
5: dim r as Results = ctype(Page.LoadControl("Results.ascx"), Results)
6: r.Info = SomeBusinessLayer.GetAllResults() 'valid
从用户控件到页面
从用户控件将信息传递到其包含的页面并不需要经常做。这样做存在时序问题,这使得事件驱动模型更有用(我将在本教程后面讨论时序问题和使用事件进行通信)。由于这可以很好地引出更常被问到的用户控件到用户控件的问题,我们将暂时忽略时序问题,快速检查一下。
正如我之前提到的,页面和用户控件最终都继承自System.Web.UI.Control
类,该类暴露了Page
属性——这是对正在运行的页面的引用。用户控件可以使用Page
属性来实现本教程中提出的大部分问题。例如,如果我们的ResultHeader
用户控件想要访问我们SamplePage
的Title
属性,我们只需要这样做:
1: //C#
2: string pageTitle = null;
3: if (Page is SamplePage){
4: pageTitle = ((SamplePage)Page).Title;
5: }else{
6: pageTitle = "unknown";
7: }
1: 'VB.NET
2: Dim pageTitle As String = Nothing
3: If TypeOf (Page) Is SamplePage Then
4: pageTitle = CType(Page, SamplePage).Title
5: Else
6: pageTitle = "unknown"
7: End If
在尝试强制转换之前,检查Page
实际上是否为SamplePage
类型非常重要 [3],否则我们将面临System.InvalidCastException
异常的风险。
从用户控件到用户控件
用户控件到用户控件的通信是我们到目前为止所见内容的延伸。我经常看到人们试图找到直接连接两个用户控件的方法,而不是依赖于共同点——页面。这是包含Results
和ResultHeader
用户控件的SamplePage
的代码隐藏文件。
1: Public Class SamplePage
2: Inherits System.Web.UI.Page
3: Private rr As Results
4: Private rh As ResultHeader
5: Private _title As String
6: Public ReadOnly Property Title() As String
7: Get
8: Return _title
9: End Get
10: End Property
11: Public ReadOnly Property Results() As Results
12: Get
13: Return rr
14: End Get
15: End Property
16: Public ReadOnly Property Header() As ResultHeader
17: Get
18: Return rh
19: End Get
20: End Property
21: ...
22: End Class
代码隐藏文件看起来与其他页面一样,只是为我们的两个用户控件添加了一个ReadOnly
属性 [11-15,16-20]。这使得一个用户控件可以通过相应的属性访问另一个用户控件。例如,如果我们的ResultHeader
想利用Result
的Info
属性,它可以通过以下方式轻松访问:
1: //C#
2: private void Page_Load(object sender, EventArgs e) {
3: DataTable info;
4: if (Page is SamplePage){
5: info = ((SamplePage)Page).Results.Info;
6: }
7: }
1: 'VB.NET
2: Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
3: Dim info As DataTable
4: If TypeOf (Page) Is SamplePage Then
5: info = CType(Page, SamplePage).Results.Info
6: End If
7: End Sub
这与上面的代码示例——一个用户控件访问页面值——是相同的。实际上,这就是正在发生的事情,ResultHeader
访问SamplePage
的Results
属性,然后深入一层,访问它的Info
属性。
没有什么魔力。我们正在使用类中的公共属性来实现我们的目标。页面通过属性设置用户控件的值,反之亦然,可以达到任何深度。只需注意,页面和用户控件是你可以编程的实际类;创建正确的公共接口(属性和方法),基本通信就会变得相当平淡(这并不总是坏事)。
访问方法
方法的访问方式与属性相同。只要它们被标记为Public
,页面就可以轻松访问其用户控件的一个方法,或者用户控件可以利用页面作为中间人来访问另一个用户控件的方法。
高级通信
虽然上一节旨在让你掌握实现[大多数]与本教程相关问题的知识,但我们将在本节重点关注更高级的主题,并大力强调良好的设计策略。
基本通信是糟糕的设计?
虽然上面各节讨论的代码和方法会起作用,有时甚至是正确的方法,但请考虑它们是否真正适合你的情况。你可能会问,为什么?因为如果它们本身不是糟糕的设计,那么除非你保持警惕,否则它们最终会导致糟糕的设计。例如,就访问方法而言,如果这些是实用/通用/静态/共享方法,请考虑将该函数移到你的业务层。
糟糕设计的另一个例子是这种通信在特定页面和用户控件之间产生的依赖关系。我们上面所有的示例用户控件,如果用在SamplePage
以外的页面上,将工作得非常不同,或者根本无法工作。用户控件是为了重用而设计的,在大多数情况下(这不是100%的规则),它们不应该需要其他用户控件或特定页面才能工作。接下来的两节将探讨改进这一点的方法。
利用接口
我们可以利用接口来减少这种通信产生的依赖关系。在最后一个例子中,ResultHeader
用户控件访问了Results
用户控件的Info
属性。这实际上是一件非常有效的事情,因为它避免了在访问记录总数时重复访问数据库(尽管有其他方法可以做到)。上述方法的问题在于,ResultHeader
只能与SamplePage
和Results
一起工作。良好地利用接口可以实际让ResultHeader
适用于任何显示结果的页面(无论结果是什么)。
什么是接口?接口是类必须遵守的合同。当你创建一个类并声明它实现了某个接口时,你必须(否则代码将无法编译)创建接口中定义的所有函数/属性/事件/索引器。就像你保证一个继承自另一个类的类将拥有**所有**父类的功能一样,你也保证一个实现接口的类将拥有**所有**接口成员的定义。你可以阅读微软的定义,或者这个教程,但我认为下面的几个例子将为你提供你所需的知识。
为了获得最大的灵活性,我们将创建两个接口。第一个将由显示结果的页面使用,并强制它们公开一个ReadOnly
属性,该属性又公开我们的另一个接口。
1: //C#
2: public interface IResultContainer{
3: IResult Result { get; }
4: }
1: 'VB.NET
2: Public Interface IResultContainer
3: ReadOnly Property Result() As IResult
4: End Interface
第二个接口IResult
,公开了一个DataTable
——即实际结果。
1: //C#
2: public interface IResult {
3: DataTable Info { get; }
4: }
1: 'VB.Net
2: Public Interface IResult
3: ReadOnly Property Info() As DataTable
4: End Interface
如果你对接口不熟悉,请注意这里实际上没有提供任何实现(没有代码)。这是因为实现这些接口的类必须提供代码(正如我们很快将看到的)。
接下来,我们让SamplePage
实现IResultContainer
并实现必要的代码。
1: Public Class SamplePage
2: Inherits System.Web.UI.Page
3: Implements IResultContainer
4:
5: Private rr As Results
6: Public ReadOnly Property Result() As IResult _
Implements IResultContainer.Result
7: Get
8: Return rr
9: End Get
10: End Property
11: ...
12: End Class
在我们能够使用它之前的最后一步是让Results
实现IResult
。
1: public class Results : UserControl, IResult {
2: private DataTable info;
3: public DataTable Info { //Implements IResult.Info
4: get { return info; }
5: }
6: ...
7: }
在做出这些更改后,ResultHeader
现在可以将自己与SamplePage
解耦,而是与更广泛的IResultContainer
接口关联。
1: Dim info As DataTable
2: If TypeOf (Page) Is IResultContainer Then
3: info = CType(Page, IResultContainer).Result.Info
4: Else
5: Throw New Exception("ResultHeader user control must be used" & _
" on a page which implements IResultContainer")
6: End If
不可否认,代码看起来和以前很相似。但它不再局限于SamplePage
,而是可以用于任何实现IResultContainer
的页面。IResult
的使用也将页面与实际的Results
用户控件解耦,并允许它使用任何实现IResult
的用户控件。
所有这些工作量似乎都是为了良好的设计。如果你有一个简单的站点,只需要显示一个结果,那么这可能有点过头了。但只要你开始添加不同的结果,接口就会带来回报,无论是减少开发时间,还是更重要的是,使你的代码易于阅读和维护。如果你不使用接口来解耦通信链接,请对你还能在哪里使用它们保持开放的心态,因为你可能会发现很多地方。
事件驱动通信
我还没有回答的一个问题是如何让页面(或其他用户控件)意识到用户控件中发生的事件。虽然可以使用上面描述的通信方法,但创建自己的事件可以完全将用户控件与页面解耦。换句话说,用户控件引发事件,并不关心谁(如果有的话)在收听。此外,这样做很有趣!
在我们的例子中,我们将创建一个第三个用户控件ResultPager
,它显示我们结果的分页信息。每当点击一个页码时,我们的用户控件就会引发一个事件,页面或其他用户控件可以捕获并做任何它们想做的事情。
1: //C#
2: public class ResultPaging : UserControl {
3: private Repeater pager;
4: public event CommandEventHandler PageClick;
5:
6: private void Page_Load(object sender, EventArgs e) {
7: //use the other communication methods to figure out how many pages
8: //there are and bind the result to our pager repeater
9: }
10:
11: private void pager_ItemCommand(object source,
RepeaterCommandEventArgs e) {
12: if (PageClick != null){
13: string pageNumber = (string)e.CommandArgument;
14: CommandEventArgs args = new CommandEventArgs("PageClicked",
pageNumber);
15: PageClick(this, args);
16: }
17: }
18: }
1: 'VB.NET
2: Public Class ResultPaging
3: Inherits System.Web.UI.UserControl
4: Private pager As Repeater
5: Public Event PageClick As CommandEventHandler
6: Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
7: 'use the other communication methods to figure out how many pages
8: 'there are and bind the result to our pager repeater
9: End Sub
10:
11: Private Sub pager_ItemCommand(ByVal source As Object, _
ByVal e As RepeaterCommandEventArgs)
12:
13: Dim pageNumber As String = CStr(e.CommandArgument)
14: Dim args As New CommandEventArgs("PageClicked", pageNumber)
15: RaiseEvent PageClick(Me, args)
16:
17: End Sub
18: End Class
我们的PageClick
事件被声明为CommandEventHandler
类型 [5],因此我们能够通知所有感兴趣的人,当一个页码被点击时。该控件的基本思想是使用Repeater
加载页码,并在Repeater
中发生事件时引发我们的PageClick
事件。因此,用户控件处理Repeater
的ItemCommand
[11],检索CommandArgument
[13],将其重新打包成CommandEventArgs
[14],最后引发PageClick
事件 [15]。C#代码必须做一些额外的工作,确保在尝试引发PageClick
之前它不是null
[12],而VB.NET的RaiseEvent
则会自动处理这一点(如果没有人收听,事件将是null/Nothing
)。
SamplePage
然后可以通过像处理其他事件一样挂钩PageClick
事件来利用这一点。
1: //C#
2: protected ResultPaging rp;
3: private void Page_Load(object sender, EventArgs e) {
4: rp.PageClick += new
System.Web.UI.WebControls.CommandEventHandler(rp_PageClick);
5: }
6: private void rp_PageClick(object sender,
System.Web.UI.WebControls.CommandEventArgs e) {
7: //do something
8: }
1: 'VB.Net WithEvents solution
2: Protected WithEvents rp As ResultPaging
3: Private Sub rp_PageClick(ByVal sender As Object, _
ByVal e As CommandEventArgs) Handles rp.PageClick
4: 'do something
5: End Sub
1: 'VB.Net AddHandler solution
2: Private rp As ResultPaging
3: Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
4: AddHandler rp.PageClick, AddressOf rp_PageClick
5: End Sub
6: Private Sub rp_PageClick(ByVal sender As Object, _
ByVal e As CommandEventArgs)
7: 'do something
8: End Sub
更有可能的是,Results
用户控件会通过SamplePage
利用这个事件,或者更好的是通过扩展IResultContainer
接口。
时序
在页面和用户控件之间通信时出现的一个困难与事件发生的时间有关。例如,如果Results
试图在ResultHeader
的RecordsPerPage
属性设置之前访问它,你将得到意外的行为。对抗这种困难的最佳武器是知识。
当以声明方式加载控件时(通过@Control
指令),页面会先触发Load
事件,然后按照它们在页面上放置的顺序触发用户控件。
同样,以编程方式加载的控件(通过Page.LoadControl
)将在它们被添加到控件树的顺序触发Load
事件(而不是在实际调用LoadControl
时)。例如,给定以下代码。
1: Control c1 = Page.LoadControl("Results.ascx");
2: Control c2 = Page.LoadControl("ResultHeader.ascx");
3: Control c3 = Page.LoadControl("ResultPaging.ascx");
4: Page.Controls.Add(c2);
5: Page.Controls.Add(c1);
c2
的Load
事件会先触发,然后是c1
的。c3
的Load
事件永远不会触发,因为它没有被添加到控件树。
当两种类型的控件都存在时(声明式和编程式),规则是相同的,只是所有声明式控件都会先加载,然后是编程式控件。即使控件是在Init
而不是Load
中以编程方式加载的,也是如此。
自定义事件与内置事件一样,也遵循同样的规则。在我们上面的事件示例中,当点击一个页码时(假设页面上除了ResultPaging
之外没有其他控件),执行顺序如下。
SamplePage
的OnLoad
事件。ResultPaging
的OnLoad
事件。ResultPaging
的pager_ItemCommand
事件处理程序。SamplePage
的rp_PageClick
事件处理程序。
当在事件中处理以编程方式创建的控件时,会出现真正的困难——例如,在按钮点击时向页面添加用户控件。问题在于,这些事情发生在页面加载视图状态之后,这可能会导致你错过用户控件中的事件,或者导致看似奇怪的行为,具体取决于你的操作。一如既往,这些都有解决办法,但远远超出了本教程的范围。一种解决方案可能是Denis Bauer的DynamicControlsPlaceholder控件(我还没有试过,但看起来非常有前景)。
结论
在结论中回顾要点似乎是好习惯,但实际上,要点在于尽可能多地利用你所学到的知识,并努力理解得尽可能透彻。尽量保持你的设计简洁,页面灵活,最重要的是,代码可读。页面是类,应该像对待类一样对待它们,即通过理解继承如何与类型转换/强制转换以及公共属性和方法相关联。