2. Conditional Code
As the name suggests, conditional code is only executed under specified
conditions. Of course, language constructs like if else,
switch and do while allow you to provide conditional code, but the
value that is tested will
be provided by the application. For the purpose of this discussion the
condition will be external, that is provided outside of the
application. This section covers two types of external conditions: values
provided at compile time, and values provided at run time.
Compile time conditions are used in two ways: to determine what code is generated and to determine what code is called. In the first case the assembly will not contain code that does not fit the condition. This does not necessarily mean that the assembly will only contain code that is called (because, of course, the logic of your code may not call all code in the assembly) but it almost means that. In contrast, conditional code is marked with a condition and calling code will only be generated if the condition is met. This means that the assembly will contain the code regardless of the conditions.
Runtime conditions are values supplied when the application is run. The assembly can then use the conditions to determine which code is executed. To a certain extent all code uses runtime conditions, but for the purpose of this discussion I will cover the facilities in the framework to allow you to use these conditions to change how your code works.
2.1 Conditional Compilation
In C# this is straightforward, C# has a simple preprocessor that processes
commands in your C# source file. Conditional compilation is based upon
symbols (not to be confused with debugging symbols, which contain
information to help the debugger). A symbol is simply a named value that can
be defined (ie it exists) or not defined. There are two ways to define a symbol. The
first way is through the compiler command line, the second way is in the actual code using
the #define directive.
To test this out create a file called app.cs and type the following code:
class App
{
static void Main()
{
#if DEBUG
Console.WriteLine("this is debug code");
#else
Console.WriteLine("this is not debug code");
#endif
}
}
The directives tell the compiler to include the first
WriteLine only if the DEBUG symbol is defined, and only
include the second WriteLine only if the DEBUG
symbol is not defined. By convention you place directives at the first
column, but this is not mandatory. Compile this code and run it:
app
This should print this is not debug code because the symbol
DEBUG is not defined. To define this symbol use the /d command line
switch:
When you run this new version you'll find that the message this is debug code is printed. Now try this test: compile with the following:
What do you find? You should see the message this is not debug code,
the same as before. The reason is that when you use /debug the DEBUG
symbol is not automatically defined. This seems counter intuitive, but keep
this in mind if you write your own makefiles or compile from the command line.
The second way of defining a symbol is to use the #define
directive. This directive will define the symbol (ie specifies that it exists) but you
cannot define a value to it unlike the C #define preprocessor directive. The directive must appear at the top of the file before any
code (but you can have comments before this directive). Add the following
line:
using System;
Now compile the code without mentioning the symbol (csc app.cs)
and run the code to confirm that the symbol is defined. To be honest, I find
little use for this directive because if I have conditional code within my
file I want to control how it is included by defining the symbol externally
and
there seems little point in specifying this in the file. There are two cases
when it might be useful to define symbols in a file. The first is if you want
to try out some new code and don't want to delete the code it replaces (just
in case the new code does not work). This is effectively equivalent to
commenting out the old code. The other situation is if you use code compiled
with the [Conditional] attribute as described later. Note that
during this workshop I will use this directive extensively, but that is
because I am trying to teach you how to use instrumentation code, so the code
I will present is written specifically to allow the instrumentation to be
defined all the time. Clearly that means that the code is contrived.
The antithesis of the #define directive is the #undef
directive which will make sure that a symbol is not defined. Change the
previous line to:
using System;
and compile this code with /d to define the DEBUG symbol.
When you run this process you'll find that you'll get a result as if the
symbol had not been defined, in other words /d tells the compiler
that the symbol is defined and then #undef tells the compiler
that it isn't defined. I find this directive useful particularly if I use
makefiles or response files to define symbols because #undef
provides a fine-grain control.
If you have a Visual Studio project then you can use the project properties to determine the symbols that are defined. For example:

