Project 5 - Delegates and Event Handling In this project you will

advertisement
Project 5 - Delegates and Event Handling
In this project you will learn what delegates are, how they improve the ‘robustness’ of your code,
and how to create an event publisher that will fire an event to its subscribers.
Reading all information is important to your understanding of the material at hand. Skipping
ahead to complete the project will not further your understanding of how things are working
concurrently with each other.
Delegates:
Delegates are used to specify a method to be called indirectly. In most programs, we specify a
method within a class, call it, and the code runs. This is a good iterative approach to breaking up
code, but what if we want to break that process into another thread, or maybe call a non-static
member from another class within a static context? Not sure what I mean? Here’s an example of
the simple iterative approach:
using System;
namespace Examples.410.NoDelegate
{
public class ProcessToRun
{
public void Process()
{
Console.WriteLine("Process() start.");
Console.WriteLine("Process() end.");
}
}
public class application
{
public static void Main()
{
ProcessToRun myProc = new ProcessToRun();
myProc.Process();
}
}
}
What we see here is a way to declare multiple classes within a namespace and file, as well as
creating a non-static class from which we can declare an object, and utilize the methods of that
object. An important note about this approach is that if we create any data from our application
and choose to process it with the object we created, that data must be sent to it. This can be a
problem when multiple instances of the object have been created.
Anyway, what we want to do now is explore delegates. Sometimes we don’t want to call a
function directly; rather, we’d just like to say this is the method I want to call; you call it when
you want to. Enter the delegate, which becomes phenomenally useful for event-driven
applications (i.e. GUIs, services) so that a particular piece of code executes at the click of a
button, or information is logged without specifying how.
The funny thing about a delegate is that it doesn’t care about the object it references. As long as a
methods argument types and return type are matched, it works fine. With this feature,
anonymous invocation can occur. There are a couple types of delegates as well (though they are
declared the same): single-cast and multicast.
A single-cast delegate basically works like this: we declare the delegate which states that now we
can use that delegate to reference any method with the return and argument types of that
delegate. In the main application, the delegate is used like a class, so we declare a new object
instance of the delegate, and we send it the method we wish to call.
Later, we can call on this object (of the delegate) which will then call the method we gave it in
the declaration. This seems kind of silly of first, but will make more sense later, just bear with
me.
Here’s what it looks like to declare a delegate:
delegate result-type identifier ([parameters])
Where:
result-type is the return type of the method
identifier is the delegate name (pseudo class name)
parameters are the parameters to the method.
So now we are ready to see this simple delegate in action:
using System;
namespace Examples.410.SimpleDelegate
{
//Create the delegate
public delegate void SimpDel();
public class application
{
//The function that will be called by the delegate
public static void MethCall()
{
Console.WriteLine("Called by delegate...");
}
public static void Main()
{
//Create an instance of the delegate
SimpDel simplex = new SimpDel(MethCall);
//Call the delegate function
simplex();
}
}
}
When compiled and executed, the output shows this:
Called by delegate...
Pretty neat huh? There still doesn’t seem to be a lot we can do, but let’s take it a little farther.
Say we have an application that calls upon another class to perform some tasks, but for error
messages to be logged, the class uses a delegate to log the message. So now we need our
application to declare an instance of this delegate and then find a function (or build one) that can
handle receiving and "logging" a string.
Here’s the code:
using System;
namespace Examples.410.LogHandlerSimple
{
public class ProcessToRun
{
// Create a delegate to receive a string
public delegate void LogHandler(string message);
//This abstract statement declares a method that receives a
//delegate of class-global type LogHandler which can be used
//e.g. a method to send data to a function that takes a string
public void Process(LogHandler logHandler)
{
//delegate must hold a method to call
if (logHandler != null)
{
logHandler("Process() begin");
}
if (logHandler != null)
{
logHandler ("Process() end");
}
}
}
// Test Application to use the defined Delegate
public class application
{
//A static method that matches the signature required by
//the delegate. This is how our application will Log
//the error messages received from the process.
static void Logger(string s)
{
Console.WriteLine(s);
}
static void Main()
{
ProcessToRun myProc = new ProcessToRun();
//Create a LogHandler delegate and tell it to use the
//static function we created.
// This delegate will be passed to the Process() function.
ProcessToRun.LogHandler myHandler
= new ProcessToRun.LogHandler(Logger);
myClass.Process(myHandler);
}
}
}
What if we want to change this application to log messages to a file? Leave the ProcessToRun
class alone, delete the application class, add ‘using System.IO;’ to the beginning of the file,
and add the following code:
public class FileLogger
{
FileStream fs;
StreamWriter sw;
public FileLogger(string filename)
{
fs = new FileStream(filename, FileMode.Append);
sw = new StreamWriter(fs);
}
//Method used within delegate to write message
public void LogMessage(string s)
{
//sw.WriteLine(s); <- this would log to the file,
// but for simplicity use this line:
Console.WriteLine(s);
}
public void Close()
{
sw.Close();
fs.Close();
}
}
public class application
{
static void Main()
{
FileLogger fl = new FileLogger(“process.log”);
ProcessToRun myProc = new ProcessToRun();
//Make a delegate and point it towards the LogMessage method
ProcessToRun.LogHandler myLogger
= new ProcessToRun.LogHandler(fl.LogMessage);
myProc.Process(myLogger);
fl.Close();
}
}
What we’ve done is successfully created a separate way to log messages, without changing any
code from our previous ProcessToRun class. Obviously this saves a lot of time in a multiple
developer setting; now whenever something needs to be logged, we have a simplified way of
doing that.
You might be saying to yourself now, this seems like a lot of work when I could have just created
another project to do the same thing much more simply. Yes that’s true, but with multicasting,
we can tell a delegate to call more than one method. Basically with multicasting, if you not only
want to log messages to a file, but also to the screen, you just tell the delegate, "Hey I have
another method for you to call."
Change your application class in the following way:
public class application
{
static void Logger(string s)
{
Console.WriteLine(s)
}
static void Main()
{
FileLogger fl = new FileLogger("process.log");
ProcessToRun myProc = new ProcessToRun();
//Make a delegate, point it towards the LogMessage method for
//both the FileLogger object and this static class.
ProcessToRun.LogHandler myLogger = null;
myLogger += new ProcessToRun.LogHandler(Logger);
myLogger += new ProcessToRun.LogHandler(fl.LogMessage);
myProc.Process(myLogger);
fl.Close();
}
}
When you run this you should see two lines for each message that is being “logged”. It should
now be obvious how powerful the delegate is.
Events:
Now what we can do is create an event. This event will occur in the following way: We create a
delegate and an event. The event uses the delegates identifier and creates one of those all too
familiar OnXXXX methods (which we will create). This will file the event when any method
calls our designated OnXXXX method. In turn, the event will call the delegates and the methods
associated with those delegates start begin executing. To see how this works, implement and run
the following:
using System;
using System.IO;
namespace Examples.410.EventHandler
{
//Publisher
public class ProcessToRun
{
// Define a delegate named LogHandler, which encapsulates any
// method that takes a string and returns nothing
public delegate void LogHandler(string message);
// Define an Event based on the above Delegate
public event LogHandler Log;
// Instead of having the Process() function take a delegate
// as a parameter, we've declared a Log event. Call the Event,
// using the OnXXXX Method, where XXXX is the Event name.
public void Process()
{
OnLog("Process() begin");
OnLog("Process() end");
}
// By Default, create an OnXXXX Method, to call the Event
protected void OnLog(string message)
{
if (Log != null)
Log(message);
}
}
// The FileLogger class merely encapsulates the file I/O
public class FileLogger
{
FileStream fs;
StreamWriter sw;
// Constructor
public FileLogger(string filename)
{
fs = new FileStream(filename, FileMode.Append);
sw = new StreamWriter(fs);
}
// Member Function which is used in the Delegate
public void Logger(string s)
{
sw.WriteLine(s);
}
public void Close()
{
sw.Close();
fs.Close();
}
}
// Subscriber
// It's now easier and cleaner to merely add instances
// of the delegate to the event, instead of having to
// manage things ourselves
public class application
{
static void Logger(string s)
{
Console.WriteLine(s);
}
static void Main(string[] args)
{
FileLogger fl = new FileLogger("process.log");
ProcessToRun myProc = new ProcessToRun();
// Subscribe the methods Logger and fl.Logger
myProc.Log += new ProcessToRun.LogHandler(Logger);
myProc.Log += new ProcessToRun.LogHandler(fl.Logger);
// The Event will now be triggered in the Process() Method
myProc.Process();
fl.Close();
}
}
}
Only a couple of things have changed in our application. It’s a little cleaner because we can add
a method to the Log event, by using our object to add a new LogHandler delegate and give it the
name of the method we want to invoke.
This all works, but what we have left out are the parameters to the event. The .NET Framework
requires that events take two parameters and return void. The first parameter is the source of the
event (publishing object) and the second is an object derived from EventArgs. Events are
properties of the class publishing the event and the keyword event controls how the event
property is accessed by the subscribing classes.
It might seem pointless to go through all of this to print a couple of lines on the screen, but think
about what we are doing. We can now subscribe any method to an event. If one were so
inclined, they could create a way to print out the time every second showing updates. It may
seem foolish to use events to accomplish this, but if you have multiple systems that depend upon
an event firing at precise increments, this could very easily make all of these systems fire
concurrently.
Finally, we can split our one large file up into multiple files or even multiple projects. The benefit
of doing this is that our ProcessToRun doesn’t care what’s being done in the methods that it calls
when the delegate fires, it just knows that it has to call them. The same goes for those methods
that know nothing of how they are being called, just that they now can perform their task. This is
extremely helpful in an environment where developers may be working on separate projects, but
have one common event that ties them together.
All code and examples in this project have been adapted from reference 1 below. This site also
offers good, alternative explanations.
References:
1. Delegates and Events in C# / .NET, Akadia AG, Fichtenweg 10, 3672 Oberdiessbach,
http://www.akadia.com/html/publications.html
2. Delegates Tutorial - MSDN Library
http://msdn2.microsoft.com/en-us/library/aa288459(VS.71).aspx
3. How to Properly Handle Cross-thread Events and Update a Label with BeginInvoke and
BackgroundWorker, http://www.codeproject.com/KB/cs/Cross_thread_Events.aspx.
Project Activity
Implement the four example programs of this project and make the indicated modifications.
Submit hard-copy of the source code with your name, the project example name, course
name/number and date in a comment header of each program.
NoDelegate
SimpleDelegate
LogHandlerSimple
EventHandler
Download