C# Corner
The Observer Pattern in .NET
The Observer Pattern is the foundation of Model View Controller (MVC) development. In this article, you'll learn how to use it by building a simple email application.
The Observer Pattern is one of the most frequently used object-oriented software design patterns. It creates a one-to-many relationship between two objects that allows one or many observed objects to be updated by the subject object. The Observer Pattern is the foundation of the Model View Controller (MVC) pattern, in which a view is updated automatically whenever the model's state changes. In the .NET Framework, delegates with events are an implementation of the Observer pattern.
Before going too deeply into an implementation of the pattern, let's go over its core components. They include a subject interface, a concrete subject, an observer interface, and a concrete observer. The subject interface defines a contract for attaching and detaching an observer to the subject, as well as notification of changes to any attached observers. The concrete subject implements the subject interface. The observer interface defines a contract for updating the observer upon a subject notification. The concrete observer class implements the observer interface and handles its needed update logic.
In .NET, the easiest way to use the Observer Pattern is by using events and delegates to define the subject/observer relationship. The class that declares the event of interest in this case is the subject and the class that registers/unregisters to the event is the observer. Let's look at a simple example where a user is able to enter a first name, last name, and email address for a person. When the email address changes I'd like to update the status label to notify the user, as well as highlighting the email address field. The observer pattern is an excellent choice to handle this use case.
To get started, create a new C# Windows Store Application. Next, create a directory named Interfaces. This directory will be used to store both the IObserver<T> and ISubject<T> generic interfaces. Next create a new interface named IObserver in the Interfaces directory. Add a generic class parameter named T to the IOBserver interface with a constraint that it must inherit from EventArgs:
public interface IObserver<in T>
where T: EventArgs
Then add an Update method that takes an Object named sender and a T named e:
void Update(Object sender,T e);
Your completed IObserver<T> interface:
using System;
namespace VSMObserverPatternDemo.Interfaces
{
public interface IObserver<in T>
where T: EventArgs
{
void Update(Object sender,T e);
}
}
Now it's time to add the ISubject<T> interface to the Interfaces directory. Add a new interface file, and define the T parameter to be contra variant by using the out keyword and of type EventArgs:
public interface ISubject<out T>
where T: EventArgs
Next, add Attach and Detach methods to the ISubject interface that both take an IObserver<T> named observer:
void Attach(IObserver<T> observer);
void Detach(IObserver<T> observer);
Here's the completed ISubject interface:
using System;
namespace VSMObserverPatternDemo.Interfaces
{
public interface ISubject<out T>
where T: EventArgs
{
void Attach(IObserver<T> observer);
void Detach(IObserver<T> observer);
}
}
The next step is to implement the Person view model, which will be the subject class. Create a new Models directory in the project, then a new class file for the Person class. Add a using statement for your Interfaces namespace:
using VSMObserverPatternDemo.Interfaces;
Then add the EmailChangedEventArgs class to the Person class file that will be used by the concrete observer classes:
public class EmailChangedEventArgs : EventArgs
{
public string Email { get; set; }
}
Next, start the Person class declaration that inherits ISubject<EmailChangedEventArgs>:
public class Person : ISubject<EmailChangedEventArgs>
Then add the _email backing property field and the EmailChanged event handler property:
private string _email;
public event EventHandler<EmailChangedEventArgs> EmailChanged;
Now add the FirstName and LastName properties:
public string FirstName { get; set; }
public string LastName { get; set; }
Next, add the Email property, which calls the OnEmailChanged event method to call any subscribers:
public string Email
{
get { return _email; }
set
{
_email = value;
OnEmailChanged(value);
}
}
Then add the OnEmailChanged event that takes a given email address value and calls the EmailChanged event subscribers:
private void OnEmailChanged(string value)
{
if (EmailChanged != null)
{
EmailChanged(this,new EmailChangedEventArgs() { Email = value});
}
}
The Attach method subscribes the observer's update method to the EmailChanged event:
public void Attach(Interfaces.IObserver<EmailChangedEventArgs> observer)
{
EmailChanged += observer.Update;
}
The Detach method unsubscribes the observer's update method from the EmailChanged event:
public void Detach(Interfaces.IObserver<EmailChangedEventArgs> observer)
{
EmailChanged -= observer.Update;
}
The completed Person class file definition:
using System;
using VSMObserverPatternDemo.Interfaces;
namespace VSMObserverPatternDemo.Models
{
public class EmailChangedEventArgs : EventArgs
{
public string Email { get; set; }
}
public class Person : ISubject<EmailChangedEventArgs>
{
private string _email;
public event EventHandler<EmailChangedEventArgs> EmailChanged;
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email
{
get { return _email; }
set
{
_email = value;
OnEmailChanged(value);
}
}
private void OnEmailChanged(string value)
{
if (EmailChanged != null)
{
EmailChanged(this,new EmailChangedEventArgs() { Email = value});
}
}
public void Attach(Interfaces.IObserver<EmailChangedEventArgs> observer)
{
EmailChanged += observer.Update;
}
public void Detach(Interfaces.IObserver<EmailChangedEventArgs> observer)
{
EmailChanged += observer.Update;
}
}
}
Now it's time to implement the concrete observer classes. Create a new directory named Observers in the project. Then add a new class file for the EmailFieldFormatter class. Add the following using statements to your EmailFieldFormatter class file:
using VSMObserverPatternDemo.Models;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
The EmailFieldFormatter class implements IObserver<EmailChangedEventArgs>:
public class EmailFieldFormatter : Interfaces.IObserver<EmailChangedEventArgs>
Next, add the _emailField private member variable to the class and initialize it in the constructor:
private readonly TextBox _emailField;
public EmailFieldFormatter(TextBox emailField)
{
_emailField = emailField;
}
It's time to implement the Update method, which highlights the _emailField textbox as yellow if it has a value and back to white if the field is empty:
public void Update(object sender, EmailChangedEventArgs e)
{
var highlightColor = Color.FromArgb(255, 255, 255, 255);
if (!string.IsNullOrEmpty(e.Email))
{
highlightColor = Color.FromArgb(255, 255, 255, 0);
}
_emailField.Background = new SolidColorBrush(highlightColor);
}
Here's the completed EmailFieldFormatter class implementation:
using VSMObserverPatternDemo.Models;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
namespace VSMObserverPatternDemo.Observers
{
public class EmailFieldFormatter : Interfaces.IObserver<EmailChangedEventArgs>
{
private readonly TextBox _emailField;
public EmailFieldFormatter(TextBox emailField)
{
_emailField = emailField;
}
public void Update(object sender, EmailChangedEventArgs e)
{
var highlightColor = Color.FromArgb(255, 255, 255, 255);
if (!string.IsNullOrEmpty(e.Email))
{
highlightColor = Color.FromArgb(255, 255, 255, 0);
}
_emailField.Background = new SolidColorBrush(highlightColor);
}
}
}
Now it's time to implement the StatusUpdater observer class. Create a new class file under the Observers directory for the StatusUpdated class. Have the class inherit from IObserver<EmailChangedEventArgs>:
public class StatusUpdater : Interfaces.IObserver<EmailChangedEventArgs>
Next, add a _statusElement private field of type TextBlock and initialize it in the class's constructor:
private readonly TextBlock _statusElement;
public StatusUpdater(TextBlock statusElement)
{
_statusElement = statusElement;
}
Next, implement the Update method, which sets the Text property of _statusElement to "Emailaddress changed to: x", where x is the newly set email address value:
public void Update(object sender, EmailChangedEventArgs e)
{
_statusElement.Text = string.Format("Email address changed to: {0}", e.Email);
}
The completed StatusUpdater class implementation should look like this:
using VSMObserverPatternDemo.Models;
using Windows.UI.Xaml.Controls;
namespace VSMObserverPatternDemo.Observers
{
public class StatusUpdater : Interfaces.IObserver<EmailChangedEventArgs>
{
private readonly TextBlock _statusElement;
public StatusUpdater(TextBlock statusElement)
{
_statusElement = statusElement;
}
public void Update(object sender, EmailChangedEventArgs e)
{
_statusElement.Text = string.Format("Email address changed to: {0}", e.Email);
}
}
}
Now it's time to add the UI for the application. Open up MainPage.xaml and paste in the StackPanel code here into the root Grid element:
<Page
x:Class="VSMObserverPatternDemo.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VSMObserverPatternDemo"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Margin="20" Name="PersonPanel">
<TextBlock>First Name</TextBlock>
<TextBox Name="FirstName" Text="{Binding FirstName, Mode=TwoWay}"></TextBox>
<TextBlock>Last Name</TextBlock>
<TextBox Name="LastName" Text="{Binding LastName, Mode=TwoWay}"></TextBox>
<TextBlock>Email</TextBlock>
<TextBox Name="Email" Text="{Binding Email, Mode=TwoWay}"></TextBox>
<TextBlock>Status</TextBlock>
<TextBlock Name="Status" Foreground="Green"></TextBlock>
</StackPanel>
</Grid>
</Page>
The final step is to tie everything together in the MainPage code-behind. Add the following using statements to the MainPage class file:
using VSMObserverPatternDemo.Models;
using VSMObserverPatternDemo.Observers;
Then add a private read-only member variable of type Person named _person to the class:
private readonly Person _person;
In the MainPage class constructor, set the _person field to a new Person object instance:
_person = new Person();
Now, bind the PersonPanel to the _person field.
PersonPanel.DataContext = _person;
Finally, attach the StatusUpdater and EmailFieldFormatter observers to the _person subject object:
_person.Attach(new StatusUpdater(Status));
_person.Attach(new EmailFieldFormatter(Email));
Here's the completed MainPage class definition:
using Windows.UI.Xaml.Controls;
using VSMObserverPatternDemo.Models;
using VSMObserverPatternDemo.Observers;
// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238
namespace VSMObserverPatternDemo
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
private readonly Person _person;
public MainPage()
{
this.InitializeComponent();
_person = new Person();
PersonPanel.DataContext = _person;
_person.Attach(new StatusUpdater(Status));
_person.Attach(new EmailFieldFormatter(Email));
}
}
}
You should now be able to run the completed application and see the Email field highlighted, along with the status TextBlock updated (see Figure 1).
The Observer Pattern is so important that it's tightly woven into the .NET Framework itself through delegates and events. The Observer Pattern allows for one subject object to update many observer objects without the classes needing to be tightly coupled. The Observer Pattern promotes the DRY (Don't Repeat Yourself) principle, as well.
About the Author
Eric Vogel is a Senior Software Developer for Red Cedar Solutions Group in Okemos, Michigan. He is the president of the Greater Lansing User Group for .NET. Eric enjoys learning about software architecture and craftsmanship, and is always looking for ways to create more robust and testable applications. Contact him at [email protected].