Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / Visual Basic

Retrieving Information From Windows Management Instrumentation

Rate me:
Please Sign up or sign in to vote.
4.93/5 (39 votes)
15 Jul 2010CDDL15 min read 84.8K   7.4K   62   33
How to use WMI to get system information and present it to the user in an easy to understand format.

Introduction

This article and code is a grounding article for those who are unfamiliar with or not sure how to exploit Microsoft's wonderful world of Windows Management Instrumentation (WMI). The article explains how to use WMI to access information about the computer system and how to display this information to the user in a meaningful format.

Note: the article is not intended to be a fully detailed, in-depth discussion on the intricacies of WMI; such an article could quite easily fill a rather large book! For some good background information from Microsoft on WMI, see this MSDN article: http://msdn.microsoft.com/en-us/library/aa384642(v=VS.85).aspx.

Background

I was looking for some detailed information about my system that wasn't available through the normal programs and utilities that come bundled with Windows. OK, yes, I know that there are utilities on the Interweb that can display this sort of information; however, I thought how difficult can it be to write my own program? This article is basically a summary of my research into designing and programming SysInfo. The article deals with accessing and formatting WMI class property data and using background worker threads.

WMI - What is It?

This is from MSDN: "WMI is the Microsoft implementation of Web-based Enterprise Management (WBEM), which is an industry initiative to develop a standard technology for accessing management information in an enterprise environment. WMI uses the Common Information Model (CIM) industry standard to represent systems, applications, networks, devices, and other managed components." Source: http://msdn.microsoft.com/en-us/library/aa384642(v=VS.85).aspx.

So, there you have it in a nutshell! Are you any closer? Because, I wasn't. In very simple layman's terms, WMI is a series of classes which expose properties related to various aspects of a computer system's underlying hardware and software.

Inspiration

To try and understand WMI better, I decided to write my own program utilising WMI: and SysInfo is the result. The inspiration for the finished program came from two sources:

  1. A CodeProject article by Alireza Shirazi: GetHardwareInformation.aspx.
  2. Windows 7 System Information screen.

Alireza wrote an article on using WMI to get hardware information from the various WMI Win32 classes, and produced a program to display the results to the user. However, Alireza's program, while being very good, suffers from a number of drawbacks, which gave me the inspiration for my program and this article.

The first problem is that the data returned isn't formatted. This is Alireza's output for the WMI class Win32_Bios:

HardwareInfo.jpg

SysInfo improves on this by creating formatted output, as can be seen from this screenshot of the same WMI class:

HardwareInfo_1.jpg

As you can see, SysInfo formats the data into a much more readable form, even to the point of decoding and grouping various properties such as BIOS characteristics. In the screenshot above, this is the grouping 'Supported Bios Capabilities'.

Another problem with Alireza's program that SysInfo addresses is some WMI classes can return a very large amount of information which can lead to the GUI locking up until all the data is displayed; there was also no implementation to abort the data collection once it had started.

SysInfo overcomes these problems in three ways:

  1. We run our data collection routines on a background worker thread, which ensures that the GUI remains responsive, and also allows the data collection to be aborted;
  2. We forewarn the user that the operation on the selected classes could take a long time;
  3. We provide a Cancel button to cancel the search.

LongOperation.jpg

You will note from the previous screenshot that we also have an MSDN button. This button, when clicked, will display an MSDN web page in our own web browser which corresponds to the selected WMI class. This can be very useful for acquiring more information on the selected class.

Image 4

One other thing you will note from the GUI screenshot is that I based the design for the GUI on Windows 7.

So, having improved on Alireza's original program, let's see how this is all thrown together.

Using the Code

As stated at the beginning of this article, I wanted to be able to display meaningfully formatted information to the user. I decided the best way to do this would be to split the information into different pages which displayed related information regarding one aspect of the user's computer system. To do this, we use Visual Studio's User Control class. We design a customised user control for each page that displays information to the user. Within this class is the data collection routine responsible for collecting that page's data.

Before we start collecting the information, we first check to make sure we have administrative privileges, as we need these to access some WMI classes.

VB
' A function to return the Admin status of the user running our program.
Public Function IsAdmin() As Boolean

    IsAdmin = False
    Dim securityGroup As WindowsPrincipal

    ' Get the security credentials of the user that is running our program.
    AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
    securityGroup = CType(Thread.CurrentPrincipal, WindowsPrincipal)

    ' Are they a member of the Administrators Group?
    If securityGroup.IsInRole(WindowsBuiltInRole.Administrator) Then
        IsAdmin = True
    End If
    Return IsAdmin

End Function

We call this function before we do anything; if we're not an admin, we politely inform the user the program won't work.

VB
If Not Misc.IsAdmin Then
    MessageBox.Show("This program requires the user to have administrative " & _
       "privileges to function correctly. " & _
       vbNewLine & vbNewLine & _
       "Please logon as a member of the Administrators Group to run this program.", _
       "Administrative Privileges Required.", MessageBoxButtons.OK, _
       MessageBoxIcon.Stop, MessageBoxDefaultButton.Button1, _
       MessageBoxOptions.ServiceNotification)
    Me.Close()
Else
    ' Initialise user controls and collect the information.

So we can inform the user of the progress of the data collection, we use Visual Studio's splash screen functionality and update this screen with what data category we're currently collecting.

SplashScreen.jpg

Now, for those of you who are in the know, you will know that you can't directly change a property of a control on the splash screen from a different form. This is because the splash screen (which is a form) is created on a different thread to our main form where our user controls are initialised and data collection occurs. This is where delegates are used.

This code, from formMain, changes the text on the splash screen and initialises the various user controls:

VB
' Initialise user controls and collect the information.

