Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C# 4.0

Propagator in C# - An Alternative to the Observer Design Pattern

Rate me:
Please Sign up or sign in to vote.
4.94/5 (19 votes)
13 Jul 2009CPOL9 min read 55.2K   368   53   12
Re-usable implementation of the Propagator Design Pattern in C#, a potentially more powerful alternative to the well-known Observer Design Pattern.
Propagator Demo Form

Introduction

This article provides a re-usable implementation of the Propagator Design Pattern in C#. The Propagator Design Pattern is a pattern for updating objects in a dependency network. It is very useful when state changes need to be pushed through a network of objects. A state change is represented by an object itself which travels through the network of Propagators. By encapsulating the state change as an object, the Propagators become loosely coupled.

I have used the Propagator pattern in a complex GUI application where different components of the user interface needed to be kept in sync. This GUI is also very configurable, so having loosely coupled GUI components was certainly to my advantage.

The demo application consists of a form called DemoForm and 2 controls: FontControl and ColorControl. DemoForm, FontControl and ColorControl all display a string with the same font and color. As the names suggest, FontControl can change the font and ColorControl can change the color of the string. DemoForm has a Reset button which resets the font and color to the original values. The font and color in these 3 components are kept in sync through the Propagator Design Pattern. The dependency network is displayed below.

Demo Form Dependency Graph

If you want to start using the Propagator classes right away, skip the next 2 sections and jump right to the "Using the Code" section.

Background

Propagator can be seen as an improved version of the Observer Design Pattern. In the Observer Design Pattern, a Subject notifies its Observers of changes. The distinction between Subject and Observer does not exist in the Propagator Design Pattern. Each object in the dependency network is a Propagator and a Propagator may have 0 or more dependent Propagators. By setting the right dependencies between Propagators, a network of dependent objects can be created.

In my design, I decided to encapsulate each state change in an object. This is similar to the Command Design Pattern, where a Command encapsulates an action to be performed in the application. A state change may be created by any entity and sent through the network of dependent Propagators. Each Propagator may choose to respond to the state change, but they will always pass it on to the dependent Propagators.

A state change object is used once only and then disposed. It keeps a list of Propagators that have already observed the state change. This prevents the state change from cycling the network forever.

Dependencies between Propagators can be bi-directional. For example, if the network represents a parent-child relationship, a state change can be initiated by the child or by the parent. To optimize the propagation of state changes, the sending Propagator is given as a parameter. In this way, the receiving Propagator will not send the state change right back to the sender.

Here is a quick overview of the characteristics of my Propagator implementation:

  • State changes are pushed directly through the dependency graph
  • Support for cyclic dependency graphs
  • Support for bi-directional dependencies
  • Depth-first iteration of dependency graph
  • Iteration of the dependency graph will stop when an exception is thrown

Design

A class diagram of the re-usable Propagator classes is displayed below:

Propagator Classes

The IPropagator interface provides methods for adding and removing dependent propagators. It also has the Process() method, which lets a StateChange object travel through the network of dependent Propagators. The IPropagator interface is shown below:

C#
public interface IPropagator
{
    void Process(StateChange stateChange);
    void Process(StateChange stateChange, StateChangeOptions options);
    void Process(StateChange stateChange, StateChangeOptions options, IPropagator sender);
    void AddDependent(IPropagator dependent, bool biDirectional);
    void RemoveDependent(IPropagator dependent, bool biDirectional);
    void RemoveAllDependents(bool biDirectional);
}

The first parameter of the AddDependent() method is the dependent propagator. The second parameter indicates if the dependency is bi-directional. For example, the following code creates a bi-directional dependency between propagatorA and propagatorB.

C#
IPropagator propagatorA = new Propagator();
IPropagator propagatorB = new Propagator();
propagatorA.AddDependent(propagatorB, true);

To remove a particular dependent, call RemoveDependent(). This method has a parameter to indicate if the dependency should be removed in both directions. There is also a RemoveAllDependents() method to remove all dependencies. Again, this method has an option to remove the dependencies in both directions. However, if a dependency happens to be uni-directional, no exception will be thrown from the Remove() methods.

