Objects With Customizable Properties
A class with customizable properties without using Reflection.
Introduction
Why did I write this? I was in need of a more or less dynamic class where I could customize properties by configuration.
Background
My first approach was the classical "use-a-dictionary", but this left me with the decision to using a string as the key (I'm staying with that) and... what type as value?
Using object
as value could bypass strong typing and I would not like to lose it. Also, I needed a possibility to iterate through properties without using Reflection in the client, even to define some of the properties to be read-only.
Last but not least, it should be possible to gain the value of a fixed property defined by an interface (read and write).
All in all, pretty much like M. Fowler mentioned in his article "Dealing with Properties".
The Code Behind
The code is divided into two assemblies:
- Interfaces to an object with customizable properties.
- An implementation of the mentioned interface.
The interface is nothing fancy. Just a small interface to an object with properties.
public interface IObjectWithProperties {
event EventHandler<SMoni.ObjectWithProperties.
Interfaces.PropertyChangedEventArgs> PropertyChanged;
object this[string Key_] { get; set; }
T getValueOf<T>(string Key_);
object getValueOf(string Key_);
void setValueOf(string Key_, object Value_);
void setValueOf(KeyValuePair<string, object> KeyValuePair_);
DictionaryEntry[] Properties { get; }
bool canWrite(string Key_);
bool hasProperty(string Key_);
bool areTypesEqual(string Key_, object Value_);
}
The method hasProperty
could be used to make sure the property exists, and canWrite
to check if the property is writable. areTypesEqual
should be called to make sure that the type of the given key and the value are the same.
For getting values, there are three possible ways (OK... there's a fourth, but I will explain this one later) to obtain a value by a given key:
index
getValueOf
method which returns an objectgetValueOf
method with a generic, which returns a value with the generic-type, if the type matches
The methods could also return the value of a fixed property.
For setting a value, there are three possible ways (this time, really only three):
index
setValueOf
method with aKeyValuePair
setValueOf
method with a straight key asString
and a value asObject
With all set-methods, it is also possible to set a fixed property.
Further, the properties in dictionaries fire an event if they have been changed. Attention: "normal" properties are not covered.
Last but not least, it is possible to query all properties using Properties[]
.
To avoid showing/using "normal" properties in Properties[]
and by get-methods, the attribute [DoNotShowInProperties]
could be used. Either in an interface or in the class itself.
[DoNotShowInProperties]
public [...] PropertyWhichIsNotShownInProperties {
get {
[...]
}
set {
[...]
}
}
or:
interface IDoNotUseAllFixedProperties {
[...]
[DoNotShowInProperties]
[...] PropertyWhichIsNotShownInProperties { get; set; }
[...]
}
The implementation is divided into initialization and handling.
The initialization-methods are protected, so they could only be called by descendants (never mind Reflection). It is possible to choose different types of initialization (I hope the signature is self-explaining).
protected virtual void initializeValueOf(String Key_,
Object Value_) {
[...]
}
protected virtual void initializeValueOf(String Key_,
Object Value_,
Boolean CreateKeyValuePair_) {
[...]
}
protected virtual void initializeValueOf(String Key_,
Object Value_,
Boolean CreateKeyValuePair_,
Boolean AsReadOnly_) {
[...]
}
It is also possible to initialize fixed-properties, if necessary. Also nothing too fancy.
The handling of the properties is a little bit more tricky, but all in all very straightforward.
Using the code
It is necessary to inherit a class from AbstractObjectWithProperties
. An inherited class could have fixed properties or initialize different Key-Value pairs.
public class MyClass : AbstractObjectWithProperties {
public MyClass() {
this.initializeValueOf("ID", Guid.NewGuid(), true, true);
this.initializeValueOf("Property 1", String.Empty, true);
this.initializeValueOf("Property 2", String.Empty, true);
}
public Guid ID {
get {
return this.getValueOf<Guid>("ID");
}
}
public String Property_1 {
get {
return this.getValueOf<String>("Property 1");
}
set {
this["Property 1"] = value;
}
}
public String Property_2 {
get {
return this.getValueOf<String>("Property 2");
}
set {
this["Property 2"] = value;
}
}
}
For examples, just look into the demo code.
Points of Interest
The first problem I encountered was that unfortunately there is no common method to parse values, but this would be necessary for initialization of values to provide type-safety, so I decided to create a common interface which would allow me to parse values without thinking about the conversion of a real type.
To create a converter or parser, you call the factory with the type you would like to convert. (Right now, there are only six converters available, but expanding should not be a problem.)
The code is included but not explained.
If you are using a different MEMBER_PREFIX
than "_", you should change it in the class AbstractObjectWithProperties
. Same for MEMBER_BINDINGFLAGS
.
/// <summary>
/// Member-prefix should be customized... if necessary...
/// </summary>
protected static String MEMBER_PREFIX = "_";
protected static BindingFlags MEMBER_BINDINGFLAGS =
BindingFlags.Instance | BindingFlags.NonPublic;
protected virtual FieldInfo getFieldInfo(String Key_) {
FieldInfo result = null;
Type contextType = this.GetType();
while ((contextType != null) && (result == null)) {
result = contextType.GetField(MEMBER_PREFIX + Key_, MEMBER_BINDINGFLAGS);
contextType = contextType.BaseType;
}
return result;
}
There are some drawbacks. I would especially recommend the article of M. Fowler: Dealing with Properties. This isn't a silver bullet, so don't use it as one.
- Dynamic add or remove of a property isn't possible out-of-the-box... but could be added if required.
- Properties not wrapping a field can not be used.
- A property whose value is computed at runtime is not possible.
- Casting is (currently) not possible (sorry, lack of an idea).
History
- 25 April, 2011
- Added a method to check equality of the type of a given key and a value.
- 20 February, 2011
- Attribute added to prevent showing fixed-properties.
- Derive from
MarshalByRefObject
by preprocessor.
- 05 September, 2010
- Initial version.