Introduction
In CIMTool for Windows Management Instrumentation - Part 1[^] we had a quick introduction to Windows Management Instrumentation (WMI), and we had a look at how browsing the WMI namespaces and classes can be implemented.
Part 3 is available here: CIMTool for Windows Management Instrumentation - Part 3[^]
In this article we will be looking at how CIMTool uses standard .Net mechanism like ICustomTypeDescriptor and custom PropertyDescriptor classes to trick .Net into exposing dynamic information as bindable properties.
Since CIMTool enables us to query WMI, we need a mechanism that allows us to display the results of those queries.
I’d like to do something as simple as this:
ObjectQuery objectQuery = new ObjectQuery(query);
ManagementObjectSearcher searcher = new ManagementObjectSearcher(managementScope, objectQuery, enumerationOptions);
using (searcher)
{
ManagementObjectCollection objectCollection = searcher.Get();
using (objectCollection)
{
ManagementObjectCollectionWrapper results = new ManagementObjectCollectionWrapper(objectCollection);
bindingSource.DataSource = results;
gridView.RefreshData();
}
}
And I would expect the UI for the results to look something like this:
ManagementObjectSearcher object searcher
gives us the results as a ManagementObjectCollection, which contains a ManagementBaseObject for each row in the result set, and while those ManagementBaseObjects’ contains the information we want to display, they certainly doesn’t expose that information as bindable .Net properties.
What we need is a mechanism to customize the information .Net uses to bind our result set to the rows and columns displayed by the grid. Luckily .Net relies on special types from the System.ComponentModel namespace to provide user interface components with information about the objects to which it is bound.
Before accessing the metadata to get property information, TypeDescriptor first checks to see if the type being examined implements the System.ComponentModel.ICustomTypeDescriptor interface. When it does the TypeDescriptor will ask the object, by calling the ICustomTypeDescriptor.GetProperties method, which properties it supports. This allows the object to provide .Net with a PropertyDescriptorCollection containing the properties it wants to expose to the user interface.
For our purpose this means that we can trick the user interface components into binding against properties that our object actually does not have, and to do that we need to create a PropertyDescriptor for each column we want displayed - in this case, one for each PropertyData element in the PropertyDataCollection exposed by the Properties property of the ManagementBaseObject representing a row in the result set.
We also need to create a set of wrapper classes around the classes in the System.Management namespace, since we need to implement ICustomTypeDescriptor for those classes that will provide custom properties. The wrapper class for ManagementBaseObject is named ManagementBaseObjectWrapper – which kind of describes the purpose of the class.
ManagementBaseObjectPropertyDescriptor
To create a PropertyDescriptor for each PropertyData element, we first need to create a class that derives from PropertyDescriptor.
public class ManagementBaseObjectPropertyDescriptor : PropertyDescriptor
{
private ManagementBaseObjectWrapper objectWrapper;
PropertyDataWrapper property;
public ManagementBaseObjectPropertyDescriptor(ManagementBaseObjectWrapper objectWrapper,
PropertyDataWrapper property)
: base(property.Name, null)
{
this.objectWrapper = objectWrapper;
this.property = property;
}
The constructor takes two arguments, a ManagementBaseObjectWrapper representing a row in the result set, and a PropertyDataWrapper for the property we want to display as a column in the grid.
public override bool Equals(object obj)
{
ManagementBaseObjectPropertyDescriptor other = obj as ManagementBaseObjectPropertyDescriptor;
return other != null && other.property.Equals(property);
}
The Equals method compares the stored PropertyDataWrapper objects rather than the ManagementBaseObjectPropertyDescriptor, and the PropertyDataWrapper calls the Equals method of the PropertyData it wraps.
public override int GetHashCode()
{
return property.GetHashCode();
}
Since any type that overrides Object.Equals should also override Object.GetHashCode, we do so here.
public override string DisplayName
{
get
{
return property.Name;
}
}
By overriding the DisplayName property we’re able to control the text that will be displayed as the column heading.
public override string Description
{
get
{
return string.Empty;
}
}
public override AttributeCollection Attributes
{
get
{
return new AttributeCollection(null);
}
}
public override bool CanResetValue(object component)
{
return false;
}
public override Type ComponentType
{
get
{
return objectWrapper.GetType();
}
}
public override bool ShouldSerializeValue(object component)
{
return true;
}
public override void ResetValue(object component)
{
}
public override void SetValue(object component, object value)
{
((ManagementBaseObjectWrapper)component)[property.Name] = value;
}
public override object GetValue(object component)
{
return ((ManagementBaseObjectWrapper)component)[property.Name];
}
SetValue and GetValue allow us to set and retrieve the data displayed for a column in a row. Note that the IsReadOnly property always return true. If you want to actually edit the data, you need to return false for properties with a write qualifier set to true.
public override bool IsReadOnly
{
get
{
return true;
}
}
public override Type PropertyType
{
get
{
return property.GetDotNetType();
}
}
}
PropertyType provides .Net with the Type representing the property.
ManagementBaseObjectWrapper
ManagementBaseObjectWrapper implements ICustomTypeDescriptor – and it also exposes the same functionality as System.Management.ManagementBaseObject, but it does so in terms of other wrapper classes.
Most of the methods mandated by the ICustomTypeDescriptor interface can be implemented by calling static methods exposed by TypeDescriptor for just that purpose, like this:
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
return TypeDescriptor.GetAttributes(this, true);
}
We only need to do something differently when we need to provide information about the properties we want to expose for the ManagementBaseObjectWrapper object:
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
return GetProperties();
}
The GetProperties() method simply iterates over the collection of PropertyDataWrapper objects, calling the virtual method GetPropertyDescriptor for each element.
PropertyDescriptorCollection propertyDescriptorCollection;
public PropertyDescriptorCollection GetProperties()
{
if (propertyDescriptorCollection == null)
{
propertyDescriptorCollection = new PropertyDescriptorCollection(null);
for (int i = 0; i < Properties.Count; i++)
{
PropertyDataWrapper property = Properties[i];
if (propertyDescriptorCollection.Find(property.Name, true) == null)
{
PropertyDescriptor propertyDescriptor = GetPropertyDescriptor(property);
propertyDescriptorCollection.Add(propertyDescriptor);
}
}
}
return propertyDescriptorCollection;
}
I’m sure you’ve noticed that the resulting PropertyDescriptorCollection is cached for future calls so that this method then just returns the cached collection rather than recomputing it.
And the GetPropertyDescriptor method just creates a ManagementBaseObjectPropertyDescriptor object for the PropertyDataWrapper object:
protected virtual PropertyDescriptor GetPropertyDescriptor(PropertyDataWrapper property)
{
ManagementBaseObjectPropertyDescriptor result = new ManagementBaseObjectPropertyDescriptor(this, property);
return result;
}
Displaying the data in other controls
Since CIMTools displays information about selected nodes in the Properties pane, it seems to be somewhat natural that it should do so for selected rows in the grid. Sometimes there are far more columns than can easily fit into the displayed portion of the grid, so we’ll have the Properties pane display the column data too.
Since the grid is bound to a BindingSource object, it’s easy to detect when the user navigates from one row to the next – we just need to attach an event handler to the CurrentChanged event:
private void bindingSource_CurrentChanged(object sender, EventArgs e)
{
try
{
MainForm.Instance.SelectedObject = bindingSource.Current;
}
catch (Exception)
{ }
}
In the above code Instance is a static property of the MainForm class, and it returns the current instance of the MainForm. MainForm exposes the SelectedObject property of the PropertyGridControl, so implementing this functionality was pretty simple, and shows that the PropertyGridControl works very well with our ICustomTypeDescriptor implementation:
Remarks
Working with the ICustomTypeDescriptor interface is pretty easy, and implementing it is an excellent choice when you need to display information that is not known at compile time.
History
- 3. of February, 2013 - Initial posting