There are a couple of overloads for the Process() method. The first overload is the one to call from your code. It will take care of updating the current Propagator and of notifying the dependent Propagators. The second overload has an argument for the StateChangeOptions enum which controls how the state change is processed. It is defined as follows:

C#
[Flags]
public enum StateChangeOptions
{
    // If set, the invoked propagator will be updated.
    Update = 1 << 0,

    // If set, dependents will be notified and asked to update.
    Notify = 1 << 1,

    // If set, the invoked propagator will be updated and 
    // dependents will be notified and asked to update.
    UpdateAndNotify = Update | Notify
}

The third Process() overload has a third parameter to specify the sending Propagator. This overload is used from the Propagator class to make the delivery of state changes more efficient. If a dependency is bi-directional, this parameter will prevent the state change from going right back to the sender.

The StateChange class represents a state change that is to be propagated through a dependency network. The StateChangeTypeID helps to identify the type of state change. The StateChange class maintains a list of Propagators that have observed the state change. This prevents a state change from going through a cyclic graph forever.

The Propagator class implements IPropagator. In addition, it implements a dispatching mechanism for handling state changes. Propagator defines a delegate for handling a state change and it maintains a dictionary of the StateChangeTypeID to such a handler method. State change handlers are registered through the AddHandler() method.

Using the Code

Before you start using propagators, you should decide which classes in your project need to be part of the dependency graph. See the Introduction section for an example of a dependency graph.

The code below shows a minimal class with a Propagator. The Propagator is instantiated with a name for debugging purposes. The Propagator is accessible through a property. Note that the return type of this property is the IPropagator interface and not the Propagator class. This prevents any outside objects from adding state change handlers to this Propagator.

C#
// Example class with propagator.
public class ClassWithPropagator
{
    // Propagator object.
    private Propagator _propagator = new Propagator("SimpleExample");

    // Constructor.
    public ClassWithPropagator()
    {
    }

    // Propagator.
    public IPropagator Propagator
    {
        get
        {
            return _propagator;
        }
    }
}

The code below shows an example of how 2 objects can be linked through their Propagators. Note that the second argument indicates that the dependency is bi-directional. For a uni-directional dependency, pass in false for this argument.

C#
ClassWithPropagator p1 = new ClassWithPropagator();
ClassWithPropagator p2 = new ClassWithPropagator();
p1.Propagator.AddDependent(p2.Propagator, true);

If a state change has associated data, you must derive a class from StateChange in order to send the associated data through the network. Make sure to give your state change class a unique ID. This can be achieved by defining an enum of all state change IDs. The state change enum and the ColorChange class are displayed below. Note that the state change ID is available as a public const field and that it is passed to the base class constructor.

C#
// Enum to ensure that state changes have a unique ID.
public enum DemoStateChanges
{
    ColorChangeID,
    FontChangeID
}
    
// Represents a change of color.
public class ColorChange : StateChange
{
    // ID of state change.
    public const int ID = (int) DemoStateChanges.ColorChange;

    // New color.
    private Color _color;

    // Constructor.
    public ColorChange(Color color)
        : base(ID) // Pass ID to base class.
    {
        _color = color;
    }

    // Get new color.
    public Color Color
    {
        get
        {
            return _color;
        }
    }
}

The next step is to define handlers for the state changes. These handlers are added to the Propagator class by calling the AddHandler() method with the ID of the state change type and a delegate with the following signature:

C#
void Handler(StateChange stateChange);

A good place for adding the state change handlers is in the constructor.

The code below shows the ColorControl from the sample code. In the constructor, 2 state change handlers are added for color and font changes. When the color changes, the HandleColorChange method is called and when the font changes, the HandleFontChange method is called. These methods are defined as private and update the user interface accordingly. If you look at HandleColorChange(), you see that the StateChange object is cast down to a ColorChange object. In this way, the new color can be retrieved.

