In my ASP.NET application, I have a need to update other user controls when data one user control changes state. As with most problems in life, there are often several ways to solve them. For this situation, I chose to apply the observer pattern. This post will discuss the Observer pattern and provide a demonstration using ASP.NET with C#.
The Observer Pattern "defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically" (Freeman, Freeman, Sierra, & Bates, 2004). Before implementing this solution, I spent some time looking into an event driven model in ASP.NET, as well as the Publish-Subscribe pattern, but in the end, I chose the Observer pattern for usability and simplicity.
The Publish-Subscribe pattern uses events and an event channel as a means of abstraction between the subject and its observers. You can read more about the Publish-Subscribe pattern here. In the end, you still need to wire up the events, which means some module needs to be responsible for establishing this relationship. The implementation of the Observer pattern I use decouples the subject from its observers using interfaces. I have read articles that pitch one pattern against another, which I do not like. I am not arguing for nor against the Publish-Subscribe pattern. Each pattern exists to serve a purpose.
In my ASP.NET applications I am using Ajax to improve the user experience wherever it is appropriate. I say, "wherever it is appropriate," because there are both pros and cons when using Ajax. I am using the Telerik Ajax framework, which is built upon Microsoft's Atlas. My implementation of the Observer pattern will execute using Ajax requests. The beauty of Telerik's framework is that I develop code as if there were no Ajax requirements, then establish a mapping of which controls I wish to "Ajaxify." I recommend reading more about Telerik here.
One last note, my applications use a Supervising Controller pattern for ASP.NET. I still refer to this as Model-View-Presenter (MVP), so the code in this post will reference "presenters" and "views." The ASPX page, or view initializer, is responsible for establishing the relationships among the one-to-many views on the ASPX page, handling page redirects and events, and in my scenario, generating the subject to observer relation.
The goal of the Observer pattern is to provide a relationship where objects, "observers," are notified when the subject, "observed," changes state. Loose coupling is another goal of this association. When objects are loosely coupled, they are able to interact without the dependency of knowing anything about the other. In the Observer pattern, the subject only knows about an observer interface. It does not actually know about the observer implementation. New observers may be added at any time and the subject remains ignorant of any changes, other than an incremented count to the observer's array.
The following image shows the Observer pattern and its relationships.
My implementation of the observer is seen below. I am not implementing the "GetState" functionality in my concrete subject, only because I do not have a need for it at this time.
Show code
The Subject and Observer interfaces |
// The Observer public interface IDoubleListObserver { void Update(IList items); }
// The Subject public interface IDoubleListObservable { void Add(IDoubleListObserver observer); void Remove(IDoubleListObserver observer); void Notify(IList items); } |
Show code
The subject or the “observed.” |
// Subject (DoubleList) interface public interface IDoubleListView { void UpdateItemList(IList items, bool isPrimaryList); void AttachPresenter(DoubleListPresenter presenter); }
// Subject (DoubleList) presenter public class DoubleListPresenter : IDoubleListObservable { IDoubleListView view; IList observers;
public DoubleListPresenter(IDoubleListView view) { this.view = view; this.observers = new ArrayList(); }
public void InitView(bool isPostBack) { if (!isPostBack) { // update the DoubleListView for the first time view.UpdateItemList(GetItemList(), true); } }
public void UpdateItems(IList items) { // update the DoubleListView view.UpdateItemList(items,false);
// notify any observers this.Notify(items); } public void Add(IDoubleListObserver observer) { if(!observers.Contains(observer)) { observers.Add(observer); } }
public void Remove(IDoubleListObserver observer) { if(observers.Contains(observer)) { observers.Remove(observer); } }
public void Notify(IList items) { foreach(IDoubleListObserver observer in observers) { // notify this observer observer.Update(items); } }
private IList GetItemList() { ArrayList list = new ArrayList(); list.Add("AAA"); list.Add("BBB"); list.Add("CCC"); list.Add("DDD"); list.Add("EEE"); list.Add("FFF"); list.Add("GGG"); return list; } }
// Subject (DoubleList) implementation public class DoubleList : UserControl, IDoubleListView { DoubleListPresenter presenter; IList list;
protected void Page_Load(object sender, EventArgs e) { }
public void UpdateItemList(IList items, bool isPrimaryList) { if (isPrimaryList) { lbx1.DataSource = items; lbx1.DataBind(); } else { ClearList(); lbx2.DataSource = items; lbx2.DataBind(); } }
public void AttachPresenter(DoubleListPresenter presenter) { this.presenter = presenter; }
private void ClearList() { this.lbx2.Items.Clear(); }
protected void btnAdd_Click(object sender, EventArgs e) { list = new ArrayList();
foreach (ListItem item in this.lbx1.Items) { if (item.Selected) { item.Selected = false; list.Add(item); } } presenter.UpdateItems(list); }
protected void btnRemove_Click(object sender, EventArgs e) { list = new ArrayList();
foreach (ListItem item in this.lbx2.Items) { if (!item.Selected) { list.Add(item.Text); } }
presenter.UpdateItems(list); } } |
Show code
|
|
// Observer 1 (UserMenu) view public interface IUserMenuView { void UpdateViewItemList(IList items); }
// Observer 1 (UserMenu) presenter public class UserMenuPresenter : IDoubleListObserver { IUserMenuView view;
public UserMenuPresenter(IUserMenuView view) { this.view = view; }
public void Update(IList items) { // update the UserMenuView view.UpdateViewItemList(items); } }
// Observer 1 (UserMenu) implementation public class UserMenu : System.Web.UI.UserControl, IUserMenuView { protected void Page_Load(object sender, EventArgs e) { }
public void UpdateViewItemList(IList items) { this.lbxUserMenu.Items.Clear(); this.lbxUserMenu.DataSource = items; this.lbxUserMenu.DataBind(); } }
|
Show code
|
|
// Observer 2 (TextView) view public interface ITextView { void UpdateViewItemList(string itemText); }
// Observer 2 (TextView) presenter public class TextPresenter : IDoubleListObserver { ITextView view;
public TextPresenter(ITextView view) { this.view = view; }
public void Update(System.Collections.IList items) { StringBuilder stringBuilder = new StringBuilder();
foreach (object item in items) { stringBuilder.Append(item.ToString()).Append(", "); }
if (stringBuilder.Length != 0) { // remove the ending ", " stringBuilder.Remove(stringBuilder.Length - 2, 2); }
// update the TextView view.UpdateViewItemList(stringBuilder.ToString()); } }
// Observer 2 (TextView) implementation public class TextView : UserControl, ITextView { protected void Page_Load(object sender, EventArgs e) { }
public void UpdateViewItemList(string itemText) { this.tbxItems.Text = itemText; } }
|
Show code
Finally, the actual page that declares the subject and observer relationships. |
public class TestAjaxConcept : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { InitView(); }
private void InitView() { // Initialize the DoubleListPresenter with its user control view DoubleListPresenter doubleListPresenter = new DoubleListPresenter(doubleList); doubleList.AttachPresenter(doubleListPresenter); doubleListPresenter.InitView(IsPostBack);
// Initialize the UserMenuPresenter with its user control view UserMenuPresenter userMenuPresenter = new UserMenuPresenter(userMenu);
// Initialize the TextPresenter with its user control view TextPresenter textPresenter = new TextPresenter(textView);
// Add the observer associations IDoubleListObservable subject = doubleListPresenter; subject.Add(userMenuPresenter); subject.Add(textPresenter); } } |
The code is fairly straight-forward. I have not included my ASPX/ASCX view code. The actual Observer-Subject associations are defined via the presenters. My master, ASPX view is responsible for initializing the relationships. Since I am not showing my ASPX/ASCX view code, one cannot tell that I am using the Telerik Ajax framework, but this entire scenario occurs during an Ajax request.
Below are screen shots to show the user flow of this sample demo. The first screen shot shows the DoubleList view initialized with the default entries for my listbox. These views are trivial. This is not a blog entry showing how to design an appealing user interface.
The next photo shows the user selecting multiple listbox items to add them to our collection.
Lastly, after adding items to our collection, the DoubleList view, and both Observer 1 and 2 are updated with information immediately. Observers 1 and 2 are updated via the DoubleList view subject.
I read through the Observer and Publish-Subscribe patterns just before retiring to sleep for the evening, with the latter on my mind as my head hit the pillow. I woke up the next morning with the implementation for the Observer fresh on my mind. It is amazing how that works. Now I need to convince my boss at work that I should nap for three hours every day to help solve mission, critical problems.
Several months back, I posted a series of articles on object-oriented principles. I have always wanted to do this for design patterns, and perhaps this could be the first of many. Only time will tell, as I struggle to find time to post because of other commitments. I hope this article was helpful in introducing and explaining the Observer pattern, and how one can apply it to ASP.NET using C#.
References:
Freeman, E., Freeman, E., Sierra, K., & Bates, B. (2004). Head First Design Patterns (1st ed.). Sebastopol, CA: O'Reilly Media, Inc.