Chapter 14. Windows Integration

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:

Note

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.

Access Runtime Environment Information

Problem

You need to access information about the runtime environment in which your application is running.

Solution

Use the members of the System.Environment class.

How It Works

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

 

CommandLine

Gets a string containing the command line used to execute the current application, including the application name. See recipe 1-5 for details.

CurrentDirectory

Gets and sets a string containing the current application directory. Initially, this property will contain the name of the directory in which the application was started.

HasShutdownStarted

Gets a bool that indicates whether the CLR has started to shut down or the current application domain has started unloading.

MachineName

Gets a string containing the name of the machine.

OSVersion

Gets a System.OperatingSystem object that contains information about the platform and version of the underlying operating system. See the discussion of this topic in this recipe for more details.

ProcessorCount

Gets the number of processors on the machine.

SystemDirectory

Gets a string containing the fully qualified path of the system directory—that is, the system32 subdirectory of the Windows folder.

TickCount

Gets an int representing the number of milliseconds that have elapsed since the system was started.

UserDomainName

Gets a string containing the Windows domain name to which the current user belongs. This will be the same as MachineName if the user has logged in on a machine account instead of a domain account.

UserInteractive

Gets a bool indicating whether the application is running in user interactive mode; in other words, its forms and message boxes will be visible to the logged-on user. UserInteractive will return false when the application is running as a service or is a web application.

UserName

Gets a string containing the name of the user that started the current thread, which can be different from the logged-on user in case of impersonation.

Version

Gets a System.Version object that contains information about the version of the CLR.

Methods

 

ExpandEnvironmentVariables

Replaces the names of environment variables in a string with the value of the variable. See recipe 14-2 for details.

GetCommandLineArgs

Returns a string array containing all elements of the command line used to execute the current application, including the application name. See recipe 1-5 for details.

GetEnvironmentVariable

Returns a string containing the value of a specified environment variable. See recipe 14-2 for details.

GetEnvironmentVariables

Returns an object implementing System.Collections.IDictionary, which contains all environment variables and their values. See recipe 14-2 for details.

GetFolderPath

Returns a string containing the path to a special system folder specified using the System.Environment.SpecialFolder enumeration. This includes folders for the Internet cache, cookies, history, desktop, and favorites; see the .NET Framework SDK documentation for a complete list of values.

GetLogicalDrives

Returns a string array containing the names of all logical drives, including network-mapped drives. Note that each drive has the following syntax: <drive letter>:.

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

Win32Windows

4

10

Windows 98

Win32Windows

4

90

Windows Me

Win32NT

4

0

Windows NT 4

Win32NT

5

0

Windows 2000

Win32NT

5

1

Windows XP

Win32NT

5

2

Windows Server 2003

Win32NT

6

0

Windows Vista, Windows Server 2008

Win32NT

6

1

Windows 7, Windows Server 2008 R2

The Code

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();
        }
    }
}

Retrieve the Value of an Environment Variable

Problem

You need to retrieve the value of an environment variable for use in your application.

Solution

Use the GetEnvironmentVariable, GetEnvironmentVariables, and ExpandEnvironmentVariables methods of the Environment class.

How It Works

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.

The Code

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();
        }
    }
}

Write an Event to the Windows Event Log

Problem

You need to write an event to the Windows event log.

Solution

Use the members of the System.Diagnostics.EventLog class to create a log (if required), register an event source, and write events.

How It Works

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.

Tip

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.

Note

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 Code

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();
        }
    }
}

Read and Write to the Windows Registry

Problem

You need to read information from or write information to the Windows registry.

Solution

Use the methods GetValue and SetValue of the Microsoft.Win32.Registry class.

Tip

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.

How It Works

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 Code

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();
        }
    }
}

Search the Windows Registry

Problem

You need to search the Windows registry for a key that contains a specific value or content.

Solution

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.

How It Works

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

ClassesRoot

HKEY_CLASSES_ROOT

CurrentConfig

HKEY_CURRENT_CONFIG

CurrentUser

HKEY_CURRENT_USER

DynData

HKEY_DYN_DATA

LocalMachine

HKEY_LOCAL_MACHINE

PerformanceData

HKEY_PERFORMANCE_DATA

Users

HKEY_USERS

Tip

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

CreateSubKey

Creates a new subkey with the specified name and returns a writable RegistryKey object. If the specified subkey already exists, CreateSubKey will return a writable reference to the existing subkey.

DeleteSubKey

Deletes the subkey with the specified name, which must be empty of subkeys (but not values); otherwise, a System.InvalidOperationException is thrown.

DeleteSubKeyTree

Deletes the subkey with the specified name along with all of its subkeys.

DeleteValue

Deletes the value with the specified name from the current key.

GetValue

Returns the value with the specified name from the current key. The value is returned as an object, which you must cast to the appropriate type. The simplest form of GetValue returns null if the specified value doesn't exist. An overload allows you to specify a default value to return (instead of null) if the named value doesn't exist.

GetValueKind

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 Microsoft.Win32.RegistryValueKind enumeration.

GetValueNames

Returns a string array containing the names of all values in the current registry key.

SetValue

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 RegistryValueKind as last parameter. If you don't provide such a value kind, one will be calculated automatically, based on the managed type of the object you pass as value to set.

Tip

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 Code

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();
        }
    }
}

Usage

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 Windows Service

Problem

You need to create an application that will run as a Windows service.

Solution

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.

Note

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.

How It Works

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.

Tip

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

OnStart

