Introduction
This is an article about using delegates in a way in which you may not have thought of using them before. As I started to write this it became obvious that this would be a long article, so I am splitting in two. This first part is an overview of delegates and how to group functionality and cross cutting concerns with them. Part two will delve into using delegates to create a validator which can validate any type of object dynamically.
http://www.codeproject.com/Articles/1051860/Doing-Delegates-Differently-Part Link to part two.
Background
A delegate is defined as a type safe function which can be passed around just like a reference variable (pointer to a memory space) and can be used to inject functionality into an object.
There are many articles you will find about delegates and multicast delegates on CodeProject or MSDN, if you need more of an introduction on the mechanics of delegates; please search for these.
If you have done Winforms development you have already used delegates in the form of event handlers.
Assume you have a button called btnExample_Click
(Region 1 in code)
In a partial class created by designer you will find the following and in the CS file you will find the referenced code.
this.btnExample.Click += new System.EventHandler(this.btnExample_Click);
private void btnExample_Click(object sender, EventArgs e)
{
}Colourised in 2ms
System.EventHandler() is defined to take a predefined delegate called void EventHandler(object sender, EventArgs e) where object it the object which generated the event (in this case the button) and EventArgs is defined as a class which allows you to pass data specific to the object which generated the event.
If you have used Linq extensions you have also used delegates in the form of functions passed to do something on behalf of the extension.
(Region 2 in code)
private void btnExample2_Click(object sender, EventArgs e)
{
List<string> lst = new List<string>();
lst.Add("Steve");
lst.Add("Sam");
lst.Add("Mark");
string result = lst.Where( w => { return w == "Steve"; }).FirstOrDefault();
result = lst.Where(SamFunction).FirstOrDefault();
result = lst.Where(MarkFunction).FirstOrDefault();
}
Colourised in 6ms
You may have seen a signature that looks like.
Lst.Where(Func<TSource, bool> predicate)Colourised in 0ms
The Where extension is asking for a function which takes an item of the list and return true or false depending on the object passed in. The parameter can be filled in a number of ways:
As a Lambda Expression:
string result = lst.Where( w => { return w == "Steve"; }).FirstOrDefault(); - //result = Steve
As a function passed in:
result = lst.Where(SamFunction).FirstOrDefault(); - //result Sam
Where Sam function is defined as:
private bool SamFunction(string inval )
{
return inval == "Sam";
}Colourised in 1ms
As a Lambda expression (or function) assigned to a variable of Func(string,bool)
result = lst.Where(MarkFunction).FirstOrDefault(); - //result Mark
Where MarkFunction is defined as:
Func<string, bool> MarkFunction = (w) => { return w == "Mark"; };Colourised in 1ms
In all cases, the function returns true or false depending on item passed in.
Going Forward
So the question is, can you create your own functions which use other functions inside of them?
There are two special keywords:
Action and Func
Which are delegates which can be used without explicitly declaring a custom delegate.
Action is used to pass void functions (sub routines in VB.Net) and Func allow you to pass functions (function in VB.Net) which return a value.
Each delegate type is overloaded to accept different number (up to 16) of input parameters and in the case of Func, TResult to return only one parameter.
Example:
Action(T) accepts one parameter of generic T type.
Action(T1, T2) accepts two parameters of generic T type.
Func(T, TResult) accepts one parameter of generic T type and returns a generic TResult.
Func(T1, T2, TResult) accepts two parameters of generic T type and returns a generic TResult.
The last parameter in a Func delegate is the type of return.
(Region 3 in code)
In the following, I am creating three functions which accept either an Action or Func delegate as a parameter.
private void MyMethod(Action a)
{
a.Invoke();
}
private void MyMethod(Action<string> a, string msg)
{
a.Invoke(msg);
}
private string MyMethodString(Func<string> f)
{
return f.Invoke();
}
private void btnExample3_Click(object sender, EventArgs e)
{
MyMethod(() => { MessageBox.Show("Show Me"); });
string s = "SHOW ME";
Action<string> ai = (val) => { MessageBox.Show(val); };
MyMethod(ai,s);
string retval = MyMethodString(() => { return "the Money";});
MessageBox.Show(retval);
}Colourised in 17ms
(Region 4 in code)
The question is then, what other things can we do with them?
One idea is to use them to group cross cutting concerns into one place:
How many times have you written functions which looks something like the following?
private void WriteHeader()
{
lg.Log("Start Header", "Program", Logger.LogLevel.Info);
try
{
var fs = new StreamWriter("test.txt", true);
fs.WriteLine("Header");
fs.Close();
}
catch (Exception ex)
{
lg.Log(ex.Message,"Header",Logger.LogLevel.Error); }
}
private void WriteBody()
{
lg.Log("Start Body", "Program", Logger.LogLevel.Info);
try
{
var fs = new StreamWriter("test.txt", true);
fs.WriteLine("Body");
fs.Close();
}
catch (Exception ex)
{
lg.Log(ex.Message,"Body",Logger.LogLevel.Error);
}
}
private void WriteFooter()
{
lg.Log("Start Footer", "Program", Logger.LogLevel.Info);
try
{
var fs = new StreamWriter("test.txt", true);
fs.WriteLine("Footer");
fs.Close();
}
catch (Exception ex)
{
lg.Log(ex.Message,"Footer",Logger.LogLevel.Error);
}
}
Colourised in 17ms
Maybe more of these…
Everything is working fine then your boss says, “Just went through the logs and realized I never told you that you should also log when the function exits. And by the way, you should be wrapping your code in a using statement.”
Now you have to go to X number of functions and add x.log(“end”) to the bottom and wrap in a using statement.
Now the boss says: “I recently went to my re-union and met an old friend who told me all about handling errors and re-throwing friendly messages”
Over time each of your functions grow and progressively harder to maintain. The next developer to support the code doesn’t know which methods need to be modified for the next request. The issues compound.
What if you could execute all these requests in one place and defer execution until you’re ready? That is what we will do using delegates.
(Region 5 in code and Example Helper classes)
What I have done is to create a class which I can add the delegates and execute and invoke in one place. The execution is deferred until I call the method to write.
(Region Example Helper Classes)
class WriteDocument
{
ILogger lg;
string fname;
public WriteDocument(ILogger log,string filename)
{
lg = log;
fname = filename;
}
private List<ActionDefinition> la = new List<ActionDefinition>();
internal void AddAction(ActionDefinition a)
{
la.Add(a);
}
internal void Write()
{
Write(true);
}
internal void Write(bool orderdirectionasc)
{
lg.Log("WriteDocument Start", "WriteDocument", Logger.LogLevel.Info);
try
{
using (var fs = new StreamWriter(fname, false))
{
List<ActionDefinition> lordered;
if (orderdirectionasc)
lordered = la.OrderBy( o => o.dOrderRun).ToList();
else
lordered = la.OrderByDescending( o => o.dOrderRun).ToList();
foreach (var action in lordered)
{
action.dAction.Invoke(fs);
lg.Log(action.dDescription, "WriteDocument", Logger.LogLevel.Info);
}
}
}
catch (Exception ex)
{
lg.Log(ex.Message, "WriteDocument", Logger.LogLevel.Error);
throw new Exception("An Error occured please check your logs");
}
finally
{
lg.Log("WriteDocument End", "WriteDocument", Logger.LogLevel.Info);
}
}
Colourised in 33ms
Notice is looks alot like what we have in each function. In addition the boss asked if we could print both forward and backword. Something we could not easily do before. :)
It uses a supporting class to hold information about the function being added and invoked:
class ActionDefinition
{
public Action<StreamWriter> dAction { get; set; }
public string dDescription { get; set; }
public int dOrderRun { get; set; }
}
Colourised in 8ms
(Region 5 in code)
Boss Alert! "I want to add a section to print, but ONLY if a test come back positive."
It is set up in the following way:
private void WriteHeader(StreamWriter fs)
{
fs.WriteLine("Header");
}
private void WriteBody(StreamWriter fs)
{
fs.WriteLine("Body");
}
private void WriteFooter(StreamWriter fs)
{
fs.WriteLine("Footer");
}
private void WriteSugarHigh(StreamWriter fs)
{
fs.WriteLine("No more sugar for you!");
}
private void btnExample5_Click(object sender, EventArgs e)
{
bool sugarhigh = true;
WriteDocument wd = new WriteDocument(lg,"textasc.txt");
wd.AddAction(new ActionDefinition() {dAction = WriteHeader,
dDescription = "Header", dOrderRun = 1} );
wd.AddAction(new ActionDefinition() {dAction = WriteBody,
dDescription = "Body", dOrderRun = 2 });
wd.AddAction(new ActionDefinition() {dAction = WriteFooter,
dDescription = "Footer", dOrderRun = 3} );
wd.Write();
WriteDocument wddesc = new WriteDocument(lg, "textdesc.txt");
wddesc.AddAction(new ActionDefinition() { dAction = WriteHeader,
dDescription = "Header", dOrderRun = 1 });
wddesc.AddAction(new ActionDefinition() { dAction = WriteBody,
dDescription = "Body", dOrderRun = 2 });
if (sugarhigh)
wddesc.AddAction(new ActionDefinition() { dAction = WriteSugarHigh,
dDescription = "HighSugar", dOrderRun = 2 });
wddesc.AddAction(new ActionDefinition() { dAction = WriteFooter,
dDescription = "Footer", dOrderRun = 3 });
wddesc.Write(false);
}
Colourised in 25ms
Three files are created:
Log File, Testasc.txt and testdesc.txt.
Although there is some setup to do, there is a lot of functionality we could not do before.
This example would be very useful when dynamically creating certain sections of a document depending on other types of data, for example a section is inserted in a document because a blood test came back that your sugar was high. In addition, you can print backwards as well!
if (sugarhigh)
wddesc.AddAction(new ActionDefinition() { dAction = WriteSugarHigh,
dDescription = "HighSugar", dOrderRun = 2 });Colourised in 2ms
In the example, the descending file will have an extra line for sugar high
Points of Interest
In addition to examples illustrated here, I have also used this technique to execute blocks of code which need to be included in a SQL transaction, comitting when all blocks are successful or rolling back when any one fails.
When debugging, it helps to seperate the Lambda expression over several lines so you can set a break point more easily. Another trick is to write as a named function, debug your logic and convert to an anonymous function later.
In Part 2 of this article, I will continue the discussion. Showing how to use delegates and Generics to create a dynamically loaded object validator.