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:
- A CodeProject article by Alireza Shirazi: GetHardwareInformation.aspx.
- 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
:

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

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:
- 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;
- We forewarn the user that the operation on the selected classes could take a long time;
- We provide a Cancel button to cancel the search.

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.

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.
Public Function IsAdmin() As Boolean
IsAdmin = False
Dim securityGroup As WindowsPrincipal
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
securityGroup = CType(Thread.CurrentPrincipal, WindowsPrincipal)
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.
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
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.

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:
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:
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)
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.
lblLoading.Invoke(New UpdateStatus(AddressOf UpdateStatusText), New Object() {text})
So .NET knows which sub is our delegated sub, we make a declaration telling it:
Private Delegate Sub UpdateStatus(ByVal text As String)
Having collected the information and informed the user, the main form is displayed.

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.
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

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:
Public Function GetProductKey(ByVal regKey As String) As String
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
dpidDataBlock = My.Computer.Registry.GetValue(regKey, "DigitalProductId", 0)
If dpidDataBlock Is Nothing Then Return notAvailable
digitalProductID = DirectCast(dpidDataBlock, Byte())
For n As Integer = 52 To 67
encodedKey(n - 52) = digitalProductID(n)
Next
For i As Integer = 28 To 0 Step -1
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)
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:
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:
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:
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.
Imports System.Management
Public Class UserControlProcesses
Dim frm_ProcessCheck As New FormMsdn
Public Sub New()
InitializeComponent()
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
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()
Dim wmi As New ManagementClass("Win32_Process")
Dim Win32_ProcessProperties() As String = {"Caption", _
"CommandLine", "HandleCount", _
"ProcessID", "Priority", "ThreadCount"}
Dim propertyIndex As Integer
Try
For Each obj As ManagementObject In wmi.GetInstances()
ReDim Preserve processList(processCount)
propertyIndex = 0
If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
processList(processCount).Caption = _
obj(Win32_ProcessProperties(propertyIndex)).ToString
End If
propertyIndex = 1
If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
processList(processCount).CommandLine = _
obj(Win32_ProcessProperties(propertyIndex)).ToString.Replace(Chr(34), "")
End If
propertyIndex = 2
If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
processList(processCount).HandleCount = _
obj(Win32_ProcessProperties(propertyIndex)).ToString
End If
propertyIndex = 3
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
If obj(Win32_ProcessProperties(propertyIndex)) IsNot Nothing Then
processList(processCount).Priority = _
obj(Win32_ProcessProperties(propertyIndex)).ToString
End If
propertyIndex = 5
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
ErrorList.AddErrorToList(ex.Message, wmi.ClassPath.ClassName(), _
Win32_ProcessProperties(propertyIndex))
Exit Sub
End Try
processCount -= 1
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
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)
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
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)
If CInt(methodResult("ReturnValue")) = 0 Then
processOwner = methodResult("Domain").ToString & _
"\" & methodResult("User").ToString
End If
Catch ex As ManagementException
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
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
Private Sub btnAdditionalInfo_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnAdditionalInfo.Click
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()
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:
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:
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
.
Private Shared Function GetProcessOwner(ByVal ProcessId As Integer) As String
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)
If CInt(methodResult("ReturnValue")) = 0 Then
processOwner = methodResult("Domain").ToString & _
"\" & methodResult("User").ToString
End If
Catch ex As ManagementException
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.

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.

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:
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:
For Each WmiProperty As ManagementObject In searcher.Get
We then walk through the list of property data within each property:
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.
Select Case pd.Name
So, for our BiosCharacteristics
, we use this strategy:
Case "BiosCharacteristics"
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"}
Dim shortData As UShort() = CType(pd.Value, UShort())
Dim lvGroup As ListViewGroup = CType(listViewCtrl.Invoke(New _
AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
{listViewCtrl, "Supported BIOS Capabilites", _
"Supported BIOS Capabilites"}), ListViewGroup)
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:
Case "AccessMask"
Dim heading As String = WmiProperty("Caption").ToString
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"}
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:
Case "Availability"
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:
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)
If Offset = -1 Then
upperBound += 1
End If
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:
Case "AccountType"
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:
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.
Case "MaxDataWidth"
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.

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:
Private Sub bwSearchWmiClass_DoWork(ByVal sender As System.Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) _
Handles bwSearchWmiClass.DoWork
Dim bw As BackgroundWorker = CType(sender, BackgroundWorker)
Wmi.Win32ClassInfo.ListClassProperties(bw, lstClassProperties, e.Argument.ToString)
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:
Me.bwSearchWmiClass.RunWorkerAsync(selectedClass)
where selectedClass
is the name of the WMI class.
If the user wants to stop our background thread, we use:
Private Sub btnCancel_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnCancel.Click
Me.bwSearchWmiClass.CancelAsync()
End Sub
And this is where we actually end up after stopping the thread:
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:
Private Sub bwSearchWMIClass_ProgressChanged(ByVal sender As Object, _
ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
Handles bwSearchWmiClass.ProgressChanged
Select Case e.ProgressPercentage
Case -1
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:
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:
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:
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:
Private Shared Sub AddListViewGroupStringArray(ByVal groupName As String, _
ByVal value As Object, ByVal listViewCtrl As ListView)
Dim lvGroup As ListViewGroup = CType(listViewCtrl.Invoke(New _
AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
{listViewCtrl, groupName, groupName}), ListViewGroup)
Dim stringArray As String() = CType(value, String())
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:
Public Shared Sub ListClassProperties(ByVal bw As BackgroundWorker, _
ByVal listViewCtrl As ListView, ByVal wmiClass As String)
Dim searcher As New ManagementObjectSearcher("Select * From " & wmiClass)
Dim classHasData As Boolean = False
Dim count As Integer = 0
Try
For Each WmiProperty As ManagementObject In searcher.Get
Dim listViewGroup As New ListViewGroup
classHasData = True
count += 1
bw.ReportProgress(count)
If bw.CancellationPending Then Exit Sub
Try
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
listViewGroup = CType(listViewCtrl.Invoke(New _
AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
{listViewCtrl, " ", " "}), ListViewGroup)
End If
Catch ex As ManagementException
listViewGroup = CType(listViewCtrl.Invoke(New _
AddItemGroup(AddressOf AddListViewItemGroup), New Object() _
{listViewCtrl, " ", " "}), ListViewGroup)
End Try
For Each pd As PropertyData In WmiProperty.Properties
If bw.CancellationPending Then Exit Sub
Dim listViewItem As New ListViewItem(listViewGroup)
If listViewCtrl.Items.Count Mod 2 <> 0 Then
listViewItem.BackColor = Color.White
Else
listViewItem.BackColor = Color.Ivory
End If
listViewItem.Text = pd.Name
If pd.Value IsNot Nothing AndAlso pd.Value.ToString <> "" Then
Select Case pd.Name
The things to note here are: we update the progress on the number of records found, by using:
count += 1
bw.ReportProgress(count)
and we stop collecting like this:
If bw.CancellationPending Then Exit Sub
This is why we pass a reference of the background worker to the collection routine:
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:
Public Shared ReadOnly Property OSArchitecture() As String
Get
Select Case IntPtr.Size
Case 4
OSArchitecture = "x86"
Case 8
OSArchitecture = "x64"
Case Else
OSArchitecture = "Not Known"
End Select
End Get
End Property
because we can't rely on other information before Vista.
A Few Other Screenshots





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!