Mastering Page-UserControl Communication

类别:Asp 点击:0 评论:0 推荐:
source:
http://openmymind.net/communication

begin:

Karl Seguin [email protected]

Table of Contents Introduction Understanding the Basics A page is a class Inheritance Basic Communication From Page to User Control From User Control to Page From User Control to User Control Accessing Methods Advanced Communication Basic Communication is Bad Design? Making Use of Interfaces Event Driven Communication Timing Conclusion
This article is available at Code Project. Check it out to make comments, discuss or rate the article

I'd like to thank Jean-Claude Manoli for developing his C# Code format, which i used in writing this tutorial. IntroductionIn the previous Mastering article (Mastering ASP.Net DataBinding) we took a detailed look at databinding - one of the most asked about topics in the newsgroup. Today we continue the series by answering another very common question : how to maximize the communication between a page and its user controls. The actual questions asked typically don't include the words "maximize the communication" in them, but are more along the lines of: How to access values from a page in a user control? How to access values from one user control in another user control? How to access values from a user control in a page? And other similar questions The goal of this tutorial isn't only to answer these questions, but more importantly to build a foundation of understanding around these answers to truly make you a master of page-user control communication.

Understanding the BasicsBefore we can answer the above questions two basic concepts should be understood. As always, these basic concepts not only go beyond the scope of this tutorial, but by really understanding them you'll be on your way to mastering ASP.Net. Both these concepts, and therefore the answers to the above questions, deal with object oriented principals. The use of solid object oriented methodologies is a reoccurring theme in developing solutions in ASP.Net, but we must be conscious that this can unfortunately be intimidating for some programmers. If you've read this far however, you're willing to do more than simply copy and paste an answer.

A page is a classThis is probably something you already know, but each codebehind file you create is actually compiled into a class. There's a good chance however that you haven't really been taking advantage of that knowledge. Before we get ahead of ourselves, let's look at the shell of a codebehind file:

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 ClassAs you can see, it's a class like any other - except that an ASP.Net page always inherits from System.Web.UI.Page. In reality though, there's nothing special about this class, it's just like any other. It's true that ASP.Net pages behave slightly differently from normal classes, for example Visual Studio.Net automatically generates some code for you called Web Form Designer generated code, and you typically use the OnInit or Page_Load events to place your initializing code - instead of a constructor. But these are difference for the ASP.Net framework, from your own point of view you should treat pages like any other classes.

So what does that really mean? Well, as we'll see when we start to look at specific answers, the System.Web.UI.Control class, which System.Web.UI.Page and System.Web.UI.UserControl both inherit from, exposes a Page property. This Page property is a reference to the instance of the current page the user is accessing. The reference is pretty useless to the actual page (since its a reference to itself), but for a user control it can be quite useful when properly used.

InheritanceI originally wrote quite a bit about what inheritance was. However, from the start it felt like the thousands of tutorials which try to explain core OO principals with a couple basic examples and simplified explanations. While inheritance isn't a complicated topic, there's something about trying to teach it so it doesn't seem cheap which my writing skills just haven't reached yet. Ask google about C# inheritance if you're really new to the topic.

Instead of talking in depth about inheritance, we'll briefly touch on what we need to know. We can clearly see in the above class shell that our SamplePage class inherits from System.Web.UI.Page (we can especially see this in the more verbose VB.Net example). This essentially means that our SamplePage class provides (at the very least) all the functionality provided by the System.Web.UI.Page class. This guarantee that an instance of SamplePage can always safely be treated as an instance of System.Web.UI.Page (or any classes it might inherit from). Of course the opposite isn't always true, an instance of System.Web.UI.Page isn't necessarily an instance of SamplePage.

The truly important thing to understand is that our SamplePage extends the functionality of the System.Web.UI.Page by providing a readonly property named Title. The Title property however is only accessible from an instance of SamplePage and not System.Web.UI.Page. Since this is really the key concept let's look at some examples:

1: //C# 2: public static void SampleFunction(System.Web.UI.Page page, SamplePage samplePage) { 3: //IsPostBack property is a member of the Page class, which all instances 4: //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 SubThe first couple cases are straightforward, first we see how our SamplePage class inherits the IsPostBack property from System.Web.UI.Page [5,6]. We then see how both SamplePage and System.Web.UI.Page inherit the ToString() function from System.Object - which all objects in .Net inherit from. Things get more interesting when we play with the Title property. First, since the System.Web.UI.Page class doesn't have a Title property, the first example is totally invalid and thankfully won't even compile [15]. Of course, since our SamplePage class does define it, the second example is perfectly sane [16]. The third and forth examples are really interesting. In order to get our code to compile, we can simply cast the page instance to the type of SamplePage which then allows us to access the Title property [17]. Of course, if page isn't actually an instance of SamplePage this will generate an exception. The forth example illustrate a much safer way to do this: by checking to see if page is an instance of SamplePage [19] and only if it is casting it [20].

