Build instructions
Harlinn.CIMTool-2013-03-30-01.zip
This file includes the required assemblies, so building from the source should be as easy as opening the project in Visual Studio 2012 and hitting F5.
Harlinn.CIMTool-2013-03-30-01-noexe.zip
This file requires that you have the following installed on your computer:
Graph#, AvalonEdit and AvalonDock are available through the nuget package manager, so you may install them using nuget, while the source for the WPF Property Grid is included with the download.
Introduction
The Common Information Model (CIM) Metamodel, is based on the Unified Modeling Language: Superstructure specification[^]. CIM schemas represent object-oriented models that can be used to represent the resources of a managed system, including their attributes, behaviors, and relationships. The CIM Metamodel includes expressions for common elements that must be clearly presented to management applications.
The CIM Schema published by DMTF[^] is an example of a particular schema that conforms to the CIM Metamodel. The model abstracts and describes a "managed environment" using an object oriented paradigm. The CIM object schema addresses systems, devices, application deployment and the physical environment. Windows Management Instrumentation (WMI) is an implementation, with a supporting infrastructure, of the Common Information Model.
CIMTool was created to provide an effective tool for understanding and browsing the information available through the Windows Management Instrumentation API.
So far CIMTool has turned out to be a useful utility, but there are still essential features that are missing, first among them a decent code generator. While .Net ships with the mgmtclassgen tool, the generated output doesn’t reflect the object oriented nature of CIM and Windows Management Instrumentation.
This is the third article about CIMTool, an alternative to Microsoft’s WMI CIM Studio:
WMI provides a rich set of meta-data that describes the available classes, and how they are related to each other by inheritance, containment and reference. As a developer you would naturally expect that a code generator would reflect this in it’s output, but surprisingly it turns out that the mgmtclassgen tool flattens the inheritance tree, and merges everything into a single class. This is perhaps fine if a single class is all you’re going to work with, but once you start working with several classes, the overhead becomes tremendous.
Take Win32_BaseService which has two descendants: Win32_Service and Win32_SystemDriver. When you use the classes generated by mgmtclassgen you will not be able to something like this:
void HandleBaseService(Win32_BaseService theBaseService)
{
if(theBaseService is Win32_Service)
{
Win32_Service theService = (Win32_Service)theBaseService;
}
}
This is because the classes generated by mgmtclassgen are not related to each other; they are each derived directly from System.ComponentModel.Component.
Below you have the class browser for CIMTool and WMI CIM Studio side by side:
While WMI CIM Studio can, obviously, be used to learn a lot about WMI; using it gets tedious when you don’t know the inheritance hierarchy of a class, such as Win32_Service, and it’s impossible to view meta-data and object data at the same time – which is why CIMTool was born.
Win32_BaseService has a property called AcceptPause:
By looking at the properties for the property definition we learn that AcceptPause is a boolean property, and that it's neither an array nor a key. I could admittedly have learned that from the Win32_BaseService documentation[^], but then I'd have to trust the documentation - which isn't allways accurate.
Take the ServiceType property of the Win32_BaseService; according to the documentation it's an unsigned byte, but that turns out to be wrong, it's a string:
This property also have a ValueMap, but no Values - which is mildly surprising, and not what I would have expected after reading the documentation[^].
So, if you want to write a code generator for WMI, you need to be prepared to handle a few surprises.
The CodeDom
The CodeDOM is an integral part of the .Net framework where it’s used to represent source code documents in a language-independent manner. The CodeDOM is an object graphs that can be used to generate source code and compiled assemblies. The System.CodeDom namespaces contains nearly 90 classes and enums that are supposed to represent the syntax of a typical programming language such as C# and Visual Basic.
Since the code generator inside mgmtclassgen was built using the capabilities of the classes in the System.CodeDom namespace, I thought it would be practical to implement the code generation for CIMTool using the CodeDom.
So, to generate the code required for a property with a setter and a getter:
public int SslFlags
{
get
{
return this.GetInt32("SslFlags");
}
set
{
this.Write("SslFlags", value);
}
}
You have to write something like this:
CodeMemberProperty property = new CodeMemberProperty();
property.Name = Name;
property.Type = new CodeTypeReference(GetPropertyType());
property.Attributes = MemberAttributes.Public | MemberAttributes.Final;
if (PropertyData.Read)
{
property.GetStatements.Add(new CodeMethodReturnStatement(
new CodeMethodInvokeExpression(
new CodeMethodReferenceExpression(
new CodeThisReferenceExpression(), GetGetterFunctionName()),
new CodePrimitiveExpression(Key))));
}
if (PropertyData.Write)
{
property.SetStatements.Add(
new CodeMethodInvokeExpression(
new CodeMethodReferenceExpression(
new CodeThisReferenceExpression(), GetSetterFunctionName()),
new CodePrimitiveExpression(Key), new CodeVariableReferenceExpression("value")));
}
Class.CodeTypeDeclaration.Members.Add(property);
Which, if nothing else, clearly illustrates the value of the services provided by the C# compiler.
I think Ray Gilbert got it right in his article CodeDom Assistant[^] - It's like cutting the grass with a pair of scissors or unloading a truck load of sand with a spoon. It's long winded, it can be done, but it is tedious.
Before we can generate any code we need to have some idea about the structure of the elements we want to generate code for:
When you select generate code from the popup menu:
We end up executing the GenerateCodeMenuItem_Click method:
private void GenerateCodeMenuItem_Click(object sender, RoutedEventArgs e)
{
ManagementScopeWrapper theManagementScope = null;
if (Data.SelectedItem is ClassNode)
{
ClassNode classNode = (ClassNode)Data.SelectedItem;
theManagementScope = classNode.Parent.ManagementScope;
}
else if (Data.SelectedItem is ClassesNode)
{
ClassesNode classesNode = (ClassesNode)Data.SelectedItem;
theManagementScope = classesNode.ManagementScope;
}
else if (Data.SelectedItem is NamespaceNode)
{
NamespaceNode namespaceNode = (NamespaceNode)Data.SelectedItem;
theManagementScope = namespaceNode.ManagementScope;
}
The first thing that happens is that the program locates the WMI namespace, which is also called a management scope.
if (theManagementScope != null)
{
GeneratorOptions generatorOptions = new GeneratorOptions();
generatorOptions.RootManagementNamespace = "Harlinn.CIMTool.Generated.Mgmt";
generatorOptions.ManagementNamespace = theManagementScope.Name;
generatorOptions.ManagementPrefix = "IIs";
generatorOptions.RootSerializationNamespace = "Harlinn.CIMTool.Generated.Mgmt." +
theManagementScope.Name;
generatorOptions.SerializationNamespace = "Types";
generatorOptions.SerializationPrefix = "IIsS";
If the program is able to locate a scope, it will use that inforamtion to populate a GeneratorOptions
object with a set of default values
GeneratorOptionsWindow generatorOptionsWindow = new GeneratorOptionsWindow();
generatorOptionsWindow.DataContext = generatorOptions;
bool? result = generatorOptionsWindow.ShowDialog();
These options are then passed to a GeneratorOptionsWindow
window, which allows you to alter the default to something more appropriate for your project.
if (true == result)
{
CodeProvider codeProvider = new CodeProvider(CodeLanguage.CSharp, generatorOptions);
using (codeProvider)
{
Namespace nameSpace = new Namespace(codeProvider, theManagementScope);
Namespace
represents the management scope containing the WMI classes we're going to generate C# code for.
nameSpace.GenerateManagementCode();
The GenerateManagementCode()
method populates the CodeDom, and once we have a populated CodeDom we're ready to generate the code.
System.IO.MemoryStream memoryStream = new System.IO.MemoryStream();
using (memoryStream)
{
System.IO.TextWriter textWriter = new System.IO.StreamWriter(memoryStream);
using (textWriter)
{
System.CodeDom.Compiler.CodeGeneratorOptions codeGeneratorOptions =
new System.CodeDom.Compiler.CodeGeneratorOptions();
codeGeneratorOptions.BracingStyle = "C";
codeProvider.CodeDomProvider
.GenerateCodeFromNamespace(nameSpace.ManagementCodeNamespace,
textWriter, codeGeneratorOptions);
textWriter.Flush();
GenerateCodeFromNamespace
writes the C# code to the
TextWriter
, and all that's left is to load it into the editor:
LayoutDocument layoutDocument = new LayoutDocument();
layoutDocument.Title = "Scope: " + theManagementScope.Name;
EditorControl editorControl = new EditorControl();
memoryStream.Seek(0, System.IO.SeekOrigin.Begin);
editorControl.LoadFromSream(memoryStream);
editorControl.VerticalAlignment =
System.Windows.VerticalAlignment.Stretch;
editorControl.HorizontalAlignment =
System.Windows.HorizontalAlignment.Stretch;
layoutDocument.Content = editorControl;
layoutDocumentPane.Children.Add(layoutDocument);
}
}
}
}
}
}
Apart from giving birds view of the procedure behind the code generation in CIMTool, the above code shows a simple and efficient way to dynamically add a WPF UserControl as a ‘document’ in AvalonDock.
How to create DataGrid columns dynamically
Speaking of UserControl’s, let’s have a look at the QueryControl.xaml file:
<UserControl x:Class="Harlinn.CIMTool.Wpf.Controls.QueryControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="500">
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Margin="5" >Scope</TextBlock>
<TextBox Grid.Column="1" Margin="5"
Text="{Binding Mode=OneWay, Path=Scope.Name}"></TextBox>
<Button Grid.Column="2"
x:Name="ExecuteButton"
Margin="15,5,8,5"
Padding="8,4,8,4"
Click="ExecuteButton_Click">Execute</Button>
</Grid>
<TextBox Grid.Row="1"
FontFamily="Lucida Console"
AcceptsReturn="True"
VerticalScrollBarVisibility="Visible"
Text="{Binding Path=QueryString}" />
</Grid>
<GridSplitter Grid.Row="0"
ResizeDirection="Rows" Height="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"/>
<Grid Grid.Row="1">
<DataGrid x:Name="dataGrid"></DataGrid>
</Grid>
</Grid>
</UserControl>
What I want you to note is the declaration of the DataGrid, no column definitions, and no bindings – and we definitely want to display some data:
What if I could show you how to populate the DataGrid using one, just one, line of code? Using a mechanism that has been a part of Windows Presentation Foundation since its initial release? Well, here goes:
dataGrid.ItemsSource = new BindingListCollectionView(objectCollection);
BindingListCollectionView is the gem that perhaps got lost. Personally I think the designers of WPF intended us to use classes implementing the System.ComponentModel.ICollectionView as the primary data source for collections - not ObservableCollection, at least not directly. Apparently many developers have skipped reading up on this and the other classes found in the System.Windows.Data namespace. I say apparently because a quick google for BindingListCollectionView gave me 7700 hits, while a search for ObservableCollection gave me 335000 hits.
The documentation for CollectionView says:
In WPF applications, all collections have an associated default collection view. Rather than working with the collection directly, the binding engine always accesses the collection through the associated view. To get the default view, use the CollectionViewSource.GetDefaultView method. CollectionView is the default view for collections that implement only IEnumerable. ListCollectionView is the default view for collections that implement IList. BindingListCollectionView is the default view for collections that implement IBindingListView or IBindingList.
What's missing here is that BindingListCollectionView also provides additional handling for classes that implement the ITypedList[^] interface. As far as I've been able to determine BindingListCollectionView provides full support for the standard mechanisms that facilitates rappid application development in Windows Forms applications, including ICustomTypeDescriptor[^]. This turns BindingListCollectionView into an extremely powerful and flexible class.
About the generated code
The code generated for a given WMI class, will typically look something like this:
[System.ComponentModel.DisplayNameAttribute("HandlersSection")]
public class IIsHandlersSection : IIsConfigurationSectionWithCollection
{
Since the WMI class HandlersSection is derived from a WMI class called ConfigurationSectionWithCollection, the code generator generates code that reflects this.
The code generator creates a class factory for the classes in a WMI namespace, in this case IIsWebAdministrationClassFactory
class. This design ensures that an object of the correct class is created for a given instance of a WMI class, even when creating objects for a collection of potential base classes.
public BindingListEx<IIsHandlerAction> Handlers
{
get
{
System.Management.ManagementObject[] managementObjects = GetObjects("Handlers");
BindingListEx<IIsHandlerAction> elements = new BindingListEx<IIsHandlerAction>();
foreach (System.Management.ManagementObject managementObject in managementObjects)
{
IIsHandlerAction element = (IIsHandlerAction)
IIsWebAdministrationClassFactory.CreateObject(managementObject, false);
elements.Add(element);
}
return elements;
}
}
The constructor takes an instance of an existing System.Management.ManagementObject object, and an optional boolean value which is used to specify that when Dispose is called on the object, it should dispose the System.Management.ManagementObject object.
public IIsHandlersSection(ManagementObject theManagementObject) :
base(theManagementObject)
{
}
public IIsHandlersSection(ManagementObject theManagementObject, bool disposeManagementObject) :
base(theManagementObject, disposeManagementObject)
{
}
Access to the properties of the WMI object is done in the usual manner:
[System.ComponentModel.DisplayNameAttribute("AccessPolicy")]
public System.Nullable<int> AccessPolicy
{
get
{
return this.GetNullableInt32("AccessPolicy");
}
set
{
this.WriteNullable("AccessPolicy", value);
}
}
}
When possible, the code genrator creates enums integer properties with Values and ValueMap qualifiers:
public enum IIsSCIMErrorPerceivedSeverityValues
{
Unknown = 0,
Other = 1,
Information = 2,
DegradedOrWarning = 3,
Minor = 4,
Major = 5,
Critical = 6,
FatalOrNonRecoverable = 7,
}
When generating code for the WebAdministration namespace the generator creates a file containing 23457 lines of C# code - so COMTool can potentially do a lot of work for you.
There is still a lot of work to be done, but I think that CIMTool is moving in the right direction.
D3.js crash course - Part2[^] shows one way to use the generated code.
By the way, a bit of positive feedback would certainly be both motivating and appreciated, there is no need to be shy about it
History
-
21. of February, 2013 - Initial posting.
-
22. of February, 2013 - User interface Improvements.
Improved view of class properties:
Improved view of property properties:
-
23. of February, 2013 - A few bugfixes - I've mostly tested the code generator against the WebAdministration namespace, and several bugs surfaced when I tried generating code for the CIMV2 namespace. The code generated for the the CIMV2 namespace will now compile.
-
25. of February, 2013 - Added namespace visualization
-
2. of March, 2013 - improved property name generation, added generation of serializable objects representing data retrieved from WMI.
-
14. of March, 2013 - Changed the color for property names in the property grid to white, as this is much more readable.
-
14. of March, 2013 - Enabling and disabling items on the context menu depending on the node type.