Recipe 7.9 Observing Additions and Modifications to a Hashtable
Problem
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.
Solution
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\r\n");
}
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\r\n");
}
}
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);}
}
}
Discussion
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 Onxxx 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.
See Also
See the "Event" keyword,
"EventHandler Delegate,"
"EventArgs Class," and
"Handling and Raising Events"
topics in the MSDN documentation.
|