This is part three 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 SubPropertyAccessor. In part two I showed how the SubPropertyAccessor works. In this part, I introduce a descendant of DataGridViewColumn that will allow us to show the indexed detail data in line with the master data as additional columns. I also introduce a descendent of TypeDescriptionProvider that allows the DataGridView to see the SubProperty as a proper property so that the rest of the grid functionality will still work.
Note: The SubPropertyAccessor was called SubAttributeAccessor in parts one and two.
The whole point of having detail values appear as part of a master record is to show the data in a grid where the detail values would appear in columns next to the other properties in the record. In order to do this I created a descendant of DataGridViewColumn called SubPropertyColumn. Actually, SubPropertyColumn is a descendant of DataGridViewTextBoxColumn. The DataGridViewTextBoxColumn has most of the functionality I want. I only need to have the data marshaled in and out of the data bound item in a specific way.
The first thing we need is to know where the SubPropertyAccessor is on the data bound item. This will be a property on the column that can be set at design time in the DataGridView designer.
Now that we know where to find the SubPropertyAccessor, we need to know which property we are looking for. This is the index that we will pass into the SubPropertyAccessor. We need another string property.
This property doesn’t have any backing data of its own. It is using the DataPropertyName of the base DataGridViewTextBoxColumn. We do this because the base DataGridColumn bases a lot of functionality on the DataPropertyName property. For example, if this property is blank the base DataGridColumn doesn’t consider the column to be a data bound column. If we are going to make use of the functionality provided by the base classes, we going to have to make them happy. This may seem like a kludge, but it’s better than having to reinvent the DataGridColumn.
Since we’re re-purposing the DataPropertyName property, let’s go ahead and hide it from the designer so it’s not confusing.
There’s one more piece if information that will make things much easier. That is the type of the data that will be shown in the column. Since the property from where we are getting the doesn’t really exist, we’ll make a property here and set it at design time.
I named the property ValueType even though there already is a ValueType property on the base class. I hide that property and replace it with this string property. I made this a string property to avoid having to make a property designer that offered a list of types. This does put a burden on the user to type in the name of an actual type. The property designer would be an obvious improvement.
I also added an internal property called InternalValueType that converts the string representation of the type to an actual Type.
The actual work of converting the string into a Type is done in a method called GetType which can be seen in the attached source code.
The rest of the work of marshaling the data in and out of the SubPropertyAccessor is done at the cell level in a class called SubPropertyCell. SubPropertyCell is derived from DataGridViewTextBoxCell and gets most of its functionality from there. There are four methods that do all of the work, GetValue, SetValue, GetFormatedValue and ParseFormattedValue. They are all overrides of methods on the base DataGridViewCell. An extra method, getAccessor contains some code that is shared by GetValue and SetValue.
The GetValue method is called by the DataGridView when it needs to get a value from the underlying bound data. The GetValue method uses the information set in the Column to get a value out of the SubPropertyAccessor.
The GetValue method first finds the accessor object and gets a PropertyInfo for the property that contains the sub-property data using the getAccessor method. It then uses reflection to get the value out of the accessor object.
The getAccessor method doesn’t look specifically for a SubPropertyAccessor. As long IndexedPropertyName on the subPropertyColumn points to a property that is an object that has a default indexed property that takes a single string parameter as an index it will work. In fact, if the IndexedPropertyName is blank and the data bound object itself has a default indexed property that takes a single string parameter, it will still work.
The DataGridView uses the SetValue method when it needs to put a value into the underlying bound data. The SetValue method uses essentially the same code as GetValue to put the data into the sub-property.
The GetFormattedValue method is used by the DataGridView to convert data from the underlying data source into a format that can be presented in the grid. Since the SubPropertyCell is derived from a DataGridTextBoxCell, I took a shortcut and converted everything to a string to be presented in the TextBox that the base DataGridTextBoxCell handles.
The ParseFormattedValue method is called by the DataGridView to convert data that was displayed in the grid into a format that can be stored in the underlying bound data source. Again, I took a shortcut and assumed that the grid would always be displaying strings. The ParseFormattedValue uses the static Convert.ChangeType to convert strings into the required data type.
There is no error handling here and it is very easy for an end user to cause an exception simply by entering the wrong data type into the grid.
With what we already have, we can show sub-property values in a grid. Using the same Linq-To-SQL classes from Part 1, we can point a BindingSource at the Song class and point a DataGridView at the DataSource.
In the screen shot of the Edit Columns dialog, we can see that there are five columns defined. The Artist and Title columns are regular TextBoxColumns that get their data directly from the Song table in the database. The Category, Score and Date columns, on the other hand, are SubPropertyColumns. In the picture, the focus is on the Score column and we can see that IndexedPropertyName is set to “Properties” and PropertyIndex is set to “Rating”. This is the equivalent of showing Song.Properties[“Rating”] in a column.
When the program is run, the DataGridView works exactly as expected. Rows and be added, deleted and edited. Any changes to the Artist and Title columns are saved to Song table, but changes to the other columns are saved to the Attributes table. As values are put in the cells in the Category, Score and Date columns, records are added to the Attributes table.
What we have so far is great, but if we tried to sort any of the SubProperty columns, they would not sort. Sorting doesn’t work because the default sorting routine built into the DataGridView doesn’t know how to sort the data that we’ve put into the cells.
Since we are dealing with bound data we cannot use programmatic sorting. This would have been the easiest route to take we could call the sort method on the grid and pass in a specialized IComparer class that knows how to access the SubProperty data. Unfortunately, the DataGridView doesn’t allow this when using bound data. If we try it, we’ll get a runtime exception like the one pictured on the left.
Another option would be to put the sorting logic into a specialized collection class. This gets complicated quickly and while it is possible, it limits how we can use our objects by limiting the collections that can can work with. I really want the sorting to work no matter what kind of collection my objects are in. I want this so I can continue to use something like Linq where I can easily call up a collection of my objects.
In parts one and two we’ve been working with a small sample database and some Linq-To-SQL classes. We were able to add the sub property functionality to one of the Linq-To-SQL classes by simply editing the partial class part of the generated class and adding a SubPropertyAccessor to the class as a field. All of the functionality is contained in the SubPropertyAccessor and is added to the business class with a few lines of code. I want this same ease of use for the sorting functionality. The best way to do this is to use a TypeDDescriptionProvider.
A type description provider is what provides the information about a type when reflection is used against that type. If we create our own TypeDescriptionProvider, we can make is seem as though all of our sub-properties are actual properties of our type. If our sub-properties appear as real properties of our type, the DataGridView should have no trouble sorting the column itself.
Our SubPropertyTypeDescriptionProvider doesn’t do anything more than override GetTypeDescriptor to return a SubPropertyTypeDescriptor. It is in SubPropertyTypeDescriptor where the work will be done.
Our SubPropertyTypeDescriptor will need to return the list of actual properties as well as a list of our custom properties.
The SubPropertyTypeDescriptor overrides the GetProperties method to return a list of properties that includes the properties that normally would be returned as well as a list of custom properties. In order for it to work, it requires that the class we are working with implement the ISubProperty interface.
The ISubProperty interface requires that the class we are working with have a property that is called “Properties” of the type SubPropertiesAccessor<T>.
When we were working in the Cell and Column overrides, we could allow the SubPropertyAccessor be named anything and it could be specified at design time as a property on the column. The TypeDescriptionProvider will be applied to the business class as an attribute on the class. There is no way to tell the TypeDescriptionProvider what the SubPropertyAccessor will be called.
We can however make our TypeDescriptionProvider a generic class. By doing that, we can specify what classes and interfaces it will work with by putting a constraint on the Type the generic class can work with. Our constraint, which passes down from The TypeDescriptionProvider to the TypeDescriptor requires the ISubProperty interface and that interface tells us that the SubPropertyAccessor is always going to be named “Properties”.
Having this constraint does, as its name implies, put a constraint on our classes, but I couldn’t figure out how to do sorting without it. Also, I think the benefits that the custom TypeDescriptor provide out weigh the cost of the constraint.
When the type descriptor has an object instance, it’s easy to see that it accesses the SubPropertyAccessor to figure out what the custom properties are, but how does it do it when there is no instance object?
The answer is in the SubPropertyAccessor itself. It now contains a static Dictionary of all of the custom properties that are created for any particular type. Since the SubPropertyAccessor is a generic class each type that is used with it actually creates a unique type. This means that there will be a single static Dictionary for each unique type.
The dictionary of custom properties get populated whenever a new instance of a SubPropertyAccessor is created.
It also gets updated whenever a new custom property is added.
Let’s take a look at what our business class looks like now.
We’ve had to make a few changes to the class, but they are negligible compared to the functionality we will get.
With the exception of the Song class itself all of the code we have written is generic and can be used over and over by making the changes that we’ve made in the Song class. The changes to the Song class amount to 46 lines and that includes verbose commenting.
One change is the addition of the TypeDescriptionProvider attribute on line 4. This tells the framework to use our SubPropertyTypeDescriptionProvider when using reflection on our Song class.
The Song class now implements the ISubProperty<T> interface as indicated on line 5. Our SubPropertyAccessor was already named “Properties”, so we didn’t have to make any changes there.
Finally, at line 14, we construct the SubPropertyAccessor as soon as the data is loaded. This will populate the static CustomProperties dictionary on the SubPropertyAccessor as soon as possible.
Now when we try to sort our columns, everything should just work.
Here we can see that the dates are sorted and sorted correctly by date rather than alphabetically. All of this sorting was handled by the grid itself. All we did was provide a way for the grid to learn about our virtual properties.
Bonus – Dynamic Runtime Properties
I did cheat a little. In the constructor for the example form, I pre-loaded the static Dictionary of custom properties on the SubPropertyAccessor with two properties that I knew about beforehand.
But what happens if we want to add a property at run-time? The same syntax can be used.
I added a few controls to the form that allow me to add a property at run time. A TextBox allows me to specify the name of the property, a dropdown lets me choose the type and a button creates the property.
Most of the click handler for the button is concerned with getting a Type from the ComboBox. The important stuff happens in lines 44 through 46. On line 44 we add the CustomProperty to the SubPropertyAccessor. Line 45 adds a column to the DataGridView to show the new CustomProperty. Finally, on line 46, the DataGridView is reloaded. This forces the grid to re-run the reflection on the bound data so that it will discover the new “Property” that was added.
There’s a lot of code that goes into presenting child data as part of a master object. Fortunately, most of the code is done in generic classes. This allows us to take advantage of this functionality without having to go through this exercise every time.
The file attached to this article contains all of the source code for all of the classes I presented and the demo program. The code changes significantly from part 1 and part 2. The file attached to this part (part 3) contains all of the functionality discussed in all three parts.
Source Code: SubPropertyColumns.zip (220 KB)