Contents
- Introduction
- Background
- Interfaces Overview
- Property System Interface
- Property Definitions Interface
- Property Definition Interface
- Property Bag Interface
- Where does the magic happen
- Using the code to manage a property system on the fly
- Create a system instance
- Create property definitions for later use by the property system
- Assign the property definitions to the property system instance
- Create a property bag instance from the property system instance
- Use the property bag instance
- Using the code to build a bag-based object
- Define property id's
- Define a specialised property system
- Create an example object definition
- Create an example system manager
- Limitations
- Points of Interest
- References
- History
Introduction
This property system example implementation is basically a property bag with some management to override values
with functions and the potential to be used to compose an entity system with inheritance, but it
can also be used as a message bus, a common base for components and much more.
Background
Property Systems, Object Systems, Entity Systems or whatever else such sort of "functionality and structure" can be called are quite a handy way to work with heterogenous data and to separate concerns.
I've read quite a lot about the subject, and before I try to repeat what others said more clearly than I probably ever could, i recommend studying the references.
However, it seems like the world agrees on the demand for something which can replace huge class hierarchies and even microsoft provides quite much around the topic; DependencyProperty and the new dynamic keyword in C# for example.
The example provided here is a partial replacement for the dynamic keyword and some related stuff and it has the potential to be a partial replacement for dependency properties. It's not a component system, but it can be used to create one.
The article focuses on building a property system which is able to store any kind of datatype and which provides the possibility to "override" the concrete value of a certain property with a function returning such. This allows to easily setup relationships between different properties or accross property systems which might be the main advantage compared to other property bag implementations.
Avoiding the use of the new dynamic keyword was a design goal as well as providing a better base for dependency property like structures. XML De-/serialization isn't part of this example and has been completely left away as it's with thread safety and performance issues. However, some of these might be covered by further articles.
The property systems can be used directly or encapsulated in dedicated classes. They're handy to compose generic objects, build component containers, serve as bus systems and for much more.
The provided code includes a testcase for their direct use and one which shows an example of a generic object.
Although there's the possibility to provide event references, these are
not (yet) covered by the article, but included in the source code. A tip/trick where their approach is explained can be found here: www.codeproject.com/Tips/454172/Referencing-events-in-Csharp-using-interfaces-A-si [^].
Hope you enjoy.
Interfaces Overview
Following is a short overview of the over all major interfaces of the property system:
Property System Interface
The property system defines a key type (K), provides a factory method to create bags and allows to work with definitions:
public interface IPropertySystem<K> where K : IComparable, IFormattable, IConvertible, IComparable<K>, IEquatable<K>
{
IPropertyBag<K> CreateBag();
event Action<IPropertyDef<K> AfterDefinitionAdded;
event Action<IPropertyDef<K> AfterDefinitionRemoved;
IPropertyDef<K> GetDef(K propId);
void SetDefs(IPropertyDefs<K> defs);
}
Property Definitions Interface
The property definitions defines the same key type as the system, provides a factory method to create definitions and allows to work with definitions:
public interface IPropertyDefs<K> where K : IComparable, IFormattable,
IConvertible, IComparable<K>, IEquatable<K>
{
IPropertyDef<K> CreateDef(K id, Type type);
event Action<IPropertyDef<K> AfterDefinitionAdded;
event Action<IPropertyDef<K> AfterDefinitionRemoved;
void SetDefs(List<IPropertyDef<K> newDefs);
void AddDefs(List<IPropertyDef<K> newDefs);
void AddDef(IPropertyDef<K> newDef);
void RemoveDef(K propertyId);
void RemoveDef(IPropertyDef<K> def);
IPropertyDef<K> GetDef(K propertyId);
List<IPropertyDef<K> Defs { get; }
}
Property Definition Interface
The property definition defines the same key type as the system and is a container for Id and ValueType values:
public interface IPropertyDef<K> where K : IComparable, IFormattable, IConvertible, IComparable<K>, IEquatable<K>
{
K Id { get; set; }
Type ValueType { get; set; }
}
Property Bag Interface
The property bag class defines the same key type as the system and allows to create/modify/delete property values:
public interface IPropertyBag<K> where K: IComparable,
IFormattable, IConvertible, IComparable<K>, IEquatable<K>
{
event Func<K, object, bool> BeforeFuncAdd;
event Action<K, object> AfterFuncAdd;
event Action<K, object> AfterFuncAddFailed;
event Func<K, object, bool> BeforeFuncSet;
event Action<K, object> AfterFuncSet;
event Action<K, object> AfterFuncSetFailed;
event Func<K, object, bool> BeforeValueAdd;
event Action<K, object> AfterValueAdd;
event Action<K, object> AfterValueAddFailed;
event Func<K, object, bool> BeforeValueSet;
event Action<K, object> AfterValueSet;
event Action<K, object> AfterValueSetFailed;
event Func<K, bool> BeforeRemove;
event Action<K> AfterRemove;
bool TryGet(K propId, out object retValue);
bool TryGetFunc(K propId, out object retValue);
bool TryGetValue<T>(K propId, out T retValue);
T GetValue<T>(K propId, T defaultValue);
T GetValue<T>(K propId, Action<T> nonNullAction);
T GetValue<T>(K propId, Action<T> nonNullAction, Action nullAction);
R GetValue<T, R>(K propId, Func<T, R> nonNullFunc, R defaultValue);
R GetValue<T, R>(K propId, Func<T, R> nonNullFunc, Func<R> nullFunc);
void Set(K propId, object value);
void SetFunc(K propId, object value);
void SetValue<T>(K propId, T value);
bool TryRemove(K propId);
}
Where does the magic happen
Following, some of the more interresting code fragments of the system are presented.
In fact, there aren't much. Most of the magic happens in the PropertyBag<K>
's TryGetValue<T>
method.
The frame is straight forward layed out by some case determinations:
public bool TryGetValue<T>(K propId, out T retValue)
{
if (properties.ContainsKey(propId))
{
object p = properties[propId];
if (p == null)
{
}
else
{
}
}
else
{
}
}
Having no property set is the easiest case - returning a default value for the type and false as return seems sufficient:
retValue = default(T);
return false;
If the stored value for the property is null, we have to check if the properties type is not a value type and raise an exception if needed, because they don't like the null:
Type Ttype = typeof(T);
if (!Ttype.IsValueType)
{
retValue = default(T);
return true;
}
else
throw new Exception(string.Format("Value types don't support null"));
The last step and maybe the most interresting part (although, it's no rocket science) is the type magic done to determine if the property is overridden by a function.
The approach is simple: The property definition in the system tells us what type to expect. If the contained type differs from the expected one, we just check if it's a function returning the expected type. If not, we're screwed and throw an exception:
Type t = p.GetType();
IPropertyDef<K> pd = system.GetDef(propId);
Type vt = pd.ValueType;
bool b = vt.IsAssignableFrom(t);
if (b)
retValue = (T)p;
else
{
Func<T> f = p as Func<T>
if (f == null)
throw new Exception(string.Format("expected Func<{0}> but got {1}", typeof(T).ToString(), t.ToString()));
else
retValue = f();
}
return true;
Finally, the whole code for the method:
public bool TryGetValue<T>(K propId, out T retValue)
{
if (properties.ContainsKey(propId))
{
object p = properties[propId];
if (p == null)
{
Type Ttype = typeof(T);
if (!Ttype.IsValueType)
{
retValue = default(T);
return true;
}
else
throw new Exception(string.Format("Value types don't support null"));
}
else
{
Type t = p.GetType();
IPropertyDef<K> pd = system.GetDef(propId);
Type vt = pd.ValueType;
bool b = vt.IsAssignableFrom(t);
if (b)
retValue = (T)p;
else
{
Func<T> f = p as Func<T>
if (f == null)
throw new Exception(string.Format("expected Func<{0}> but got {1}", typeof(T).ToString(), t.ToString()));
else
retValue = f();
}
return true;
}
}
else
{
retValue = default(T);
return false;
}
}
Using the code to manage a property system on the fly
The following paragraphs show how to create a property system on the fly and some basic use cases:
Create a system instance
A property system instance needs at least a key type. Although the property definitions can be set up before
constructing the system instance, it makes sense to split system and definition creation for this example.
PropertySystem<string> ps = new PropertySystem<string>();
Create property definitions for later use by the property system
For the property system being able to distinguish between values and functions returning values, it needs
proper definitions set up:
PropertyDefs<string> pd = new PropertyDefs<string>();
pd.AddDef(pd.CreateDef("test-int", typeof(int)));
pd.AddDef(pd.CreateDef("a func returning a boolean", typeof(Func<bool>)));
Assign the property definitions to the property system instance
The property system instance needs to know about its possible properties because the bag it creates uses the information
to distinguish between value- and function types:
ps.SetDefs(pd);
Create a property bag instance from the property system instance
As the property system may be seen as a class definition (although, the comparison is misleading),
the property bag may be seen as an instance [of the property system seen as a class]
(although, the comparison is misleading as well):
PropertyBag<string> pb = ps.CreateBag() as PropertyBag<string>;
Use the property bag instance
Now that everything is set up, we can start using it.
Because the bag is still empty, we first fill in some data:
pb.SetValue("test-int", 42);
pb.SetValue("a func returning a boolean", new Func<bool>(() => true));
Getting the data back from the bag is very easy. Thanks to generic type inference, we don't even have to specify the return types explicitely if we're storing them directly:
bool ret;
int iValue;
ret = pb.TryGetValue("test-int", out iValue);
Func<bool> fBool;
ret = pb.TryGetValue("a func returning a boolean", out fBool);
However, there are some convenience methods for a more fluent use.
If it's possible to provide a default value, there's no need for a return value indicating the success of the get operation:
iValue = pb.GetValue("test-int", 0815);
Chaining through functions is also possible, however, the compiler then needs a type hint:
pb.GetValue<int>("test-int", v => Debug.WriteLine(string.Format("successfully got {0}", v)), () => Debug.WriteLine("GetValue failed"));
Transformations are also possible; here, our int
is returned as a string
:
pb.GetValue<int, string>("test-int", v => v.ToString(), () => "GetValue failed");
Remember that we defined "test-int" as typeof(int)
and "a func returning a boolean" as typeof(Func<bool>)
and that there's the possibility to override any property value with a corresponding function returning the type we defined for the property:
pb.SetValue("test-int", new Func<int>(() => 42));
pb.SetValue("a func returning a boolean", new Func<Func<bool>>(() => new Func<bool>(() => true)));
Getting the values remains the same as before because the interface is transparent. However, there may be occasions when you know that there's a function behind a value property. To directly access it, there's another method in the bag:
object o;
ret = pb.TryGetFunc("test-int", out o);
ret = pb.TryGetFunc("a func returning a boolean", out o);
Using the code to create a property-bag based object
The following paragraphs show how to create a property-bag based object.
Because the example is so much used and widely understood, we're going to create a generic definition of amphibian vehicles.
For sake of simplicity, there will be only one method: Accelerate
.
Define property id's
Even if the key type of the system is string
, it sometimes is very handy to have constants ready to avoid polluting the source code with magic numbers.
Because we're using integers as keys in the example, it's no question for me whether to define proper constants for the property id's or not.
Since they're used quite often, it could make sense to keep the container names short:
abstract class Ipsc
{
public const int Id = 0;
public const int MaxDriveSpeed = 1;
public const int MaxFlySpeed = 2;
public const int MaxSwimSpeed = 3;
public const int CurrentMedium = 4;
public const int CurrentSpeed = 5;
public const int Accelerate = 10;
}
Define a specialised property system
The system definition can be a derivation of the original PropertySystem
. We're going to approach it with aggregation and just implement the interface IPropertySystem
. For performance reasons, the key type is defined as integer.
class IntKeyPropertySystem : IPropertySystem<int>
{
IPropertySystem<int> propSys;
public IntKeyPropertySystem()
: base()
{
}
}
The next step is to implement all needed interfaces. Proxying them to the base object is sufficient for this example:
public event Action<IPropertyDef<int>> AfterDefinitionAdded;
public event Action<IPropertyDef<int>> AfterDefinitionRemoved;
public void SetDefs(IPropertyDefs<int> defs)
{
propSys.SetDefs(defs);
}
public IPropertyBag<int> CreateBag()
{
return propSys.CreateBag();
}
public IPropertyDef<int> GetDef(int propId)
{
return propSys.GetDef(propId);
}
We may not forget to create the system and setup the event routes:
public IntKeyPropertySystem() : IPropertySystem<int>
: base()
{
PropertySystemFactory psFactory = new PropertySystemFactory();
propSys = psFactory.Create<int>();
propSys.AfterDefinitionAdded += new Action<IPropertyDef<int>>(propSys_AfterDefinitionAdded);
propSys.AfterDefinitionRemoved += new Action<IPropertyDef<int>>(propSys_AfterDefinitionRemoved);
}
void propSys_AfterDefinitionAdded(IPropertyDef<int> def)
{
if (AfterDefinitionAdded != null)
AfterDefinitionAdded(def);
}
void propSys_AfterDefinitionRemoved(IPropertyDef<int> def)
{
if (AfterDefinitionRemoved != null)
AfterDefinitionRemoved(def);
}
Finally, we define the available properties and assign them to the system:
IPropertyDefs<int> defs = psFactory.CreateDefs<int>();
defs.SetDefs(new List<IPropertyDef<int>>{
{ defs.CreateDef(Ipsc.Id, typeof(int)) },
{ defs.CreateDef(Ipsc.MaxDriveSpeed, typeof(double)) },
{ defs.CreateDef(Ipsc.MaxFlySpeed, typeof(double)) },
{ defs.CreateDef(Ipsc.MaxSwimSpeed, typeof(double)) },
{ defs.CreateDef(Ipsc.CurrentMedium, typeof(Medium)) },
{ defs.CreateDef(Ipsc.CurrentSpeed, typeof(double)) },
{ defs.CreateDef(Ipsc.Accelerate, typeof(Action<double>)) }
});
propSys.SetDefs(defs);
Create an example object definition
The example object has a reference to the shared bag, a global unique object-id and provides some proxified accessors:
The shared bag is simply set up during construction:
class ExampleObject
{
IPropertyBag<int> pbag;
public ExampleObject(IPropertyBag<int> pbag)
{
this.pbag = pbag;
}
}
The UID management is also held simple and at this time just here for reference and easier debugging; however, it often seems to make sense for several reasons to have an object-uid:
class ExampleObject
{
IPropertyBag<int> pbag;
static int nextId = 0;
public ExampleObject(IPropertyBag<int> pbag)
{
this.pbag = pbag;
this.pbag.SetValue(Ipsc.Id, nextId);
++nextId;
}
}
Finally we define the accessor methods. They may completely differ from the ones from the bag, but in the easiest case, we can just proxify them:
class ExampleObject
{
static int nextId = 0;
IPropertyBag<int> pbag;
public ExampleObject(IPropertyBag<int> pbag)
{
this.pbag = pbag;
this.pbag.SetValue(Ipsc.Id, nextId);
++nextId;
}
public bool TryGetFunc(int propId, out object retValue)
{
return this.pbag.TryGetFunc(propId, out retValue);
}
public bool TryGetValue<T>(int propId, out T retValue)
{
return this.pbag.TryGetValue<T>(propId, out retValue);
}
public T GetValue<T>(int propId, T defaultValue)
{
return this.pbag.GetValue<T>(propId, defaultValue);
}
public T GetValue<T>(int propId, Action<T> nonNullAction)
{
return this.pbag.GetValue<T>(propId, nonNullAction);
}
public T GetValue<T>(int propId, Action<T> nonNullAction, Action nullAction)
{
return this.pbag.GetValue<T>(propId, nonNullAction, nullAction);
}
public R GetValue<T, R>(int propId, Func<T, R> nonNullFunc, R defaultValue)
{
return this.pbag.GetValue<T, R>(propId, nonNullFunc, defaultValue);
}
public R GetValue<T, R>(int propId, Func<T, R> nonNullFunc, Func<R> nullFunc)
{
return this.pbag.GetValue<T, R>(propId, nonNullFunc, nullFunc);
}
public void Set(int propId, object value)
{
this.pbag.Set(propId, value);
}
public void SetFunc(int propId, object value)
{
this.pbag.SetFunc(propId, value);
}
public void SetValue<T>(int propId, T value)
{
this.pbag.SetValue<T>(propId, value);
}
public bool TryRemove(int propId)
{
return this.pbag.TryRemove(propId);
}
}
Create an example system manager
So far, we've defined the property system, its properties and the example object. But we still left out the whole logic.
To keep it simple, all logic which is left is put together in one class which serves also as a container for the test definition:
class TestIntKeyPropertySystem
{
public void Run()
{
}
}
Creating the system and some example object instances is very easy:
public void Run()
{
IntKeyPropertySystem ikps = new IntKeyPropertySystem();
ExampleObject eo1 = new ExampleObject(ikps.CreateBag());
ExampleObject eo2 = new ExampleObject(ikps.CreateBag());
}
Setting property values is also quite painless:
public void Run()
{
const double maxDriveSpeedEo1 = 100;
const double maxSwimSpeed = 40;
eo1.SetValue(Ipsc.MaxDriveSpeed, maxDriveSpeedEo1);
eo1.SetValue(Ipsc.MaxFlySpeed, 300);
eo1.SetValue(Ipsc.MaxSwimSpeed, maxSwimSpeed);
eo1.SetValue(Ipsc.CurrentMedium, Medium.Ground);
eo1.SetValue(Ipsc.CurrentSpeed, 0);
eo2.SetValue(Ipsc.MaxDriveSpeed, 90);
eo2.SetValue(Ipsc.MaxFlySpeed, 250);
eo2.SetFunc(Ipsc.MaxSwimSpeed, new Func<double>(() => eo1.GetValue(Ipsc.MaxSwimSpeed, 50.0)));
eo2.SetValue(Ipsc.CurrentMedium, Medium.Water);
eo2.SetValue(Ipsc.CurrentSpeed, 0);
}
The only "real" method of our "objects" is Accelerate
:
public void Run()
{
eo1.SetValue<Action<double>>(Ipsc.Accelerate, s => Accelerate(eo1, s));
eo2.SetValue<Action<double>>(Ipsc.Accelerate, s => Accelerate(eo2, s));
}
Of course, we have to implement the Accelerate
method:
void Accelerate(ExampleObject eo, double speed)
{
Medium medium;
if (!eo.TryGetValue(Ipsc.CurrentMedium, out medium))
return;
int prop;
switch (medium)
{
case Medium.Ground:
prop = Ipsc.MaxDriveSpeed;
break;
case Medium.Air:
prop = Ipsc.MaxFlySpeed;
break;
case Medium.Water:
prop = Ipsc.MaxSwimSpeed;
break;
default:
return;
}
double maxSpeed;
if (eo.TryGetValue(prop, out maxSpeed))
speed = Math.Min(speed, maxSpeed);
eo.SetValue(Ipsc.CurrentSpeed, speed);
}
Limitations
Because SetValue
raises the value-changed-event, there is no possibility yet to subscribe for changes of properties which are overridden with functions.
Points of Interest
The presented approach to a property system is only one out of many and is not meant to be used in productive
environments yet.
Static type checking is a very powerful mechanism and doing it at runtime obviously raises some performance and
safety issues.
Distinguishing between value properties and their function equivalents depends on runtime type information which might
be replaced by either putting the info to the properties or to the system. This would also be cool because it would allow
to remove the back-reference from the bag to the system.
Readonly properties are completely left out from the implementation, however, they could be added quite easily,
for example by replacing the object container by a more clever one which provides a property for it, or by delegating the management to a specialized bag which keeps such information differently - for example by using separate lists for the properties.
References
-
stackoverflow/Tomas Vana: Implementation of a general-purpose object structure (property bag)
^
-
gamedev.net/JTippetts: Entity system
^
-
west-wind.com/Rick Strahl's Web Log: An Xml Serializable PropertyBag Dictionary Class for .NET
^
-
cowboyprogramming/Mick West: Evolve Your Hierarchy
^
-
codeproject.com/Graham Harrison: Implementing a PropertyBag in C#
^
- www.academia.edu/401854/A_Generic_Data_Structure_for_An_Architectural_Design_Application ^
History
- 2013/01/31: Added a link to the "event referencing" tip/trick, added references link to contents and fixed some spellings
- 2013/01/24: Added a reference
- 2012/08/31: Added limitations and an explanation of the implementation
- 2012/08/29: First version submitted