To wrap up this [painful] section, the key point to understand is that when you create a new ASPX page, the page itself is a class, which inherits from System.Web.UI.Page. If you have access to an instance of System.Web.UI.Page and you know the actual type (for example SamplePage), you can cast it to this type and then access its functionality - much like we were able to do with page and get the Title.

Basic CommunicationWe'll first discuss basic communication strategies between a page and its user controls in all directions. While this section alone will likely answer your questions, the important stuff comes in the following section where we discuss more advanced strategies. For the basic communication we'll use a single page with two user controls and keep everything fairly simple. We'll use our sample page from above, and these two user controls:

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

From Page to User ControlWhile communicating from a page to a user control isn't something frequently asked (because most people know how to do it) it nevertheless seems like the right place to start. When placing a user control on a page (ie, via the @Control directive) passing values is pretty straightforward for simple types:

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>We can see that the CurrentPage and RecordsPerPage properties of our ResultHeader user control are assigned a value like any other HTML property [5]. However, since the the Results user control's Info property is a more complex type and must thus be set via code:

1: protected Results rr; 2: private void Page_Load(object sender, EventArgs e) { 3: if (!Page.IsPostBack){ 4: rr.Info = SomeBusinessLayer.GetAllResults(); 5: } 6: }When loading a control dynamically, via Page.LoadControl, it's important to realize that an instance of System.Web.UI.Control is returned - not the actual class of the control loaded. Since we know the exact type, we simply need to cast it first:

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

From User Control to PageCommunicating information from a user control to its containing page is not something you'll need to do often. There are timing issues associated with doing this, which tends to make an event-driven model more useful (I'll cover timing issues and using events to communicate later in this tutorial). Since this provides a nice segue into the far more frequently asked user control to user control question we'll throw timing issues to the wind and quickly examine it.

As I've already mentioned, pages and user controls eventually inherit from the System.Web.UI.Control class which exposes the Page property - a reference to the page being run. The Page property can be used by user controls to achieve most of the questions asked in this tutorial. For example, if our ResultHeader user control wanted to access the our SamplePage's Title property, we simply need to:

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 IfIt's important to check that Page is actually of type SamplePage before trying to cast it [3] otherwise we'd risk of having a System.InvalidCastException thrown.

From User Control to User ControlUser control to user control communication is an extension of what we've seen so far. Too often have I seen people try to find ways to directly link the two user controls, as opposed to relying on common ground - the page. Here's the codebehind for SamplePage containing a Results and ResultHeader user control:

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 ClassThe codebehind looks like any other page, except a readonly property for our two user controls has been added [11-15,16-20]. This allows a user control to access any other via the appropriate property. For example, if our ResultHeader wanted to make use of the Result's info property, it could easily access it via:

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 SubThis is identical to the code above example - where a user control accessed a page value. In reality this is exactly what's happening, the ResultHeader is accessing the Results property of SamplePage and then going a level deeper and accessing it's Info property.

