Click here to Skip to main content
15,886,258 members
Articles / Programming Languages / C#

Type safe blackboard (property bag)

Rate me:
Please Sign up or sign in to vote.
4.78/5 (12 votes)
14 Oct 2012CPOL4 min read 26.2K   32   4
Implementing a property bag (blackboard) with type/name safe properties

Introduction

It is some times useful for objects in an application to collaborate and share information without maintaining direct references to each other (an example might be an application with plugins, some of which want to communicate with each other). So let's assume we have multiple objects, some contributing certain data, and others consuming different subsets of that data. Instead of tightly coupling data-producer and data-consumer objects by having them maintain references to each other, we can instead opt for a more decoupled approach, namely, creating a "blackboard" object which allows objects to freely read and write data to/from it. This decouples the producer and consumer objects by allowing the consumer to get a hold of the data it needs without knowing or caring where the data came from. For more on the blackboard pattern...well as they say...google is your friend. 

A simplistic blackboard object could be a Dictionary<string, object> - a simple dictionary of named values. All interested objects would share a reference to the dictionary allowing them to freely exchange named data. The problems with this approach are name and type safety - the data producer and data consumer objects must share a string identifier of each data value, and also the consumer does not have compile-time type checking of the value in the dictionary (i.e., it may expect to read a decimal, and instead at run-time get a string value). This article demonstrates one solution to these two problems.    

Background

Recently I was developing an engine for asynchronous execution of general purpose tasks. My "tasks" were units of work with Do/Undo functionality, in principle independent of each other, but some of the tasks I implemented required information from already executed tasks. For instance, one task could set up an API object for a hardware device and subsequent tasks could then use the created API object to manipulate the hardware device in different ways. But I did not want my execution engine to know anything about the tasks it executes and I did not want to have to wire up references manually from task to task.  

The Blackboard class    

 The blackboard class is basically a wrapper for a Dictionary<string, object>. It exposes a Get and a Set method. The blackboard allows clients to store and retrieve data from it, but it requires that the data be accessed using an identifier of type BlackboardProperty<T>. The BlackboardProperty instance should be shared between objets that read or write the property to the blackboard, so it should be a static member in a shared location, usually on the class that uses it or the class that provides it (much like dependency properties in WPF/Silverlight are static members of controls they "belong" to).  

 Note: Name safety could have been achieved in the same way by sharing a static string reference, but that would still not solve the type safety issue. So anyway... to the meat and potatoes, here is the code for the blackboard class: 

C#
public class Blackboard : INotifyPropertyChanged, INotifyPropertyChanging
{
    Dictionary<string, object> _dict = new Dictionary<string, object>();

    public T Get<T>(BlackboardProperty<T> property)
    {
        if (!_dict.ContainsKey(property.Name))
            _dict[property.Name] = property.GetDefault();
        return (T)_dict[property.Name];
    }

    public void Set<T>(BlackboardProperty<T> property, T value)
    {
        OnPropertyChanging(property.Name);
        _dict[property.Name] = value;
        OnPropertyChanged(property.Name);
    }

    #region property change notification

    public event PropertyChangingEventHandler PropertyChanging;

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanging(string propertyName)
    {
        if (PropertyChanging != null)
            PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}  

The BlackboardProperty class   

The BlackboardProperty class serves as an identifier for accessing data in a blackboard object.  It defines the name, and the type of values it accesses on a blackboard. It also defines the default value which should be returned in case the blackboard does not contain a value for the property.  

C#
/// <summary>
/// Strongly typed property identifier for properties on a blackboard
/// </summary>
/// <typeparam name="T">The type of the property value it identifies</typeparam>
public class BlackboardProperty<T>
{
    /// <summary>
    /// The name of the property.
    /// <remarks>
    /// Properties on the blackboard are stored by name, use caution NOT to have different 
    /// properties using the same name, as they will overwrite each others values if used on
    /// the same blackboard.
    /// </remarks>
    /// </summary>
    public string Name { get; set; }

