CIMTool 用于 Windows Management Instrumentation - 第 3 部分






4.72/5 (13投票s)
WMI代码生成的初步尝试,以及一些用于WPF DataGrid的魔术
构建说明
Harlinn.CIMTool-2013-03-30-01.zip
此文件包含所需的程序集,因此从源代码构建应与在 Visual Studio 2012 中打开项目并按 F5 一样简单。
Harlinn.CIMTool-2013-03-30-01-noexe.zip
此文件要求您在计算机上安装以下软件
- AvalonEdit[^]
- AvalonDock 2.0[^]
- WPF 属性网格[^]
- Graph#[^]
Graph#、AvalonEdit 和 AvalonDock 可通过 nuget 包管理器获得,因此您可以使用 nuget 安装它们,而 WPF 属性网格的源代码包含在下载文件中。

引言
通用信息模型 (CIM) 元模型基于统一建模语言:Superstructure specification[^]。CIM 模式代表面向对象的模型,可用于表示托管系统的资源,包括它们的属性、行为和关系。CIM 元模型包含必须向管理应用程序清晰呈现的通用元素的表达式。
DMTF[^]发布的 CIM 模式是符合 CIM 元模型的特定模式的一个示例。该模型使用面向对象的范例抽象并描述“托管环境”。CIM 对象模式涉及系统、设备、应用程序部署和物理环境。Windows Management Instrumentation (WMI) 是通用信息模型的一种实现,并具有支持基础设施。
CIMTool 的创建旨在提供一个有效的工具来理解和浏览通过 Windows Management Instrumentation API 可用的信息。
到目前为止,CIMTool 已被证明是一个有用的实用程序,但仍缺少关键功能,其中最主要的是一个像样的代码生成器。虽然 .Net 提供了 mgmtclassgen 工具,但生成的输出并不反映 CIM 和 Windows Management Instrumentation 的面向对象特性。

这是关于 CIMTool 的第三篇文章,它是 Microsoft WMI CIM Studio 的替代品。

WMI 提供了丰富的元数据集,描述了可用的类以及它们如何通过继承、包含和引用相互关联。作为开发人员,您自然会期望代码生成器在其输出中反映这一点,但令人惊讶的是,mgmtclassgen 工具会展平继承树,并将所有内容合并到一个类中。也许如果您只处理一个类,这就可以了,但一旦您开始处理多个类,开销就会变得巨大。
以 Win32_BaseService 为例,它有两个子类:Win32_Service 和 Win32_SystemDriver。当您使用 mgmtclassgen 生成的类时,您将无法执行类似这样的操作
void HandleBaseService(Win32_BaseService theBaseService) { if(theBaseService is Win32_Service) { Win32_Service theService = (Win32_Service)theBaseService; } }
这是因为 mgmtclassgen 生成的类之间没有关联;它们都直接派生自 System.ComponentModel.Component。
下面是 CIMTool 和 WMI CIM Studio 的类浏览器并排对比。
![]() |
![]() |
虽然 WMI CIM Studio 显然可以用来了解很多关于 WMI 的知识;当您不知道类的继承层次结构(例如 Win32_Service)时,使用它会很乏味,并且无法同时查看元数据和对象数据——这就是 CIMTool 诞生的原因。
Win32_BaseService 有一个名为 AcceptPause 的属性

通过查看属性定义的属性,我们了解到 AcceptPause 是一个布尔属性,它既不是数组也不是键。我承认我可以从 Win32_BaseService 文档[^] 中了解到这一点,但那样我就必须相信文档——这并不总是准确的。
以 Win32_BaseService 的 ServiceType 属性为例;根据文档,它是一个无符号字节,但事实证明这是错误的,它是一个字符串。

此属性也有一个 ValueMap,但没有 Values——这有点令人惊讶,而且与我阅读 文档[^] 后预期的不符。
所以,如果您想编写 WMI 的代码生成器,您需要准备好应对一些意外情况。
CodeDom
CodeDom 是 .Net 框架的一个组成部分,它用于以语言无关的方式表示源代码文档。CodeDom 是一个对象图,可用于生成源代码和已编译的程序集。System.CodeDom 命名空间包含近 90 个类和枚举,它们用于表示 C# 和 Visual Basic 等典型编程语言的语法。
由于 mgmtclassgen 中的代码生成器是使用 System.CodeDom 命名空间中的类实现的,因此我认为使用 CodeDom 来实现 CIMTool 的代码生成会很实用。
因此,要生成带有 setter 和 getter 的属性所需的代码
public int SslFlags { get { return this.GetInt32("SslFlags"); } set { this.Write("SslFlags", value); } }
您必须编写类似这样的代码
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);
这至少清楚地说明了 C# 编译器提供的服务的价值。
我认为 Ray Gilbert 在他的文章 CodeDom Assistant[^] 中说得很对——这就像用一把剪刀割草,或者用一把勺子卸下一卡车的沙子。虽然可以做到,但过程冗长而乏味。
在生成任何代码之前,我们需要对要生成代码的元素的结构有一定的了解。

当您从弹出菜单中选择生成代码时

我们最终会执行 GenerateCodeMenuItem_Click 方法。
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; }
首先,程序会找到 WMI 命名空间,该命名空间也称为管理范围。
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";
如果程序能够找到一个范围,它将使用该信息用一组默认值填充 GeneratorOptions
对象。
GeneratorOptionsWindow generatorOptionsWindow = new GeneratorOptionsWindow(); generatorOptionsWindow.DataContext = generatorOptions; bool? result = generatorOptionsWindow.ShowDialog();
然后,这些选项将被传递给 GeneratorOptionsWindow
窗口,该窗口允许您修改默认设置,使其更适合您的项目。