There's no magic. We are using public properties in classes to achieve our goals. A page sets a user control's value via a property, or vice versa which can be done to any depth. Simply be aware that pages and user controls are actual classes you can program against, create the right public interface (properties and methods) and basic communication becomes rather bland (this isn't always a bad thing).

Accessing MethodsMethods are accessed the same way we've done properties. As long as they are marked public, a page can easily access one of it's user control's methods, or a user control can use the page as a broker to access another user control's method.

Advanced CommunicationWhile the above section aimed at giving you the knowledge to implement a solution to [most of] the questions related to this tutorial, here we'll concentrate on more advanced topics with a strong focus on good design strategies.

Basic Communication is Bad Design?While the code and methods discussed in the above sections will work, and are even at times the right approach, consider if they are truly the right approach for your situation. Why? you ask. Because if they aren't bad design as-is, they will lead to it unless you are vigilant. Take for example the last little blurb about accessing methods. If these are utility/common/static/shared methods, consider moving the function to your business layer instead.

Another example of bad design is the dependency such communication creates between specific pages and user controls. All of our example user controls above would either work very differently or cease to work entirely if they were used on a page other than SamplePage. User controls are meant to be reused, and for the most part (this isn't a 100% rule) shouldn't require other user controls or a specific page to work. The next two sections look at ways of improving this.

Making Use of InterfacesWe can leverage interfaces to reduce the dependency created by such communication. In the last example, the ResultHeader user control accessed the Info property of the Results user control. This is actually a pretty valid thing to do as it avoids having to re-hit the database in order to access the total number of records (although there are certainly alternatives to this approach). The problem with the above approach is that ResultHeader would only work with SamplePage and Results. Making good use of interfaces can actually make ResultHeader work for any page which displays a result (whatever that might be).

What is an interface? An interface is a contract which a class must fulfill. When you create a class and say that it implements a certain interface, you must (otherwise your code won't compile) create all the functions/properties/event/indexers defined in the interface. Much like you are guaranteed that a class which inherits from another will have all of the parent's class functionality, so too are you guaranteed that a class which implements an interface will have all of the interfaces members defined. You can read Microsoft's definition, or this tutorial, but I think the couple example bellow will give you the exposure you need.

To get the most flexibility, we'll create two interfaces. The first will be used by pages which display results and will force them to expose a readonly property which in turn exposes our other interface:

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

The second interface, IResult exposes a DataTable - the actually results:

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 InterfaceIf you are new to interfaces, notice how no implementation (no code) is actually provided. That's because classes which implement these interfaces must provide the code (as we'll soon see).

Next we make SamplePage implement IResultContainer and implement the necessary code:

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 ClassThe last step before we can make use of this is to make Results implement 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: }With these changes in place, ResultHeader can now decouple itself from SamplePage and instead tie itself to the broader IResultContainer interface:

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 IfThere's no denying that the code looks a lot as it did before. But instead of having to be placed on SamplePage, it can now be used with any page which implements IResultContainer. The use of IResult also decouples the page from the actual Results user control and instead allows it to make use of any user control which implements IResult.

All of this might seem like a lot of work in the name of good design. And if you have a simple site which will only display a single result, it might be overkill. But the minute you start to add different results interfaces will pay off both in lower development time and, more importantly, by making your code easily readable and maintainable. And if you don't use interfaces to decouple your communication links, keep an open mind for where else you might be able to use them because you'll probably find a tone.

Event Driven CommunicationOne of the questions I haven't answered yet is how to make a page (or another user control) aware of an event which occurred in a user control. While its possible to use the communication methods described above, creating your own events totally decouples the user control from the page. In other words, the user control raises the event and doesn't care who (if anyone) is listening. Besides, its fun to do!

For our example we'll create a third user control ResultPager which displays paging information for our results. Whenever one of the page numbers is clicked, our user control simply raises an event which the page, or other user controls can catch and do what they will with it:

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 ClassWith our PageClick event declared of type CommandEventHandler [5] we are able to notify anyone who's interested when a page number is clicked. The general idea behind the control is to load a repeater with the page numbers, and to raise our PageClick event when an event fires within this repeater. As such the user control handle the repeater's ItemCommand [11], retrieve the CommandArgument [13], repackages it into a CommandEventArgs [14] and finally raises the PageClick event [15]. The C# code must do a little extra work by making sure that PageClick isn't null [12] before trying to raise it, whereas VB.Net's RaiseEvent takes care of this (the event will be null/nothing if no one is listening).

SamplePage can then take advantage of this by hooking into the PageClick event like any other:

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: Private 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 SubMore likely though, the Results user control would take advantage of this event through SamplePage, or better yet by expanding the IResultContainer interface.

TimingOne of the difficulties which arises from communicating between page and user control has to do with when events happen. For example, if Results where to try and access ResultHeader's RecordsPerPage property before it was set, you would get unexpected behavior. The best weapon against such difficulties is knowledge.

When loading controls declaratively (via the @Control directive) the Load event of the page will fire first followed by the user controls in the order which they are placed on the page.

Similarly controls loaded programmatically (via Page.LoadControl) will have their Load event fire in order that they are added to the control tree (not when the call to LoadControl is actually made). For example, given the following code:

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's Load event will fire first followed by c1's. c3's load event will never fire because it isn't added to the control tree.

When both types of controls exist (declarative and programmatic), the same rules apply, except all declarative controls are loaded first then the programmatic ones. This is even true if controls are programmatically loaded in Init instead of Load

The same holds true for custom events as with builtin ones. In our event example above the following is the order of execution when a page number is clicked (assuming no control is on the page except ResultPaging): SamplePage's OnLoad event ResultPaging's OnLoad event ResultPaging's pager_ItemCommand event handler SamplePage's rp_PageClick event handler

The real difficulties arise when dealing with programmatically created controls within events - such as adding a user control to the page when a button is clicked. The problem is that such things happen after the page loads the viewstate, which, depending on what you are doing, might cause you to miss events within your user controls or cause seemingly odd behavior. As always, there are workarounds to such things, but they are well outside the scope of this tutorial. One solution might be Denis Bauer's DynamicControlsPlaceholder control (I haven't tried it yet, but looks very promising).

ConclusionIt seems like good practice to conclude by visiting the key points but really, the key points are to use what you can and try and understand as much as possible. Try to keep your designs clean, your pages flexible and above all your code readable. Pages are classes and should be treated as such, namely by understanding how inheritance works with respect to casting/ctyping and public properties and methods.

本文地址:http://com.8s8s.com/it/it7633.htm