The Microsoft .NET Framework is intended to run on a wide variety of operating systems to improve code mobility and simplify cross-platform integration. At the time this book was written, versions of the .NET Framework were available for various operating systems, including Microsoft Windows, FreeBSD, Linux, and Mac OS X. However, many of these implementations have yet to be widely adopted. Microsoft Windows is currently the operating system on which the .NET Framework is most commonly installed. Therefore, the recipes in this chapter describe how to perform the following tasks, specific to the Windows operating system:
Retrieve runtime environment information (recipes 14-1 and 14-2)
Write to the Windows event log (recipe 14-3)
Read, write, and search the Windows registry (recipe 14-4 and 14-5)
Create and install Windows services (recipes 14-6 and 14-7)
Create a shortcut on the Windows Start menu or desktop (recipe 14-8)
Create Windows 7 Jump Lists (recipe 14-9)
Use the Windows Search service (recipe 14-10)
Check Internet connectivity (recipe 14-11)
Display a task dialog (recipe 14-12)
Read and write performance counters (recipes 14-13 and 14-14)
Obtain elevated privileges (recipe 14-15)
The majority of functionality discussed in this chapter is protected by code access security permissions enforced by the Common Language Runtime (CLR). See the .NET Framework software development kit (SDK) documentation for the specific permissions required to execute each member.
The static Environment
class provides a set of static members that you can use to obtain (and in some cases modify) information about the environment in which an application is running. Table 14-1 describes some of the most commonly used Environment
members.
Table 14.1. Commonly Used Members of the Environment Class
Member | Description |
---|---|
Properties | |
| Gets a |
| Gets and sets a |
| Gets a |
| Gets a |
| Gets a |
| Gets the number of processors on the machine. |
| Gets a |
| Gets an |
| Gets a |
| Gets a |
| Gets a |
| Gets a |
Methods | |
| Replaces the names of environment variables in a |
| Returns a |
| Returns a |
| Returns an object implementing |
| Returns a |
| Returns a |
The System.OperatingSystem
object returned by OSVersion
contains four properties:
The Platform
property returns a value of the System.PlatformID
enumeration identifying the current operating system; valid values are Unix, Win32NT, Win32S, Win32Windows
, and WinCE
.
The ServicePack
property returns a string
identifying the service pack level installed on the computer. If no service packs are installed, or service packs are not supported, an empty string
is returned.
The Version
property returns a System.Version
object that identifies the specific operating system version.
The VersionString
property returns a concatenated string summary of the Platform, ServicePack
, and Version
properties.
To determine the operating system on which you are running, you must use both the platform and the version information, as detailed in Table 14-2.
Table 14.2. Determining the Current Operating System
PlatformID | Major Version | Minor Version | Operating System |
---|---|---|---|
| 4 | 10 | Windows 98 |
| 4 | 90 | Windows Me |
| 4 | 0 | Windows NT 4 |
| 5 | 0 | Windows 2000 |
| 5 | 1 | Windows XP |
| 5 | 2 | Windows Server 2003 |
| 6 | 0 | Windows Vista, Windows Server 2008 |
| 6 | 1 | Windows 7, Windows Server 2008 R2 |
The following example uses the Environment
class to display information about the current environment to the console:
using System; namespace Apress.VisualCSharpRecipes.Chapter14 { class Recipe14_01 { public static void Main() { // Command line. Console.WriteLine("Command line : " + Environment.CommandLine); // OS and CLR version information. Console.WriteLine(Environment.NewLine); Console.WriteLine("OS PlatformID : " + Environment.OSVersion.Platform); Console.WriteLine("OS Major Version : " + Environment.OSVersion.Version.Major); Console.WriteLine("OS Minor Version : " + Environment.OSVersion.Version.Minor); Console.WriteLine("CLR Version : " + Environment.Version); // User, machine, and domain name information. Console.WriteLine(Environment.NewLine); Console.WriteLine("User Name : " + Environment.UserName); Console.WriteLine("Domain Name : " + Environment.UserDomainName); Console.WriteLine("Machine name : " + Environment.MachineName); // Other environment information. Console.WriteLine(Environment.NewLine); Console.WriteLine("Is interactive? : " + Environment.UserInteractive); Console.WriteLine("Shutting down? : " + Environment.HasShutdownStarted); Console.WriteLine("Ticks since startup : " + Environment.TickCount); // Display the names of all logical drives. Console.WriteLine(Environment.NewLine); foreach (string s in Environment.GetLogicalDrives()) { Console.WriteLine("Logical drive : " + s); } // Standard folder information. Console.WriteLine(Environment.NewLine); Console.WriteLine("Current folder : " + Environment.CurrentDirectory); Console.WriteLine("System folder : " + Environment.SystemDirectory);
// Enumerate all special folders and display them. Console.WriteLine(Environment.NewLine); foreach (Environment.SpecialFolder s in Enum.GetValues(typeof(Environment.SpecialFolder))) { Console.WriteLine("{0} folder : {1}", s, Environment.GetFolderPath(s)); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Use the GetEnvironmentVariable, GetEnvironmentVariables
, and ExpandEnvironmentVariables
methods of the Environment
class.
The GetEnvironmentVariable
method allows you to retrieve a string containing the value of a single named environment variable, whereas the GetEnvironmentVariables
method returns an object implementing IDictionary
that contains the names and values of all environment variables as strings. The .NET Framework includes an overload of the GetEnvironmentVariables
method that takes a System.EnvironmentVariableTarget
argument, allowing you to specify a subset of environment variables to return based on the target of the variable: Machine, Process
, or User
.
The ExpandEnvironmentVariables
method provides a simple mechanism for substituting the value of an environment variable into a string by including the variable name enclosed in percent signs (%
) within the string.
Here is an example that demonstrates how to use all three methods:
using System; using System.Collections; namespace Apress.VisualCSharpRecipes.Chapter14 { class Recipe14_02 { public static void Main() { // Retrieve a named environment variable. Console.WriteLine("Path = " + Environment.GetEnvironmentVariable("Path")); Console.WriteLine(Environment.NewLine); // Substitute the value of named environment variables. Console.WriteLine(Environment.ExpandEnvironmentVariables( "The Path on %computername% is %Path%")); Console.WriteLine(Environment.NewLine); // Retrieve all environment variables targeted at the process and // display the values of all that begin with the letter U. IDictionary vars = Environment.GetEnvironmentVariables( EnvironmentVariableTarget.Process); foreach (string s in vars.Keys) { if (s.ToUpper().StartsWith("U")) { Console.WriteLine(s + " = " + vars[s]); } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Use the members of the System.Diagnostics.EventLog
class to create a log (if required), register an event source, and write events.
You can write to the Windows event log using the static methods of the EventLog
class, or you can create an EventLog
object and use its members. Whichever approach you choose, before writing to the event log you must decide which log you will use and register an event source against that log. The event source is simply a string that uniquely identifies your application. An event source may be registered against only one log at a time.
By default, the event log contains three separate logs: Application, System, and Security. Usually, you will write to the Application log, but you might decide your application warrants a custom log in which to write events. You do not need to explicitly create a custom log; when you register an event source against a log, if the specified log doesn't exist, it's created automatically.
You must have administrator privileges to create an event source—this is because the .NET Framework checks all of the event logs to ensure that the source name is unique and this means being able to read the security log.
Once you have decided on the destination log and registered an event source, you can start to write event log entries using the WriteEntry
method. WriteEntry
provides a variety of overloads that allow you to specify some or all of the following values:
A string
containing the event source for the log entry (static versions of WriteEntry
only).
A string
containing the message for the log entry.
A value from the System.Diagnostics.EventLogEntryType
enumeration, which identifies the type of log entry. Valid values are Error, FailureAudit, Information, SuccessAudit
, and Warning
.
An int
that specifies an application-specific event ID for the log entry.
A short
that specifies an application-specific subcategory for the log entry.
A byte
array containing any raw data to associate with the log entry.
The methods of the EventLog
class also provide overloads that support the writing of events to the event log of remote machines; see the .NET Framework SDK documentation for more information.
The following example demonstrates how to use the static members of the EventLog
class to write an entry to the event log of the local machine. You must run the example as administrator in order to create the event source—if you do not, a security exception will be thrown.
using System; using System.Diagnostics; namespace Apress.VisualCSharpRecipes.Chapter14 { class Recipe14_03 { public static void Main () { // If it does not exist, register an event source for this // application against the Application log of the local machine. // Trying to register an event source that already exists on the // specified machine will throw a System.ArgumentException. if (!EventLog.SourceExists("Visual C# Recipes")) { EventLog.CreateEventSource("Visual C# Recipes", "Application"); } // Write an event to the event log. EventLog.WriteEntry( "Visual C# Recipes", // Registered event source "A simple test event.", // Event entry message EventLogEntryType.Information, // Event type 1, // Application-specific ID 0, // Application-specific category new byte[] {10, 55, 200} // Event data );
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Use the methods GetValue
and SetValue
of the Microsoft.Win32.Registry
class.
The GetValue
and SetValue
methods open a registry key, get or set its value, and close the key each time they are called. This means they are inefficient when used to perform many read or write operations. The GetValue
and SetValue
methods of the Microsoft.Win32.RegistryKey
class discussed in recipe 14-5 will provide better performance if you need to perform many read or write operations on the registry.
The GetValue
and SetValue
methods allow you to read and write named values in named registry keys. GetValue
takes three arguments:
A string
containing the fully qualified name of the key you want to read. The key name must start with one of the following root key names:
HKEY_CLASSES_ROOT
HKEY_CURRENT_CONFIG
HKEY_CURRENT_USER
HKEY_DYN_DATA
HKEY_LOCAL_MACHINE
HKEY_PERFORMANCE_DATA
HKEY_USERS
A string
containing the name of the value in the key you want to read.
An object
containing the default value to return if the named value is not present in the key.
GetValue
returns an object
containing either the data read from the registry or the default value specified as the third argument if the named value is not found. If the specified key does not exist, GetValue
returns null
.
SetValue
offers two overloads. The most functional expects the following arguments:
A string
containing the fully qualified name of the key you want to write. The key must start with one of the root key names specified previously.
A string
containing the name of the value in the key you want to write.
An object
containing the value to write.
An element of the Microsoft.Win32.RegistyValueKind
enumeration that specifies the registry data type that should be used to hold the data.
If the registry key specified in the SetValue
call does not exist, it is automatically created.
The following example demonstrates how to use GetValue
and SetValue
to read from and write to the registry. Every time the example is run, it reads usage information from the registry and displays it to the screen. The example also updates the stored usage information, which you can see the next time you run the example.
using System; using Microsoft.Win32; namespace Apress.VisualCSharpRecipes.Chapter14 { class Recipe14_04 { public static void Main(String[] args) { // Variables to hold usage information read from registry. string lastUser; string lastRun; int runCount; // Read the name of the last user to run the application from the // registry. This is stored as the default value of the key and is // accessed by not specifying a value name. Cast the returned Object // to a string. lastUser = (string)Registry.GetValue( @"HKEY_CURRENT_USERSoftwareApressVisual C# Recipes", "", "Nobody");
// If lastUser is null, it means that the specified registry key // does not exist. if (lastUser == null) { // Set initial values for the usage information. lastUser = "Nobody"; lastRun = "Never"; runCount = 0; } else { // Read the last run date and specify a default value of // "Never". Cast the returned Object to string. lastRun = (string)Registry.GetValue( @"HKEY_CURRENT_USERSoftwareApressVisual C# Recipes", "LastRun", "Never"); // Read the run count value and specify a default value of // 0 (zero). Cast the Object to Int32 and assign to an int. runCount = (Int32)Registry.GetValue( @"HKEY_CURRENT_USERSoftwareApressVisual C# Recipes", "RunCount", 0); } // Display the usage information. Console.WriteLine("Last user name: " + lastUser); Console.WriteLine("Last run date/time: " + lastRun); Console.WriteLine("Previous executions: " + runCount); // Update the usage information. It doesn't matter if the registry // key exists or not, SetValue will automatically create it. // Update the "last user" information with the current username. // Specify that this should be stored as the default value // for the key by using an empty string as the value name. Registry.SetValue( @"HKEY_CURRENT_USERSoftwareApressVisual C# Recipes", "", Environment.UserName, RegistryValueKind.String); // Update the "last run" information with the current date and time. // Specify that this should be stored as a string value in the // registry. Registry.SetValue( @"HKEY_CURRENT_USERSoftwareApressVisual C# Recipes", "LastRun", DateTime.Now.ToString(), RegistryValueKind.String); // Update the usage count information. Specify that this should // be stored as an integer value in the registry. Registry.SetValue( @"HKEY_CURRENT_USERSoftwareApressVisual C# Recipes", "RunCount", ++runCount, RegistryValueKind.DWord);
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Use the Microsoft.Win32.Registry
class to obtain a Microsoft.Win32.RegistryKey
object that represents the root key of a registry hive you want to search. Use the members of this RegistryKey
object to navigate through and enumerate the registry key hierarchy, as well as to read the names and content of values held in the keys.
You must first obtain a RegistryKey
object that represents a base-level key and navigate through the hierarchy of RegistryKey
objects as required. The Registry
class implements a set of seven static fields that return RegistryKey
objects representing base-level registry keys; Table 14-3 describes the registry location to where each of these fields maps.
Table 14.3. Static Fields of the Registry Class
Field | Registry Mapping |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The static method RegistryKey.OpenRemoteBaseKey
allows you to open a registry base key on a remote machine. See the .NET Framework SDK documentation for details of its use.
Once you have the base-level RegistryKey
object, you must navigate through its child subkeys recursively. To support navigation, the RegistryKey
class allows you to do the following:
Get the number of immediate subkeys using the SubKeyCount
property.
Get a string
array containing the names of all subkeys using the GetSubKeyNames
method.
Get a RegistryKey
reference to a subkey using the OpenSubKey
method. The OpenSubKey
method provides two overloads: the first opens the named key as read-only; the second accepts a bool
argument that, if true
, will open a writable RegistryKey
object.
Once you obtain a RegistryKey
, you can create, read, update, and delete subkeys and values using the methods listed in Table 14-4. Methods that modify the contents of the key require you to have a writable RegistryKey
object.
Table 14.4. RegistryKey Methods to Create, Read, Update, and Delete Registry Keys and Values
Method | Description |
---|---|
| Creates a new subkey with the specified name and returns a writable |
| Deletes the subkey with the specified name, which must be empty of subkeys (but not values); otherwise, a |
| Deletes the subkey with the specified name along with all of its subkeys. |
| Deletes the value with the specified name from the current key. |
| Returns the value with the specified name from the current key. The value is returned as an |
| Returns the registry data type of the value with the specified name in the current key. The value is returned as a member of the |
| Returns a |
| Creates (or updates) the value with the specified name. In 2.0, you can specify the data type used to store the value with the overload that takes a |
On 64-bit versions of Windows, separate portions of the registry exist for 32-bit and 64-bit applications. The RegistryView
enumeration can be used as an argument to the static OpenBaseKey
method of RegistryKey
to specify which portion of the registry is accessed. See the .NET Framework SDK documentation for further details.
The RegistryKey
class implements IDisposable
; you should call the IDisposable.Dispose
method to free operating system resources when you have finished with the RegistryKey
object.
The following example takes a single command-line argument and recursively searches the CurrentUser
hive of the registry looking for keys with names matching the supplied argument. When the example finds a match, it displays all string
type values contained in the key to the console.
using System; using Microsoft.Win32; namespace Apress.VisualCSharpRecipes.Chapter14 { class Recipe14_05 { public static void SearchSubKeys(RegistryKey root, String searchKey) { try { // Get the subkeys contained in the current key. string[] subkeys = root.GetSubKeyNames(); // Loop through all subkeys contained in the current key. foreach (string keyname in subkeys) { try { using (RegistryKey key = root.OpenSubKey(keyname)) {
if (keyname == searchKey) PrintKeyValues(key); SearchSubKeys(key, searchKey); } } catch (System.Security.SecurityException) { // Ignore SecurityException for the purpose of the example. // Some subkeys of HKEY_CURRENT_USER are secured and will // throw a SecurityException when opened. } } } catch (UnauthorizedAccessException) { // Ignore UnauthorizedAccessException for the purpose of the example // - this exception is thrown if the user does not have the // rights to read part of the registry. } } public static void PrintKeyValues(RegistryKey key) { // Display the name of the matching subkey and the number of // values it contains. Console.WriteLine("Registry key found : {0} contains {1} values", key.Name, key.ValueCount); // Loop through the values and display. foreach (string valuename in key.GetValueNames()) { if (key.GetValue(valuename) is String) { Console.WriteLine(" Value : {0} = {1}", valuename, key.GetValue(valuename)); } } } public static void Main(String[] args) { if (args.Length > 0) { // Open the CurrentUser base key. using (RegistryKey root = Registry.CurrentUser) { // Search recursively through the registry for any keys // with the specified name. SearchSubKeys(root, args[0]); } }
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Running the example using the command Recipe14-05 Environment
will display output similar to the following when executed using the command on a machine running Windows 7:
Registry key found : HKEY_CURRENT_USEREnvironment contains 2 values Value : TEMP = C:UsersAdamAppDataLocalTemp Value : TMP = C:UsersAdamAppDataLocalTemp
Create a class that extends System.ServiceProcess.ServiceBase
. Use the inherited properties to control the behavior of your service, and override inherited methods to implement the functionality required. Implement a Main
method that creates an instance of your service class and passes it to the static ServiceBase.Run
method.
The ServiceBase
class is defined in the System.Serviceprocess.dll
assembly, so you must include a reference to this assembly when you build your service class.
To create a Windows service manually, you must implement a class derived from the ServiceBase
class. The ServiceBase
class provides the base functionality that allows the Windows Service Control Manager (SCM) to configure the service, operate the service as a background task, and control the life cycle of the service. The SCM also controls how other applications can control the service programmatically.
If you are using Microsoft Visual Studio, you can use the Windows Service project template to create a Windows service. The template provides the basic code infrastructure required by a Windows service class, which you can extend with your custom functionality.
To control your service, the SCM uses the eight protected methods inherited from the ServiceBase
class described in Table 14-5. You should override these virtual methods to implement the functionality and behavior required by your service. Not all services must support all control messages. The CanXXX
properties inherited from the ServiceBase
class declare to the SCM which control messages your service supports; Table 14-5 specifies the property that controls each operation.
Table 14.5. Methods That Control the Operation of a Service
Method | Description |
---|---|
| All services must support the |
| Called by the SCM to stop a service—the SCM will call |
| Called by the SCM to pause a service—the SCM will call |
| Called by the SCM to continue a paused service—the SCM will call |
| Called by the SCM when the system is shutting down—the SCM will call |
| Called by the SCM when a system-level power status change occurs, such as a laptop going into suspend mode. The SCM will call |
| Allows you to extend the service control mechanism with custom control messages; see the .NET Framework SDK documentation for more details. |
| Called by the SCM when a change event is received from the Terminal Services session or when users log on and off on the local machine. A |
As mentioned in Table 14-5, the OnStart
method is expected to return within 30 seconds, so you should not use OnStart
to perform lengthy initialization tasks if possible. A service class should implement a constructor that performs initialization, including configuring the inherited properties of the ServiceBase
class. In addition to the properties that declare the control messages supported by a service, the ServiceBase
class implements three other important properties:
ServiceName
is the name used internally by the SCM to identify the service and must be set before the service is run.
AutoLog
controls whether the service automatically writes entries to the event log when it receives any of the OnStart, OnStop, OnPause
, or OnContinue
control messages from Table 14-5.
EventLog
provides access to an EventLog
object that's preconfigured with an event source name that's the same as the ServiceName
property registered against the Application log. (See recipe 14-3 for more information about the EventLog
class.)
The final step in creating a service is to implement a static Main
method. The Main
method must create an instance of your service class and pass it as an argument to the static method ServiceBase.Run
.
The following Windows service example uses a configurable System.Timers.Timer
to write an entry to the Windows event log periodically. You can start, pause, and stop the service using the Services application in the Control Panel.
using System; using System.Timers; using System.ServiceProcess; namespace Apress.VisualCSharpRecipes.Chapter14 { class Recipe14_06 : ServiceBase { // A Timer that controls how frequently the example writes to the // event log. private System.Timers.Timer timer;
public Recipe14_06() { // Set the ServiceBase.ServiceName property. ServiceName = "Recipe 14_06 Service"; // Configure the level of control available on the service. CanStop = true; CanPauseAndContinue = true; CanHandleSessionChangeEvent = true; // Configure the service to log important events to the // Application event log automatically. AutoLog = true; } // The method executed when the timer expires and writes an // entry to the Application event log. private void WriteLogEntry(object sender, ElapsedEventArgs e) { // Use the EventLog object automatically configured by the // ServiceBase class to write to the event log. EventLog.WriteEntry("Recipe14_06 Service active : " + e.SignalTime); } protected override void OnStart(string[] args) { // Obtain the interval between log entry writes from the first // argument. Use 5000 milliseconds by default and enforce a 1000 // millisecond minimum. double interval; try { interval = Double.Parse(args[0]); interval = Math.Max(1000, interval); } catch { interval = 5000; } EventLog.WriteEntry(String.Format("Recipe14_06 Service starting. " + "Writing log entries every {0} milliseconds...", interval)); // Create, configure, and start a System.Timers.Timer to // periodically call the WriteLogEntry method. The Start // and Stop methods of the System.Timers.Timer class // make starting, pausing, resuming, and stopping the // service straightforward. timer = new Timer(); timer.Interval = interval;
timer.AutoReset = true; timer.Elapsed += new ElapsedEventHandler(WriteLogEntry); timer.Start(); } protected override void OnStop() { EventLog.WriteEntry("Recipe14_06 Service stopping..."); timer.Stop(); // Free system resources used by the Timer object. timer.Dispose(); timer = null; } protected override void OnPause() { if (timer != null) { EventLog.WriteEntry("Recipe14_06 Service pausing..."); timer.Stop(); } } protected override void OnContinue() { if (timer != null) { EventLog.WriteEntry("Recipe14_06 Service resuming..."); timer.Start(); } } protected override void OnSessionChange(SessionChangeDescription change) { EventLog.WriteEntry("Recipe14_06 Session change..." + change.Reason); } public static void Main() { // Create an instance of the Recipe14_06 class that will write // an entry to the Application event log. Pass the object to the // static ServiceBase.Run method. ServiceBase.Run(new Recipe14_06()); } } }
If you want to run multiple services in a single process, you must create an array of ServiceBase
objects and pass it to the ServiceBase.Run
method. Although service classes have a Main
method, you can't execute service code directly or run a service class directly. Recipe 14-7 describes what you must do to install your service before it will execute.
Add a new class to your Windows service project that extends the System.Configuration.Install.Installer
class to create an installer class containing the information necessary to install and configure your service class. Use the Installer tool (Installutil.exe
) to perform the installation, which is installed as part of the .NET Framework.
You must create the installer class in the same assembly as the service class for the service to install and function correctly.
As recipe 14-6 points out, you cannot run service classes directly. The high level of integration with the Windows operating system and the information stored about the service in the Windows registry means services require explicit installation.
If you have Microsoft Visual Studio .NET, you can create an installation component for your service automatically by right-clicking in the design view of your service class and selecting Add Installer from the context menu. You can call this installation component by using deployment projects or by using the Installer tool to install your service. You can also create installer components for Windows services manually by following these steps:
In your project, create a class derived from the Installer
class.
Apply the attribute System.ComponentModel.RunInstallerAttribute(true)
to the installer class.
In the constructor of the installer class, create a single instance of the System.ServiceProcess.ServiceProcessInstaller
class. Set the Account, User
, and Password
properties of ServiceProcessInstaller
to configure the account under which your service will run. This account must already exist.
In the constructor of the installer class, create one instance of the System.ServiceProcess.ServiceInstaller
class for each individual service you want to install. Use the properties of the ServiceInstaller
objects to configure information about each service, including the following:
ServiceName
, which specifies the name Windows uses internally to identify the service. This must be the same as the value assigned to the ServiceBase.ServiceName
property.
DisplayName
, which provides a user-friendly name for the service.
StartType
, which uses values of the System.ServiceProcess.ServiceStartMode
enumeration to control whether the service is started automatically or manually, or is disabled.
ServiceDependsUpon
, which allows you to provide a string array containing a set of service names that must be started before this service can start.
Add the ServiceProcessInstaller
object and all ServiceInstaller
objects to the System.Configuration.Install.InstallerCollection
object accessed through the Installers
property, which is inherited by your installer class from the Installer
base class.
The following example is an installer for the Recipe14_06
Windows service created in recipe 14-6. The sample project contains the code from recipe 14-6 and for the installer class. This is necessary for the service installation to function correctly. To compile the example, you must reference two additional assemblies: System.Configuration.Install.dll
and System.ServiceProcess.dll
.
using System.Configuration.Install; using System.ServiceProcess; using System.ComponentModel; namespace Apress.VisualCSharpRecipes.Chapter14 { [RunInstaller(true)] public class Recipe14_07 : Installer { public Recipe14_07() { // Instantiate and configure a ServiceProcessInstaller. ServiceProcessInstaller ServiceExampleProcess = new ServiceProcessInstaller(); ServiceExampleProcess.Account = ServiceAccount.LocalSystem; // Instantiate and configure a ServiceInstaller. ServiceInstaller ServiceExampleInstaller = new ServiceInstaller(); ServiceExampleInstaller.DisplayName = "Visual C# Recipes Service Example";
ServiceExampleInstaller.ServiceName = "Recipe 14_06 Service"; ServiceExampleInstaller.StartType = ServiceStartMode.Automatic; // Add both the ServiceProcessInstaller and ServiceInstaller to // the Installers collection, which is inherited from the // Installer base class. Installers.Add(ServiceExampleInstaller); Installers.Add(ServiceExampleProcess); } } }
To install the Recipe14_06
service, build the project, navigate to the directory where Recipe14-07.exe
is located (bindebug
by default), and execute the command Installutil Recipe14-07.exe
—you will need to do this as an administrator. You can then see and control the Visual C# Recipes Service Example service using the Windows Computer Management console. However, even though you have specified a StartType
of Automatic
, the service is initially installed without being started; you must start the service manually (or restart your computer) before the service will write entries to the event log. Once the service is running, you can view the entries it writes to the Application event log using the Event Viewer application. To uninstall the Recipe14_06
service, add the /u
switch to the Installutil
command, as follows: Installutil /u Recipe14-07.exe
.
If you have the Service application from the Control Panel open when you uninstall the service, the service will not uninstall completely until you close the Service application. Once you close the Service application, you can reinstall the service; otherwise, you will get an error telling you that the installation failed because the service is scheduled for deletion.
Use COM Interop to access the functionality of the Windows Script Host. Create and configure an IWshShortcut
instance that represents the shortcut. The folder in which you save the shortcut determines whether it appears on the desktop or in the Start menu.
The .NET Framework class library does not include the functionality to create desktop or Start menu shortcuts; however, this is relatively easy to do using the Windows Script Host component accessed through COM Interop. Chapter 15 describes how to create an Interop assembly that provides access to a COM component. If you are using Visual Studio, add a reference to the Windows Script Host object model, listed on the COM tab of the Add Reference dialog box. If you don't have Visual Studio .NET, use the Type Library Importer (Tlbimp.exe
) to create an Interop assembly for the wshom.ocx
file, which is usually located in the WindowsSystem32
folder. (You can obtain the latest version of the Windows Script Host from http://msdn.microsoft.com/scripting
.)
Once you have generated the Interop assembly and imported it into your project, follow these steps to create a desktop or Start menu shortcut.
Instantiate a WshShell
object, which provides access to the Windows shell.
Use the SpecialFolders
property of the WshShell
object to determine the correct path of the folder where you want to put the shortcut. You must specify the name of the folder you want as an index to the SpecialFolders
property. For example, to create a desktop shortcut, specify the value Desktop
, and to create a Start menu shortcut, specify StartMenu
. Using the SpecialFolders
property, you can obtain the path to any of the special system folders. If the specified folder does not exist on the platform you are running on, SpecialFolders
returns an empty string
. Other commonly used values include AllUsersDesktop
and AllUsersStartMenu
; you can find the full list of special folder names in the section on the SpecialFolders
property in the Windows Script Host documentation.
Call the CreateShortcut
method of the WshShell
object, and provide the fully qualified file name of the shortcut file you want to create. The file should have the extension .lnk. CreateShortcut
will return an IWshShortcut
instance.
Use the properties of the IWshShortcut
instance to configure the shortcut. You can configure properties such as the executable that the shortcut references, a description for the shortcut, a hotkey sequence, and the icon displayed for the shortcut.
Call the Save
method of the IWshShortcut
instance to write the shortcut to disk. The shortcut will appear either on the desktop or in the Start menu (or elsewhere), depending on the path specified when the IWshShortcut
instance was created.
The following example class creates a shortcut to Notepad.exe
on both the desktop and Start menu of the current user. The example creates both shortcuts by calling the CreateShortcut
method and specifying a different destination folder for each shortcut file. This approach makes it possible to create the shortcut file in any of the special folders returned by the WshShell.SpecialFolders
property.
using System; using System.IO; using IWshRuntimeLibrary; namespace Apress.VisualCSharpRecipes.Chapter14 { class Recipe14_08 { public static void CreateShortcut(string destination) { // Create a WshShell instance through which to access the // functionality of the Windows shell. WshShell wshShell = new WshShell(); // Assemble a fully qualified name that places the Notepad.lnk // file in the specified destination folder. You could use the // System.Environment.GetFolderPath method to obtain a path, but // the WshShell.SpecialFolders method provides access to a wider // range of folders. You need to create a temporary object reference // to the destination string to satisfy the requirements of the // Item method signature. object destFolder = (object)destination; string fileName = Path.Combine( (string)wshShell.SpecialFolders.Item(ref destFolder), "Notepad.lnk" ); // Create the shortcut object. Nothing is created in the // destination folder until the shortcut is saved. IWshShortcut shortcut = (IWshShortcut)wshShell.CreateShortcut(fileName); // Configure the fully qualified name to the executable. // Use the Environment class for simplicity. shortcut.TargetPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.System), "notepad.exe" ); // Set the working directory to the Personal (My Documents) folder. shortcut.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal); // Provide a description for the shortcut. shortcut.Description = "Notepad Text Editor"; // Assign a hotkey to the shortcut. shortcut.Hotkey = "CTRL+ALT+N"; // Configure Notepad to always start maximized. shortcut.WindowStyle = 3;
// Configure the shortcut to display the first icon in Notepad.exe. shortcut.IconLocation = "notepad.exe, 0"; // Save the configured shortcut file. shortcut.Save(); } public static void Main() { // Create the Notepad shortcut on the desktop. CreateShortcut("Desktop"); // Create the Notepad shortcut on the Windows Start menu of // the current user. CreateShortcut("StartMenu"); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Use the features of Windows API CodePack for Microsoft .NET Framework. Create and configure a JumpList
instance for your application. The items that you add to the JumpList
define the tasks, documents, and categories that appear in the Windows taskbar.
Windows API CodePack for Microsoft .NET Framework is a source code library published by Microsoft to simplify integration with Windows using managed code. One feature of the CodePack is support for the Windows 7 taskbar. There is nothing in the CodePack that you could not write yourself, but using the CodePack is simpler and quicker, and provides a consistent API that Microsoft will support across Windows versions. (You can download the CodePack from
http://code.msdn.microsoft.com/WindowsAPICodePack
).
Once you have compiled the library, reference the Microsoft.WindowsAPICodePack.dll
and Microsoft.WindowsAPICodePack.Shell.dll
assemblies (these files will be in Shellindebug
folder within the CodePack directory) and follow these steps to create and populate a Jump List:
Import the Microsoft.WindowsAPICodePack.Shell
and Microsoft.WindowsAPICodePack.Taskbar
namespaces with the using
directive.
Add an event handler to the Shown
member of your application class.
Within the event handler, call the static method JumpList.CreateJumpList
to create a new JumpList
instance for your application.
Create instances of JumpListLink
to represent tasks and documents and add them to the JumpList
instance using the AddUserTasks
method.
The JumpList
class can also be used to display recently used documents, but only if your application is registered as a handler for the document types you display. See the Windows API CodePack for details and example code to handle document type registration.
The following example uses the Microsoft.WindowsAPICodePack.Taskbar.JumpList
class to populate the Windows 7 Jump List with three items. The first item opens a command prompt. The second item opens Notepad and displays the Notepad icon in the Jump List. The third item shows how to open another application's file using a Jump List. To compile the example, you must build the Windows API CodePack for .NET Framework and reference the Microsoft.WindowsAPICodePack.dll
and Microsoft.WindowsAPICodePack.Shell.dll
assemblies.
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.IO; using System.Windows.Forms; using Microsoft.WindowsAPICodePack.Shell; using Microsoft.WindowsAPICodePack.Taskbar; namespace Recipe14_09 { public partial class Form1 : Form { public Form1() {
// Call the default intializer. InitializeComponent(); // Register an event handler for when the windows is shown. this.Shown += new EventHandler(onWindowShown); } void onWindowShown(object sender, EventArgs e) { // Create a new Jump List. JumpList jumpList = JumpList.CreateJumpList(); // Create a user task. jumpList.AddUserTasks( new JumpListLink("cmd.exe", "Open Command Prompt")); // Create a user task with an icon. jumpList.AddUserTasks(new JumpListLink("notepad.exe", "Open Notepad") { IconReference = new IconReference("notepad.exe", 0) }); // Create a user task with a document. jumpList.AddUserTasks( new JumpListLink(@"C:UsersAdamDesktop est.txt", "Open Text File")); } } }
Use Microsoft Windows API CodePack for .NET Framework to access the Windows Search feature. Create one or more Microsoft.WindowsAPICodePack.Shell.SearchCondition
s to define the constraints for your search and pass them to an instance of Microsoft.WindowsAPICodePack.Shell.ShellSearchFolder
. The ShellSearchFolder
acts as a collection whose members represent the results of your search. The Windows API CodePack for .NET Framework is a source code library published by Microsoft to simplify integration with Windows using managed code. (You can download the CodePack from http://code.msdn.microsoft.com/WindowsAPICodePack
).
The search model is built around conditions and locations. Conditions are the individual aspects of a file that match your search—for example, if you needed to find files text files that have "windows" in the file name, the constrains would be:
Files that have the .txt
extension
Files that have the word windows in the file name.
Locations are where you want the search to be conducted and can include Windows 7 libraries. When using the CodePack, searches are created and performed as follows:
Create one or more instances of SearchCondition
using the SearchConditionFactory.CreateLeafCondition
method. The CreateLeafCondition
method accepts three parameters:
A property of the SystemProperties.System
class. The System
class contains properties for all of the attributes you can search for—some of the most commonly used are described in Table 14-6.
The data value to match when searching. This must match the data type of the preceding attribute.
A value from the SearchConditionOperation
enumeration. This value specifies the way in which the data value is matched (equals, greater than, less than, etc.). Commonly used members are described in Table 14-7.
If you have created more than one SearchCondition
, you must combine them with the SearchConditionFactory.CreateAndOrCondition
method. This method takes a value from the SearchConditionType
enumeration (And, Or, Not
) that specifies how the individual conditions are combined.
Create a new instance of the ShellSearchFolder
class, using the SearchCondition
you previously created and one or more locations as constructor parameters. The CodePack includes the KnownFolders
enumeration, whose members reference useful directories and libraries.
Treat the ShellSearchFolder
as a collection. Enumerating the contents will return the results of your search, with each file represented by an instance of ShellObject
.
Your search is not performed until you try to access the members of the ShellSearchFolder
collection—at which point the current thread is blocked until the search has completed. Perform the search in a background thread to avoid an unresponsive application.
Table 14.6. Commonly Used Members of the SystemProperties.System Class
Property | Data Type | Description |
---|---|---|
|
| The last time that the file was changed |
|
| The time the file was created |
|
| The extension for the file, including the period |
|
| The name of the file, including the file extension |
|
| The owner of the file |
Table 14.7. Commonly Used Members of the SearchConditionOperation Class
Member | Description |
---|---|
| The file attribute and the target value are the same. |
| The file attribute and the target value are different. |
| The file attribute contains the target value (for example, "windows" contains "win"). |
| The file attribute starts with the target value (for example, "windows" starts with "win"). |
| The file attribute ends with the target value (for example, "Microsoft" ends with "soft"). |
| The file attribute does not contains the target value (for example, "Microsoft" does not contain "win"). |
| The file attribute is less than the target value. |
| The file attribute is greater than the target value. |
The following example is a Windows Forms application that uses the FileExtension
and FileName
attributes to search for files in the current user's directories. The user interface, built using the Visual Studio designer, is shown in Figure 14-1. To compile the example, you must build Windows API CodePack for Microsoft .NET Framework and reference the Microsoft.WindowsAPICodePack.dll
and Microsoft.WindowsAPICodePack.Shell.dll
assemblies.
The button1_Click
method is called when the Search button is pressed. A SearchCondition
is created using the values that the user has entered for the file name and extension, and combined using the SearchConditionType.And
enumeration value. The search results are read from the ShellSearchFolder
and name of each file found is added to the large text box.
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using Microsoft.WindowsAPICodePack.Shell; using Microsoft.WindowsAPICodePack.Shell.PropertySystem; namespace Recipe14_10 { public partial class Recipe14_10 : Form {
public Recipe14_10() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { // Create the leaf condition for the file extension. SearchCondition fileExtCondition = SearchConditionFactory.CreateLeafCondition( SystemProperties.System.FileExtension, textBox1.Text, SearchConditionOperation.Equal); // Create the leaf condition for the file name. SearchCondition fileNameCondition = SearchConditionFactory.CreateLeafCondition( SystemProperties.System.FileName, textBox2.Text, SearchConditionOperation.ValueContains); // Combine the two leaf conditions. SearchCondition comboCondition = SearchConditionFactory.CreateAndOrCondition( SearchConditionType.And, false, fileExtCondition, fileNameCondition); // Create the search folder. ShellSearchFolder searchFolder = new ShellSearchFolder( comboCondition, (ShellContainer)KnownFolders.UsersFiles); // Clear the result text box. textBox3.Clear(); textBox3.AppendText("Processing search results... "); // Run through each search result. foreach (ShellObject shellObject in searchFolder) { textBox3.AppendText("Result: " + shellObject.ParsingName + " "); } // Display a final message to the user. textBox3.AppendText("All results processed "); } } }
Use the Windows API CodePack for Microsoft .NET Framework to access to enumerate the available network connections and determine which, if any, are connected to the Internet. The Windows API CodePack for Microsoft .NET Framework is a source code library published by Microsoft to simplify integration with Windows using managed code. (You can download the CodePack from http://code.msdn.microsoft.com/WindowsAPICodePack
).
The Microsoft.WindowsAPICodePack.Net.NetworkListManager
class contains the IsConnectedToInternet
property. If this returns true
, the GetNetworks
method can be used to obtain a collection of connected networks, each of which is represented by the Microsoft.WindowsAPICodePack.Net.Network
class.
The following example uses the IsConnectedToInternet
property of the Microsoft.WindowsAPICodePack.Net.NetworkListManager
class, and if the result is positive, gets the list of network connections and writes out the name of those that are connected. To compile the example, you must build Windows API CodePack for Microsoft .NET Framework and reference the Microsoft.WindowsAPICodePack.dll
assembly.
using System; using Microsoft.WindowsAPICodePack.Net; namespace Recipe14_11 { class Recipe14_11 { static void Main(string[] args) { // Check the internet connection state. bool isInternetConnected = NetworkListManager.IsConnectedToInternet; Console.WriteLine("Machine connected to Internet: {0}", isInternetConnected); if (isInternetConnected) {
// Get the list of all network connections. NetworkCollection netCollection = NetworkListManager.GetNetworks( NetworkConnectivityLevels.Connected); // Work through the set of connections and write out the // name of those that are connected to the internet. foreach (Network network in netCollection) { if (network.IsConnectedToInternet) { Console.WriteLine( "Connection {0} is connected to the internet", network.Name); } } } Console.WriteLine(" Main method complete. Press Enter."); Console.ReadLine(); } } }
You need to display a task dialog—a standard Windows-specific dialog box, such as an elevated task request.
Use Windows API CodePack for Microsoft .NET Framework to create and display task dialogs. Windows API CodePack for Microsoft .NET Framework is a source code library published by Microsoft to simplify integration with Windows using managed code—it contains extensive support for Windows-specific dialog boxes, which allow your application to better integrate with the platform. (You can download the CodePack from http://code.msdn.microsoft.com/WindowsAPICodePack
).
Create an instance of the Microsoft.WindowsAPICodePack.Dialogs.TaskDialog
class and use the class properties to configure the task dialog. Add event handlers so that you are notified when buttons are pressed and then call the Show
method to display the dialog. Some of the most useful properties of the TaskDialog
class are shown in Table 14-8.
Table 14.8. Selected Properties of the TaskDialog Class
Property | Description |
---|---|
| Determines if the user can dismiss the dialog. |
| The set of controls embedded within the task dialog. See the recipe code for an example. |
| The summary text displayed in the dialog. |
| The standard buttons for the dialog. Set with the values of the |
| The detailed text, displayed below the |
The following example is a Windows Forms application comprising a button and a text area. To compile the example, you must build the Windows API CodePack for Microsoft .NET Framework and reference the Microsoft.WindowsAPICodePack.dll
assembly. The user interface is shown in Figure 14-2. We have added an event handler such that the showElevatedTaskRequest
method is called when the button is clicked.
When the button is clicked, we create a new instance of TaskDialog
and configure the basic settings, allowing the user to cancel the dialog and specifying which buttons should be displayed at the bottom of the dialog window. We then create an instance of TaskDialogCommandLink
, using sample text as the constructor parameters and add it to the TaskDialog.Controls
property. We register an event handler so that we can add a line of text to the text box on the main window when the elevated task button is clicked. The dialog interface is shown in Figure 14-3.
Using the task dialog to allow your application to indicate to a user that a particular task requires elevated privileges does not elevate the privilege level for your application. See recipe 14-15 for details of how to perform an elevated task.
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using Microsoft.WindowsAPICodePack.Dialogs; namespace Recipe14_12 { public partial class Form1 : Form {
public Form1() { InitializeComponent(); } void showElevatedTaskRequest(object sender, EventArgs args) { // Create the TaskDialog and configure the basics. TaskDialog taskDialog = new TaskDialog(); taskDialog.Cancelable = true; taskDialog.InstructionText = "This is a Task Dialog"; taskDialog.StandardButtons = TaskDialogStandardButtons.Ok | TaskDialogStandardButtons.Close; // Create the control that will represent the elevated task. TaskDialogCommandLink commLink = new TaskDialogCommandLink( "adminTask", "First Line Of Text", "Second Line of Text"); commLink.ShowElevationIcon = true; // Add the control to the task dialog. taskDialog.Controls.Add(commLink); // Add an event handler to the command link so that // we are notified when the user presses the button. commLink.Click += new EventHandler(performElevatedTask); // display the task dialog taskDialog.Show(); } void performElevatedTask(object sender, EventArgs args) { textBox1.AppendText("Elevated task button pressed "); } } }
To set up the counters, add one or more instances of System.Diagnostics.CounterCreateData
, add them to an instance of System.Diagnostics.CounterCreationDataCollection
, and pass the collection as an argument to the Create
method of the System.Diagnostics.PerformanceCounterCategory
class.
Creating new counters requires administrator privileges.
To write to a counter, create an instance of System.Diagnostics.PerformanceCounter
using the same details you specified when creating the corresponding CounterCreateData
instance. Ensure that the ReadOnly
property is false
. Use the Increment, IncrementBy, Decrement
, and DecrementBy
methods to change the value of the counter.
Counters are grouped together in categories. You can determine if a category already exists by using the PerformanceCategory.Exists
method—an exception will be thrown if you try to create a category that already exists. An individual counter is created using the CounterCreationData
class. The three key properties are CounterName
(the name of the counter), CounterHelp
(a descriptive string that can be displayed to the user), and CounterType
, which defines the kind of counter that will be created. There are many kinds of counters available, ranging from simple 32- and 64-bit values to pairs of counters that must be created together so that Windows can calculate rate information (see the recipe code for an example of this). The range of counter types available is described in the System.Diagnostic.PerformanceCounterType
enumeration.
Writing to performance counters uses a different set of classes. To write to a counter, create an instance of the PerformanceCounter
class, setting the CategoryName
property and CounterName
properties to those you used when creating the category and counters. PerformanceCounter
values can be incremented using the Increment
and IncrementBy
methods, decremented using the Decrement
and DecrementBy
methods, and set to a specific value using the RawValue
property.
The following example creates a new performance counter category called Recipe 14-13 Performance Counters
and populates it with three counters: NumberOfItems32, AverageTimer32
, and AverageBase
.
Two of the counters are closely related. When creating a counter of the AverageTimer32
type, the next counter that is created must be of the AverageBase
type. The two counters are used together to report the number of occurrences of an operation over time. We update the AverageBase
value to report how many operations have been performed and the AverageTimer32
value to report how many ticks have passed since you last updated the AverageBase
value.
Having created the category and counters, we then create three instance of PerformanceCounter
and enter a loop so that the counter values are updated randomly.
AverageTimer32
should be updated with the number of ticks reported by the Windows high-resolution performance counter. The counter value is not available through a managed library, and must be obtained using the QueryPerformanceCounter
method in Kernel32.dll
. You can see how the DLL is imported and used in the example.
using System; using System.Security.Principal; using System.Diagnostics; using System.Runtime.InteropServices; namespace Recipe14_13 { class Recipe14_13 { [DllImport("Kernel32.dll")] public static extern void QueryPerformanceCounter(ref long ticks); static void Main(string[] args) { if (!checkElevatedPrivilege()) { Console.WriteLine("This recipe requires administrator rights"); Console.ReadLine(); Environment.Exit(1); } // Define the category name for the performance counters. string categoryName = "Recipe 14-13 Performance Counters"; if (!PerformanceCounterCategory.Exists(categoryName)) { Console.WriteLine("Creating counters."); // We need to create the category. CounterCreationDataCollection counterCollection = new CounterCreationDataCollection(); // Create the individual counters. CounterCreationData counter1 = new CounterCreationData(); counter1.CounterType = PerformanceCounterType.NumberOfItems32; counter1.CounterName = "Number of Items Counter"; counter1.CounterHelp = "A sample 32-bit number counter"; CounterCreationData counter2 = new CounterCreationData(); counter2.CounterType = PerformanceCounterType.AverageTimer32;
counter2.CounterName = "Average Timer Counter"; counter2.CounterHelp = "A sample average timer counter"; CounterCreationData counter3 = new CounterCreationData(); counter3.CounterType = PerformanceCounterType.AverageBase; counter3.CounterName = "Average Base Counter"; counter3.CounterHelp = "A sample average base counter"; // Add the counters to the collection. counterCollection.Add(counter1); counterCollection.Add(counter2); counterCollection.Add(counter3); // Create the counters category. PerformanceCounterCategory.Create(categoryName, "Category for Visual C# Recipe 14-13", PerformanceCounterCategoryType.SingleInstance, counterCollection); } else { Console.WriteLine("Counters already exist."); } // Open the counters for reading. PerformanceCounter perfCounter1 = new PerformanceCounter(); perfCounter1.CategoryName = categoryName; perfCounter1.CounterName = "Number of Items Counter"; perfCounter1.ReadOnly = false; PerformanceCounter perfCounter2 = new PerformanceCounter(); perfCounter2.CategoryName = categoryName; perfCounter2.CounterName = "Average Timer Counter"; perfCounter2.ReadOnly = false; PerformanceCounter perfCounter3 = new PerformanceCounter(); perfCounter3.CategoryName = categoryName; perfCounter3.CounterName = "Average Base Counter"; perfCounter3.ReadOnly = false; // Create a number generator to produce values. Random numberGenerator = new Random(); // Enter a loop to update the values every second. long startTickCount = 0, endTickCount = 0; while (true) { // Get the high-frequency tick count. QueryPerformanceCounter(ref startTickCount); // put the thread to sleep for up to a second System.Threading.Thread.Sleep(numberGenerator.Next(1000));
// Get the high-frequency tick count again. QueryPerformanceCounter(ref endTickCount); Console.WriteLine("Updating counter values."); perfCounter1.Increment(); perfCounter2.IncrementBy(endTickCount - startTickCount); perfCounter3.Increment(); } } static bool checkElevatedPrivilege() { WindowsIdentity winIdentity = WindowsIdentity.GetCurrent(); WindowsPrincipal winPrincipal = new WindowsPrincipal(winIdentity); return winPrincipal.IsInRole(WindowsBuiltInRole.Administrator); } } }
Create an instance of the System.Diagnostics.PerformanceCounter
class for each counter that you want to read, specifying the counter category and name as constructor arguments. Read data values by calling the NextValue
method.
The process for reading performance counter values is very similar to that for writing values, except that instead of using the Increment
and Decrement
methods, the NextSample
method is called to return data points as float
values.
Administrator privileges are required to read performance counters.
The following example reads values from the counters that we created in the previous recipe. In the previous recipe, we noted that two of the counters were related. When reading data from such a pair, you only read values from the first counter—Windows returns the calculated value (the number of operations/second). If you need to access the underlying data, then consult the .NET documentation for details of the System.Diagnostics.CounterSample
class, instances of which can be obtained from the PerformanceCounter.NextSample
method. You must run the previous example at the same time as this example; otherwise, you will only be able to read zeros from the counters, as no updates will be generated.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; using System.Security.Principal; namespace Recipe14_14 { class Recipe14_14 { static void Main(string[] args) { if (!checkElevatedPrivilege()) { Console.WriteLine("This recipe requires administrator rights"); Console.ReadLine(); Environment.Exit(1); } // Define the category name for the performance counters. string categoryName = "Recipe 14-13 Performance Counters"; // Open the counters for reading. PerformanceCounter perfCounter1 = new PerformanceCounter(); perfCounter1.CategoryName = categoryName; perfCounter1.CounterName = "Number of Items Counter"; PerformanceCounter perfCounter2 = new PerformanceCounter(); perfCounter2.CategoryName = categoryName; perfCounter2.CounterName = "Average Timer Counter"; while (true) { float value1 = perfCounter1.NextValue(); Console.WriteLine("Value for first counter: {0}", value1); float value2 = perfCounter2.NextValue(); Console.WriteLine("Value for second counter: {0}", value2);
// Put the thread to sleep for a second. System.Threading.Thread.Sleep(1000); } } static bool checkElevatedPrivilege() { WindowsIdentity winIdentity = WindowsIdentity.GetCurrent(); WindowsPrincipal winPrincipal = new WindowsPrincipal(winIdentity); return winPrincipal.IsInRole(WindowsBuiltInRole.Administrator); } } }
Use the runas
command to start a second instance of your application with elevated privileges using a command-line argument to indicate that the privileged operations should be performed.
Windows doesn't support temporarily elevating privileges for a process. If your application needs elevated privileges for specific tasks, create a second process that starts your application with elevated privileges and use command-line arguments to indicate that elevated tasks should be performed.
To execute a process with elevated privileges, create a new instance of the System.Diagnostics.ProcessStartInfo
class, set the Verb
property to runas
and the Arguments
property to be a string that represents a request for elevated actions (we use elevated
in the following example). Pass the ProcessStartInfo
instance to the static System.Diagnostics.Process.Start
method. In your application's Main
method, check the arguments to determine whether you should perform the elevated tasks or run normally. Encapsulate the tasks that require elevated privileges in separate methods and invoke them when your application is started using the command-line argument.
If your application needs to perform different sets of elevated tasks, use an additional argument to indicate which set should be executed.
In the following example, the performNormalTasks
method represents normal operation and the performElevatedTasks
method represents the tasks that require elevation. When the example is started, the Main
method is called and the arguments are checked to determine which of these methods should be called.
The checkElevatedPrivilege
method uses the System. Security.Principal.WindowsIdentity
and System.
Security.Principal.WindowsPrincipal
classes to establish our privilege level. We don't want to start a new process if the application has been started with elevated privileges, so the performNormalTasks
method checks the elevation level before calling the startElevatedInstance
method.
Starting the example normally will result in an elevated process being started with the elevated
argument. The new process will perform the elevated task and then exit. Starting the process as administrator will result in the elevated tasks being performed within the same process.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Security.Principal; using System.Diagnostics; namespace Recipe14_15 { class Program { static void Main(string[] args) { // Check to see if the first argument is "elevated". if (args.Length > 0 && args[0] == "elevated") { Console.WriteLine("Started with command line argument"); performElevatedTasks(); } else { Console.WriteLine("Started without command line argument"); performNormalTasks(); } } static void performNormalTasks() { Console.WriteLine("Press return to perform elevated tasks"); Console.ReadLine(); // Check to see if we have been started with elevated privileges. if (checkElevatedPrivilege()) {
// We already have privileges - perform the tasks. performElevatedTasks(); } else { // We need to start an elevated instance. startElevatedInstance(); } } static void performElevatedTasks() { // Check to see that we have elevated privileges. if (checkElevatedPrivilege()) { // perform the elevated task Console.WriteLine("Elevated tasks performed"); } else { // We are not able to perform the elevated tasks. Console.WriteLine("Cannot perform elevated tasks"); } Console.WriteLine("Press return to exit"); Console.ReadLine(); } static bool checkElevatedPrivilege() { WindowsIdentity winIdentity = WindowsIdentity.GetCurrent(); WindowsPrincipal winPrincipal = new WindowsPrincipal(winIdentity); return winPrincipal.IsInRole (WindowsBuiltInRole.Administrator); } static void startElevatedInstance() { ProcessStartInfo procstartinf = new ProcessStartInfo("Recipe14-15.exe"); procstartinf.Arguments = "elevated"; procstartinf.Verb = "runas"; Process.Start(procstartinf).WaitForExit(); } } }