FormSplash.Status("Retrieving Operating System Information")
userControlOS = New UserControlOS

FormSplash.Status("Retrieving System Information")
userControlSystem = New UserControlSystem

FormSplash.Status("Retrieving Memory Information")
userControlMemory = New UserControlMemory

FormSplash.Status("Retrieving Network Information")
userControlNetwork = New UserControlNetwork

FormSplash.Status("Retrieving Video Information")
userControlVideo = New UserControlVideo

FormSplash.Status("Retrieving Special Folder Locations")
userControlFolders = New UserControlSpecialFolders

FormSplash.Status("Retrieving Environment Variables")
userControlEnv = New UserControlEnvironmentVariables

FormSplash.Status("Retrieving Disk Information")
userControlStorage = New UserControlStorage

FormSplash.Status("Retrieving Processor Information")
userControlProcessor = New UserControlProcessor

FormSplash.Status("Retrieving Device Information")
userControlDevices = New UserControlDevices

FormSplash.Status("Retrieving Services Information")
userControlServices = New UserControlServices

FormSplash.Status("Retrieving Processes Information")
userControlProcesses = New UserControlProcesses

FormSplash.Status("Retrieving System && Device Driver Information")
userControlDrivers = New UserControlDrivers

FormSplash.Status("Retrieving Detailed Information")
userControlDetailed = New UserControlDetailed

FormSplash.Status("Loading Main Form")
userControlHome = New UserControlHome

The corresponding code in the splash screen looks like this:

VB
' Our declared delegated routine
' for updating the splash screen on a different thread.
Private Delegate Sub UpdateStatus(ByVal text As String)

Private Sub UpdateStatusText(ByVal text As String)
    lblLoading.Text = text
End Sub

Public Sub Status(ByVal text As String)

    ' Had to put this check in because
    ' on some slow machines the main form thread would be
    ' trying to update the status text
    ' before the splash form had been created.
    If Me.Created Then
        lblLoading.Invoke(New UpdateStatus(AddressOf UpdateStatusText), _
                          New Object() {text})
    End If

End Sub

What's happening here is our main code calls the formSplash.Status sub with the text that we want to display on the splash screen. The Status sub calls the Invoke method of the Label control we want to change the text of. As you can see from the Status sub, we don't change the text here; we point to the address of a delegated subroutine which will change the text for us.

VB
lblLoading.Invoke(New UpdateStatus(AddressOf UpdateStatusText), New Object() {text})

So .NET knows which sub is our delegated sub, we make a declaration telling it:

VB
Private Delegate Sub UpdateStatus(ByVal text As String)

Having collected the information and informed the user, the main form is displayed.

Home.JPG

FormMain consists of two panels: one that displays the menu (on the left), and one where we can change the user control that corresponds to the selected menu item. Each menu item is a LinkLabel control which responds to a LinkClicked event. In the LinkClicked event sub, we change the user control in the panel on the right. Note: we also change the appearance of the selected LinkLabel so it's obvious which one was clicked.

VB
Private Sub LinkLabelProcessor_LinkClicked(ByVal sender As System.Object, _
            ByVal e As System.Windows.Forms.LinkLabelLinkClickedEventArgs) _
            Handles LinkLabelProcessor.LinkClicked
    If Not pnlUserCtrls.Contains(userControlProcessor) Then

        lastLabelClicked.Font = New Font("Tahoma", 9.0, _
                                    FontStyle.Regular, GraphicsUnit.Point)
        lastLabelClicked = LinkLabelProcessor

        LinkLabelProcessor.Font = New Font("Tahoma", 9.0, _
                                      FontStyle.Bold, GraphicsUnit.Point)

        pnlUserCtrls.Controls.Clear()
        pnlUserCtrls.Controls.Add(userControlProcessor)

    End If
End Sub

Processor.jpg

Each of the other menu items performs in the same way.

Nearly all of the information we collect is through the use of WMI classes. However, some information that isn't contained within a WMI class is collected through other methods.

One of the pieces of information we present to the user is the product key that was used to install Windows. I'm not going to explain here how the function works, as its comments should make things reasonably clear:

VB
' A function that retrieves the product key used to install Windows.
Public Function GetProductKey(ByVal regKey As String) As String

    ' This function will retreive the digital product ID from the registry 
    ' and decode it into the CD key used to install a Microsoft product.
    ' All that is needed is the registry path to the digital proudct ID block
    ' for the product in question.

    Dim validChars() As String = {"B", "C", "D", "F", "G", "H", "J", "K", "M", _
                                  "P", "Q", "R", "T", "V", "W", "X", "Y", "2", _
                                  "3", "4", "6", "7", "8", "9"}
    Dim CDKey As String = ""

    Dim encodedKey(15) As Byte
    Dim digitalProductID As Byte()
    Dim dpidDataBlock As Object

    ' Get the Digital Product ID data-block from the registry.
    dpidDataBlock = My.Computer.Registry.GetValue(regKey, "DigitalProductId", 0)

    If dpidDataBlock Is Nothing Then Return notAvailable

    digitalProductID = DirectCast(dpidDataBlock, Byte())

    ' Extract the encoded CD key (15 bytes) from the digital product ID block.
    For n As Integer = 52 To 67
        encodedKey(n - 52) = digitalProductID(n)
    Next

    ' Decode the CD key.
    ' Note: The actual CD key is not stored in the registry; only the positions  
    ' within the validChars() array of the characters that make up the CD key
    ' are stored and encoded.

    For i As Integer = 28 To 0 Step -1
        ' Calculate where the dashes are.
        If ((i + 1) Mod 6) = 0 Then
            CDKey += " - "
        Else
            Dim j As Integer = 0
            For k As Integer = 14 To 0 Step -1
                Dim Value As Integer = CInt(CLng(j * 2 ^ 8) Or encodedKey(k))
                encodedKey(k) = CByte(Value \ 24)
                ' Position within the validChar() array 
                ' of the character to add to the CD key string.
                j = Value Mod 24
            Next
            CDKey += validChars(j)
        End If
    Next
    Return StrReverse(CDKey)