    //factory method used to provide a default value when a blackboard 
    //does not contain data for this property
    Func<T> _createDefaultValueFunc;

    public BlackboardProperty(string name)
        : this(name, default(T))
    {
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="name"></param>
    /// <param name="defaultValue">
    /// The value which will be returned if the blackboard does not 
    /// contain an entry for this property.
    /// <remarks>
    /// Use this constructor if the default value is a constant or a value type.
    /// </remarks>
    /// </param>
    public BlackboardProperty(string name, T defaultValue)
    {
        Name = name;
        _createDefaultValueFunc = () => defaultValue;
    }

    /// <summary>
    /// </summary>
    /// <remarks>
    /// Use this constructor if the default value is a reference type, and you
    /// do not want to share the same instance across multiple blackboards.  
    /// </remarks>
    /// <param name="name"></param>
    /// <param name="createDefaultValueFunc"></param>
    public BlackboardProperty(string name, Func<T> createDefaultValueFunc)
    {
        Name = name;
        _createDefaultValueFunc = createDefaultValueFunc;
    }

    public BlackboardProperty()
    {
        Name = Guid.NewGuid().ToString();
    }

    public T GetDefault()
    {
        return _createDefaultValueFunc();
    }
}  

Using the code    

Here is a demonstration which illustrates how to use the blackboard: 

C#
class ClassA
{
    public static BlackboardProperty<int> SomeImportantDataProperty = new BlackboardProperty<int>();

    public void DoStuff(Blackboard blackboard)
    {
        int result = 0;
        //res = ...do some very important calculation

        //write the result to the blackboard
        blackboard.Set(SomeImportantDataProperty, result);
    }
}

class ClassB
{
    public void DoStuff(Blackboard blackboard)
    {
        //get the result of A's calculation
        //we dont need a reference to the object A that created the result
        int theImportantDataClassAContributed = blackboard.Get(ClassA.SomeImportantDataProperty);

        Console.WriteLine("The result of A's operation was {0}", theImportantDataClassAContributed);
    }
}

class ClassC
{
    public void DoStuff(Blackboard blackboard)
    {
        int theImportantDataClassAContributed = blackboard.Get(ClassA.SomeImportantDataProperty);
        File.WriteAllText("C:\\importantData.txt", string.Format("This is A's result {0}", theImportantDataClassAContributed));
    }
}

class Demo()
{
    static void Main()
    {
        ClassA objA = new ClassA();
        ClassB objB = new ClassB();
        ClassC objC = new ClassC();

        Blackboard blackboard = new Blackboard();

        objA.DoStuff(blackboard);
        objB.DoStuff(blackboard);
        objC.DoStuff(blackboard);
    }
} 

Not terribly useful code, I admit, but it illustrates the use of the two classes.  

This next example is a bit more "real world"-ish, but is of course also quite simplified. In it I define several types of tasks which I use for setting up a connection to a hardware device, manipulating the device, and for closing the connection to the device. Tasks are executed sequentially by an execution engine. The tasks share data through a common blackboard.  The implementations of the Task and ExecutionEngine classes are possibly material for another article... 

C#
//example interface for a device
interface IDevice
{
    void Connect();
    void Reset();
    decimal Read(string obis);
    void Close();
}
 
//this task instantiates the device api, sets it up and connects to a device
class InitiateDeviceTask : Task
{
    //the instance used to identify the DeviceAPI instance on a blackboard
    public static BlackboardProperty<IDevice> DeviceAPIProperty = new BlackboardProperty<IDevice>();
 
    protected override void Execute(Blackboard context)
    {
        IDevice deviceAPI = null;
        //deviceAPI = ...create and set up device API, connect to device
        deviceAPI.Connect();
 
        //this is the important part - I store the deviceAPI on the blackboard 
        //and use the DeviceProperty as the identifier for this object
        context.Set(DeviceAPIProperty, deviceAPI);
    }
}
 
//this task resets the device
class ResetDeviceTask : Task
{
    protected override void Execute(Blackboard context)
    {
        //retrieve the deviceAPI from the blackboard
        //this requires ofcourse that the InitiateDeviceTask was executed before this task.
        IDevice deviceAPI = context.Get(InitiateDeviceTask.DeviceAPIProperty);//notice there is no need for casting
        deviceAPI.Reset();
    }
}
 
//this task reads a register from the device
class ReadRegisterTask : Task
{
    public string RegisterName { get; set; }
 
