65.9K
CodeProject 正在变化。 阅读更多。
Home

CIMTool 用于 Windows Management Instrumentation - 第 3 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (13投票s)

2013年2月21日

CPOL

8分钟阅读

viewsIcon

54294

downloadIcon

1934

WMI代码生成的初步尝试,以及一些用于WPF DataGrid的魔术

构建说明

Harlinn.CIMTool-2013-03-30-01.zip

此文件包含所需的程序集,因此从源代码构建应与在 Visual Studio 2012 中打开项目并按 F5 一样简单。

Harlinn.CIMTool-2013-03-30-01-noexe.zip

此文件要求您在计算机上安装以下软件

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 日 - 根据节点类型启用和禁用上下文菜单项。

© . All rights reserved.