End Function

Now then, that's all well and good, I can hear you scream, but get to the point, just how do you use this new-fangled WMI stuff? I'm glad you asked, all will be revealed.

As stated somewhere in this article, near the top I think, WMI is basically a series of classes that contain properties related to a group of homogeneous information. Like nearly all classes, WMI classes have methods and properties. To be able to use a WMI class, we first need to instantiate a WMI class object to expose its properties and methods. One thing I haven't mentioned yet is that WMI classes are part of a group of classes that belong to a CIM class. CIM stands for Common Information Management, and to be able to instantiate a WMI class, we effectively instantiate a CIM class with the name of the WMI class we're interested in as an argument. Therefore, to instantiate the Win32_Process WMI class, we use:

VB
Dim wmi As New ManagementClass("Win32_Process")

Win32_Process reveals information on all the processes running on the system.

Once we've got our Win32_Process object, we can now use its methods to get the information we're interested in.

To do this, we use a For Each loop such as:

VB
For Each obj As ManagementObject In wmi.GetInstances()

Here, we're getting all of the class' instances of the user's running processes. I.e., we're getting a list of all the running processes on the user's computer, as what you would see in the Task Manager's Process tab.

Each obj on each iteration of the loop corresponds to a different process within our system. We can use obj to get information on each process. This is done by using a 'property identifier' as an argument; thus:

VB
processName = obj("Caption").ToString

This would get the name of each process every time this statement is encountered in the For Each loop. Notice, we use the .ToString at the end. This is because each piece of data is returned as an object, and not as the type indicated in the MSDN documentation. The documentation is telling you what type to cast the object to.

So, let's put all of this together to get information on all the running processes on a computer.

VB
Imports System.Management

Public Class UserControlProcesses

    ' Instantiate a web form for displaying information
    ' from the Internet on the selected process.
    Dim frm_ProcessCheck As New FormMsdn

    Public Sub New()

        ' This call is required by the Windows Form Designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.

        ToolTips.SetToolTip(Me.btnAdditionalInfo, _
           "Retrieves additional information from " & _ 
           "the Internet about the process. " & vbNewLine & _
           "This can be useful in identifying programs " & _ 
           "that are running that shouldn't be; such as spyware etc.")

        DisplayProcesses()

    End Sub

    ' Create a UDT to hold the collected information.
    Private Structure ProcessData
        Dim Caption As String
        Dim CommandLine As String
        Dim Owner As String
        Dim Priority As String
        Dim ProcessId As String
        Dim ThreadCount As String
        Dim HandleCount As String
    End Structure

    Dim processList() As ProcessData
    Dim processCount As Integer

    Private Sub DisplayProcesses()

        ' Instantiate a Win32_Process object
        Dim wmi As New ManagementClass("Win32_Process")

        ' Create a list of the properties we're interested in.
        Dim Win32_ProcessProperties() As String = {"Caption", _
            "CommandLine", "HandleCount", _
            "ProcessID", "Priority", "ThreadCount"}
        Dim propertyIndex As Integer

        Try
            ' Get each process in turn.
            For Each obj As ManagementObject In wmi.GetInstances()

                ReDim Preserve processList(processCount)

                propertyIndex = 0 ' Caption property.
                If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
                    processList(processCount).Caption = _
                       obj(Win32_ProcessProperties(propertyIndex)).ToString
                End If

                propertyIndex = 1 ' Commandline property.
                If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
                    processList(processCount).CommandLine = _
                      obj(Win32_ProcessProperties(propertyIndex)).ToString.Replace(Chr(34), "")
                End If

                propertyIndex = 2 ' HandleCount property.
                If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
                    processList(processCount).HandleCount = _
                      obj(Win32_ProcessProperties(propertyIndex)).ToString
                End If

                propertyIndex = 3 ' ProcessID property.
                If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
                    processList(processCount).ProcessId = _
                      obj(Win32_ProcessProperties(propertyIndex)).ToString
                    processList(processCount).Owner = _
                      GetProcessOwner(CInt(obj(Win32_ProcessProperties(propertyIndex))))
                End If

                propertyIndex = 4 ' Priority property.
                If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
                    processList(processCount).Priority = _
                      obj(Win32_ProcessProperties(propertyIndex)).ToString
                End If

                propertyIndex = 5 ' ThreadCount property.
                If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
                    processList(processCount).ThreadCount = _
                      obj(Win32_ProcessProperties(propertyIndex)).ToString
                End If

                processCount += 1
            Next
        Catch ex As ManagementException
            ' Keep a record of any errors that may have occurred.
            ErrorList.AddErrorToList(ex.Message, wmi.ClassPath.ClassName(), _
              Win32_ProcessProperties(propertyIndex))
            Exit Sub
        End Try

        processCount -= 1

        ' Sort the list of processes based on the process Caption property.
        Dim cache As ProcessData
        Dim sorted As Boolean

        Do
            sorted = True
            For n As Integer = 0 To processCount - 1
                If processList(n).Caption.ToUpperInvariant > _
                         processList(n + 1).Caption.ToUpperInvariant Then
                    cache = processList(n)
                    processList(n) = processList(n + 1)
                    processList(n + 1) = cache
                    sorted = False
                End If
            Next
        Loop While sorted = False

        ' Add the sorted list of processes to our listview control.
        For n = 0 To processList.GetUpperBound(0) - 1

            Dim lvi As New ListViewItem
            lvi.Text = processList(n).Caption
            lvi.SubItems.Add(processList(n).ProcessId)
            lvi.SubItems.Add(processList(n).Owner)
            lvi.SubItems.Add(processList(n).Priority)
            lvi.SubItems.Add(processList(n).ThreadCount)
            lvi.SubItems.Add(processList(n).HandleCount)

            ' Alternate background colour.
            If lstProcesses.Items.Count Mod 2 <> 0 Then
                lvi.BackColor = Color.White
            Else
                lvi.BackColor = Color.Ivory
            End If
            lstProcesses.Items.Add(lvi)
        Next

    End Sub

    Private Shared Function GetProcessOwner(ByVal ProcessId As Integer) As String

        ' Get the process's owner.
        Dim processOwner As String = notAvailable

        Try
            Dim mo As New ManagementObject("root\CIMV2", _
               "Win32_Process.Handle=" & ProcessId, Nothing)
            Dim methodResult As ManagementBaseObject = _
              mo.InvokeMethod("GetOwner", Nothing, Nothing)

            ' Method executed successfully.
            If CInt(methodResult("ReturnValue")) = 0 Then
                processOwner = methodResult("Domain").ToString & _
                  "\" & methodResult("User").ToString
            End If
        Catch ex As ManagementException
            ' Record any errors.
            ErrorList.AddErrorToList(ex.Message, "Win32_Process.Handle= " & _
                                     ProcessId, "Getting Process Owner Info.")
        End Try
        Return processOwner

    End Function

    Private Sub lstProcesses_SelectedIndexChanged(ByVal sender _
            As System.Object, ByVal e As System.EventArgs) _
            Handles lstProcesses.SelectedIndexChanged

        ' Display the command line for the selected process.
        If lstProcesses.SelectedIndices.Count > 0 Then
            Dim item As ListViewItem = _
              DirectCast(lstProcesses.Items(lstProcesses.SelectedIndices.Item(0)), _
              ListViewItem)
            lblCommandLine.Text = processList(item.Index).CommandLine
        End If

    End Sub

    ' Display additional information from the Internet on the selected process.
    Private Sub btnAdditionalInfo_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles btnAdditionalInfo.Click

        ' Here we instantiate a new form from class FormMsdn for displaying the info.
        If lstProcesses.SelectedIndices.Count > 0 Then
            Dim item As ListViewItem = _
                        DirectCast(lstProcesses.Items(_
                        lstProcesses.SelectedIndices.Item(0)), ListViewItem)
            If frm_ProcessCheck.IsDisposed Then
                frm_ProcessCheck = New FormMsdn
            End If

            Dim URL As New System.Uri("http://www.processlibrary.com/" & _ 
                "directory/files/" & _
                item.Text.Remove(Len(item.Text) - 4) & "/")

            frm_ProcessCheck.ShowUrl(URL)
            frm_ProcessCheck.Show()
        End If

    End Sub

    Public Sub CollectReportData()

        ' Write the collected information to the report file.
        Dim newReport As TextReport = FormMain.TextReportFile

        For n = 0 To processCount
            newReport.WriteItem("Process", processList(n).Caption)
            newReport.WriteItem("Process ID", processList(n).ProcessId)
            newReport.WriteItem("Owner", processList(n).Owner)
            newReport.WriteItem("Priority Level", processList(n).Priority)

            newReport.WriteSubtitle("")
        Next
    End Sub

