Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#4.0

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

4.94/5 (19 votes)
14 Jul 2009CPOL9 min read 57.4K   369  
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)