    //note the factory method for providing a default value for this property
    public static BlackboardProperty<Dictionary<string, decimal>> ReadingsProperty = new BlackboardProperty<Dictionary<string, decimal>>("Readings", () => new Dictionary<string, decimal>());
 
    protected override void DoExecute(Blackboard context)
    {
        IDevice deviceAPI = context.Get(InitiateDeviceTask.DeviceAPIProperty);
        decimal value = deviceAPI.Read(RegisterName);//we can store the value to a repository maybe...

        //We can also store the register name and value in a dictionary so other 
        //tasks may use these values as well.
        //If a readings property is not present on the blackboard, an empty dictionary
        //will be created for us by the createDefaultValueFunc factory method
        //of the ReadingsProperty.
        context.Get(ReadingsProperty)[RegisterName] = value;
    }
}
 
//this task closes the connection to the device
class CloseConnectionTask : Task
{
    protected override void Execute(Blackboard context)
    {
        IDevice deviceAPI = context.Get(InitiateDeviceTask.DeviceAPIProperty);
        deviceAPI.Close();
    }
}
 
class Demo
{
    public void StartDemo()
    {
        ExecutionEngine engine = new ExecutionEngine();
        engine.Enque(new InitiateDeviceTask()); //creates the deviceAPI object, connects, puts it on the blackboard
        engine.Enque(new ReadRegisterTask() { RegisterName = "1.8.0" }); //uses the device API to read a value
        engine.Enque(new ReadRegisterTask() { RegisterName = "3.8.0" }); //uses the device API to read another value 
        engine.Enque(new ResetDeviceTask()); //uses the device API to reset the device
        engine.Enque(new CloseConnectionTask()); //uses the device API object to close the connection
 
        //the engine here creates a blackboard, 
        //starts a new thread and in it executes the tasks one by one
        //passing each of them the created blackboard instance
        engine.Start();
    }
}

Another possible use for the blackboard is in a plugin-enabled application, to allow plugins to communicate with each other if they need to. The property change notification might be useful in this scenario. 

One important thing to note is that BlackboardProperty instances are meant to be static memebers of classes that logically own the property. So since it is static, the same BlackboardProperty instance can easily appear in multiple blackboards. When no data is present on the blackboard for a given property, the Blackboard will ask the BlackboardProperty instance for a default value. The default value might be a reference type, so if you do not wish to share the same reference accross multiple blackboards be sure to use the following constructor when creating a BlackboardProperty:   

C#
public BlackboardProperty(string name, Func<T> createDefaultValueFunc)   

That will ensure that the default value created is not shared among multiple blackboards.

Points of Interest   

I should note that this solution was in part influenced by the DependencyProperty system in WPF, and also by a very usefull article I had read about enum classes a while ago.       

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Recro-Net
Croatia Croatia
I have been an a(tra)ctive software developer since 2005 mostly working on .NET. Currently living and working in Zagreb Croatia. I have earned my masters degree in Computer Science at the Faculty of Electrical Engineering and Computer Science in Zagreb in 2006.

Comments and Discussions

 
QuestionThank you Pin
Albarhami13-Sep-12 4:11
Albarhami13-Sep-12 4:11 
GeneralMy vote of 4 Pin
Albarhami13-Sep-12 4:11
Albarhami13-Sep-12 4:11 
GeneralMy vote of 5 Pin
ScruffyDuck13-Sep-12 4:01
ScruffyDuck13-Sep-12 4:01 
GeneralRe: My vote of 5 Pin
Antonio Nakić Alfirević14-Sep-12 3:13
Antonio Nakić Alfirević14-Sep-12 3:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.