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 Propagator
s. By setting the right dependencies between Propagator
s, 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 Propagator
s. Each Propagator
may choose to respond to the state change, but they will always pass it on to the dependent Propagator
s.
A state change object is used once only and then disposed. It keeps a list of Propagator
s that have already observed the state change. This prevents the state change from cycling the network forever.
Dependencies between Propagator
s 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 Propagator
s. The IPropagator
interface is shown below:
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
.
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 Propagator
s. The second overload has an argument for the StateChangeOptions enum
which controls how the state change is processed. It is defined as follows:
[Flags]
public enum StateChangeOptions
{
Update = 1 << 0,
Notify = 1 << 1,
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 Propagator
s 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
.
public class ClassWithPropagator
{
private Propagator _propagator = new Propagator("SimpleExample");
public ClassWithPropagator()
{
}
public IPropagator Propagator
{
get
{
return _propagator;
}
}
}
The code below shows an example of how 2 objects can be linked through their Propagator
s. Note that the second argument indicates that the dependency is bi-directional. For a uni-directional dependency, pass in false
for this argument.
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.
public enum DemoStateChanges
{
ColorChangeID,
FontChangeID
}
public class ColorChange : StateChange
{
public const int ID = (int) DemoStateChanges.ColorChange;
private Color _color;
public ColorChange(Color color)
: base(ID)
{
_color = 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:
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 Propagator
s are notified of the state change.
public partial class ColorControl : UserControl
{
private Color _currentColor = Color.Black;
private Propagator _propagator = new Propagator("ColorControl");
public ColorControl()
{
InitializeComponent();
_propagator.AddHandler(ColorChange.ID, HandleColorChange);
_propagator.AddHandler(FontChange.ID, HandleFontChange);
}
public IPropagator Propagator
{
get
{
return _propagator;
}
}
private void buttonSelectColor_Click(object sender, EventArgs e)
{
ColorDialog colorDialog = new ColorDialog();
colorDialog.Color = _currentColor;
DialogResult result = colorDialog.ShowDialog();
if (result == DialogResult.OK)
{
ColorChange colorChange = new ColorChange(colorDialog.Color);
_propagator.Process(colorChange);
}
}
private void HandleColorChange(StateChange stateChange)
{
ColorChange colorChange = stateChange as ColorChange;
if (colorChange != null)
{
Color newColor = colorChange.Color;
labelExample.ForeColor = newColor;
panelColor.BackColor = newColor;
_currentColor = newColor;
}
}
private void HandleFontChange(StateChange stateChange)
{
FontChange fontChange = stateChange as FontChange;
if (fontChange != null)
{
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 Propagator
s 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 Propagator
s, the state change will travel to all other Propagator
s.
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.
public partial class DemoForm : Form
{
private Propagator _propagator = new Propagator("DemoForm");
public DemoForm()
{
InitializeComponent();
_propagator.AddDependent(fontControl.Propagator, true);
_propagator.AddDependent(colorControl.Propagator, true);
_propagator.AddHandler(FontChange.ID, HandleFontChange);
_propagator.AddHandler(ColorChange.ID, HandleColorChange);
Initialize();
}
private void buttonReset_Click(object sender, EventArgs e)
{
Initialize();
}
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);
}
public void HandleColorChange(StateChange stateChange)
{
ColorChange colorChange = stateChange as ColorChange;
if (colorChange != null)
{
labelExample.ForeColor = colorChange.Color;
}
}
public void HandleFontChange(StateChange stateChange)
{
FontChange fontChange = stateChange as FontChange;
if (fontChange != null)
{
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