Presenting Detail Values as part of the Master – part 2

By Bill at May 10, 2009 19:59
Filed Under: .Net Programming

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;}
}

The attribute class on the other hand has at least two properties, a key and a value.  The key property is a string that will be used to identify the attribute and the value property would hold the actual value of the attribute.  Rather than a certain type or even an object, the value will hold a byte buffer where we can store anything using serialization.
 
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.
 
In part one, we saw that in order to create a SubAttributeAccessor we must specify the type of the attribute object and pass in the names of three properties.  We must pass in the name of the property on the parent object that holds a list of child objects, we must pass in the name of the property on the child (or attribute) object that is the key and we must pass in the name of the property on the child object that is the value.

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.

 

kick it on DotNetKicks.com

Comments

Add comment


(Will show your Gravatar icon)

  Country flag

biuquotecode
  • Comment
  • Preview
Loading



Authors

  RSS Feed Bill Fugina

Bill is Director of Technology for Coleman Insights. He enjoys programming, software design, walking, reading, dining out and watching movies, most of which he enjoys even more when he doing them with his wife, Deb, and or his son, Isaac.  Bill and Isaac are working on a video game, but they haven't made very much progress yet.

  RSS Feed Debra Hill

Deb dabbled in Project Management in the Advertising industry for (too) many years. She has happily ditched that and is taking some time to decide what is next career-wise. She enjoys gardening, knitting, sewing and various other crafty things. She also enjoys vegetating on the weekends with the family.

RSS Feed Isaac Hill-Fugina

Isaac has his own blog called Isaac's Place.

Recent Comments

Comment RSS

Bill's Run.GPS Stats

Training Sessions 16
Total Distance 50.17 mi
Total Time 0.11:50:02
Calories 6961 kcal
Average Speed 4.24 mph
Min Altitude -157 ft
Max Altitude 590 ft
Total Ascent 226 ft
Total Descent 236 ft