End Class

Having looked through the code, you're probably shouting at your computer monitor: "that's not the way you showed us earlier!" And you'd be right. I have slightly changed the way I described in as much that each obj doesn't have a literal property identifier such as obj("Caption"). Instead, I use an array of property identifiers. The only reason I have done things like this is so I can record which property threw an exception, if one occurred, by using:

VB
Win32_ProcessProperties(propertyIndex))

in the Catch block. The only other thing that I've done, which I didn't in fact mention, is to check if the property actually contains any information, by using:

VB
If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then

One other thing you will notice from the code block above is that we call one of the Win32_Process class' methods: GetOwner.

VB
Private Shared Function GetProcessOwner(ByVal ProcessId As Integer) As String

    ' Get the process's owner.
    Dim processOwner As String = notAvailable

    Try
        Dim mo As New ManagementObject("root\CIMV2", _
               "Win32_Process.Handle=" & ProcessId, Nothing)
        Dim methodResult As ManagementBaseObject = _
            mo.InvokeMethod("GetOwner", Nothing, Nothing)

        ' Method executed successfully.
        If CInt(methodResult("ReturnValue")) = 0 Then
            processOwner = methodResult("Domain").ToString & _
                           "\" & methodResult("User").ToString
        End If
    Catch ex As ManagementException
        ' Record any errors.
        ErrorList.AddErrorToList(ex.Message, "Win32_Process.Handle= " & _
                  ProcessId, "Getting Process Owner Info.")
    End Try
    Return processOwner

End Function

By using this method, we can get the owner of the process in question.

There you have it, a brief practical introduction on how to get WMI class data.

Processes.jpg

And this is what the finished code looks like when it's running.

Formatting WMI Class Data; Strategies Based on Data Type

Uint Arrays

Quite often, the data returned by WMI doesn't mean much until you look up what the values are actually trying to tell you. To help things along, we have a few routines that format the WMI data.

In the screenshot earlier in the article that showed the improved output for the Win32_Bios class, this is how we achieve it.

You will see from this screenshot of the MSDN documentation for the Win32_Bios class that the BiosCharacteristics property returns an array of uint16 values which are then listed below.

win32_bios.jpg

To be able to display meaningful information to the user, we interpret each of the values in the array of uint16 values and list these under a group heading in our listview control.