You can use the top edit box to give a semicolon delimited list of the
symbols that you want defined. Two standard symbols are DEBUG and
TRACE and these get their own check boxes. Both of these symbols
are defined for Debug configuration builds. The screenshot here is for
a Release configuration build, but notice that TRACE is defined
by default. I will have a lot more to say about this later.
The C# compiler provides other directives that are worth pointing out here.
Firstly there is an #elif directive that is essentially else
if, so you can build up a series of lines testing for the existence of
many different symbols. You can also use simple Boolean expressions with
==, !=, &&, ||, !
and parenthesis. Bear in mind that a symbol is treated as having a Boolean
value of true if it exists and false if it does not exist. So the following is
the same as the code above:
Console.WriteLine("this is not debug code");
#else
Console.WriteLine("this is debug code");
#endif
The #warning directive will generate a warning during
compilation. This is useful if you want to add a message that will be
displayed whenever the code is compiled. For example, remove the #undef
you added earlier and add the following line:
{
#if DEBUG
#warning using debug code
Console.WriteLine("this is debug code");
#else
Console.WriteLine("this is not debug code");
#endif
}
Notice that the line number (8) includes the line for the #if
directive. You can change the line numbering using the #line
directive:
{
#if DEBUG
#line 7
#warning using debug codebsp; Console.WriteLine("this is debug code");
#else
Console.WriteLine("this is not debug code");
#endif
}
When you compile this code you'll see that the warning will be given for
line 7, so the #line directive changes the line numbering
starting at the next line. The #error directive is similar, except that it
will generate a compiler error rather than a warning.
A final way to change the code that is called using command line arguments
is to provide an alternative version of the entry point. The convention in C#
is that the entry point method is a static method called Main. At
compile time the compiler will look for a class in the file(s) being compiled
for a suitable method and use that method as the entry point. The corollary is
that only one class can have a method called Main. However, you
can define more than one Main method as long as you tell the compiler which
one will be used as the entry point. Create a new file called
main.cpp:
class App
{
static void Main()
{
Console.WriteLine("App.Main");
App2.Main();
}
}
class App2
{
public static void Main()
{
Console.WriteLine("App2.Main");
}
}
Compile this code like this: csc main.cs. You will get two
errors: each saying that there are too many Main methods in the
file. Change the command line to indicate which one you want to use:
This time the code will compile and if you run this process you'll see that
App.Main will be called followed by App2.Main.
App2.Main is called because App.Main calls it, however, it
can only be called by another class if the method is public. The entry point
method does not have to be public, as shown with App.Main. If you
would prefer Main2 to be the entry point then you can specify
this method instead:
Providing different entry point methods means that you can have different initialization code, and yu can choose which code that is used through the command line.
2.2 Conditional Code
The framework provides a pseudo-custom attribute called [Conditional]
that can be used on classes or methods. This attribute provides a
.custom metadata on the item (which is why the attribute is a custom
attribute) but it is not recognised by the runtime (which is why it is a
pseudo-attribute). This attribute is a message to the compiler, it has no
effect at all on the runtime.
To see why this attribute is important consider how you would use
conditional compilation to add a debugging method. To do this create a file (app.cs)
and add the following:
class App
{
static void Main()
{
DumpData();
}
#if DEBUG
static void DumpData()
{
Console.WriteLine("this is debugging information");
}
#endif
}
The idea is that if the DEBUG symbol is defined then the debugging method,
DumpData, will be called. Compile this code and run it to confirm
that this is the case:
The problem occurs when you create a release mode application. Recompile
this code
but do not define the DEBUG symbol, you will get this error:
The problem is that the body of the method will not exist in release mode, so when the compiler comes to the line that calls the method it will not find the code to call. You can mitigate this issue with the following code:
{
#if DEBUG
DumpData();
#endif
}
#if DEBUG
static void DumpData()
{
Console.WriteLine("this is debugging information");
}
#endif
The problem is that the more directives yo have the more unreadable the code becomes.
The [Conditional] attribute is used to tell the compiler to
call the method only if the specified symbol is defined. Contrast this to
conditional compilation which says that the compiler should compile the
code only if the specified symbol is defined.
Change the code by removing the conditional compilation directives and adding the following lines:
using System.Diagnostics;
class App
{
static void Main()
{
DumpData();
}
Conditional("DEBUG"]
static void DumpData()
{
Console.WriteLine("this is debugging information");
}
}
Now compile the application twice and run it each time. For the first compilation define the DEBUG symbol,
and for the second time do
not define the DEBUG symbol. The compilation will succeed each
time. You should find that in the first
case DumpData will be called, but in the second case it will not.
Using this second version (compiled without the DEBUG symbol),
view the code with ILDASM. You will see code like this:
{
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 2 (0x2)
.maxstack 8
IL_0000: nop
IL_0001: ret
} // end of method App::Main
.method private hidebysig static void DumpData() cil managed
{
.custom instance void [mscorlib]System.Diagnostics.ConditionalAttribute::.ctor(string)
= ( 01 00 05 44 45 42 55 47 00 00 ) // ...DEBUG..
// Code size 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "this is debugging information"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
} // end of method App::DumpData
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method App::.ctor
} // end of class App
Notice that the assembly contains the conditional method, DumpData, but there is no
code in the Main method to call it. The corresponding Main method
for the DEBUG version of the process is:
{
.entrypoint
// Code size 8 (0x8)
.maxstack 8
IL_0000: nop
IL_0001: call void App::DumpData()
IL_0006: nop
IL_0007: ret
} // end of method App::Main
In this case there is a call to the method.
The [Conditional] attribute tells the compiler to include
that code marked with this attribute in the final assembly, but it says that
the compiler should only make calls that method only if the specified symbol is defined
for the compilation unit that calls the method.
In this example the conditional method is defined in the same assembly and the
same file as the code that might call it, but this does not have to be the
case. The calling code and conditional code can be in different source files
and they can be in different assemblies.
Note that a method like this presents a possibility of information disclosure because typically a method like this will be used to validate your data, or to perform some additional processing of your data. In most cases you will not want your customers to see such debugging information, but since the code will be part of your final assembly your customers can use a decompiler to view this code.
Since [Conditional] affects how code is called it is most
useful on library code. The framework uses this attribute on methods on the
Debug class (so that they are called only when the DEBUG symbol
is defined) and on methods on the Trace class (so that the methods are only
called when TRACE
is defined).
Out of interest I decided to search through all the framework classes and
see which classes, and which class members are marked with [Conditional].
The code to do this is pretty straightforward with reflection, all you have to
do is load all the types in an assembly and on each type call
GetCustomAttributes for ConditionalAttribute. Then if the
attribute is obtained you can access the ConditionalString
property. You can read the list here. It is
not particularly exciting, but it is interesting to see that Microsoft is not
fallible - the internal class, System.Net.GlobalLog, has
[Conditional] methods based on the symbol TRAVE. Since C
and V are next to each other on the keyboard I suspect that this is a typing
error.
2.3 Switches
Your application can be started with command line switches and these are
passed as a string array to the Main method of your application.
Typically, you will use your Main method to process the switches
passed through the command line and then use these values to alter how the
application works. If you want the switches to be made available to code
throughout the application you will have to find some mechanism to share this
data, perhaps using static properties.
You can also get access to all command line parameters anywhere in your
application as a single string
through the Environment.CommandLine property. However, note that
this is the complete command line, including the command that started
the process. The Environment.GetCommandLineArgs method will
return a string array where each item is a command line parameter (the command
line string is parsed treating spaces as the parameter separator, except when
parameters are enclose in quotes). This is not the same as the array passed to
the Main method because, like the CommandLine
property, the first parameter is the command used to start the process.
The CommandLine property and GetCommandLineArgs
method are static and will be available anywhere in your process.
2.4 .NET Switches
The framework also provides switch classes to allow you to use information
in the application configuration file. The relevant section of the
configuration file is the <switches> section of the <system.diagnostics>
section. To get a switch value you use a class derived from the abstract Switch
class. The constructor of the derived class takes the name of the switch
element to read, and this is passed to the base class constructor. The Switch class will look under the
<switches> node for an <add> node that has a
name attribute with the name you passed to the constructor. The
Switch class then looks for a value attribute and
then makes the value of this attribute available through the protected
property,
SwitchSetting.
The framework provides switch classes for the configuration of trace
sources. I will cover trace sources later in this workshop, so I will not
cover that aspect here. Version 1.1 and 1.0 of the framework used Switch
classes to provide configuration information, and that is the action that I
will cover here. The switch classes have changed since they appeared in the
first beta version of .NET (in those days, Switch also gave
access to environment variables and registry keys, so you had three options
where to store your switches) and as a consequence the actual implementation
is a little odd. The actual value of the switch is accessed through
SwitchSetting and is returned as an integer. However, the switch value
(the value attribute of the <add> node with the
appropriate name attribute) is actually a string value. This is
converted to an integer with a call to Int32.Parse. In .NET
3.0/2.0 classes derived from Switch
can get access to the string value through the Value property. If
the value attribute cannot be converted to an integer then the
SwitchSetting will be a value of zero.
The Switch constructor takes two parameters, the name of the
switch and a description. The second parameter is not used in this
implementation and you can pass an empty string. There is an additional constructor
that has three parameters where the third parameter is the default value for
the switch if the there is no switch in the configuration file.
Switch is abstract, and although the framework provides
several derived classes only one is relevant to this section:
BooleanSwitch. As the name suggests, the class gives access to the
switch as a Boolean through the Enabled property. For example,
create a file called app.cs
and add this code:
|
using System;
using System.Diagnostics; class App { public static BooleanSwitch isDebug; static void Main() { isDebug = new BooleanSwitch("debug", ""); if (isDebug.Enabled) { Console.WriteLine("debug code enabled"); } else { Console.WriteLine("debug code disabled"); } } } |
This has a static member initialized in the Main
function. Since it is public and static it means that this member can be
accessed by any object in the application. This switch is associated with a
node with a name of debug and if there is such a
switch, and it has a value of 1, then the
Enabled property will be true. As you can see, you can
access this object throughout your code to determine if the application should
run debugging code.
Compile this and run it. You'll find that debug code disabled will
be printed because there is no switch of the specified name. To get the
debugging code called you need to create a configuration file with an appropriate
switch. Create a file called app.exe.config and add the
following:
<system.diagnostics>
<switches>
<add name="debug" value="1"/>
</switches>
</system.diagnostics>
</configuration>
Now run the application again and confirm that this time the message
debug code enabled will be printed. If you change the value
to 0 then you'll get the previous message, debug code disabled.
As you can see, you can put extra features in your application and
selectively enable or disable them using a BooleanSwitch and a
configuration file.
There is an important point to be made about switches which is not apparent
from this short piece of code. When you first access a section in a
configuration file the contents of the entire section will be cached in
memory, so that the next time an entry in the section is read, the cached
value will be used. Furthermore, when you initialize a switch the switch value
will be saved in a class static member, so that if you create a new
Switch object for the same switch then the cached value will be used.
This means that if you have a long running process and your code has already
read a switch and then you change the configuration file then the existing
switch object will not reflect that change, and even if you create a new
switch object it will not show the change. There is one method that will clear
the switch cache (Trace.Refresh) but you have to explicitly call
it and re-recreate your switch objects.
Another switch class the framework provides is called TraceSwitch. This
allows you to read a value between 0 and 4 from a
switch in the configuration file. You can get the actual value (as an
enumeration, TraceLevel) through the
Level property or you
can access one of the four Boolean properties.
TraceLevel
looks like this:
The Boolean properties on TraceSwitch
correspond to whether Level is equal to one of the last four
values: TraceError, TraceWarning, TraceInfo
and TraceVerbose.
You can, of course, create your own switch class. As I mentioned earlier the switch can only return an integer value because
historically SwitchSetting returned the switch value in this way.
However, the actual
string that is read is stored in the object and is accessible through the Value
property, but because this is protected
it means that it can only be accessed by derived classes. It is
trivial to derive a class that gives access to this value:
{
public MySwitch(string name, string desc) : base(name, desc) {}
public new string Value { get { return base.Value; } }
protected override void OnValueChanged() { }
}
The Value property is straightforward all: it does is give
public access to the protected inherited property. However, the
OnValueChanged needs some explanation. This is the method that converts
the value's string value into an integer for SwitchSetting. The
default implementation will use In32.Parse and this will throw an
exception if the value is not one that can be converted to an integer. In this
implementation we do not use SwitchSetting so the implementation
of OnValueChanged does nothing.
The MySwitch class treats the switch as a name-value pair,
which is why I have exposed the Value property as a public
property. Switch has the ability to give access to other
attributes on the XML node. This feature is for trace sources, but it is worth
mentioning here. To allow other attributes in the configuration file node to
be read you need to specify their names. To do this you have to override the
protected GetSupportedAttributes method which returns a string
array with the attribute names. Now when an instance of your switch is created
the name and value of each attribute is available through the Attributes
property.
2.5 Switch Attributes
The .NET framework also provides switch attributes. To be honest I think
these are more trouble than they are worth, but I will document them here
anyway. If you use switches in your application you can add a [Switch]
attribute to indicate the name and type of the switch to an assembly, a class
or a type member. The SwitchAttribute class has a static member
called GetAll which you can use to get an array of all the
SwitchAttribute objects in the assembly: that is, the attributes that
have been applied to the
assembly, to any type in the assembly or any type member. The SwitchAttribute
class has three properties: SwitchType, SwitchName
and SwitchDescription. You can use reflection with the
SwitchType type object to create an instance of the appropriate type passing
SwitchName and SwitchDescription to the type's
constructor.
Of course, when you create a Switch object it will
read its value from the configuration file, so this is one way that you can
write generic code to get initialize all of the switches used by the assembly.
However, it all seems to me to be a lot
of effort.
Anyway, give this a try. Create a file (app.cs)
and add the following:
using System.Diagnostics;
using System.Reflection;
class App
{
static void Main()
{
Switch[] switches = GetSwitches();
foreach(Switch sw in switches)
{
Console.WriteLine(sw.ToString());
}
}
static Switch[] GetSwitches()
{
SwitchAttribute[] switchesAttr = SwitchAttribute.GetAll(Assembly.GetExecutingAssembly());
Switch[] switches = new Switch[switchesAttr.Length];
for (int x = 0; x < switches.Length; ++x)
{
ConstructorInfo ci = switchesAttr[x].SwitchType.GetConstructor(
new Type[]{typeof(string), typeof(string)});
switches[x] = (Switch)ci.Invoke(
new object[] { switchesAttr[x].SwitchName, switchesAttr[x].SwitchDescription });
}
return switches;
}
}
The GetSwitches method gets all of the [Switch]
attributes defined in the assembly and then uses the information in the
attribute to instantiate a Switch object. Note that the code gets
the constructor that takes two strings and invokes it with the name and
description in the attribute. If you create your own switch class you must
make sure that you provide such a constructor.
Compile and run this code, you should find that it will run, but it provides no results because the assembly has no switch attributes. Now add the following attributes:
[Switch("bClass", typeof(BooleanSwitch))]
class App
This says that the configuration will have two switches, bAssem
and bClass, both of them will have Boolean values.
Compile and run this code. This time you'll get the following results:
System.Diagnostics.BooleanSwitch
The problem with the abstract Switch class is that there is
no virtual member that gives a clue as to the value of the switch. As I
mentioned in the last section, the Value and SwitchSetting
properties are protected. You have no option other than to get
the switch value by downcasting:
foreach(Switch sw in switches)
{
if (sw is BooleanSwitch)
{
Console.WriteLine("{0} is {1}",
sw.DisplayName,
((BooleanSwitch)sw).Enabled);
}
else
{
Console.WriteLine(sw.ToString());
}
}
Compile and run this code. Now you will see:
bClass is False
This is to be expected because you do not have a configuration file and
hence the switches are not defined. Now create a configuration file that contains switch data (app.exe.config):
<system.diagnostics>
<switches>
<add name="bAssem" value="1"/>
<add name="bClass" value="1"/>
</switches>
</system.diagnostics>
</configuration>
Run the application again. You'll now see the following:
bClass is True
Change the switch values and run the application again and verify that the switches are being read.
As you can see, the SwitchAttribute class gives you access to
the names and types of the switches that the application will use, but you
have to do the work to actually instantiate those switches. To be honest, I
don't really see much use for this mechanism, because you already know that
you will use the switches, so what is the point of the [Switch]
attribute?
2.6 Monitoring
Try this code (monitor.cs):
using System.Threading;
using System.Diagnostics;
class App
{
static bool data;
static bool running;
static void Main()
{
Thread thread = new Thread(new ThreadStart(MonitorProc));
thread.Start();
Console.WriteLine("press a key to finish");
running = true;
Console.ReadKey();
running = false;
}
static void MonitorProc()
{
BooleanSwitch bs = new BooleanSwitch("debug", "");
data = bs.Enabled;
Console.WriteLine("initial value {0}", data);
do
{
bs = new BooleanSwitch("debug", "");
if (bs.Enabled != data)
{
data = bs.Enabled;
Console.WriteLine("changed to {0}", data);
}
Thread.Sleep(0);
}
while(running);
}
}
This creates a new thread that creates a new BooleanSwitch to
read the value of the debug switch from the configuration file and if the
value has changed print the new value on the command line. At the end of each
loop Thread.Sleep is called to give the main thread an
opportunity to read the keyboard buffer. If a key has been pressed the
running variable is set to false (to kill the worker thread) and the
main thread dies. This arrangement, polling for a change in a configuration
setting, is necessary because .NET does not give a mechanism to notify you
when a configuration setting has changed.
Next create a configuration file with this data (monitor.exe.config):
<system.diagnostics>
<switches>
<add name="debug" value="1"/>
</switches>
</system.diagnostics>
</configuration>
Compile the process. Now make sure that the configuration file is open in
Notepad (notepad monitor.exe.config) and run the process. Change
value to 0 and save the file. What do you see?
Change value to 1 and save the file. What do you
see? Here are my results:
initial value True
press a key to finish
C:\>
In other words, whenever a BooleanSwitch instance is created
it always has a value of Enabled to be true. In general, once one
BooleanSwitch instance has been created for a particular switch in the
application domain, all other instances for this particular switch will have
the same value. This means that the <switches> section in the
configuration file should be treated in the same way as switches passed to the
command line: they are read once and the cached value is used for subsequent
reads. The only difference between the two is that command line switches are
for the entire process, whereas configuration settings are for an application
domain.
The reason for this behaviour is that the configuration classes will read
the configuration file a section at a time (the definition of a section
varies, but in this case it means the <system.diagnostics>
element and its children) and the data is cached in memory. At a later point
in time a value is read from the same section the cached data will be used.
So, to get around this issue you need to get the configuration system to
re-read the section when a switch has changed.
Change the using statements like this:
using System.Threading;
using System.IO;
Now replace the code that uses BooleanSwitch to use a new
method called ReadSwitch:
{
data = (ReadSwitch("debug") != "0");
Console.WriteLine("initial value {0}", data);
do
{
bool b = (ReadSwitch("debug") != "0");
if (b != data)
{
data = b;
Console.WriteLine("changed to {0}", data);
}
Thread.Sleep(0);
}
while(running);
}
The ReadSwitch method is straight forward:
{
BooleanSwitch bs = new BooleanSwitch(switchName, "");
return bs.Enabled.ToString();
}
There are two issues with this code. We still have not fixed the fact that
once a section is read from the configuration file the values are cached in
memory. Furthermore, the configuration file is polled for a
change. Even if the ReadSwitch method read the configuration file
data (rather than a cached value) polling like this is a performance drain. Instead, it
would be better to
read the file only when the file has changed. Admittedly, configuration
files contain many types of information, other than switches, and if the
configuration file changes then it could
easily be non-switch information that has changed. The .NET framework provides the FileSystemWatcher class
to detect when a file changes and this class can call your handler code.
The first thing to do is change the using statements to
replace the threading namespace with the generic collection namespace:
using System.Collections.Generic;
using System.IO;
Now create a new class called SwitchMonitor, place in it
the ReadSwitch method and add an instance field
configFile:
{
string configFile
string ReadSwitch(string switchName)
{
// Rest of method
}
}
This is initialized in the constructor, as is the FileSystemWatcher object.
The design of this class will allow you to watch specific switches, and these
are held in a dictionary. The constructor looks like this:
public SwitchMonitor()
{
switches = new Dictionary<string, string>();
configFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
fsw = new FileSystemWatcher(Path.GetDirectoryName(configFile));
fsw.Filter = Path.GetFileName(configFile);
fsw.Changed += new FileSystemEventHandler(FileChanged);
fsw.IncludeSubdirectories = false;
}
The code obtains the configuration file name and uses this to initialize
the FileSystemWatcher object, any changes that occur to the file
will trigger a call to the FileChanged method. The switches
member has the name of the switches that you are watching. This makes the
logic easier because this class can look for a change in one or more specific
switches rather than all switches. You need to indicate the switch by calling
the Add method:
{
if (!switches.ContainsKey(key))
{
string val = ReadSwitch(key);
switches.Add(key, val);
}
}
Note that a switch is only added to the collection if it is not there already, and when it is added to the collection the value of the switch is obtained so that you will be informed only if this value changes. The user of this class will be given the ability of determining whether the file is monitored or not:
{
fsw.EnableRaisingEvents = true;
}
public void Stop()
{
fsw.EnableRaisingEvents = false;
}
Your code will be informed when a switch changes and this is done through an event. Add the following delegate declaration at file scope:
Now add the event and the handler method:
void FileChanged(object sender, FileSystemEventArgs args)
{
if (args.ChangeType == WatcherChangeTypes.Changed)
{
Trace.Refresh();
Dictionary<string, string> changes = null;
foreach (KeyValuePair<string, string> pair in switches)
{
string val = ReadSwitch(pair.Key);
if (val != pair.Value)
{
if (changes == null)
{
changes = new Dictionary<string, string>();
}
changes.Add(pair.Key, val);
if (SwitchChanged != null)
{
SwitchChanged(pair.Key, val);
}
}
}
if (changes != null)
{
foreach (string key in changes.Keys)
{
switches[key] = changes[key];
}
}
}
}
The handler only handles when the file has changed (ie, not when it is
created or deleted). It then calls Trace.Refresh. When you create
a Switch derived object the value of the switch will be read from
the config file and cached. The Refresh method clears that cache
so that the next time a switch is created it will be read from the config
file.
The code then loops through all switches that you identified and read the value of the switch. If the value has changed then the new value is stored temporarily until the loop completes. This temporary store is necessary because you cannot change a dictionary while it is being enumerated. If a switch has changed then the event is raised to give your code the opportunity to handle it. Finally, if any switches have changed then when the loop completes the dictionary is updated.
Finally, the SwitchMonitor object is created in the Main
method and a method is provided to handle the SwitchChanged
event, remove the MonitorProc method and add the following:
{
SwitchMonitor sw = new SwitchMonitor();
sw.Add("debug");
sw.SwitchChanged += new SwitchChangedDelegate(SwitchChanged);
sw.Start();
Console.WriteLine("press a key to finish");
Console.ReadKey();
sw.Stop();
}
static void SwitchChanged(string name, string val)
{
Console.WriteLine("{0} = {1}", name, val);
}>
Compile the new code and test it out as before.
There are several things to note about this code. First, it only detects
changes in the application configuration file, any changes made to the machine
configuration file will not be detected. Second, the monitor code runs on a single thread, which means that you will
avoid most threading issues. However, you should be aware that the
FileChanged handler is called by a system thread, therefore you should
make sure that the SwitchChanged handlers do not perform lengthy
code. One way to mitigate this issue is to invoke the SwitchChanged
event on another thread, perhaps passing the changes collection
to this new thread so that there is no multi threaded access to the
switches collection. All of this code is straightforward to do and is
left as an exercise to the reader.
| I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com. |
Errata
If you see an error on this page, please contact me and I will fix the problem.