You have multiple objects
that need to observe modifications to a Hashtable
.
When an item is added, deleted, or modified in the
Hashtable
, each of these observer objects should
be able to vote to allow or disallow the action. In order for an
action to be allowed to complete, all observer objects must vote to
allow the action. If even one observer object votes to disallow the
action, the action is prevented.
Use
the HashtableObserver
class to observe additions
and modifications to a HashtableSubject
object
that is registered with this object. The
HashtableSubject
class is an extension of the
regular Hashtable
class and allows itself to be
observed by the HashtableObserver
class. Its
source code is:
public class HashtableSubject : Hashtable { public event HashtableEventHandler BeforeAddItem; public event HashtableEventHandler AfterAddItem; public event HashtableEventHandler BeforeChangeItem; public event HashtableEventHandler AfterChangeItem; protected virtual bool OnBeforeAdd(HashtableEventArgs e) { if (BeforeAddItem != null) { BeforeAddItem(this, e); return (e.KeepChanges); } return (true); } protected virtual void OnAfterAdd(HashtableEventArgs e) { if (AfterAddItem != null) { AfterAddItem(this, e); } } protected virtual bool OnBeforeChange(HashtableEventArgs e) { if (BeforeChangeItem != null) { BeforeChangeItem(this, e); return (e.KeepChanges); } return (true); } protected virtual void OnAfterChange(HashtableEventArgs e) { if (AfterChangeItem != null) { AfterChangeItem(this, e); } } public override void Add(object key, object value) { HashtableEventArgs hashArgs = new HashtableEventArgs(key, value); OnBeforeAdd(hashArgs); if (hashArgs.KeepChanges) { base.Add(key, value); } else { Console.WriteLine("Addition of key/value cannot be performed"); } OnAfterAdd(hashArgs); } public override object this[object key] { get { return (base[key]); } set { HashtableEventArgs hashArgs = new HashtableEventArgs(key, value); OnBeforeChange(hashArgs); if (hashArgs.KeepChanges) { base[key] = value; } else { Console.WriteLine("Change of value cannot be performed"); } OnAfterChange(hashArgs); } } }
The HashtableEventHandler
is defined as follows:
[Serializable] public delegate void HashtableEventHandler(object sender, HashtableEventArgs e);
The code for the HashtableObserver
class is:
using System; using System.Collections; // The observer object that will observe a registered HashtableSubject object public class HashtableObserver { public HashtableObserver( ) {} public void Register(HashtableSubject hashtable) { hashtable.BeforeAddItem += new HashtableEventHandler(BeforeAddListener); hashtable.AfterAddItem += new HashtableEventHandler(AfterAddListener); hashtable.BeforeChangeItem += new HashtableEventHandler(BeforeChangeListener); hashtable.AfterChangeItem += new HashtableEventHandler(AfterChangeListener); } public void UnRegister(HashtableSubject hashtable) { hashtable.BeforeAddItem -= new HashtableEventHandler(BeforeAddListener); hashtable.AfterAddItem -= new HashtableEventHandler(AfterAddListener); hashtable.BeforeChangeItem -= new HashtableEventHandler(BeforeChangeListener); hashtable.AfterChangeItem -= new HashtableEventHandler(AfterChangeListener); } public void BeforeAddListener(object sender, HashtableEventArgs e) { if (((string)e.Value).Length > 3) { e.KeepChanges = false; } else { e.KeepChanges = true; } Console.WriteLine("[NOTIFY] Before Add..."); } public void AfterAddListener(object sender, HashtableEventArgs e) { Console.WriteLine("[NOTIFY] ...After Add "); } public void BeforeChangeListener(object sender, HashtableEventArgs e) { if (((string)e.Value).Length > 3) { e.KeepChanges = false; } else { e.KeepChanges = true; } Console.WriteLine("[NOTIFY] Before Change..."); } public void AfterChangeListener(object sender, HashtableEventArgs e) { Console.WriteLine("[NOTIFY] ...After Change "); } }
The HashtableEventArgs
class is a specialization
of the EventArgs
class, which provides the
Hashtable
key and value being added or modified to
the HashtableObserver
object, as well as a Boolean
flag, KeepChanges
, that’s passed
by reference. This flag indicates whether the addition or
modification in the HashtableSubject
object will
succeed or be rolled back. The source code for the
HashtableEventArgs
class
is:
// Event arguments for HashtableSubject public class HashtableEventArgs : EventArgs { public HashtableEventArgs(object key, object value) { this.key = key; this.value = value; } private object key = null; private object value = null; private bool keepChanges = true; public bool KeepChanges { get {return (keepChanges);} set {keepChanges = value;} } public object Key { get {return (key);} } public object Value { get {return (value);} } }
The observer design pattern allows one or more observer objects to act as spectators over one or more subject objects. Not only do the observer objects act as spectators, but they can also induce change in the subject objects. According to this pattern, any subject object is allowed to register itself with one or more observer objects. Once this is done, the subject can operate as it normally does. The key feature is that the subject doesn’t have to know what it is being observed by—this allows the coupling between subjects and observers to be minimized. The observer object(s) will then be notified of any changes in state to the subject objects. When the subject object’s state changes, the observer object(s) can change the state of other objects in the system to bring them into line with changes that were made to the subject object(s). In addition, the observer could even make changes or refuse changes to the subject object(s) themselves.
The observer pattern is best implemented with events in C#. The event
object provides a built-in way of implementing the observer design
pattern. This recipe implements this pattern on a
Hashtable
. The Hashtable
object
must raise events for any listening observer objects to handle. But
the Hashtable
class found in the FCL does not
raise any events. In order to make a Hashtable
raise events at specific times, we must derive a new class,
HashtableSubject
, from the
Hashtable
class.
This
HashtableSubject
class overrides the
Add
and indexer members of the base
Hashtable
. In addition, four events
(BeforeAddItem
, AfterAddItem
,
BeforeChangeItem
, and
AfterChangeItem
) are created that will be raised
before and after items are added or modified in the
HashtableSubject
object. To raise these events,
the following four methods are created, one to raise each event:
The OnBeforeAdd
method raises the
BeforeAddItem
event.
The OnAfterAdd
method raises the
AfterAddItem
event.
The OnBeforeChange
method raises the
BeforeChangeItem
event.
The OnAfterChange
method raises the
AfterChangeItem
event.
The Add
method calls the
OnBeforeAdd
method, which then raises the event to
any listening observer objects. The OnBeforeAdd
method is called before the base.Add
method—which adds the key/value pair to the
Hashtable
—is called. After the key/value
pair has been added, the OnAfterAdd
method is
called. This operation is similar to the indexer
set
method.
The On
xxx
methods that
raise the events in the HashtableSubject
class are
marked as protected
virtual
to
allow classes to subclass this class and implement their own method
of dealing with the events. Note that this statement is not
applicable to sealed
classes. In those cases, you
can simply make the methods public
.
The
HashtableEventArgs
class contains three private fields defined as follows:
key
The key that is to be added to the Hashtable
.
value
The value that is to be added to the Hashtable
.
keepChanges
A flag indicating whether the key/value pair should be added to the
Hashtable
.true
indicates that
this pair should be added to the Hashtable
.
The keepChanges
field is used by the observer to
determine whether an add or change operation should proceed. This
flag is discussed further when we look at the
HashtableObserver
observer object.
The
HashtableObserver
is the observer object that
watches any HashtableSubject
objects it is told
about. Any HashtableSubject
object can call the
HashtableObserver.Register
method in order to tell
the HashtableObserver
object that it wants to be
observed. This method accepts a pointer to a
HashtableSubject
object
(hashtable
) as its only parameter. This method
then hooks up the event handlers in the
HashtableObserver
object to the events that can be
raised by the HashtableSubject
object passed in
through the hashtable
parameter. Therefore, the
following events and event handlers are bound together:
The HashtableSubject.BeforeAddItem
event is bound
to the HashtableObserver.BeforeAddListener
event
handler.
The HashtableSubject.AfterAddItem
event is bound
to the HashtableObserver
.
AfterAddListener
event handler.
The HashtableSubject.BeforeChangeItem
event is
bound to the
HashtableObserver.BeforeChangeListener
event
handler.
The HashtableSubject.AfterChangeItem
event is
bound to the HashtableObserver.AfterChangeListener
event handler.
The BeforeAddListener
and
BeforeChangeListener
methods watch for additions
and changes to the key/value pairs of the watched
HashtableSubject
object(s). Since we have an event
firing before and after an addition or modification occurs, we can
determine whether the addition or change should occur. This is where
the keepChanges
field of the
HashtableEventArgs
object comes into play. The
HashtableObserver
object will set this flag
according to whether it determines that the action should proceed or
be prematurely terminated. The HashtableEventArgs
object is passed back to the OnBeforeAdd
and
OnBeforeChange
methods. These methods then return
the value of the KeepChanges
property to either
the calling Add
method or indexer. The
Add
method or indexer then uses this flag to
determine whether the base Hashtable
object should
be updated.
The following code shows how to instantiate subjects and observers, and to register, use, and unregister them:
// Create three subject objects ObserverPattern.HashtableSubject H1 = new ObserverPattern.HashtableSubject( ); ObserverPattern.HashtableSubject H2 = new ObserverPattern.HashtableSubject( ); ObserverPattern.HashtableSubject H3 = new ObserverPattern.HashtableSubject( ); // Create an observer for the three subject objects ObserverPattern.HashtableObserver observer = new ObserverPattern.HashtableObserver( ); // Register the three subjects with the observer observer.Register(H1); observer.Register(H2); observer.Register(H3); // Use the subjects H1.Add(1,"one"); H2.Add(2,"two"); H3.Add(3,"three"); // Unregister the subjects observer.UnRegister(H3); observer.UnRegister(H2); observer.UnRegister(H1);
Note that if the subject objects are used without registering them, no events will be raised. Since no events are raised, the observer cannot do its job, and values may be added to the unregistered subjects that are out of bounds for the application.
Many other scenarios exist in which the observer design pattern can
be used. For example, if you wanted another
Hashtable
object to be updated to reflect the
additions or modifications of a HashtableSubject
object, you could modify the HashtableObserver
object as shown in the highlighted text here:
public void Register(Hashtable hashtable) { MirrorTable = hashtable; } Hashtable MirrorTable = null; public void BeforeAddListener(object sender, EventArgs e) { HashtableEventArgs hashE = (HashtableEventArgs)e; if (((string)hashE.Value).Length > 3) { hashE.KeepChanges = false; } else { hashE.KeepChanges = true; if (MirrorTable != null) { MirrorTable.Add(hashE.Key, hashE.Value); } } Console.WriteLine("[NOTIFY] Before Add..."); } public void BeforeChangeListener(object sender, EventArgs e) { HashtableEventArgs hashE = (HashtableEventArgs)e; if (((string)hashE.Value).Length > 3) { hashE.KeepChanges = false; } else { hashE.KeepChanges = true; if (MirrorTable != null) { MirrorTable[hashE.Key] = hashE.Value; } } Console.WriteLine("[NOTIFY] Before Change..."); }
A new field, MirrorTable
, has been added; it
points to a Hashtable
mirroring the observed
HashtableSubject
object. The
MirrorTable
object is set through the constructor
of this class. The MirrorTable
object is updated
whenever the observed object is successfully modified. With these
modifications to the HashtableObserver
object, you
should observe only one HashtableSubject
object at
any one time. If you are observing more than one subject object, you
run the risk of attempting to add duplicate keys to the
MirrorTable
object.
When using the observer design pattern, you should keep in mind that fine-grained events, such as the ones in this recipe, should be watched carefully so that they do not drag down performance. If you have many subjects raising many events, your application could fail to meet performance expectations. If this occurs, you need to either minimize the number of actions that cause events to be raised or remove some events.