In this case, we get our information in a slightly different way. We use a searcher object and pass the WMI class we're interested in to the searcher, where wmiClass is the name of our WMI class:

VB
Dim searcher As New ManagementObjectSearcher("Select * From " & wmiClass)

Once we have our searcher object, we then use its Get method to walk the list of properties within the WMI class:

VB
' Walk the list of properties.
For Each WmiProperty As ManagementObject In searcher.Get

We then walk through the list of property data within each property:

VB
' Walk the list of property data in the class.
For Each pd As PropertyData In WmiProperty.Properties

We then use a Select Case construct to use different formatting strategies based on the name of the property. The strategy is determined by the type of information returned, which is obtained by consulting the MSDN documentation.

VB
' Format or enumerate the property value based on the property name (pd.name).
Select Case pd.Name

So, for our BiosCharacteristics, we use this strategy:

VB
Case "BiosCharacteristics"
' Bios Characteristics consists of an array of uint16 (ushort)
' values depicting various BIOS capabilities.
Dim BiosCharacteristics() As String = {"Reserved", "Reserved", _
     "Unknown", "BIOS Characteristics Not Supported", _
     "ISA is supported", "MCA is supported", _
     "EISA is supported", "PCI is supported", _
     "pd Card (PCMCIA) is supported", _
     "Plug and Play is supported", "APM is supported", _
     "BIOS is Upgradable (Flash)", _
     "BIOS shadowing is allowed", "VL-VESA is supported", _
     "ESCD support is available", "Boot from CD is supported", _
     "Selectable Boot is supported", _
     "BIOS ROM is socketed", "Boot From pd Card (PCMCIA) is supported", _
     "EDD (Enhanced Disk Drive) Specification is supported", _
     "Int 13h - Japanese Floppy for NEC 9800 1.2mb" & _ 
        " (3.5, 1k Bytes/Sector, 360 RPM) is supported", _
     "Int 13h - Japanese Floppy for Toshiba 1.2mb (3.5, 360 RPM) is supported", _
     "Int 13h - 5.25 / 360 KB Floppy Services are supported", _
     "Int 13h - 5.25 /1.2MB Floppy Services are supported", _
     "Int 13h - 3.5 / 720 KB Floppy Services are supported", _
     "Int 13h - 3.5 / 2.88 MB Floppy Services are supported", _
     "Int 5h, Print Screen Service is supported", _
     "Int 9h, 8042 Keyboard services are supported", _
     "Int 14h, Serial Services are supported", _
     "Int 17h, printer services are supported", _
     "Int 10h, CGA/Mono Video Services are supported", _
     "NEC pd-98", "ACPI is supported", "USB Legacy is supported", _
     "AGP is supported", "I2O boot is supported", _
     "LS-120 boot is supported", _
     "ATAPI ZIP Drive boot is supported", "1394 boot is supported", _
     "Smart Battery is supported"}

' Get the property data to format.
Dim shortData As UShort() = CType(pd.Value, UShort())

' Create a new group to list the BIOS charateristics under.
Dim lvGroup As ListViewGroup = CType(listViewCtrl.Invoke(New _
    AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
    {listViewCtrl, "Supported BIOS Capabilites", _
    "Supported BIOS Capabilites"}), ListViewGroup)

' Walk the list. Only the first 39 characteristics are supported, the 
' remainder are reserved for BIOS vendor and System vendor usage and
' therfore, we don't list them; basically, because we don't know what 
' they mean.
For Each sd As UShort In shortData
    If sd <= 39 Then
        Dim lvGroupItem As New ListViewItem(lvGroup)

        lvGroupItem.SubItems.Add(BiosCharacteristics(sd))
        listViewCtrl.Invoke(New AddItem(AddressOf AddListViewItem), _
                            New Object() {listViewCtrl, lvGroupItem})
    Else
        Exit For
    End If
Next

As can be seen, we create a group heading in our listview control and then add, in turn, each BIOS capability, as determined by its position within the string array of capabilities.

Bit Masks

Another strategy we use for dealing with multiple returned values is this one:

VB
Case "AccessMask" ' Win32_Directory
Dim heading As String = WmiProperty("Caption").ToString

' Create a new group to list the Access Rights under.
Dim lvGroup As ListViewGroup = CType(listViewCtrl.Invoke(New _
            AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
            {listViewCtrl, "Access Permissions For " & heading, _
            "Access Permissions For " & heading}), ListViewGroup)

Dim accessRightsBitMask As UInt32 = CType(pd.Value, UInt32)
Dim accessRights() As String = {"FILE_READ_DATA (file), FILE_LIST_DIRECTORY (directory)", _
                               "FILE_WRITE_DATA (file), FILE_ADD_FILE (directory)", _
                               "FILE_APPEND_DATA", "FILE_READ_EA", "FILE_WRITE_EA", _
                               "FILE_EXECUTE (file), FILE_TRAVERSE (directory)", _
                               "FILE_DELETE_CHILD", "FILE_READ_ATTRIBUTES", _
                               "FILE_WRITE_ATTRIBUTES", "", "", "", "", "", "", "", _
                               "DELETE", "READ_CONTROL", "WRITE_DAC", _
                               "WRITE_OWNER", "SYNCHRONIZE"}

' Decode the Access Rights Bit Mask.
For rights = 0 To accessRights.GetUpperBound(0)
  If CBool(CLng(2 ^ rights) And accessRightsBitMask) Then

     Dim lvGroupItem As New ListViewItem(lvGroup)
     lvGroupItem.SubItems.Add(accessRights(rights))
     listViewCtrl.Invoke(New AddItem(AddressOf AddListViewItem), _
                         New Object() {listViewCtrl, lvGroupItem})

  End If
Next

