In general, when I'm creating websites in ASP.Net, I tend to create a lot of user controls. More or less, all of the user interface of embedded in user controls, so that I can encapsulate and reuse specific functionality on any page of my website.
For example, lets say I have a screen that asks a user to login: on a successful login, the screen displays a welcome message to the user. I would create two user controls, one for the login panel and one for the welcome message, and display them like this:

The diagram above is a fairly simple layout, and a fairly typical way of designing controls. You might imagine that, on a successful login, the LoginPanel disappears, and the WelcomeMessage displays something along the lines of "Welcome, Bob! You have two new messages."; maybe on an unsuccessful login, the WelcomeMessage displays something along the lines of "Oops, wrong username or password."
Since our LoginPanel is contained in its own usercontrol, it does not know of the existence of the WelcomeMessage control; since the WelcomeMessage usercontrol is also self-contained, it does not know about the LoginPanel. Yet, we have to make the WelcomeMessage change its state when a user is successfully authenticated from the LoginPanel -- somehow, our LoginPanel and WelcomeMessage controls need to communicate with one another.
Here is some sourcecode to show the problem in a more concrete sense:
-----------
LoginPanel.ascx
<%@ Control Language="C#" ClassName="LoginPanel" %>
<script runat="server">
public bool IsAuthenticated
{
get
{
return ViewState["IsAuthenticated"] == null
? false
: (bool)ViewState["IsAuthenticated"];
}
private set
{
ViewState["IsAuthenticated"] = value;
}
}
protected void btnLogin_Click(object sender, EventArgs e)
{
IsAuthenticated =
(txtUsername.Text == "user" && txtPassword.Text == "secret");
}
</script>
<div>
Username: <asp:TextBox runat="server" ID="txtUsername" /><br />
Password: <asp:TextBox runat="server" ID="txtPassword"
TextMode="Password" /><br />
<asp:Button runat="server" ID="btnLogin" Text="Login"
onclick="btnLogin_Click" />
</div>
WelcomeMessage.ascx
<%@ Control Language="C#" ClassName="WelcomeMessage" %>
<script runat="server">
public void SetMsg(string msg)
{
lblMsg.Text = msg;
}
</script>
<div>
<asp:Literal runat="server" ID="lblMsg" />
</div>
Default.aspx
<%@ Page Language="C#" %>
<%@ Register src="LoginPanel.ascx" tagname="LoginPanel" tagprefix="uc1" %>
<%@ Register src="WelcomeMessage.ascx" tagname="WelcomeMessage" tagprefix="uc2" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<uc1:LoginPanel ID="LoginPanel1" runat="server" />
<uc2:WelcomeMessage ID="WelcomeMessage1" runat="server" />
</form>
</body>
</html>
-----------
(The WelcomeMessage control is admittedly pretty lame.)
So, we have two user controls. When a user logs in, we need to display the correct view in mviewControlPanel and set the welcome message appropriately. Now, there are a few "obvious" solutions to this problem; I'll run through a list of possible solutions and comment on them:
#1) You could simply check the LoginPanel1.Authenticated property on Page_Load, and set everything appropriately. For example, on Default.aspx, you could replace the Page_Load event with the following:
protected void Page_Load(object sender, EventArgs e)
{
if (Page.IsPostBack)
{
if (LoginPanel1.IsAuthenticated)
{
LoginPanel1.Visible = false;
WelcomeMessage1.SetMsg("Hello, User!");
}
}
}
That looks like it should do the trick, but it doesn't actually work because the code exceutes in the wrong order. Oops! The ASP.Net page life cycle processes the events in the following order:
- Default.aspx.Page_Load
- LoginPanel.aspx.btnLogin_Click
Since the Page_Load event reads the IsAuthenticated property *before* it gets assigned, your page processes incorrectly.
#2) To solve the the little "events not executing in the right order" problem above, you might be tempted to move the code above out of the Page_Load event, and into the Page_PreRender event so that it happens later in the page lifecycle.
This works, but its a notoriously bad work around. The Page_PreRender event gets called on every postback, and its not reasonable to check the LoginPanel.IsAuthenticated variable and re-setup all of the dependent user controls everytime. Setting up a user control for Authenticated/Unauthenticated mode might require a number of database queries or heavy data processing of some kind.
Additionally, this solution simply scale very well. If we had a large number of interacting controls, the Page_PreRender event would begin to get very bloated. Similarly, if we had a requirement to make the WelcomeMessage talk back to the login control (for example, by putting a logout button in the WelcomeMessage), it would simply be a nightmare to program correctly: on the one hand, you hide the LoginPanel when its IsAuthenticated property is true, on the other you show the LoginPanel when the WelcomeMessage's LoggedOut property is true. What happens when a user logs in, then logs back out? How do you know which order to check the variables?
Ideally, when a user logs in (or logs out), you want all of your controls to respond appropriately on demand.
#3) We want the WelcomeMessage text to be set on demand whenever we log in, so the WelcomeMessage needs to respond to an event raised by the LoginPanel. Since ASP.Net is an event-driven language, its trivial to create an event handler
public event EventHandler UserLoggedIn;
// ,..
protected void btnLogin_Click(object sender, EventArgs e)
{
IsAuthenticated =
(txtUsername.Text == "user" && txtPassword.Text == "secret");
if (UserLoggedIn != null)
UserLoggedIn(this, EventArgs.Empty);
}
Now, we can wire up Default.aspx to this event handler:
<%@ Page Language="C#" %>
<%@ Register src="LoginPanel.ascx" tagname="LoginPanel" tagprefix="uc1" %>
<%@ Register src="WelcomeMessage.ascx" tagname="WelcomeMessage" tagprefix="uc2" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected void LoginPanel1_UserLoggedIn(object sender, EventArgs e)
{
if (LoginPanel1.IsAuthenticated)
{
LoginPanel1.Visible = false;
WelcomeMessage1.SetMsg("Welcome, User!");
}
else
{
LoginPanel1.Visible = true;
WelcomeMessage1.SetMsg("Bad username or password");
}
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<uc1:LoginPanel ID="LoginPanel1" runat="server"
OnUserLoggedIn="LoginPanel1_UserLoggedIn"/>
<uc2:WelcomeMessage ID="WelcomeMessage1" runat="server" />
</form>
</body>
</html>
The event-driven approach is definitely superior to the methods enumerated above: we change the state of our WelcomeMessage the very moment we click the 'login' button, and we don't have to worry about the page lifecycle. Additionally, if the WelcomeMessage needed to trigger some kind of response in the LoginPanel.aspx control, we could just create an event on WelcomeMessage, catch it on Default.aspx, and call the right methods LoginPanel.
Notice that LoginPanel and WelcomeMessage do not talk directly to one another. Since they don't know about each others existence, the web page itself acts as a kind of proxy to channel communication from one control to the next.
Now, while the code above looks pretty good, it is not scalable in the least:
- Since the web page is our proxy, we can only we can only notify WelcomeMessage of events triggered in LoginPanel through WelcomeMessage's public methods. This forces us to expose a lot of implementation detail to our web page whenever we need two controls to communicate.
- The example above is somewhat contrived because LoginPanel and WelcomeMessage located on the same container. In the real world, there are probably going to be multiple layers of containers between your LoginPanel and your WelcomeMessage control, similar to this:

It is too cumbersome and tedious to raise your UserLoggedIn event up through all of its parent user controls, then pass the information back down through all of the nested user controls. As a result, your pages become bloated with code to pass events up and down through controls, and your pages become difficult to modify because moving or deleting any control on your website causes a compilation error.
- What if, for some reason, you wanted to add another control, such as a NewMessages control, which listened for the UserLoggedIn event? Well, now you have to write more code to funnel events to the WelcomeMessage control and NewMessages control at the same time.
- Even the graphic above is too generous. For example, if your LoginPanel control is on a master page, and your WelcomeMessage control is on a child page, you don't have the luxury of even knowing what controls exist on a page to funnel messages through, you don't even know if an instance of WelcomeMessage exists to handle your events.
Hopefully now we can see the problem with the method above. We need a more robust message-passing interface to satisfy the following conditions:
- We don't want to go through hierarchies of controls. No matter where controls are placed on a page, no matter how deeply nested inside other controls they are, we never want more than one proxy of communication between any two controls at a time.
- We cannot assume that our controls are conveniently embedded into our user interfaces at design time, rather than dynamically created on the fly at runtime.
- We do not want to expose the internal implementation of our controls if we can avoid it.
- We need to accomodate new controls who need to listen for events transparently; that is, without needing to change any lines of code in our message-passing interface
#4) We can satisfy all of the requirements above very easily if we just think about the problem a little differently. It really helps to seperate out message-passing logic from our user interface, so we should create a class which serves the role as a proxy between our LoginNotifier and any other controls:
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.SessionState;
namespace Notification
{
public interface ILoginListener
{
void NotifyUserLoggedIn(bool authenticated);
}
public class LoginNotifier
{
public static LoginNotifier SessionInstance
{
get
{
HttpSessionState session = HttpContext.Current.Session;
if (session["LoginNotifier"] == null)
session["LoginNotifier"] = new LoginNotifier();
return (LoginNotifier)session["LoginNotifier"];
}
}
private LinkedList<ILoginListener> notifiers = new LinkedList<ILoginListener>();
private LoginNotifier() { }
public void Register(ILoginListener listener)
{
notifiers.AddLast(listener);
}
public void Unregister(ILoginListener listener)
{
notifiers.Remove(listener);
}
public void Notify(bool authenticated)
{
foreach (ILoginListener listener in notifiers)
listener.NotifyUserLoggedIn(authenticated);
}
}
}
Now we have an adequate proxy to notify controls of the UserLoggedIn event. To use this class, we need to re-write the LoginPanel.btnLogin_Click event as follows:
public bool IsAuthenticated
{
get
{
return ViewState["IsAuthenticated"] == null
? false
: (bool)ViewState["IsAuthenticated"];
}
private set
{
ViewState["IsAuthenticated"] = value;
}
}
protected void btnLogin_Click(object sender, EventArgs e)
{
IsAuthenticated =
(txtUsername.Text == "user" && txtPassword.Text == "secret");
Notification.LoginNotifier.SessionInstance.Notify(IsAuthenticated);
}
Now, we need to register WelcomeMessage.ascx with the notifier. I'd prefer to implement the ILoginListener interface on my control; however, since I originally wrote the WelcomeMessage.ascx file using inline script blocks, but I want to provide my own constructor and implement an interface, which means I need to convert to the code-behind model.
WelcomeMessage.ascx:
<%@ Control Language="C#" AutoEventWireup="true"
CodeFile="WelcomeMessage.ascx.cs" Inherits="WelcomeMessage" %>
<div>
<asp:Literal runat="server" ID="lblMsg" />
</div>
WelcomeMessage.ascx.cs:
using System;
using System.Web.UI.WebControls;
using Notification;
public partial class WelcomeMessage : System.Web.UI.UserControl, ILoginListener
{
public WelcomeMessage()
{
LoginNotifier.SessionInstance.Register(this);
}
public override void Dispose()
{
LoginNotifier.SessionInstance.Unregister(this);
base.Dispose();
}
public void NotifyUserLoggedIn(bool authenticated)
{
string msg = authenticated
? "Welcome, User!"
: "Bad username or password";
SetMsg(msg);
}
private void SetMsg(string msg)
{
lblMsg.Text = msg;
}
}
Everything simply works like magic. My default.aspx page never needs to know or care about the LoginPanel or the WelcomeMessage controls anymore. More imporantly, this code is reusable; an indefinite number of controls can register themselves with this notifier, and I don't need to add any new code to pass messages up and down through any control hierarchies whatsoever.
As of right now, if we wanted to create a new event to listen on, we'd have to create a new interface and listener class. There are a lot of ways to make the pattern above a little more generic and reusable, for example:
public class Notifier
{
public static Notifier SessionInstance
{
get
{
HttpSessionState session = HttpContext.Current.Session;
if (session["EventNotifier"] == null)
session["EventNotifier"] = new Notifier();
return (Notifier)session["EventNotifier"];
}
}
private Dictionary<string, EventHandler> eventPool;
private Notifier()
{
eventPool = new Dictionary<string, EventHandler>();
}
public void Register(string eventID, EventHandler handler)
{
EventHandler temp;
if (eventPool.TryGetValue(eventID, out temp))
eventPool[eventID] = temp + handler;
else
eventPool.Add(eventID, handler);
}
public void Unregister(string eventID, EventHandler handler)
{
EventHandler temp;
if (eventPool.TryGetValue(eventID, out temp))
{
temp -= handler;
if (temp != null)
eventPool[eventID] = temp;
else
eventPool.Remove(eventID);
}
}
public void Notify(string eventID, object sender)
{
Notify(eventID, sender, EventArgs.Empty);
}
public void Notify(string eventID, object sender, EventArgs e)
{
EventHandler temp;
if (eventPool.TryGetValue(eventID, out temp) && temp != null)
temp(sender, e);
}
}
The class above allows a client to register named events. For example:
Notifier.SessionInstance.Register("UserLoggedIn", SomeEventHandler);
There are many variations on the pattern above, such as notifying listeners asyncronously, using generics to pass more specific type information than the EventArgs base class, etc.
As a final note: none of the code above is threadsafe. Make sure that you put locks around any code that adds, removes, or enumerates through items in a collection.