if (true == result) { CodeProvider codeProvider = new CodeProvider(CodeLanguage.CSharp, generatorOptions); using (codeProvider) { Namespace nameSpace = new Namespace(codeProvider, theManagementScope);
Namespace
表示我们将为其生成 C# 代码的 WMI 类所在的管理范围。
nameSpace.GenerateManagementCode();
GenerateManagementCode()
方法用于填充 CodeDom,一旦我们有了已填充的 CodeDom,就可以生成代码了。
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
将 C# 代码写入 TextWriter
,剩下的就是将其加载到编辑器中。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); } } } } } }
除了提供 CIMTool 代码生成过程的鸟瞰图之外,上面的代码还展示了一种简单有效的方法,可以将 WPF UserControl 动态添加为 AvalonDock 中的“文档”。
如何动态创建 DataGrid 列
说到 UserControl,让我们看看 QueryControl.xaml 文件。
<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>
我想让您注意的是 DataGrid 的声明,没有列定义,也没有绑定——而我们肯定想显示一些数据。

如果我能向您展示如何使用一行代码填充 DataGrid 呢?使用一种自 WPF 最初发布以来就一直存在于 WPF 中的机制?好吧,开始了。
dataGrid.ItemsSource = new BindingListCollectionView(objectCollection);
BindingListCollectionView 可能是被遗忘的宝藏。我个人认为 WPF 的设计者 intended 我们使用实现 System.ComponentModel.ICollectionView 的类作为集合的主要数据源——而不是 ObservableCollection,至少不是直接使用。显然,许多开发人员跳过了阅读这方面以及 System.Windows.Data 命名空间中的其他类。我之所以这么说,是因为快速搜索 BindingListCollectionView 得到了 7700 个结果,而搜索 ObservableCollection 得到了 335000 个结果。
CollectionView 的文档说:
在 WPF 应用程序中,所有集合都有一个关联的默认集合视图。绑定引擎不是直接处理集合,而是始终通过关联视图访问集合。要获取默认视图,请使用 CollectionViewSource.GetDefaultView 方法。CollectionView 是实现仅 IEnumerable 的集合的默认视图。ListCollectionView 是实现 IList 的集合的默认视图。BindingListCollectionView 是实现 IBindingListView 或 IBindingList 的集合的默认视图。
这里缺少的是 BindingListCollectionView 还为实现 ITypedList[^] 接口的类提供了额外的处理。据我所能确定的,BindingListNewCollection 提供了对标准机制的全面支持,这些机制促进了 Windows Forms 应用程序中的快速应用程序开发,包括 ICustomTypeDescriptor[^]。这使得 BindingListCollectionView 成为一个极其强大和灵活的类。
关于生成的代码
为给定的 WMI 类生成的代码通常看起来像这样:
[System.ComponentModel.DisplayNameAttribute("HandlersSection")] public class IIsHandlersSection : IIsConfigurationSectionWithCollection {
由于 WMI 类 HandlersSection 派生自一个名为 ConfigurationSectionWithCollection 的 WMI 类,因此代码生成器会生成反映这一点的代码。
代码生成器为 WMI 命名空间中的类创建一个类工厂,在本例中是 IIsWebAdministrationClassFactory
类。这种设计确保为给定的 WMI 类实例创建正确的类对象,即使在创建潜在基类集合的对象时也是如此。
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; } }
构造函数接受一个现有的 System.Management.ManagementObject 对象实例和一个可选的布尔值,该值用于指定当调用该对象的 Dispose 时,它应该 Dispose System.Management.ManagementObject 对象。
public IIsHandlersSection(ManagementObject theManagementObject) : base(theManagementObject) { } public IIsHandlersSection(ManagementObject theManagementObject, bool disposeManagementObject) : base(theManagementObject, disposeManagementObject) { }
对 WMI 对象属性的访问以通常的方式进行。
[System.ComponentModel.DisplayNameAttribute("AccessPolicy")] public System.Nullable<int> AccessPolicy { get { return this.GetNullableInt32("AccessPolicy"); } set { this.WriteNullable("AccessPolicy", value); } } }
在可能的情况下,代码生成器会为带有 Values 和 ValueMap 限定符的枚举整数属性创建枚举。
public enum IIsSCIMErrorPerceivedSeverityValues { Unknown = 0, Other = 1, Information = 2, DegradedOrWarning = 3, Minor = 4, Major = 5, Critical = 6, FatalOrNonRecoverable = 7, }
在为 WebAdministration 命名空间生成代码时,生成器会创建一个包含 23457 行 C# 代码的文件,因此 COMTool 可能会为您完成大量工作。
仍然有很多工作要做,但我认为 CIMTool 正朝着正确的方向发展。
D3.js 速成课程 - 第二部分[^] 展示了一种使用生成代码的方法。
顺便说一句,一些积极的反馈肯定会具有激励作用并受到赞赏,不必对此有所保留。
历史
-
2013 年 2 月 21 日 - 初始发布。
-
2013 年 2 月 22 日 - 用户界面改进。
改进的类属性视图
改进的属性属性视图
-
2013 年 2 月 23 日 - 一些 bug 修复 - 我主要针对 WebAdministration 命名空间测试了代码生成器,并且在我尝试为 CIMV2 命名空间生成代码时出现了几个 bug。为 CIMV2 命名空间生成的代码现在可以编译。
-
2013 年 2 月 25 日 - 添加了命名空间可视化。
-
2013 年 3 月 2 日 - 改进了属性名称生成,添加了代表从 WMI 检索的数据的可序列化对象的生成。
-
2013 年 3 月 14 日 - 将属性网格中属性名称的颜色更改为白色,因为这样更易读。
-
2013 年 3 月 14 日 - 根据节点类型启用和禁用上下文菜单项。