Another interesting method of ColorControl is buttonSelectColor_Click(). This method displays the .NET ColorDialog to let the user pick a color. If the user clicks OK, a new ColorChange object is created and passed to the Process() method. This method makes sure that the state change handlers of the current Propagator are executed, after which the other Propagators are notified of the state change.

C#
// Control for choosing color.
public partial class ColorControl : UserControl
{
    private Color _currentColor = Color.Black;
    private Propagator _propagator = new Propagator("ColorControl");

    // Constructor.
    public ColorControl()
    {
        InitializeComponent();

        // Add state change handlers.
        _propagator.AddHandler(ColorChange.ID, HandleColorChange);
        _propagator.AddHandler(FontChange.ID, HandleFontChange);
    }

    // Access to propagator.
    public IPropagator Propagator
    {
        get
        {
            return _propagator;
        }
    }

    // Select new color.
    private void buttonSelectColor_Click(object sender, EventArgs e)
    {
        ColorDialog colorDialog = new ColorDialog();
        colorDialog.Color = _currentColor;

        DialogResult result = colorDialog.ShowDialog();
        if (result == DialogResult.OK)
        {
            // Create state change object for color change.
            ColorChange colorChange = new ColorChange(colorDialog.Color);
            
            // Handle and notify dependent Propagators.
            _propagator.Process(colorChange);
        }
    }

    // Handle color change event.
    private void HandleColorChange(StateChange stateChange)
    {
        ColorChange colorChange = stateChange as ColorChange;
        if (colorChange != null)
        {
            Color newColor = colorChange.Color;

            // Set color of example text.
            labelExample.ForeColor = newColor;

            // Set background of color panel.
            panelColor.BackColor = newColor;

            // Remember the current color.
            _currentColor = newColor;
        }
    }

    // Handle font change event.
    private void HandleFontChange(StateChange stateChange)
    {
        FontChange fontChange = stateChange as FontChange;
        if (fontChange != null)
        {
            // Set font of example text.
            labelExample.Font = fontChange.Font;
        }
    }
}

DemoForm contains a ColorControl and a FontControl. Just like ColorControl and FontControl, it contains a Propagator object. In the DemoForm constructor, the Propagators of ColorControl and FontControl are added as bi-directional dependents using the AddDependent() method. This connects ColorControl, FontControl and DemoForm in a single network. Whenever Process() is called on any of the Propagators, the state change will travel to all other Propagators.

DemoForm responds to changes of color and font by updating its example string label. DemoForm itself also sends state changes from its Initialize() method. This method sends font and color state changes with default values. The Initialize() method is called in the DemoForm() constructor to initialize the dependency network. It is also called when the Reset button is pressed. 

C#
public partial class DemoForm : Form
{
    private Propagator _propagator = new Propagator("DemoForm");

    // Constructor.
    public DemoForm()
    {
        InitializeComponent();

        // Add font and color controls as dependents.
        _propagator.AddDependent(fontControl.Propagator, true);
        _propagator.AddDependent(colorControl.Propagator, true);

        // Add font and color change handlers.
        _propagator.AddHandler(FontChange.ID, HandleFontChange);
        _propagator.AddHandler(ColorChange.ID, HandleColorChange);

        // Initialize font and color.
        Initialize();
    }
            
    // Reset font and color.
    private void buttonReset_Click(object sender, EventArgs e)
    {
        Initialize();
    }

    // Initialize font and color.
    private void Initialize()
    {
        ColorChange colorChange = new ColorChange(Color.Blue);
        _propagator.Process(colorChange);

        Font font = new Font("Arial", 14);
        FontChange fontChange = new FontChange(font);
        _propagator.Process(fontChange);
    }

    // Handle color change event.
    public void HandleColorChange(StateChange stateChange)
    {
        ColorChange colorChange = stateChange as ColorChange;
        if (colorChange != null)
        {
            // Set color of example text.
            labelExample.ForeColor = colorChange.Color;
        }
    }