Enumeration

For cases where only a single value is returned, we use a different strategy for determining what the value actually means; we enumerate the value. For example, the property 'Availability' returns one value, so we enumerate its meaning:

VB
Case "Availability" ' CIM_LogicalDevice Class.
Dim list As String() = {"Other", "Unknown", "Running or Full Power", _
                        "Warning", "In Test", _
                        "Not Applicable", "Power Off", _
                        "Off Line", "Off Duty", "Degraded", _
                        "Not Installed", "Install Error", "Power Save - Unknown", _
                        "Power Save - Low Power Mode", "Power Save - Standby", _
                        "Power Cycle", "Power Save - Warning"}
Return EnumeratePropertyList(wmiData, list, -1, "")

The function code:

VB
' This function returns a string from the list
' of possible property items as indicated by the item's 
' position within the list. We use the offset
' to correctly align the list with the wmi data value.
' For example, some wmi lists start with
' the first item equating to 1 and others start at zero.
'
' For lists that start at 0, set offset to 0.
' For those lists that start at 1, set offset to -1.
'
' If a list has an associated qualifier,
' we add that to the returned string as well. A qualifier
' would be something like Volts, Bits or Mbytes etc.

Private Shared Function EnumeratePropertyList(ByVal wmiData As Object, _
               ByVal List As String(), ByVal Offset As Integer, _
               ByVal Qualifier As String) As String
    Dim value As Long = CType(wmiData, Long)
    Dim upperBound As Integer = List.GetUpperBound(0)

    ' Adjust the upper bound of the list array if the offset is -1.
    If Offset = -1 Then
        upperBound += 1
    End If

    ' A little error checking on the value returned from the wmi class.
    If (Offset = -1 AndAlso value <= 0) Or (value > upperBound) Then
        Return "Invalid data passed. The data was: " & _
               value.ToString(CultureInfo.InvariantCulture)
    Else
        Return List(CInt(value + Offset)) & Qualifier
    End If
End Function

One interesting thing I've found while testing the program is that WMI classes can return information or values that are not documented on MSDN; the reason we do some error checking on the values passed to the enumeration routine.

Unenumerable Types

Some WMI class data can't be handled by the EnumeratePropertyList routine. This is usually because the list of returnable values doesn't increase by one for each available option, and therefore, can't be enumerated. The class property 'AccountType' from Win32_UserAccount is such an example. In these cases, we use a different strategy again:

VB
Case "AccountType" ' Win32_UserAccount.
    Select Case CType(wmiData, UInt32)
        Case 256
            Return "Local user account for users who " & _ 
                   "have a primary account in another domain."
        Case 512
            Return "Default account type that represents a typical user."
        Case 2048
            Return "Account for a system domain that trusts other domains."
        Case 4096
            Return "Computer account for a computer system running " & _ 
                   "Windows 2000 or Windows NT that is a member of this domain."
        Case 8192
            Return "Account for a system backup domain controller " & _ 
                   "that is a member of this domain."
        Case Else
            Return CastToString(wmiData, "")
    End Select

Single Value Types and Qualifiers

For other properties that only return a single numerical value, such as monitor screen refresh rate, we use this strategy:

VB
Case "CurrentRefreshRate", "MaxRefreshRate", "MinRefreshRate"
    Return CastToString(wmiData, " Hertz")

We cast the numerical value to a string, and append a qualifier, in this case: Hertz, so we would end up with something like '50 Hertz' as our value for the property.

VB
Case "MaxDataWidth" ' Win32_SystemSlot
    Dim list As String() = {"8", "16", "32", "64", "128"}
    Return EnumeratePropertyList(wmiData, list, 0, " Bits")

Here, we use the qualifier "Bits", with an enumerated list of options to give us, say, '16 Bits' if the wmiData value was 1.

Well folks, there you have it: how to retrieve, format, and qualify WMI class property data.

Properly formatted and qualified data can create an air of professionalism in your program that it might not otherwise have without it.

Points of Interest

While testing the program with the various WMI classes, it became rather apparent that quite a few classes don't return any information at all. This is usually because the entity that the class returns information on doesn't actually exist. For example, because my computer is a tower and not a laptop, it doesn't have a battery. If we select the WMI class Win32_Battery, no information is returned. Rather than leaving the user with an empty listview control and no explanation, we inform the user that we couldn't find any readable data.

WMIClassError_1.jpg

VB
If Not classHasData Then
    MsgBox("Class " & wmiClass & " doesn't contain any readable data.", _
           MsgBoxStyle.Critical, "Error Retrieving Data.")
    bw.ReportProgress(-1)
End If

Background Worker Threads

One of the things I mentioned near the beginning of this article is that we run our data collection routine on a background worker thread. This is so we can abort the data collection and also make our GUI responsive. Implementing the background worker proved to be the biggest headache of the whole programming exercise.

A lot of head scratching, pulling of hair, swearing, and Illegal Cross Threading exceptions later, the penny finally dropped. Let me say just one thing: MSDN documentation is your best friend!

The main stumbling block was how to add the data (which is being collected on the background thread) to the listview control on our main thread in real time.

The secret to this little conundrum, as those of you veteran programmers out there will know, is to use the Invoke method of the control we want to manipulate and to use a delegate to do the manipulation on our behalf.

For those not 'in the know' about using the BackgroundWorker control, let me explain, as best I can: the BackgroundWorker control enables portions of code to run in the background on a separate thread to the main thread that the rest of the program runs on. This enables multiple things to be happening at the same time, which can increase program responsiveness or give increased productivity when, say, doing complicated, long-winded calculations. A good example can be found here: http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx.

To use the BackgroundWorker control, drag the control from the Visual Studio toolbox onto your form or User Control. I've renamed mine bwSearchWmiClass.