All services must support the OnStart method, which the SCM calls to start the service. The SCM passes a string array containing arguments specified for the service. These arguments can be specified when the ServiceController.Start method is called, and are usually configured in the service's property window in the Windows Control Panel. However, they are rarely used, because it is better for the service to retrieve its configuration information directly from the Windows registry. The OnStart method must normally return within 30 seconds, or else the SCM will abort the service. Your service must call the RequestAdditionalTime method of the ServiceBase class if it requires more time; specify the additional milliseconds required as an int.

OnStop

Called by the SCM to stop a service—the SCM will call OnStop only if the CanStop property is set to true.

OnPause

Called by the SCM to pause a service—the SCM will call OnPause only if the CanPauseAndContinue property is set to true.

OnContinue

Called by the SCM to continue a paused service—the SCM will call OnContinue only if the CanPauseAndContinue property is set to true.

OnShutdown

Called by the SCM when the system is shutting down—the SCM will call OnShutdown only if the CanShutdown property is set to true.

OnPowerEvent

Called by the SCM when a system-level power status change occurs, such as a laptop going into suspend mode. The SCM will call OnPowerEvent only if the CanHandlePowerEvent property is set to true.

OnCustomCommand

Allows you to extend the service control mechanism with custom control messages; see the .NET Framework SDK documentation for more details.

OnSessionChange

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 System.ServiceProcess.SessionChangeDescription object passed as an argument by the SCM contains details of what type of session change occurred. The SCM will call OnSessionChange only if the CanHandleSessionChangeEvent property is set to true. This method is new in the .NET Framework 2.0.

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 Code

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());
        }
    }
}

Usage

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.

Create a Windows Service Installer

Problem

You have created a Windows service application and need to install it.

Solution

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.

Note

You must create the installer class in the same assembly as the service class for the service to install and function correctly.

How It Works

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:

  1. In your project, create a class derived from the Installer class.

  2. Apply the attribute System.ComponentModel.RunInstallerAttribute(true) to the installer class.

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

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

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

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);
        }
    }
}

Usage

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.

Note

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.

Create a Shortcut on the Desktop or Start Menu

Problem

You need to create a shortcut on the user's Windows desktop or Start menu.

Solution

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.

How It Works

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.

  1. Instantiate a WshShell object, which provides access to the Windows shell.

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

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

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

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

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();
        }
    }
}

Create a Windows 7 Jump List

Problem

You need to create a Windows 7 Jump List for your application.

Solution

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.

How It Works

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:

  1. Import the Microsoft.WindowsAPICodePack.Shell and Microsoft.WindowsAPICodePack.Taskbar namespaces with the using directive.

  2. Add an event handler to the Shown member of your application class.

  3. Within the event handler, call the static method JumpList.CreateJumpList to create a new JumpList instance for your application.

  4. Create instances of JumpListLink to represent tasks and documents and add them to the JumpList instance using the AddUserTasks method.

Note

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 Code

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 Windows Search

Problem

You need to search the Windows file system.

Solution

Use Microsoft Windows API CodePack for .NET Framework to access the Windows Search feature. Create one or more Microsoft.WindowsAPICodePack.Shell.SearchConditions 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).

How It Works

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:

  1. Files that have the .txt extension

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

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

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

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

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

Tip

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

DateModified

DateTime

The last time that the file was changed

DateCreated

DateTime

The time the file was created

FileExtention

String

The extension for the file, including the period

FileName

String

The name of the file, including the file extension

FileOwner

String

The owner of the file

Table 14.7. Commonly Used Members of the SearchConditionOperation Class

Member

Description

Equal

The file attribute and the target value are the same.

NotEqual

The file attribute and the target value are different.

ValueContains

The file attribute contains the target value (for example, "windows" contains "win").

ValueStartsWith

The file attribute starts with the target value (for example, "windows" starts with "win").

ValueEndsWith

The file attribute ends with the target value (for example, "Microsoft" ends with "soft").

ValueNotContains

The file attribute does not contains the target value (for example, "Microsoft" does not contain "win").

LessThan

The file attribute is less than the target value.

GreaterThan

The file attribute is greater than the target value.

The Code

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.

Example search application

Figure 14.1. Example search application

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
");

        }
    }
}

Check Internet Connectivity

Problem

You need to check that the computer has Internet access.

Solution

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

How It Works

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 Code

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();
        }
    }
}

Display a Task Dialog

Problem

You need to display a task dialog—a standard Windows-specific dialog box, such as an elevated task request.

Solution

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

How It Works

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

Cancelable

Determines if the user can dismiss the dialog.

Controls

The set of controls embedded within the task dialog. See the recipe code for an example.

InstructionText

The summary text displayed in the dialog.

StandardButtons

The standard buttons for the dialog. Set with the values of the TaskDialogStandardButtons enumeration. See the recipe code for an example.

Text

The detailed text, displayed below the InstructionText.

The Code

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.

Windows Forms interface

Figure 14.2. Windows Forms interface

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.

Elevated task dialog

Figure 14.3. Elevated task dialog

Note

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
");
        }
    }
}

Write Custom Performance Counters

Problem

You need to create and write to performance counters to instrument your application.

Solution

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.

Note

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.

How It Works

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 Code

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.

Warning

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);
        }
    }
}

Read Performance Counters

Problem

You need to read performance counter values.

Solution

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.

How It Works

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.

Note

Administrator privileges are required to read performance counters.

The Code

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);
        }
    }
}

Obtain Elevated Privileges

Problem

You need elevated (administrator) privileges for part of your application's functionality.

Solution

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.

How It Works

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.

Tip

If your application needs to perform different sets of elevated tasks, use an additional argument to indicate which set should be executed.

The Code

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();
        }
    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.71.64