    // Handle font change event.
    public void HandleFontChange(StateChange stateChange)
    {
        FontChange fontChange = stateChange as FontChange;
        if (fontChange != null)
        {
            // Set font of example text.
            labelExample.Font = fontChange.Font;
        }
    }
}

When to Use Propagator

Propagator is useful when you want to decouple components while keeping them up to date with the current state of the application. Decoupling is especially useful if the number of state changes may change over time or if certain components are only interested in some state changes.

If state changes are only of interest within a particular component, direct method calls are more efficient and make more sense. For example, in the sample project, it would not make sense to use the Propagator within ColorControl just to update the label. As a general rule, if a state change may be of interest in the entire dependency network, use the Propagator infrastructure. If it is only of interest to 'local' or 'nearby' objects, use a more direct method.

Propagator pushes changes directly into the dependency network. If a passive response to changes is required, the Blackboard Design Pattern may be more useful.

Points of Interest

In order to keep different components in my GUI in sync, I started out with a 'Controller' class (just a name! ;-)) with parent and child Controllers. The issue I ran into was with the hierarchy of parent and child Controllers: a state change would either go up or down the structure, but sometimes it needs to go into both directions. I also ran into the issue of state changes travelling the entire tree back and forward more than once. I had to come up with complicated solutions. For example, when sending a state change, you would have to specify the direction, e.g. 'ToParent' or 'ToCurrentAndChildren'.

I came across an old article called "Propagator: A Family of Patterns", and a couple of pennies dropped. It is much easier to see the GUI components as a network of dependent objects without any parent/child hierarchy. I made the state change objects smart enough to avoid infinite cycles and the 'sender check' also makes the graph iteration quite efficient. I was pretty excited to end up with something simpler and more powerful!

History

  • July 13, 2009 - Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Phi International
Canada Canada
Grew up in Amsterdam, now living in downtown Vancouver. There are definitely more mountains here.

My first internship was with the first company in the Netherlands to teach C++ (www.datasim.nl). During this internship I got to know Object Oriented Design, which kept my interest until this day. In the mean time, I have worked for different companies in the Netherlands and Canada. I have done most of my recent work in C#, developing Database/Web/Desktop applications.

I am currently working as a freelance Software Developer for PHI International in Amsterdam.

The CodeProject rocks!

Comments and Discussions

 
QuestionPropagation in business object Pin
Jason Law19-Oct-09 22:19
Jason Law19-Oct-09 22:19 
AnswerRe: Propagation in business object Pin
Martijn Boeker20-Oct-09 15:43
Martijn Boeker20-Oct-09 15:43 
GeneralRe: Propagation in business object Pin
Jason Law20-Oct-09 18:50
Jason Law20-Oct-09 18:50 
GeneralVery Nice Pin
RTS@WORK20-Jul-09 11:43
RTS@WORK20-Jul-09 11:43 
GeneralRe: Very Nice Pin
Martijn Boeker26-Jul-09 20:56
Martijn Boeker26-Jul-09 20:56 
GeneralCool pattern Pin
Ethan Woo14-Jul-09 22:10
Ethan Woo14-Jul-09 22:10 
GeneralRe: Cool pattern Pin
Martijn Boeker14-Jul-09 23:38
Martijn Boeker14-Jul-09 23:38 
GeneralRe: Cool pattern Pin
Ethan Woo15-Jul-09 5:00
Ethan Woo15-Jul-09 5:00 
GeneralRe: Cool pattern Pin
Martijn Boeker15-Jul-09 12:14
Martijn Boeker15-Jul-09 12:14 
GeneralRe: Cool pattern Pin
Ethan Woo15-Jul-09 15:45
Ethan Woo15-Jul-09 15:45 
QuestionVery nice! Pin
BigTuna14-Jul-09 10:24
BigTuna14-Jul-09 10:24 
AnswerRe: Very nice! Pin
Martijn Boeker14-Jul-09 12:21
Martijn Boeker14-Jul-09 12:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.