This is part two of a three part series on working with master-detail data. In part one I showed how to present detail values on the master using a utility I wrote called the SubAttributeAccessor. In this article I will go into detail as to how the SubAttributeAccessor works. In part three, I’ll introduce a descendant of DataGridViewColumn that will allow us to show the indexed detail data in line with the master data as additional columns.
The SubAttributeAccessor is a class that when added to a parent class is able to present a list of children as an indexed property of the parent. For example, let’s imagine that we had a class called song and that is has among other properties one that is a list of attribute objects.
public class Song {
public List<Attribute> Attributes {get; set;}
}
public class Attribute {
public string Key {get; set;}
public byte[] Value {get; set;}
}
These two classes form the basis of the imaginary scenario that I used in part one of this article. A song can have an unlimited number of attributes. As things stand, we can add an attribute to a song like so:
Song mySong = new Song();
Attribute myAttribute = new Attribute();
myAttribute.Key = "Tempo";
myAttribute.Value = "slow";
mySong.Attributes.Add(myAttribute);
Actually, even this won’t work as it is putting a string into a byte array. In the real world, we would have to write a property setter that converted whatever data was presented to it into an array of bytes. Since the SubAttributeAccessor will take care of this, I’m going to ignore this for now.
This seems to work alright and with a little more programming, we can make it fairly easy to add any amount of attributes to a song:
Song mySong = new Song();
mySong.Attributes.Add(new Attribute("Genre", "Rock"));
The real problem is getting the attributes back out. For example, if I wanted to know if a song had a value for tempo or genre, I would have to iterate through all of the attributes checking each of the keys. The SubAttributeAccessor allows much simpler access to the attributes:
Song MySong = new Song();
MySong["Tempo"] = "Medium";
MySong["Rating"] = 5;
MySong["Genre"] = "Rock";
Song anotherSong = new Song();
anotherSong["Genre"] = MySong["Genre"];
Part one of this series showed how to add the SubAttributeAccessor to a class like Song to achieve this syntax. In this part we will look at exactly how the SubAttributeAccessor works.
1: using System.Collections.Generic;
2: using System.IO;
3: using System.Runtime.Serialization.Formatters.Binary;
4: using System.Text;
5: using System;
6: using System.Reflection;
7: using System.Diagnostics;
8:
9: namespace SubPropertyColumns {
10: /// <summary>
11: /// This class is used to access a list of child properties as an indexed property of the parent.<br/>
12: /// The child properties will be accessible via a <code>ParentObject.AccessorProperty[Key] = Value</code> syntax.<br/>
13: /// This class assumes that the values in the sub attributes have been seralized using a binary formatter.
14: /// If the value in the sub attribute cannot be de-serialized, it is returned as a string.
15: /// </summary>
16: /// <typeparam name="T">The type of the object that will hold the attributes.</typeparam>
17: public class SubAttributeAccessor<T> {
18: private const string STR_MissingParentIEnumerable =
19: "Class \"{0}\" must have a property named \"{1}\" that must return an object that implements IEnumberable<{0}>";
20:
21: private const string STR_MissingParentIList =
22: "Class \"{0}\" must have a property named \"{1}\" that must return an object that implements IList<{0}>";
23:
24: private const string STR_MissingKey =
25: "Class \"{0}\" must have a property named \"{1}\" that is of type string. Check the property name or return type.";
26:
27: private const string STR_StrMissingValue =
28: "Class \"{0}\" must have a property named \"{1}\". Check the property name.";
29:
30: /// <summary>
31: /// The accessor must have a reference to a parent object in order to access its properties
32: /// </summary>
33: private object fParent = null;
34:
35: /// <summary>
36: /// The property on the parent object that holds a list of child objects.</br>
37: /// This list of child objects holds the sub attribute keys and values.</br>
38: /// Each child object must hold a key and a value.
39: /// </summary>
40: private string fPropertyName = "";
41:
42: /// <summary>
43: /// The property on the child object that holds the key of the sub attribute.
44: /// </summary>
45: private string fKeyPropertyName = "";
46:
47: /// <summary>
48: /// The property on the child object that will hold the actual value of the sub attribute.
49: /// </summary>
50: private string fValuePropertyName = "";
51:
52: private PropertyInfo fParentPropertyInfo;
53: private PropertyInfo fKeyPropertyInfo;
54: private PropertyInfo fValuePropertyInfo;
55:
56: private IEnumerable<T> fAttributeEnumeration;
57: private IList<T> fAttributeList;
58:
59: /// <summary>
60: /// Constructor for the SubAttributeAccessor
61: /// </summary>
62: /// <param name="parent">The main object that will be accessed using this accessor.</param>
63: /// <param name="propertyName">The property on the parent object that holds a list of child objects.</param>
64: /// <param name="fieldName">The property on the child object that will be the key of the sub attributes.</param>
65: /// <param name="valueName">The property on the child object that will be the value of the sub attributes.</param>
66: public SubAttributeAccessor(object parent,
67: string propertyName,
68: string fieldName,
69: string valueName) {
70:
71: fParent = parent;
72: fPropertyName = propertyName;
73: fKeyPropertyName = fieldName;
74: fValuePropertyName = valueName;
75:
76: Type parentType = fParent.GetType();
77: Type itemType = typeof(T);
78:
79: //PropertyInfo for the property on the parent object that holds the list of attribute objects
80: fParentPropertyInfo = parentType.GetProperty(fPropertyName);
81:
82: if ( fParentPropertyInfo == null
83: || !typeof(IEnumerable<T>).IsAssignableFrom(fParentPropertyInfo.PropertyType)) {
84:
85: throw new ArgumentException(
86: string.Format(STR_MissingParentIEnumerable, typeof(T).Name, fPropertyName));
87: }
88:
89: if (!typeof(IList<T>).IsAssignableFrom(fParentPropertyInfo.PropertyType)) {
90:
91: throw new ArgumentException(
92: string.Format(STR_MissingParentIList,
93: typeof(T).Name, fPropertyName));
94: }
95:
96:
97: //PropertyInfo for the property on the child attribute obeject that holds the key for the attribute
98: fKeyPropertyInfo = itemType.GetProperty(fKeyPropertyName);
99: if (fKeyPropertyInfo == null || fKeyPropertyInfo.PropertyType != typeof(string)) {
100:
101: throw new ArgumentException(
102: string.Format(STR_MissingKey, typeof(T).Name, fKeyPropertyName));
103: }
104:
105: //PropertyInfo for the property on the child attribute objects that holds the value for the attribute
106: fValuePropertyInfo = itemType.GetProperty(fValuePropertyName);
107: if (fValuePropertyInfo == null) {
108:
109: throw new ArgumentException(
110: string.Format(STR_StrMissingValue, typeof(T).Name, fValuePropertyName));
111: }
112:
113: object propertyObject = fParentPropertyInfo.GetValue(fParent, new object[] { });
114:
115: if (propertyObject is IEnumerable<T>) {
116: fAttributeEnumeration = (IEnumerable<T>)propertyObject;
117: }
118:
119: if (propertyObject is IList<T>) {
120: fAttributeList = (IList<T>)propertyObject;
121: }
122:
123: }
124:
125: /// <summary>
126: /// A Dictionary to organize the attributes by their name
127: /// </summary>
128: private Dictionary<string, T> fAttributeDictionary = null;
129:
130: /// <summary>
131: /// A Dictionary to organize the attributes by their name.<br/>
132: /// This property ensures that the Dictionary is
133: /// created and populated the first time it is accessed
134: /// </summary>
135: private Dictionary<string, T> AttributeDictionary {
136: get {
137: if (fAttributeDictionary == null) {
138: fAttributeDictionary = new Dictionary<string, T>();
139:
140: foreach (T attribute in fAttributeEnumeration) {
141: string fieldNameObject = fKeyPropertyInfo.GetValue(attribute, new object[] { }) as string;
142: fAttributeDictionary.Add(fieldNameObject, attribute);
143: }
144: }
145:
146: return fAttributeDictionary;
147: }
148: }
149:
150: /// <summary>
151: /// This is the indexed property that provides access to
152: /// the list of child attributes.
153: /// </summary>
154: /// <param name="index">The value of the key property on the object to be looked up</param>
155: /// <returns>The value of the value property on the object that has the matching key property</returns>
156: public object this[string index] {
157: get {
158: object result = null;
159:
160: //if the value for a given key exists, it should be in the dictionary
161: if (AttributeDictionary.ContainsKey(index)) {
162:
163: //Assume that the value in the attribute has been serialized using a binary formatter
164:
165: //create a memory stream for storing the binary data
166: MemoryStream stream = new MemoryStream();
167:
168: //attribute is the object that contains the key and value properties
169: T attribute = AttributeDictionary[index];
170:
171: //valueObject is the data that was stored in the value property of the value object
172: object valueObject = fValuePropertyInfo.GetValue(attribute, new object[] { });
173:
174: //if valueObject is indeed an array of bytes, store it in a strongly typed object.
175: byte[] value = new byte[] { };
176: if (valueObject is byte[]) {
177: value = (byte[])valueObject;
178: }
179:
180:
181: try {
182: //write the binary data from the database into a stream
183: stream.Write(value, 0, value.Length);
184: stream.Position = 0;
185:
186: //use the BinaryFormatter to deserialize the object
187: BinaryFormatter formatter = new BinaryFormatter();
188:
189: //result is the deserialized data from the value property of the object
190: //with the key property matching the specified index
191: result = formatter.Deserialize(stream);
192: }
193:
194: catch {
195: //if something goes wrong, get a string representation
196: //of the binary data instead
197: StringBuilder sb = new StringBuilder();
198: foreach (byte aByte in value) {
199: sb.Append((char)aByte);
200: }
201:
202: //result is the string represetation of the data from the value property of the object
203: //with the key property matching the specified index
204: result = sb.ToString();
205: }
206:
207: finally {
208: //no matter what happens we should always close the stream.
209: stream.Close();
210: }
211:
212: }
213:
214: return result;
215: }
216:
217: set {
218: //create a memory stream to hold the data as we serialize it
219: MemoryStream stream = new MemoryStream();
220:
221: try {
222: //use the BinaryFormatter to serialize the object into a stream
223: BinaryFormatter formatter = new BinaryFormatter();
224: formatter.Serialize(stream, value);
225:
226: //put the stream into a buffer
227: stream.Position = 0;
228: byte[] buffer = new byte[stream.Length];
229: stream.Read(buffer, 0, (int)stream.Length);
230:
231:
232: //if the dictionary already contains an entry for this key
233: //just use it
234: if (AttributeDictionary.ContainsKey(index)) {
235: T attribute = AttributeDictionary[index];
236:
237: //put the binary data buffer into the value property of the attribute object
238: //with the key property that matches
239: fValuePropertyInfo.SetValue(attribute, buffer, new object[] { });
240: }
241:
242: else {
243: //if this is a new attribute we need to put the value into
244: //a new attribute and put the new attribute into the parent object
245: //as well as into the Dictionary
246:
247:
248: //Create a new attribute object
249: T newAttribute = Activator.CreateInstance<T>();
250:
251: //set the value for the key property
252: fKeyPropertyInfo.SetValue(newAttribute, index, new object[] { });
253:
254: //set the value for the value property
255: fValuePropertyInfo.SetValue(newAttribute, buffer, new object[] { });
256:
257: //add the attribute to the parent object
258: fAttributeList.Add(newAttribute);
259:
260: //put the attribute into the dictionary
261: AttributeDictionary.Add(index, newAttribute);
262: }
263: }
264: finally {
265: //always close the stream
266: stream.Close();
267: }
268:
269: }
270: }
271:
272: }
273: }
There are three main things that make this work: There is generic dictionary that holds the attributes in an indexed data structure, there is the indexed property that that brings data in and out of the dictionary and finally it is reflection that allows access to the underlying data regardless of which class it is accessing.
In order to quickly and easily access the attributes they are stored in a generic dictionary. The dictionary itself is declared on line 128:
128: private Dictionary<string, T> fAttributeDictionary = null;
However, it is the AttributeDictionary property that accesses the dictionary that creates it and populates it with all of the keys and references to the attribute object.
135: private Dictionary<string, T> AttributeDictionary {
136: get {
137: if (fAttributeDictionary == null) {
138: fAttributeDictionary = new Dictionary<string, T>();
139:
140: foreach (T attribute in fAttributeEnumeration) {
141: string fieldNameObject = fKeyPropertyInfo.GetValue(attribute,
new object[] { }) as string;
142: fAttributeDictionary.Add(fieldNameObject, attribute);
143: }
144: }
145:
146: return fAttributeDictionary;
147: }
148: }
This allows easy access to any of the attributes directly by specifying the key of the attribute to be accessed. At line 141, reflection is used to get the key of each attribute object. The fKeyPropertyInfo object is a PropertyInfo object that was created in the constructor using the property name that is a required parameter of the constructor. The GetValue method of the PropertyInfo class allows us to get the value of the key property on the attribute object.
The signature of the constructor is as follows:
66: public SubAttributeAccessor(object parent,
67: string propertyName,
68: string fieldName,
69: string valueName)
The “magic” happens in the indexed property on line 156
156: public object this[string index] {
When getting data it first pulls the attribute object out of the dictionary. Then it uses reflection to get the data out of the value field of the attribute object.
172: object valueObject = fValuePropertyInfo.GetValue(attribute, new object[] { });
The fValuePropertyInfo object was created in the constructor at the same time as the fKeyPropertyInfo and works the same way.
The data that came out of the attribute object is then deserialized and returned.
191: result = formatter.Deserialize(stream);
To set data the reverse is done. The new value is serialized and put into an attribute object in the dictionary. In the case were an attribute is set that is not yet in the dictionary, a new attribute is created and added both to the parent object and the dictionary.
248: //Create a new attribute object
249: T newAttribute = Activator.CreateInstance<T>();
250:
251: //set the value for the key property
252: fKeyPropertyInfo.SetValue(newAttribute, index, new object[] { });
253:
254: //set the value for the value property
255: fValuePropertyInfo.SetValue(newAttribute, buffer, new object[] { });
256:
257: //add the attribute to the parent object
258: fAttributeList.Add(newAttribute);
259:
260: //put the attribute into the dictionary
261: AttributeDictionary.Add(index, newAttribute);
At this point, we can add this class as a child of any class that has a list of child objects just as we did in part one. In part three, I’ll introduce a descendant of DataGridViewColumn that will allow us to show the indexed detail data in line with the master data as additional columns.
Source Code: SubPropertyColumns.zip (215 KB)