Introduction
In the previous Mastering article (Mastering ASP.NET DataBinding), we took a detailed look at databinding - one of the most asked about topics in the newsgroups. 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 Basics
Before 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 class
This is probably something you already know, but each code-behind 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 code-behind file:
Collapse
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: }
Collapse
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
As 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, whichSystem.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 it's a reference to itself), but for a user control, it can be quite useful when properly used.Inheritance
I originally wrote quite a bit about what inheritance was. However, from the start, it felt like the thousands of tutorials try to explain core OO principles with a couple of 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 guarantees 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 read-only 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: Collapse
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: }
Collapse
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
The first couple of cases are straightforward. First, we see how our
SamplePage
class inherits the IsPostBack
property fromSystem.Web.UI.Page
[5,6]. We then see how both SamplePage
and System.Web.UI.Page
inherit the ToString()
function fromSystem.Object
- which all objects in .NET inherit from. Things get more interesting when we play with the Title
property. First, since theSystem.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 fourth 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 fourth example illustrates 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 Communication
We'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:
Collapse
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
Collapse
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 Control
While 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 (i.e., via the
@Control
directive), passing values is pretty straightforward for simple types: Collapse
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 Results
user control's Info
property is a more complex type, and must thus be set via code: Collapse
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: Collapse
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
Collapse
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 Page
Communicating 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 our SamplePage
's Title
property, we simply need to: Collapse
1: //C#
2: string pageTitle = null;
3: if (Page is SamplePage){
4: pageTitle = ((SamplePage)Page).Title;
5: }else{
6: pageTitle = "unknown";
7: }
Collapse
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
It's important to check that
Page
is actually of type SamplePage
before trying to cast it [3], otherwise we'd risk of having aSystem.InvalidCastException
thrown.From User Control to User Control
User control to user control communication is an extension of what we've seen so far. Too often have I seen people trying to find ways to directly link the two user controls, as opposed to relying on common ground - the page. Here's the code-behind for
SamplePage
containing a Results
andResultHeader
user controls: Collapse
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
The code-behind 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: Collapse
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: }
Collapse
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
This is identical to the code example above - 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 its 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 Methods
Methods are accessed the same way we've done properties. As long as they are marked
Public
, a page can easily access one of its user control's methods, or a user control can use the page as a broker to access another user control's method.Advanced Communication
While 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 Interfaces
We 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/events/indexers defined in the interface. Much like you are guaranteed that a class which inherits from another will have all of the parent class' functionality, so too are you guaranteed that a class which implements an interface will have all of the interface's members defined. You can read Microsoft's definition, or this tutorial, but I think the couple example below 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: Collapse
1: //C#
2: public interface IResultContainer{
3: IResult Result { get; }
4: }
Collapse
1: 'VB.NET
2: Public Interface IResultContainer
3: ReadOnly Property Result() As IResult
4: End Interface
The second interface,
IResult
, exposes a DataTable
- the actual results: Collapse
1: //C#
2: public interface IResult {
3: DataTable Info { get; }
4: }
Collapse
1: 'VB.Net
2: Public Interface IResult
3: ReadOnly Property Info() As DataTable
4: End Interface
If 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: Collapse
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
The last step before we can make use of this is to make
Results
implement IResult
: Collapse
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: Collapse
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
There'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 ton.
Event Driven Communication
One 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 it's 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, it's 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: Collapse
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: }
Collapse
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
With 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 handles the Repeater
's ItemCommand
[11], retrieves 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 thatPageClick
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: Collapse
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: }
Collapse
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
Collapse
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
More likely though, the
Results
user control would take advantage of this event through SamplePage
, or better yet by expanding theIResultContainer
interface.Timing
One 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 in which they are placed on the page.Similarly, controls loaded programmatically (via
Page.LoadControl
) will have their Load
event fired in the order that they are added to the control tree (not when the call to LoadControl
is actually made). For example, given the following code: Collapse
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 built-in 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
'sOnLoad
event.ResultPaging
'sOnLoad
event.ResultPaging
'spager_ItemCommand
event handler.SamplePage
'srp_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).
No comments:
Post a Comment