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.

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:

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:

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.

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:

[Flags]publicenum 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.

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.

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.

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:

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.

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.

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!

Share

About the Author

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.

Comments and Discussions

Interesting read - Propagator feels a little like the Composite pattern but without any enforced hierarchy. I'm curious since you say you used this in a complex application... assuming you had more than two buttons in that GUI , did you run into class bloat issues with all the state change handlers?

Thanks! You're right, it does feel a bit like the Composite pattern in a sense that individual objects and compositions can be treated uniformly. Both patterns allow for deep hierarchies and for messages to travel through the hierarchy.

I didn't run into serious class bloat issues. First of all, a class decides which state changes it wants to handle. In my GUI, this ranges from 1 to 15 state change handlers per class. Secondly, state changes should be fairly high level so the total number of state changes should be limited. In my GUI, there are 22 types of state changes. Not every state change has an associated state change class.

The GUI I work on displays physiological signal data and consists of a hierarchy of tabs and panels. There are different types of panels: some panels only display information, some panels allow for navigation (in time) and some panels also have certain editing capabilities. The main window, containing the tabs, initiates most state changes, but some state changes are initiated from one of the panels. The state changes in this GUI are related to navigation, cursor animation, editing and high-level GUI management such as 'Refresh Screen'. Many other actions, such as changing a signal's color, do not use the Propagator infrastructure. This is because such actions are more local and not of interest of the entire dependency network.