Interacting with the Operating System
As you know, your .NET applications run inside an environment managed by the common language runtime (CLR). However, there are many cases where your application might need to communicate with underlying operating system features or services. This chapter examines some classes in the Base Class Library that allow you to interact with the operating system to perform tasks such as communicating with the COM port, launching and monitoring processes, and using Windows Management Instrumentation.
Application #54 Work with Environment Settings
This topic introduces two classes your applications can use to retrieve information about the current system configuration, such as the location of the TEMP directory, the current display size, and a list of available drives.
Building Upon…
Application #35: Create an Explorer-Style Application
Application #70: Reflection
New Concepts
The .NET Framework contains two classes in the System namespace that provide simple access to your system’s environment settings. You can use this information to determine whether the current system meets your application’s operational requirements. The Environment class exposes shared properties and methods dealing with the platform details, such as what operating system is running, what the current user name is, and the values for various environment settings. The SystemInformation class describes the operating system’s current user-interface settings with properties such as DoubleClickTime, MonitorCount, and WorkingArea.
Code Walkthrough
The sample application displays information about the current system by invoking properties and methods of the System.Environment class. You can view the information by running the sample and selecting the various tabs.
Retrieving Environment Information
The frmEnvironment_Load event handler calls a set of utility procedures that populate the form’s controls. The first call is to the LoadList procedure to populate the lstFolders list box with the names of each of the system’s special folders. LoadList uses reflection to iterate over the members of the Environment.SpecialFolder enumeration.
LoadList(lstFolders,GetType(Environment.SpecialFolder))
Another call to LoadList populates the lstEnvironmentVariables list box with the key values of the collection returned by Environment.GetEnvironmentVariables.
LoadList(lstEnvironmentVariables,_ Environment.GetEnvironmentVariables.Keys)
The LoadProperties procedure simply populates some text boxes with various properties of the Environment class, and the RunMethods procedure displays the command-line arguments and local logical drives in some list boxes. Lastly, the LoadSystemInformation procedure uses properties of the SystemInformation class to populate the lvwSystemInformation list view control.
⋮ WithlvwSystemInformation.Items.Add("DoubleClickTime") .SubItems.Add(SystemInformation.DoubleClickTime.ToString()) EndWith ⋮
On the Properties tab, you can click the Display Current Stack Trace button, which will display the stack trace in a message box by retrieving the StackTrace property of the System.Environment class.
MsgBox(Environment.StackTrace,MsgBoxStyle.OKOnly,Me.Text)
On the Methods tab, you can click the Expand button, which will parse the text in the Input text box and convert any environment variables delimited with percent symbols to their system values and display the results in the Results label. The ExpandEnvironmentVariables method of the Environment class performs all this work.
lblExpandResults.Text=_ Environment.ExpandEnvironmentVariables(txtExpand.Text)
Conclusion
You can easily enforce platform run-time requirements for your applications by examining the information returned by the Environment and SystemInformation classes.
Application #55 Use the File System
This topic addresses how to retrieve information about a system’s drive and directory structure. We also examine how to retrieve various attributes about files and directories from the file system. The sample application shows one way of creating an interface to navigate through drives, folders, and files.
Building Upon…
Application #1: Use Arrays
Application #73: Read From and Write To a Text File
New Concepts
The .NET Framework provides facilities for querying the file system through a number of different objects. You can find out what drives exist on a system, the contents of a directory, and various file attributes such as those displayed in the Window Explorer detail view.
DirectoryInfo and FileInfo Classes
The DirectoryInfo and FileInfo classes are available in the System.IO namespace. You can use instances of these classes to retrieve information about file-system objects. To create an instance of either, you must pass in a valid file-system path. With a DirectoryInfo instance, you can find out what that directory contains by using the GetFiles and GetDirectories methods. Each of these returns a string array containing the paths of the appropriate items. The DirectoryInfo and FileInfo classes also contain properties that expose file-system attributes such as LastAccessTime, CreationTime, and LastWriteTime.
TreeView Control
The TreeView control is commonly used to display directory structures to end users. The contents of a TreeView control consist of a hierarchy of TreeNode objects. You can add items to the TreeView by creating a new instance of the TreeNode class, setting its properties, and adding it to the Nodes collection of the TreeView. The TreeNode class has a number of properties that determine how it is displayed on screen. For most TreeViews, you’ll want to set at least the Text property of each TreeNode, as this is what will display to the user. Also, any TreeNode with an item in its Nodes collection will automatically have an expand/collapse button next to it.
Code Walkthrough
The sample application uses various methods of the DirectoryInfo and FileInfo classes to create a file-system viewer. Users are able to expand directories and view metadata about various items. The file-system structure is displayed to the user through a TreeView control, and file-system object properties are displayed in the labels below the TreeView.
Retrieving Directory Contents
When the application starts, the frmMain_Load event handler calls the LoadTreeView method. The LoadTreeView method calls the shared GetLogicalDrives method of the Directory object. This returns an array of strings, each containing a drive letter. For each item in the array, a new TreeNode is added to the TreeView. In addition, each drive node is given a child node so that it appears with a plus sign next to it.
DimstrDriveAsString tvwRoot.Nodes.Clear() ForEachstrDriveInDirectory.GetLogicalDrives() WithtvwRoot.Nodes.Add(strDrive) .Nodes.Add(DUMMY) EndWith Next
The next activity occurs when a user clicks one of the plus signs to the left of the drive letters. The BeforeExpand event handler of the TreeView clears any existing child nodes and calls two functions, AddFolders and AddFiles, passing a reference to the node that is being expanded as a parameter.
e.Node.Nodes.Clear() AddFolders(e.Node) AddFiles(e.Node)
The AddFolders method retrieves the path for the passed-in TreeNode using the FullPath property. This string is then passed into the GetDirectories method of the Directory class. GetDirectories returns an array of strings containing the paths of the subdirectories. For each subdirectory, a child node is added to the current node. However, because we don’t want the full path displaying in the TreeView, the GetFileName method of the Path class is used to extract the folder name.
PrivateSubAddFolders(ByValnodAsTreeNode) DimstrPathAsString=nod.FullPath DimstrDirAsString ForEachstrDirInDirectory.GetDirectories(strPath) ⋮ Next EndSub
Each child node added has its Tag property set to ItemType.Directory. ItemType is a private enumeration we are using to distinguish directories and files in the TreeView. This distinction will be important later in the application.
Withnod.Nodes.Add(Path.GetFileName(strDir)) .Tag=ItemType.Directory .Nodes.Add(DUMMY) EndWith
The AddFiles method is almost identical to the AddFolders method except that it uses the GetFiles method of the Directory class and sets the Tag property of the child nodes to ItemType.File. Also, no Dummy child is added to the children because we don’t want a plus sign to appear next to files in the TreeView.
PrivateSubAddFiles(ByValnodAsTreeNode) DimstrPathAsString=nod.FullPath DimstrFileAsString ForEachstrFileInDirectory.GetFiles(strPath) Withnod.Nodes.Add(Path.GetFileName(strFile)) .Tag=ItemType.File EndWith Next EndSub
Retrieving Directory and File Information
When a user selects an item from the TreeView, that item’s metadata is displayed. The AfterSelect event of the TreeView initiates this process. The Tag property for the selected node is interrogated to determine whether a file or directory item was clicked.
Withe.Node SelectCase.Tag CaseItemType.File ⋮ CaseItemType.Directory
Within each Case statement, various controls are modified and a call to the DisplayFSIProperties method is made. Depending on whether a file or directory was selected, either an instance of the FileInfo or DirectoryInfo class is created and passed into the method call. The code for the file case is as follows:
DimfiAsNewFileInfo(.FullPath) lblLength.Text=fi.Length.ToString DisplayFSIProperties(fi)
The DisplayFSIProperties procedure accepts a single parameter of type FileSystemInfo. Fortunately, both FileInfo and DirectoryInfo inherit from FileSystemInfo, so the procedure can operate on both object types. The procedure simply populates the interface with a variety of properties from the FileSystemInfo instance.
PrivateSubDisplayFSIProperties(ByValfsiAsFileSystemInfo) lblAttributes.Text=fsi.Attributes.ToString lblCreationTime.Text=fsi.CreationTime.ToString lblLastAccessTime.Text=fsi.LastAccessTime.ToString lblLastWriteTime.Text=fsi.LastWriteTime.ToString lblExtension.Text=fsi.Extension lblFullName.Text=fsi.FullName lblName.Text=fsi.Name EndSub
Conclusion
As you can see, not many steps are involved in retrieving file-system information. The majority of the work involved in creating a file-system viewing component deals with maintaining the TreeView control. Keep in mind that creating your own viewer is best suited to situations where you want to limit access to a specific set of directories. If you need an interface that allows a user to select a file or directory anywhere on the system, consider using one of the common dialog controls.
Application #56 Receive File Notifications
Many applications use the file system to maintain information. You can instruct the Framework to notify you when various aspects of the file system change. For example, you can design your application to automatically reload a configuration file whenever that file is updated rather than wait for the next application launch to apply the changes. This is a very useful feature for server applications where restarting the program might not be an option.
Building Upon…
Application #73: Read From and Write To a Text File
New Concepts
The Framework contains a class in the System.IO namespace named FileSystemWatcher. This class allows you to specify what aspects of the file system you want to monitor and raises events when those aspects change. You identify the target directory by setting the Path property, and you specify what files to watch by setting the Filter property. Setting the Filter property to an empty string tells the watcher to watch all files. Assigning the NotifyFilter property an instance of the NotifyFilters enumeration determines what events the watcher will raise. Once all the properties are set, you can enable the watcher by setting the EnableRaisingEvents property to True.
Code Walkthrough
The sample application uses the FileSystemWatcher component to raise events when files in a specified location are manipulated. The list box displays messages from the FileSystemWatcher as you create, modify, and delete files in the specified directory. You can change the type of files to watch and the location to watch in by clicking the Enable Raising Events check box (it looks like a button) and selecting the events from the check-box list.
The chkEvents_CheckedChanged event handler calls the GatherFSWProperties procedure and then enables watching with fsw.EnableRaisingEvents = blnIsRunning. The GatherFSWProperties procedure sets the Path, Filter, IncludeSubdirectories, and NotifyFilter properties of the provided FileSystemWatcher.
PrivateSubGatherFSWProperties(ByValfswAsFileSystemWatcher) fsw.Path=txtPath.Text fsw.Filter=txtFilter.Text fsw.IncludeSubdirectories=chkIncludeSubdirectories.Checked fsw.NotifyFilter=CType(GetChecks(clstNotifyFilter),NotifyFilt ers) EndSub
The GetChecks procedure is a utility function that converts the checked items in the check-box list into an instance of the NotifyFilters enumeration.
Once the FileSystemWatcher is enabled, it will raise events when the specified file-system changes occur. The HandleChangedCreatedDeleted event handler is configured to handle its Changed, Created, and Deleted events and post a message to the lstEvents list box.
PrivateSubHandleChangedCreatedDeleted(ByValsenderAsObject,_ ByValeAsSystem.IO.FileSystemEventArgs)_ Handlesfsw.Changed,fsw.Created,fsw.Deleted DimstrTextAsString=String.Format("{0}was{1}",_ e.Name,e.ChangeType) AddItem(strText) EndSub
Conclusion
Responding to changes in the file system can make your applications more flexible and increase availability. The FileSystemWatcher provides a simple interface for connecting your application to the file system.
Application #57 Use the Event Log
Diagnosing and troubleshooting application errors are important parts of application maintenance. You can increase your ability to accurately diagnose application behavior by coding the application to report important conditions, such as errors or changes in configuration, to an external and convenient data store. The Event Log is an excellent place for such messages because it’s fast, easily accessible, and built into the operating system.
New Concepts
The Windows event log system maintains a set of logs and a list of valid event sources. An event source is simply a string your applications use to identify themselves when sending event messages. Each source maps to a single log. This allows applications some flexibility in sending messages to the event log. An application can explicitly state what log to send the message to, or it can simply provide a source name, letting the operating system decide which log to use.
EventLog Class
The EventLog class in the System.Diagnostics namespace provides an interface for working with the system-defined logs and for creating your own custom logs. To work with a specific log, you create an instance of the EventLog class and provide the name of the log you want to use. You can optionally provide a machine name if the log resides on another machine. Once you have a reference to a log, you can read from it using the Entries property, which returns an EventLogEntryCollection object containing an EventLogEntry instance for each entry in the log. Entry details are available through properties such as UserName, TimeWritten, and Message as well as others. You use the WriteEntry method of the EventLog class to send a message to the log. At a minimum, you must provide a message and a message source, which is typically the name of your application. This method is heavily overloaded, so you can optionally specify a message type, category, and event ID.
You can let the operating system choose which log to use by providing an empty string for the logName parameter of the EventLog constructor. This forces the operating system to pick a log based on the source name you provide when sending a message with the WriteEntry method. The following code demonstrates this approach:
DimelAsNewEventLog("",Environment.MachineName,mySource) el.WriteEntry(myMessage)
In general, applications can write messages to the Application event log. However, you might decide that your application should have its own log. This is useful during development when you might be generating an unusually high number of log messages while testing. It’s also useful in production when you want each application to have its own log.
Tip |
Consider abstracting the event log name to the application configuration file. That way you can easily change what log the application writes to. See “Application #72: Configuration Settings” in Chapter 8 for information on using configuration files. |
Use the shared CreateEventSource method to define a new log. This method requires a log name and the name of an event source that can write to the log. Keep in mind that a source can point to only one log, so if you want to point an existing source to the new log you’ll first need to delete the existing source mapping. The DeleteEventSource method will do this.
Important |
You’ll need to reboot your machine for any event-source mapping changes to take effect. |
Code Walkthrough
The sample application demonstrates how to read from and write to event logs as well as create and delete custom application logs. After adding an entry or creating a new log, click the Read From The Event Log button to verify your changes.
Writing to an Event Log
The btnWriteEntry_Click event handler on frmWrite sends a message to the event log system with a source value of “VB.NET How To: Using the Event Log”. The system will put this message in whatever log is currently mapped to that source. When you first run the application, this is an unmapped source, so the system will automatically map it to the Application log. After you create a custom log and reboot as described later, the messages will go to the custom log.
DimevAsNewEventLog("",Environment.MachineName,_ "VB.NETHowTo:UsingtheEventLog") ev.WriteEntry(txtEntry.Text,entryType,CInt(txtEventID.Text)) ev.Close()
Reading from an Event Log
The btnViewLogEntries_Click event handler in frmRead creates a reference to the log identified in the logType variable. logType is set when you select a log from the list box. You don’t need to specify an event source because this reference will not be used to write to the log.
DimevAsNewEventLog(logType)
Next, we loop backward through the Entries collection and output information for each EventLogEntry. The loop uses a Step value of -1 because the items in the Entries property are ordered by date and we want to display the ten latest entries.
DimentryAsEventLogEntry ⋮ DimiAsInteger Fori=ev.Entries.Count-1ToLastLogToShowStep-1 DimCurrentEntryAsEventLogEntry=ev.Entries(i) rchEventLogOutput.Text&="EventID:"&_ CurrentEntry.EventID&vbCrLf rchEventLogOutput.Text&="EntryType:"&_ CurrentEntry.EntryType.ToString()&vbCrLf rchEventLogOutput.Text&="Message:"&_ CurrentEntry.Message&vbCrLf&vbCrLf Next
Managing Event Logs
The btnCreateLog_Click event handler in frmCreateDelete creates a new log and remaps the “VB.NET How To: Using the Event Log” source to the new log. First, we check the existence of the log and the source. If the source exists, we must delete it before it can be mapped to the new log.
IfEventLog.SourceExists("VB.NETHowTo:UsingtheEventLog")Then EventLog.DeleteEventSource("VB.NETHowTo:UsingtheEventLog") EndIf
Now we can create the new log and map the source to it with a call to CreateEventSource.
EventLog.CreateEventSource("VB.NETHowTo:UsingtheEventLog",&_ txtLogNameToCreate.Text)
Remember, if you send a message to the event log with this source, it will continue to go to the Application log until you reboot the machine.
The btnDeleteLog_Click event handler in frmCreateDelete checks the existence of the specified log and deletes it.
IfEventLog.Exists(txtLogNameToDelete.Text)Then EventLog.Delete(txtLogNameToDelete.Text)
Conclusion
Reporting application status to the Event Log allows users and administrators to more easily troubleshoot your applications. The degree to which the event log is useful is dependent on the quality of information you choose to have your application place in it. Fortunately, the EventLog class makes it very easy to supply rich error data with little impact on performance.
Application #58 Read and Write Performance Counters
Performance counters provide an extremely valuable mechanism for monitoring the performance and health of a machine or a specific application. Your applications can even create their own custom counters in which to output detailed performance metrics.
Building Upon…
Application #1: Use Arrays
Application #7: Object-Oriented Features
New Concepts
The System.Diagnostics namespace contains classes for working with performance counters. Counters are organized into categories, and there can be multiple instances within a category. Each instance contains its own set of counters. For example, there is a category named .NET CLR Data. This category maintains a default instance named _global_ and another instance for each process hosted by the runtime. Each instance contains its own set of counters, such as “SqlClient: Total # failed connects”, and the _global_ instance maintains totals across all instances. When retrieving counter data, you must first get a reference to a category with the PerformanceCounterCategory class. You can call the GetCounters method to retrieve an array of PerformanceCounter objects for that category. If the category contains multiple instances, you must provide the instance name to the GetCounters method. The following code demonstrates this:
DimcatAsPerformanceCounterCategory=_ NewPerformanceCounterCategory(".NETCLRData") DimcountersAsPerformanceCounter()=cat.GetCounters("someInstance ")
Once you have a PerformanceCounter instance, you can retrieve its value by calling the NextValue method. If the counter is a custom counter, you can modify its value with the Increment, IncrementBy, and Decrement methods.
Code Walkthrough
The sample application demonstrates how to enumerate through all the performance counters on a system and how to modify the values of custom counters. You’ll also see how to determine whether a counter is built-in or custom.
Retrieving Counter Information
The frmMain_Load event handler populates the cboCategories combo box with the names of all the performance counter categories. First, we retrieve the categories on the system by calling the shared GetCategories method.
DimmyCategories()AsPerformanceCounterCategory myCategories=PerformanceCounterCategory.GetCategories()
Next, the name of each category is added to the myCategoryNames array.
DimmyCategoryNames(myCategories.Length-1)AsString DimiAsInteger=0'Usedasacounter ForEachmyCategoryInmyCategories myCategoryNames(i)=myCategory.CategoryName i+=1 Next
After sorting the array, each name is added to the combo box.
Array.Sort(myCategoryNames) DimnameStringAsString ForEachnameStringInmyCategoryNames Me.cboCategories.Items.Add(nameString) Next
The cboCategories_SelectedIndexChanged event handler runs when you select a category from the category combo box. The first step in retrieving all the counters for a category is to determine whether the category has any instances. We do this by calling the GetInstanceNames method and checking the length of the returned array. If the length is zero, no instances are defined and we can just retrieve the counters by calling GetCounters.
myCategory=NewPerformanceCounterCategory(_ Me.cboCategories.SelectedItem.ToString()) myCounterNames=myCategory.GetInstanceNames() IfmyCounterNames.Length=0Then myCounters.AddRange(myCategory.GetCounters())
However, if there are instances defined, we must loop through the instances and retrieve each instance’s counters by passing the instance name into GetCounters.
Else DimiAsInteger Fori=0TomyCounterNames.Length-1 myCounters.AddRange(_ myCategory.GetCounters(myCounterNames(i))) Next EndIf
Now that the myCounters array is populated, we can add the items to the cboCounters combo box. However, we have created a utility class named CounterDisplayItem. This class wraps a PerformanceCounter instance and overrides the ToString method to return instance and counter name information as a single string. For each counter in our array, we create an instance of CounterDisplayItem populated with the current counter and add it to the combo box. When the combo box renders, it calls the ToString method on each of its items.
Me.cboCounters.Items.Clear() Me.cboCounters.Text="" ForEachmyCounterInmyCounters Me.cboCounters.Items.Add(NewCounterDisplayItem(myCounter)) Next
The cboCounters_SelectedIndexChanged event handler retrieves the counter from the currently selected CounterDisplayItem and outputs the counter’s properties to the form.
DimmyCounterDisplayAsCounterDisplayItem myCounterDisplay=CType(cboCounters.SelectedItem,CounterDisplayIte m) m_Counter=myCounterDisplay.Counter Me.txtCounterType.Text=m_Counter.CounterType.ToString() Me.txtCounterHelp.Text=m_Counter.CounterHelp.ToString() Me.sbrStatus.Text=""
The controls for modifying counter values are enabled or disabled depending on whether the current counter is a custom counter, because only custom counters can be modified. Unfortunately, there is no IsCustom property on the PerformanceCounter class. Instead, we have created an IsCustom property in the CounterDisplayItem utility. This method sets the counter’s ReadOnly property to false and then attempts to retrieve the counter’s value. This operation will throw an error if the counter is not a custom counter.
DimisReadOnlyAsBoolean=m_Counter.ReadOnly Try m_Counter.ReadOnly=False m_Counter.NextValue() ReturnTrue CatchexcAsException ReturnFalse Finally m_Counter.ReadOnly=isReadOnly EndTry
Modifying Counter Values
Once you have identified that a counter is a custom counter, you can modify its value using the Increment and Decrement methods of the PerformanceCounter class. These each modify the current value by 1. If you want to modify by a value larger than 1, you can use the IncrementBy method, supplying negative numbers if you want to decrement the counter.
Conclusion
Performance counters offer an easy way for your applications to export real-time performance and fault metrics to the operating system. The types in the System.Diagnostics namespace provide a complete framework for working with these counters and even creating your own.
Application #59 Use the Process Class and Shell Functionality
This topic discusses how to start additional processes from a .NET application. This can be useful in scenarios where one application needs to selectively enlist the help of another application or service.
Building Upon…
Application #55: Use the File System
Application #73: Read From and Write to a Text File
New Concepts
The System.Diagnostics namespace contains the Process class, which contains a variety of methods for spawning processes. The easiest way to launch a process is to call the shared Start method and provide the file path to the executable or file of interest. You can specify any file type that has an executable mapped to its Open action. The Start method is overloaded, so you can optionally specify command-line arguments to pass to the new process. If you want even greater control over how the new process is started, you can create an instance of the ProcessStartInfo class and pass it into the Start method. The ProcessStartInfo class provides many properties that affect how an application is launched. The WindowStyle property determines how the new process will display: Maximized, Minimized, Normal, or Hidden. The Arguments property provides another way to supply command-line arguments. An especially powerful member is the Verb property. Setting this property allows you to perform an action on the file other than opening it. The only restriction is that the verb you supply must be defined for that file type on the system.
Code Walkthrough
The sample application demonstrates how to use the Process class to launch applications. The btnStartProcess_Click event handler simply launches Notepad using the shared Start method.
Process.Start("notepad.exe")
The btnProcessStartInfo_Click event handler also launches Notepad, but it uses the ProcessStartInfo class to specify that it should be maximized.
DimstartInfoAsNewProcessStartInfo("notepad.exe") startInfo.WindowStyle=ProcessWindowStyle.Maximized Process.Start(startInfo)
The btnUseVerb_Click event handler creates a text file and then creates a ProcessStartInfo object for the file.
DimswAsNewSystem.IO.StreamWriter("demofile_shell.txt") sw.WriteLine("Eureka!You'veprinted!") sw.Close() DimstartInfoAsNewProcessStartInfo("demofile_shell.txt")
Next, we indicate that we want to print the document instead of opening it.
startInfo.Verb="print"
This time, when we start the process we’ll maintain a reference to the returned Process instance so that we can call WaitForExit, which will force our application to block until the printing finishes.
DimpAsProcess=Process.Start(startInfo) p.WaitForExit()
Finally, we can delete the sample file and display a message box with the ExitCode of the process. In general, an exit code of 0 indicates a success and a nonzero number indicates some error condition.
System.IO.File.Delete("demofile_shell.txt") MessageBox.Show("Printingfinishedwithanexitcodeof"+_ p.ExitCode.ToString())
The btnCommandLine_Click event handler uses the Arguments property of the ProcessStartInfo object to send a command-line argument to a new instance of Windows Explorer.
DimstartInfoAsNewProcessStartInfo("explorer.exe") startInfo.Arguments="/n" Process.Start(startInfo)
Conclusion
The Process class provides a simple way to interact with other applications with very little overhead. This is especially convenient for interaction with applications whose only automation interface might be through the command line.
Application #60 View Process Information
Viewing process information can be an extremely useful feature for administrative tools and utilities. With the appropriate information, you can better diagnose problematic machine behavior. For example, you can identify how much of your system’s resources a particular application is consuming, such as memory or processor time.
Building Upon…
Application #3: String Manipulation
Application #35: Create an Explorer-Style Application
New Concepts
The Process class in the System.Diagnostics namespace provides a detailed interface for retrieving information about the processes currently running on a machine. The shared GetCurrentProcess, GetProcessByID, and GetProcessByName methods each return a single Process object. The GetProcesses method, however, returns an array of Process objects, each associated with the appropriate resource. You can identify how much processor time a process is using with the PrivilegedProcessorTime, UserProcessorTime, and TotalProcessorTime properties. PrivilegedProcessorTime is the amount of CPU time a processor spends executing core operating system code, while UserProcessorTime is the amount of time spent executing application code. TotalProcessorTime equals the sum of the two. If memory usage is what you’re interested in, you should use the WorkingSet property to determine how much physical memory a process is consuming.
The Process class has an unusual behavior related to its property values. The properties of the class are organized into groups. The first time you access a member of a group, the class retrieves all the property values for the group and caches them. The group’s values are not updated again until you call the Refresh method.
Sometimes, you might need to know how specific threads or modules within a process are affecting the system. The Process class has a Threads property, which returns an array of ProcessThread objects containing thread-specific CPU usage properties. The Process class’s Modules property returns an array of ProcessModule objects identifying each module’s file location and memory consumption.
Code Walkthrough
The sample application demonstrates how to use the Process class to view information about all the running processes on a machine. Clicking one of the listed processes displays resource-use statistics in the right pane and thread information in the bottom pane. You can view which code modules a process has loaded by right-clicking the process and choosing View Modules.
Reading Process Data
The frmMain_Load event handler calls the EnumProcesses method, which retrieves an array of process objects by calling the shared GetProcesses method of the Process class.
DimProcesses()AsProcess ⋮ Processes=Process.GetProcesses()
Each process is then added to the module-level collection mcolProcesses, and some CPU usage–related properties are retrieved.
ForEachpInProcesses mcolProcesses.Add(p,p.Id.ToString()) tppt=p.PrivilegedProcessorTime tupt=p.UserProcessorTime tpt=p.TotalProcessorTime
The statistics of the current process are added to variables to maintain some total usage information.
mtpt=mtpt.Add(tpt) mtppt=mtppt.Add(tppt) mtupt=mtupt.Add(tupt)
After some formatting of the CPU usage numbers, the statistics are added to the lvProcesses list view control.
WithMe.lvProcesses.Items.Add(p.ProcessName&"(0x"&_ Hex(p.Id).ToLower()&")") .SubItems.Add(p.Id.ToString()) .SubItems.Add(strTPT) .SubItems.Add(strPPPT) .SubItems.Add(strPUPT) EndWith
Finally, a custom entry is added to the list view containing the calculated CPU usage totals.
⋮ WithMe.lvProcesses.Items.Add(PROCESS_NAME_TOTAL) .SubItems.Add(PID_NA) .SubItems.Add(mstrTPT) .SubItems.Add(mstrPPPT) .SubItems.Add(mstrPUPT) EndWith
Selecting a process from the list view invokes the lvProcesses_SelectedIndexChanged event handler. This procedure retrieves the process ID from the selected item and then resets some interface properties.
DimlvAsListView=CType(sender,ListView) Iflv.SelectedItems.Count=1Then DimstrProcessIdAsString=lv.SelectedItems(0).SubItems(1).Tex t ⋮
Next, the appropriate process is retrieved from the process collection and passed into calls to EnumProcess and EnumThreads.
p=CType(mcolProcesses.Item(strProcessId),Process) p.Refresh() EnumProcess(p) EnumThreads(p)
The EnumProcess method makes a series of calls to the AddNameValuePair method to add numerous process properties to the lvProcessDetail list view. Many of these property calls are wrapped in Try/Catch blocks to handle various exceptions.
⋮ DimlvAsListView=Me.lvProcessDetail lvProcessDetail.Items.Clear() mits=lvProcessDetail.Items ConstNAAsString="NotAuthorized" Try AddNameValuePair("StartTime",p.StartTime.ToLongDateString( )&_ ""&p.StartTime.ToLongTimeString()) AddNameValuePair("Responding",p.Responding.ToString())
The EnumThreads method outputs the properties of each ProcessThread in the process’s Threads collection of the process. CPU usage statistics, priority settings, and thread start time are output to the lvThreads list view control.
ForEachtInp.Threads tppt=t.PrivilegedProcessorTime ⋮ WithMe.lvThreads.Items.Add(t.Id.ToString()) .SubItems.Add(t.BasePriority.ToString()) .SubItems.Add(t.CurrentPriority.ToString()) ⋮
The mnuModules_Click event handler retrieves the appropriate process from the process collection and calls its Refresh method to ensure we get up-to-date information.
⋮ DimpAsProcess p=CType(mcolProcesses.Item(strProcessId),Process) ⋮ p.Refresh()
Next, a call to the Count property of the Modules property is used to determine whether you are able to access the modules for the process. If not, this action will result in a Win32Exception.
Try DimiAsInteger=p.Modules.Count CatchexpAsSystem.ComponentModel.Win32Exception MessageBox.Show("Sorry,youarenotauthorizedtoreadthisŒ information.",Me.Text,MessageBoxButtons.OK,_ MessageBoxIcon.Exclamation) ExitSub EndTry
The actual work for displaying the module data occurs in frmModules. This form has a ParentProcess property to which we assign the current process reference before making a call to RefreshModules.
IfmfrmModIsNothingThen mfrmMod=NewfrmModules() EndIf mfrmMod.ParentProcess=p mfrmMod.RefreshModules() mfrmMod.ShowDialog(Me)
The RefreshModules method resets the interface and calls the EnumModules method.
Me.sbInfo.Text="Process="&mParentProcess.ProcessName Me.lvModDetail.Items.Clear() EnumModules()
EnumModules loops through the array returned by the Modules property, adding each ProcessModule to the mcolModules collection and each module name to the lvModules list view.
⋮ DimmAsProcessModule ForEachmInmParentProcess.Modules Me.lvModules.Items.Add(m.ModuleName) Try mcolModules.Add(m,m.ModuleName) ⋮
Upon selecting a module, the lvModules_SelectedIndexChanged event handler calls EnumModule, which clears the lvModDetail list view and displays a set of the ProcessModule property values.
Me.lvModDetail.Items.Clear() Try AddNameValuePair("BaseAddress",Hex(m.BaseAddress.ToInt32).ToLo wer()) AddNameValuePair("EntryPointAddress",_ Hex(m.EntryPointAddress.ToInt32).ToLower()) ⋮
Conclusion
The system’s Task Manager is useful for viewing process statistics but does not expose thread or module details. It is also viewable only on the local machine. With the Process class, you have greater control over what information you want to expose and through what interface you want to expose it.
Application #61 Use WMI
The Windows Management Instrumentation (WMI) framework provides a standardized model for retrieving operating system, hardware, and application information. Using WMI can reduce the number of unique APIs you need to learn and simplify your coding effort.
Building Upon…
Application #35: Create an Explorer-Style Application
New Concepts
WMI is built into the Windows operating systems and is accessible through the classes in the System.Management namespace. WMI is intended to provide a single API for retrieving system data across an enterprise. Each type of resource that exposes data through WMI is identified by a WMI Class. For example, Win32_DisplayConfiguration describes a system’s display capabilities, and Win32_NetworkAdapter describes the properties of a physical network adapter. You retrieve data through WMI by executing queries that indicate the class of data you’re interested in. The ManagementObjectSearcher provides one way to submit a query. The constructor can take a query in the form of a string or an ObjectQuery instance. Call the ManagementObjectSearcher Get method to execute the query. The Get method returns an instance of the ManagementObjectCollection containing one ManagementObject instance for each entity matching your query criteria. The ManagementObject contains a key/value collection named Properties from which you can retrieve information about that entity. The following code shows how to issue a query and display a property for each returned item:
DimsQueryasstring="SELECT*FROMWin32_networkadapter" DimmosSearcherAsNewManagementObjectSearcher(sQuery) DimmoItemAsManagementObject ForEachmoItemInmosSearcher.Get() MessageBox.Show(moItem.Properties("ProductName").Value.ToString( )) Next
Similar to database access, some WMI queries can return large amounts of data and take a significant amount of time to execute. In these cases, it’s sometimes desirable to execute the query asynchronously. The ManagementObjectSearcher Get method has an overload that accepts a ManagementOperationObserver instance as a parameter. A call to this overload does not block, allowing your application to continue executing. The ManagementOperationObserver raises events as the status of the query changes. You must make sure you have connected some event handlers before you execute the Get method.
Code Walkthrough
The sample application demonstrates how to retrieve some commonly used information about the operating system, machine BIOS, and hardware. The Asynchronous Enumeration tab shows how to perform asynchronous queries, and the WMI Classes tab shows how to retrieve a list of all the WMI classes installed on the system.
Synchronous Queries
The btnOperatingSytem_Click event handler defines a query as a string to retrieve operating system information with the Win32_OperatingSystem class.
DimsearchAsNew_ ManagementObjectSearcher("SELECT*FROMWin32_OperatingSystem")
The results of the query are output to the txtOutput text box.
DiminfoAsManagementObject ForEachinfoInsearch.Get() txtOutput.Text="Name:"&info("name").ToString()&CRLF ⋮ Next
The btnProcessor_Click event handler is almost identical except it uses a SelectQuery instance to define a query to retrieve processor information. The SelectQuery object reduces the complexity of the query you need to define by requiring only the class of the entities you want to retrieve.
DimqueryAsNewSelectQuery("Win32_processor") DimsearchAsNewManagementObjectSearcher(query) ⋮
Asynchronous Queries
The btnStartEnum_Click event handler asynchronously executes a query by connecting the OnEnumObjectReady method as an event handler to a ManagementOperationObserver object. After defining the query, you should create an instance of the ManagementOperationObserver and attach event handlers to any of the events you’re interested in. The ObjectReady event fires once for each object returned by the query and will be handled by the OnEnumObjectReady method.
⋮ DimobserverAsNewManagementOperationObserver() AddHandlerobserver.ObjectReady,AddressOfOnEnumObjectReady
When you’re ready to execute the query, pass the ManagementOperationObserver instance to the ManagementObjectSearcher Get method.
search.Get(observer)
The OnEnumObjectReady procedure uses the ObjectReadyEventArgs parameter NewObject property to reference the returned ManagementObject.
IfNotIsNothing(e.NewObject("VolumeName"))Then item.SubItems.Add(e.NewObject("VolumeName").ToString()) item.SubItems.Add(e.NewObject("Size").ToString()&"bytes") Ife.NewObject("FreeSpace").ToString()<>"0"Then item.SubItems.Add(e.NewObject("FreeSpace").ToString()&"by tes") Else item.SubItems.Add("(none)") EndIf EndIf
Conclusion
The WMI framework can be leveraged to expose a tremendous amount of information about systems on your enterprise. The majority of the work involved in querying the WMI data stores is handled by the classes in the System.Management namespace, allowing you to focus on learning the names of the specific WMI entities and properties your applications need.
Application #62 Respond to System Events
A robust application should be able to react automatically to changes in system configuration. Many aspects of a system can change at run time and affect how your application operates. Designing your application to automatically react to these changes makes it easier to use and more reliable.
New Concepts
The Microsoft.Win32 namespace contains a class named SystemEvents. This class exposes many shared events that are raised in response to various system changes. The UserPreferenceChanged event is raised when a user modifies system properties such as mouse, keyboard, or display appearance settings. The LowMemory event notifies your application when the system is running out of free RAM. Other events notify you of a system shutdown, user log off, and system time changes.
You can attach to these events using the normal Handles statement, but you might want some flexibility in determining when your application should respond to these events. The AddHandler and RemoveHandler functions allow you to selectively attach and detach event handlers to a particular event.
Important |
AddHandler and RemoveHandler can be used with any event, not just those exposed by SystemEvents. |
AddHandler requires a reference to an event for the first parameter and a delegate to an event handler as the second parameter. You can manually create the delegate or use the AddressOf operator to automatically create the delegate for you. You must make sure the signature of the event handler is identical to that of the event you are attaching to. Use the RemoveHandler function to detach an event handler from an event using the same parameters as your call to AddHandler.
Code Walkthrough
The sample application uses the AddHandler and RemoveHandler functions to dynamically attach and detach from the shared events exposed by the SystemEvents class. The application will display a notification message in the text box for any system event that is selected in the list of check boxes. For example, check the Handle Time Changes check box and then change the system time through the Date/Time Properties control panel. A message will appear indicating receipt of the system event.
The chkTimeChanges_CheckedChanged event handler uses the AddHandler function to assign the TimeHandler procedure as an event handler for the SystemEvents.TimeChanged event.
PrivateSubchkTimeChanges_CheckedChanged(ByValsenderAsSystem.Obj ect,ByValeAsSystem.EventArgs)HandleschkTimeChanges.CheckedChan ged IfchkTimeChanges.CheckedThen AddHandlerSystemEvents.TimeChanged,_ AddressOfTimeHandler
If the check box is deselected, we remove the TimeHandler procedure from the SystemEvent.TimeChanged event handler list with a call to the RemoveHandler function.
RemoveHandlerSystemEvents.TimeChanged,_ AddressOfTimeHandler
The application performs an identical set of actions for each demonstrated system event.
Conclusion
Responding to system events requires very little code because the SystemEvents class handles all low-level communication with the operating system. Although their use is not required, the AddHandler and RemoveHandler functions can increase application performance by allowing you to selectively respond to events only when necessary.
Application #63 Use the COM Port
Modern computers contain many types of interfaces to communicate with hardware devices. Even with newer interfaces like USB and FireWire, the COM port continues to be heavily supported by hardware manufacturers for devices ranging from home PC peripherals to industrial factory machines. Communicating with COM ports is a great way to extend the power of your applications beyond the desktop.
Building Upon…
Application #7: Object-Oriented Features
Application #79: Use Thread Pooling
New Concepts
Unlike the other topics in the chapter, there is no Framework namespace or class for communicating with COM ports. Unfortunately, this means the only means .NET provides is through calls directly to the operating system. Even worse, you need to be familiar with some 22 system functions to successfully open, write to, read from, and close a COM port. On the bright side, this topic and its associated sample application provide a wrapper class that has all the operating system code already written. This class is named after the original specification developed in the 1960s, Rs232.
Rs232 Class
You need to be familiar with a number of hardware communication topics—such as parity, baud rate, and stop bits—to use the system APIs for COM port communication. Fortunately, the Rs232 class handles these nasty details and requires only that you assign the Port property an integer to identify the specific COM port you want to work with and call the Open method. You can override the default settings through properties such as BaudRate, BufferSize, and Parity. The Open method throws an exception if it’s unable to connect to the port.
The Write method allows you to send a String or Byte array to the connected device and does not return a value. If you expect your device to respond, you need to set up a mechanism to monitor for responses. The Read method attempts to read the specified number of bytes from the port. It returns an integer indicating how many bytes were actually read, with a -1 indicating that no data was read. The new data can be retrieved through the InputStream property. A call to Read throws an exception if the port isn’t in a valid state for reading, such as being closed before the Read method is called.
Although you don’t need to explicitly make Win32 API calls to use the Rs232 class, an understanding of the basic mechanics will help you should you choose to dive into the inner workings of the class. As mentioned earlier, Rs232 uses a number of functions in the Kernel32 system DLL. Kernel32 is known as a C-style DLL because it doesn’t expose any of the COM interfaces. To gain access to its functions, you have to use the DllImportAttribute class defined in the System.Runtime.InteropServices namespace. First, you declare a function with a signature matching the DLL’s function. You can then place the DllImport attribute on the function declaration and indicate what DLL the function resides in. The following code imports the GetLastError function from Kernel32:
PrivateSharedFunctionGetLastError()AsInteger EndFunction
Once a function is imported, you can call it just like any other .NET function. The Framework’s Platform Invoke system handles the marshalling of parameters and return values to the DLL and back.
Code Walkthrough
The sample application uses the Rs232 class to check for available ports, check for modems on those ports, and send messages to a modem if one is found. The frmMain Form maintains an Rs232 instance in a module-level variable named m_CommPort. The btnCheckForPorts_Click event handler uses this instance to check whether any devices are connected to COM ports 1 through 4 by calling the IsPortAvailable function for each port number. The IsPortAvailable function attempts to open a connection to the specified port by invoking the Open method of the m_CommPort object.
Try m_CommPort.Port=ComPort m_CommPort.Open() m_CommPort.Close() ReturnTrue Catch ReturnFalse EndTry
The btnCheckModems_Click event handler calls the IsPortAModem method for each of the available ports. This is accomplished by opening the port and sending an “AT” command to the device with the Write method. The string “AT” is converted to an ASCII byte array as it is passed into the Write method.
m_CommPort.Port=ComPort m_CommPort.Open() m_CommPort.Write(Encoding.ASCII.GetBytes("AT"&Chr(13)))
A modem will acknowledge the “AT” command with an “OK”. This procedure blocks execution for 200 milliseconds to give the device a chance to respond.
System.Threading.Thread.Sleep(200) Application.DoEvents()
You can call the Read method to determine whether the device responded. If it didn’t, the Read method will throw an error. In this case, it doesn’t matter what data was returned by the device, so retrieving data from InputBuffer is not necessary.
Try DimbAsByte m_CommPort.Read(1) m_CommPort.ClearInputBuffer() m_CommPort.Close() ReturnTrue CatchexcAsException m_CommPort.Close() ReturnFalse EndTry
The btnSendUserCommand_Click event handler opens the port that a modem was found on and executes whatever command you enter in the txtUserCommand TextBox. Some valid commands to try are ATI3, ATI4, and ATI7. These all return information about the installed modem.
m_CommPort.Port=(m_ModemPort) m_CommPort.Open() m_CommPort.Write(Encoding.ASCII.GetBytes(Me.txtUserCommand.Text&Ch r(13)))
After sending the message, you need to enable some mechanism to watch for responses. You could manually start another thread or use the Timer control. This procedure uses a Timer instance named tmrReadCommPort.
Me.tmrReadCommPort.Enabled=True
The tmrReadCommPort_Tick event handler fires every 100 milliseconds and attempts to read from the port. If no data is available, the Catch block simply catches the thrown exception. If data is available, it is read in one byte at a time and displayed in the txtStatus TextBox by the WriteMessage procedure.
While(m_CommPort.Read(1)<>-1) 'Writetheoutputtothescreen. WriteMessage(Chr(m_CommPort.InputStream(0)),False) m_ResponseReceived=True EndWhile
After outputting the received data, the event handler finishes by closing the port and disabling the timer and enabling the command button.
Conclusion
Although the details of COM port communication are rather involved, the Rs232 class encapsulates most of that complexity and provides an easy-to-use interface.
Application #64 Interact with Services
Although Windows server operating systems provide mechanisms for viewing and manipulating the services running on a computer, you might need to write your own code to manage some services. Server applications often need to ensure that prerequisite services exist and are running before executing, or you might have a system that needs to periodically send custom commands to a service.
Building Upon…
Application #78: Create a Windows Service
New Concepts
The System.ServiceProcess namespace contains a class named ServiceController. This class allows you to interrogate and manipulate Windows service applications running on your network. Using the constructor, you specify the name of the service and the machine it’s running on. The returned instance exposes information about the state of the service through properties such as Status and ServiceType. You can start, stop, pause, and resume a service by using the like-named methods. There are also properties that tell you whether the service can be manipulated, such as CanStop and CanPauseContinue. You should check these properties before attempting to change the service.
The ExecuteCommand method allows you to tell the service to perform some predefined task. The service doesn’t have to be running to call this method. ExecuteCommand does not return a value, and the only input parameter is an integer indicating what command you want to execute. The commands available are determined by the service developer.
Code Walkthrough
The sample application demonstrates how to create your own service administration program. It displays a list of all the installed services and their current status. You can start, pause, resume, and stop services using the provided buttons. The application starts by calling the EnumServices procedure when the form loads. EnumServices retrieves an array of ServiceController objects by calling the shared GetServices method on the ServiceController class.
⋮ DimsvcAsServiceController DimsvcsAsServiceController()=ServiceController.GetServices()
You can display the service information by iterating over the array and adding items to the lvServices ListView control.
ForEachsvcInsvcs WithMe.lvServices.Items.Add(svc.DisplayName) .SubItems.Add(svc.Status.ToString()) .SubItems.Add(svc.ServiceType.ToString()) EndWith ⋮
You should also persist references to the ServiceControllers in a separate collection.
⋮ mcolSvcs.Add(svc,svc.DisplayName) Nextsvc
Selecting an item in the lvServices list view executes the UpdateUIForSelectedService procedure, which retrieves the appropriate ServiceController from the mcolSvcs collection based on the name of the selected item.
⋮ strName=lvServices.SelectedItems(0).SubItems(0).Text mSvc=CType(mcolSvcs.Item(strName),ServiceController)
The Enabled state of the command buttons are set based on the values of the CanStop, CanPauseAndContinue, and Status properties.
WithmSvc cmdStart.Enabled=(.Status=ServiceControllerStatus.Stopped) cmdStop.Enabled=(.CanStopAndAlso_ (Not.Status=ServiceControllerStatus.Stopped)) cmdPause.Enabled=(.CanPauseAndContinueAndAlso_ (Not.Status=ServiceControllerStatus.Paused)) cmdResume.Enabled=(.Status=ServiceControllerStatus.Paused) EndWith ⋮
The form has a Timer control named tmrStatus. The tmrStatus_Tick event handler calls the UpdateServiceStatus procedure, which loops through our persisted ServiceController collection calling the Refresh method on each controller and updating that item’s status in the list view.
⋮ DimlviAsListViewItem ForEachlviInMe.lvServices.Items mSvc=CType(mcolSvcs.Item(lvi.Text),ServiceController) mSvc.Refresh() lvi.SubItems(1).Text=mSvc.Status.ToString() Nextlvi ⋮
Each of the command buttons executes the appropriate method on the selected ServiceController and displays any exceptions in a message box.
Conclusion
Creating your own service manipulation scripts or applications can simplify an administrator’s task of managing your application, and the ServiceController class makes this very easy.
Application #65 Interact with a Windows Service
The Windows Registry provides a convenient place to store user-configuration settings. While .NET provides facilities for application-configuration files, these files have no concept of user-specific settings. The Registry, on the other hand, has a structure well suited for storing user-specific information.
Building Upon…
Application #35: Create a Windows Explorer–Style Application
Application #67: Understand the Garbage Collector
New Concepts
The Microsoft.Win32 namespace provides the Registry class as an entry point to the system Registry. The Registry class contains shared properties for each of the Registry’s major hives: CurrentUser, LocalMachine, ClassesRoot, Users, and CurrentConfig. Each of these properties returns an instance of the RegistryKey class. The RegistryKey class has properties and methods for reading and manipulating the contents of a key. The names of a key’s subkeys and values can be retrieved by the GetSubKeyNames and GetValueNames methods, respectively. These methods return an array of strings. The OpenSubKey method returns another RegistryKey instance when provided with the name of the subkey to open. The GetValue method returns the data in the registry for the specified value. You can modify the Registry structure with the CreateSubKey, DeleteSubKey, SetValue, and DeleteValue methods.
Code Walkthrough
The sample application allows you to browse your system Registry and modify the string values of existing keys. The AddChildNode procedure accepts a TreeNode and a RegistryKey as parameters. The Name property of the key is provided to the node’s constructor so that it will be displayed as the text for the node. A reference to the key itself is persisted in the node’s Tag property for future use.
DimnewNodeAsNewTreeNode(key.Name) newNode.Tag=key
If the key has subkeys, a placeholder child node is added to the current node. This placeholder ensures that an expand icon will appear next to the node in the TreeView.
Ifkey.SubKeyCount>0Then newNode.Nodes.Add("placeholder") EndIf
Finally, the new node is added as a child of the provided parent node.
parent.Nodes.Add(newNode)
The frmMain_Load event handler makes a call to AddChildNode for each root-level key in the registry.
AddChildNode(tvReg.TopNode,Registry.ClassesRoot) ⋮
Expanding an item causes the tvReg_BeforeExpand event handler to fire before the item is expanded on screen. This procedure retrieves the RegistryKey instance stored in the selected item’s Tag property and adds a child node for each of that instance’s subkeys. Before adding the child nodes, you should clear the Nodes collection because it currently contains the placeholder node added earlier.
⋮ DimkeyAsRegistryKey=CType(e.Node.Tag,RegistryKey) node.Nodes.Clear() DimsubKeyNameAsString ForEachsubKeyNameInkey.GetSubKeyNames() Try AddChildNode(node,key.OpenSubKey(subKeyName,True)) Catch EndTry Next
The tvReg_AfterCollapse event handler fires when you collapse an expanded item. In an effort to conserve memory, this application clears the Nodes collection of the collapsed node. This relinquishes all those child nodes and the RegistryKey instances associated with them to the garbage collector.
DimnodeAsTreeNode=e.Node Ifnode.Text="Registry"ThenExitSub node.Nodes.Clear() node.Nodes.Add("placeholder")
A call to the garbage collector Collect method forces that memory to be returned to the system.
GC.Collect()
Tip |
With Task Manager running, repeatedly expand and collapse the HKEY_CLASSES_ROOT node and monitor the application’s memory usage. Do so again with the GC.Collect line commented out. |
When you select a key in the TreeView, the tvReg_AfterSelect event handler displays that key’s values in the lvValues ListView to the right. The GetValueNames method of the current RegistryKey returns an array of strings. For each name in the array, you can call the key’s GetValue method and place the return from ToString in the ListView.
⋮ DimkeyAsRegistryKey=CType(e.Node.Tag,RegistryKey) Ifkey.ValueCount>0Then DimsValNameAsString DimsValAsString ForEachsValNameInkey.GetValueNames() sVal=key.GetValue(sValName,String.Empty).ToString() WithlvValues.Items.Add(sValName) .SubItems.Add(sVal) EndWith Next EndIf
You can double-click an entry in the ListView to edit that value. The lvValues_DoubleClick event handler determines whether the selected value is an array. This application does not support editing an array, so it simply exits the procedure.
⋮ IflvValues.SelectedItems.Count=1Then DimsValNameAsString=lvValues.SelectedItems(0).Text DimsValAsString=lvValues.SelectedItems(0).SubItems(1).Text IfsVal="System.Byte[]"OrsVal="System.String[]"ThenExit Sub
If the value is not an array, the user is presented with an instance of the frmEditValue dialog box and the returned value is written to the Registry by using the SetValue method of the current RegistryKey.
⋮ DimkeyAsRegistryKey=CType(tvReg.SelectedNode.Tag,RegistryKey) key.SetValue(sValName,sVal)
The procedure ends by updating the ListView with the new setting value.
lvValues.SelectedItems(0).SubItems(1).Text=sVal
Conclusion
The Registry and RegistryKey classes provide a simple mechanism for working with the system Registry. While application-configuration files provide a convenient place for most of an application’s general settings, the Registry is often better suited for user-specific information.