We then create a 'DoWork' event sub:

VB
Private Sub bwSearchWmiClass_DoWork(ByVal sender As System.Object, _
                                    ByVal e As System.ComponentModel.DoWorkEventArgs) _
                                    Handles bwSearchWmiClass.DoWork

    ' Get a reference to our background worker thread.
    Dim bw As BackgroundWorker = CType(sender, BackgroundWorker)

    ' Start the background thread and do the work.
    Wmi.Win32ClassInfo.ListClassProperties(bw, lstClassProperties, e.Argument.ToString)

    ' Does the user want to cancel the search?
    If bw.CancellationPending Then
        e.Cancel = True
    End If

End Sub

Any code in here will run on our background thread. Note: we pass a reference to our BackgroundWorker (bw) to our data collection routine, as well as a reference to the ListView control (lstClassProperties) we want to add data to. The actual name of the WMI class we want to get data from is passed via the e.Argument.ToString statement.

To actually start the background thread, we use the command:

VB
Me.bwSearchWmiClass.RunWorkerAsync(selectedClass)

where selectedClass is the name of the WMI class.

If the user wants to stop our background thread, we use:

VB
Private Sub btnCancel_Click(ByVal sender As System.Object, _
                                ByVal e As System.EventArgs) _
                                Handles btnCancel.Click

    ' Send a cancellation message to our background thread.
    Me.bwSearchWmiClass.CancelAsync()
End Sub

And this is where we actually end up after stopping the thread:

VB
Private Sub bwSearchWMIClass_RunWorkerCompleted(ByVal sender As Object, _
            ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
            Handles bwSearchWmiClass.RunWorkerCompleted

    cboClassList.Enabled = True
    Me.Cursor = Cursors.Arrow

    If e.Cancelled Then
        MsgBox("Operation Was Cancelled by User.")
    End If

End Sub

We can also use the BackgroundWorker's ProgressChanged event to update our UI with how many records we've found so far:

VB
Private Sub bwSearchWMIClass_ProgressChanged(ByVal sender As Object, _
        ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
        Handles bwSearchWmiClass.ProgressChanged

    Select Case e.ProgressPercentage

        Case -1 ' Nothing found, hide the 'Searching....." text on UI.
            lblSearching.Text = ""
        Case 1
            lblSearching.Text = "Found " & _
              e.ProgressPercentage.ToString(CultureInfo.InvariantCulture) & _
              " Record"
        Case Else
            lblSearching.Text = "Found " & _
              e.ProgressPercentage.ToString(CultureInfo.InvariantCulture) & _
              " Records"
    End Select

End Sub

Delegates

So, now that we've started our background thread, we need to be able to update our ListView control in real time as the data comes rolling in. In our data collection routine, we declare our delegates using:

VB
' Because we run our routine that gets
' the selected class's properties on a background thread using the
' BackgroundWorker component, we can't update
' our listview directly using this thread, if we try, we get 
' a cross-threading exception; our listview was
' created on the program's main thread and can't be changed 
' directly by our background thread.

' To get round this little problem, we create delegate
' routines and then call the listview control's 
' invoke method which uses the delegated routine
' to update the listview control on the main thread.

Private Delegate Sub AddItem(ByVal lv As ListView, ByVal lvi As ListViewItem)
Private Delegate Function AddItemGroup(ByVal lv As ListView, _
        ByVal key As String, ByVal header As String) As ListViewGroup

These are our actual routines referenced by the delegates:

VB
Private Shared Sub AddListViewItem(ByVal listViewCtrl As ListView, _
               ByVal lvi As ListViewItem)
    listViewCtrl.Items.Add(lvi)
End Sub

Private Shared Function AddListViewItemGroup(ByVal listViewCtrl As ListView, _
               ByVal key As String, ByVal header As String) As ListViewGroup
    Return listViewCtrl.Groups.Add(key, header)
End Function

We then use these delegated routines by calling the ListView control's Invoke method.

To add a single formatted item:

VB
listViewItem.SubItems.Add(FormatWmiData(wmiClass, pd.Name, pd.Value))
listViewCtrl.Invoke(New AddItem(AddressOf AddListViewItem), _
                    New Object() {listViewCtrl, listViewItem})

and to add a group of items under a heading:

VB
Private Shared Sub AddListViewGroupStringArray(ByVal groupName As String, _
               ByVal value As Object, ByVal listViewCtrl As ListView)
   ' Create the listview group heading.
   Dim lvGroup As ListViewGroup = CType(listViewCtrl.Invoke(New _
               AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
               {listViewCtrl, groupName, groupName}), ListViewGroup)
   ' Get the data and convert to a string array.
   Dim stringArray As String() = CType(value, String())
      ' Add each string to the listview under the group heading.
      For Each [string] As String In stringArray
         Dim lvGroupItem As New ListViewItem(lvGroup)
         lvGroupItem.SubItems.Add([string])
         listViewCtrl.Invoke(New AddItem(AddressOf AddListViewItem), _
                             New Object() {listViewCtrl, lvGroupItem})
      Next
End Sub

Using the BackgroundWorker in the Data Collection Routine

This snippet is part of our data collection routine:

VB
Public Shared Sub ListClassProperties(ByVal bw As BackgroundWorker, _
                  ByVal listViewCtrl As ListView, ByVal wmiClass As String)

    ' This sub walks the WMI class passed to it and processes
    ' the class's properties. The property name,
    ' along with its value are added to the listview if,
    ' and only if the property exists and the property
    ' has a value associated with it. The value, where relevant,
    ' is formatted or enumerated to make it more meaningful.

    ' Those properties that yield multiple values
    ' are processed here and given a seperate group listing in
    ' the listview control. Those properties
    ' that don't yield multiple values are processed in the 
    ' FormatWmiData function and then added to the listview control.

    ' This code is based, in part, on the codeproject article
    ' "How To Get Hardware Information (CPU ID,
    ' MainBoard Info, Hard Disk Serial, System Information...)"
    ' By Alireza Shirazi.
    ' GetHardwareInformation.aspx

    ' I have added grouping, formatting, additional error
    ' checking and running on a background thread to the original C# code.

    Dim searcher As New ManagementObjectSearcher("Select * From " & wmiClass)
    Dim classHasData As Boolean = False
    Dim count As Integer = 0

    Try
        ' Walk the list of properties.
        For Each WmiProperty As ManagementObject In searcher.Get

            Dim listViewGroup As New ListViewGroup
            classHasData = True

            ' Report the progress as to how many items we've found so far.
            count += 1
            bw.ReportProgress(count)

            ' Stop collecting the data if the user clicked the cancel button.
            If bw.CancellationPending Then Exit Sub

            Try
                'Add a group heading to the listview control.
                'We put this in a Try...Catch...End Try construct because
                'the "Caption" property isn't available in some WMI classes.

                If WmiProperty("Caption") IsNot Nothing Then

                    listViewGroup = CType(listViewCtrl.Invoke(New _
                       AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
                       {listViewCtrl, WmiProperty("Caption").ToString, _
                       WmiProperty("Caption").ToString}), ListViewGroup)
                Else
                    ' This is in case the Caption property
                    ' does exist but doesn't contain anything.
                    listViewGroup = CType(listViewCtrl.Invoke(New _
                        AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
                        {listViewCtrl, "  ", "  "}), ListViewGroup)
                End If

            ' And this will catch if the Caption property
            ' doesn't exist at all; amongst other things.
            Catch ex As ManagementException

                listViewGroup = CType(listViewCtrl.Invoke(New _
                   AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
                   {listViewCtrl, "  ", "  "}), ListViewGroup)
            End Try

            ' Walk the list of property data in the class.
            For Each pd As PropertyData In WmiProperty.Properties

                ' Stop collecting the data if the user clicked the cancel button.
                If bw.CancellationPending Then Exit Sub

                ' Create a list view item for our group.
                Dim listViewItem As New ListViewItem(listViewGroup)

                ' Alternate each property's background colour.
                If listViewCtrl.Items.Count Mod 2 <> 0 Then
                    listViewItem.BackColor = Color.White
                Else
                    listViewItem.BackColor = Color.Ivory
                End If

                ' Add the property name to the list view control.
                listViewItem.Text = pd.Name

                ' Has the property got an associated value?
                If pd.Value IsNot Nothing AndAlso pd.Value.ToString <> "" Then

                    ' Format or enumerate the property value
                    ' based on the property name (pd.name).
                    Select Case pd.Name

The things to note here are: we update the progress on the number of records found, by using:

VB
' Report the progress as to how many items we've found so far.
count += 1
bw.ReportProgress(count)

and we stop collecting like this:

VB
' Stop collecting the data if the user clicked the cancel button.
If bw.CancellationPending Then Exit Sub

This is why we pass a reference of the background worker to the collection routine:

VB
Public Shared Sub ListClassProperties(ByVal bw As BackgroundWorker, _
              ByVal listViewCtrl As ListView, ByVal wmiClass As String)

Determining whether we are running on a 32 or 64 bit OS platform

One of the other problems encountered was to accurately determine the architecture of the Operating System platform we're running on. We use this code:

VB
Public Shared ReadOnly Property OSArchitecture() As String
    ' Determine whether we are running on a 32 or 64 bit OS.

    ' You would have thought Microsoft would have introduced a proper 
    ' method for determining the OS architecture when they first released
    ' a 64-bit OS. On Vista and upwards, the WMI can be consulted to 
    ' determine OS architecture. However, we use this method instead.
    Get
        Select Case IntPtr.Size
            Case 4
                OSArchitecture = "x86"
            Case 8
                OSArchitecture = "x64"
            Case Else
                ' x128? 
                OSArchitecture = "Not Known"
        End Select
    End Get
End Property

because we can't rely on other information before Vista.

A Few Other Screenshots

ComputerSystem.JPG

MemoryPage.jpg

StorageDevices.jpg

Network.jpg

SpecialFolders.jpg

History

  • July 13, 2010: Code and article: Version 1.
  • July 15, 2010: Bug fix. If user didn't have admin privileges, the program crashed on exit. Was trying to close the form before it was created! Doh!

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)


Written By
Other
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralRe: My vote of 5 Pin
Euhemerus18-Jul-10 7:28
Euhemerus18-Jul-10 7:28 
GeneralMy vote of 5 Pin
hakkietakkie17-Jul-10 0:30
hakkietakkie17-Jul-10 0:30 
GeneralRe: My vote of 5 Pin
Euhemerus17-Jul-10 0:54
Euhemerus17-Jul-10 0:54 
GeneralMy vote of 5 Pin
Ron Schuler15-Jul-10 2:09
Ron Schuler15-Jul-10 2:09 
GeneralNeed to remove the vbproj user settings file Pin
Ron Schuler15-Jul-10 2:07
Ron Schuler15-Jul-10 2:07 
GeneralRe: Need to remove the vbproj user settings file Pin
Euhemerus15-Jul-10 5:35
Euhemerus15-Jul-10 5:35 
GeneralMy vote of 5 Pin
Daniel Joubert14-Jul-10 4:47
professionalDaniel Joubert14-Jul-10 4:47 
GeneralRe: My vote of 5 Pin
Euhemerus14-Jul-10 6:23
Euhemerus14-Jul-10 6:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.