Events play a larger role in .NET than they do in Visual FoxPro. Learn how events work in the .NET world to write powerful applications.
Visual FoxPro developers are familiar with events and how they work in VFP. But working with events in .NET is a bit different. And when delegates come into the picture, things get even more complicated. In this article, we look at how events work in .NET and explain what delegates are, as always from a VFP perspective to make it easier for you to understand.
Events in VFP
Here's how events work in VFP: You drop a button onto a form, double-click on it, and an editor window for the Click method opens. You then write in any code you want, and everything just works. That's how events work in VFP, isn't it? To make a long story short, yes, this is it.
An understanding of how that magic works under the hood will help you understand how events work in .NET. As a bonus, you'll also acquire a better understanding of how the event-binding features work in VFP.
Take the example of the button's Click event, which is built into the CommandButton class. Clicking on a button fires its Click event. VFP doesn't know what to do then because every button has a set of actions to execute. VFP needs a method containing code to execute when the event fires. By default, the Click event is bound to a Click method on the same object to which the event belongs. (That's why you read "Click Event" on the Properties window; every method associated with an event has the Event word by it. This helps identify what methods are associated with an event and what methods are pure.) In other words, when the Click event fires, VFP looks for the Click method and automatically executes it. How can you make other methods run when the Click event fires? A feature new in VFP 8, called event binding, makes that possible.
VFP has always allowed the creation of references to objects in memory. For example, if a form in memory has a text box on it, you could create a variable that holds a reference to that text box, and set properties of the text box by accessing them through the new variable, like this:
Local loTextBox as TextBox
loTextBox = oForm.txtCustomerName
loTextBox.Value = "John and Jones"
However, an event requires more than a reference to an object. It requires a reference to a method of an object because the method is what's going to be called when the event fires.
Local Ref
Ref = MyObject.MyMethod
That isn't valid VFP code. The way you create a pointer to a method (in VFP 8 and later) is by using the BindEvents method. Its basic use is straightforward (code still takes the button's Click event example):
BindEvents(MyButton, "Click", ;
MyOtherObject, "MyMethod")
That way, you can attach as many methods to your Click event as you want. The line above creates references to the MyButton's Click method and MyOtherObject's MyMethod method, whenever the first one executes, the second one is called. In other words, whenever MyButton.Click happens, you delegate execution to MyOtherObject.MyMethod.
Callbacks make it easier to explain delegates
Consider the following scenario: There's a business object that deals with payments processing, and one of its methods can take a long time to execute. You can use this object from the visual user interface (such as a form), or instantiate it from a non-visual program (such as a simple PRG). Because the process takes a while to execute, it's a good idea to call back the user and report the status on the process, so he or she knows that something is running and the application is still responding.
We'll show you how to implement such a scenario in VFP in a way that facilitates the point we're going to make. First, we create this class:
Define Class LogProgress As Custom
Procedure LogProgress(lcProgress As String) As VOID
EndProc
EndDefine
The LogProgress class has a LogProgress method that accepts a parameter of type string, and it isn't supposed to return anything. The parameter it receives is a message about the progress of something that's running. Notice the LogProgress method doesn't have a single line of code; that's because at this point, we don't know where we should log the progress. Next, we create the PaymentBizObj class:
Define Class PaymentsBizObj As Custom
Procedure ProcessPayments(loLog As LogProgress) As VOID
loLog.LogProgress("Starting...")
Inkey(3, "H")
loLog.LogProgress("Making progress...")
Inkey(3, "H")
loLog.LogProgress("Done.")
EndProc
EndDefine
The class has a ProcessPayments method, which is the method that takes a while to execute (to simulate that, we use the Inkey() function just to cause the program to pause for a while; in a real-world situation, we'd have a lot more business code in there). The main thing about the ProcessPayments method is it receives a parameter of type LogProgress. We call the LogProgress method on that object every time we want to report any progress on the code the method is running.
It may seem strange that the LogProgress method doesn't do anything (remember, it doesn't have code in it), so what's the point in calling that method? The object is architected in such a way that we don't care whether the user will see the update by means of a form, a message on VFP's _Screen, etc., because all the ProcessPayments method does is generically call the LogProgress method on the loLog object.
Now, suppose you instantiate the PaymentsBizObj class in a simple PRG, and you want the progress to show in VFP's _Screen. To accomplish that, you create a special class that takes care of printing the messages to the screen:
Define Class ScreenLogger As Custom
Procedure Log(lcProgress As String) As VOID
Activate Screen
? lcProgress
EndProc
EndDefine
The ScreenLogger class couldn't be simpler. Its Log method simply activates the Screen and uses the quotation marks to print out the message it receives as a parameter. The most important thing to note is that the Log method's signature exactly matches the signature on the LogProgress method defined on the LogProgress class, meaning it receives a single parameter of type string, and doesn't return anything. You'll see shortly why that's important.
We've put all the classes we created so far in a file called Sample01.prg. Now all that's left to do is use the classes. We created a new .PRG file with the following content:
*-- Set procedure to the file that contains the classes.
Set Procedure To Sample01.prg
*-- Instantiate PaymentsBizObj class.
oPayments = CreateObject("PaymentsBizObj")
*-- Instantiate LogProgress class.
oLog = CreateObject("LogProgress")
*-- Instantiate ScreenLogger class.
oScreenLogger = CreateObject("ScreenLogger")
*-- Bind method on the Log object
*-- to method on the ScreenLogger object.
BindEvent(oLog, "LogProgress", oScreenLogger, "Log")
*-- Fire ProcessPayments method,
*-- passing in the Log object.
oPayments.ProcessPayments(oLog)
When you execute that code, messages ("Starting...," "Making progress...," and "Done...") are printed to the screen, with a time lag of 3 seconds between each (to simulate a process that takes a while to execute). We'll analyze each piece of code next.
After using the Set Procedure command, we instantiate the PaymentsBizObj class, the class that runs the long process:
oPayments = CreateObject("PaymentsBizObj")
The next step is to instantiate the LogProgress class:
oLog = CreateObject("LogProgress")
Even though that class doesn't do anything, we're interested in it because it determines the signature for the LogProgress method. Remember, you can create references to objects, and store the reference in a variable, but you can't create references to a method on an object the same way.
Next, you instantiate the ScreenLogger class, which is the object that knows how to print the messages to the screen:
oScreenLogger = CreateObject("ScreenLogger")
Now, remember the ProcessPayments method calls the LogProgress method on the LogProgress object? Well, you need a pointer to that method because whenever it fires, you want to call the Log method on the ScreenLogger object. How do you do this? This is where the BindEvent function comes to the rescue:
BindEvent(oLog, "LogProgress", oScreenLogger, "Log")
You could interpret that line as: Whenever someone takes the oLog object and calls its LogProgress method, please go take the oScreenLogger object and call its Log method.
Now that you have all the objects you need instantiated in memory, you just have to fire the ProcessPayments method and pass a reference to the oLog object:
oPayments.ProcessPayments(oLog)
The beauty of this architecture is you can easily enhance things without changing much. For example, say that besides printing the progress to the screen, you also want to log the message to a text file on disk. That would be easy. You'd have to create a new class, like this:
Define Class TextFileLogger As Custom
Procedure Log(lcProgress As String) As VOID
Strtofile(Chr(13) + Chr(10) +;
Transform(Datetime())+ ": "+;
lcProgress, "c:\temp\Log.txt", .T.)
EndProc
EndDefine
Like the ScreenLogger class, the TextFileLogger class has a method (called Log) with a signature that matches the signature of the LogProgress method on the LogProgress object. You can name the method anything you want; what matters is its signature. The code in the Log method is straightforward; it uses the StrToFile function to add a string to a text file on disk.
Now you just have to instantiate the TextFileLogger class, and use the BindEvent function to link this:
oTextFileLogger = CreateObject("TextFileLogger")
BindEvent(oLog, "LogProgress", oTextFileLogger, "Log")
When the oPayments.ProcessPayments method runs, it'll print messages on the screen and log the messages to a text file. We didn't have to change the method's implementation or the call to it; the architecture just allows for this extensibility. That means if you now want to also have a visual interface for it (figure 1), you can do that easily. The form in figure 1 has a custom method called DisplayProgress that updates the value of the text box and looks like this:
Lparameters lcProgress as String
thisform.txtProgress.Value = lcProgress
The Click method on the button that fires the process has this code:
Set PROCEDURE to Sample01.prg
Local loLog, loTextFileLogger, loScreenLogger, loPayments
loLog = Createobject("LogProgress")
loTextFileLogger = Createobject("TextFileLogger")
loScreenLogger = Createobject("ScreenLogger")
loPayments = Createobject("PaymentsBizObj")
BindEvent(loLog, "LogProgress", loTextFileLogger, "Log")
BindEvent(loLog, "LogProgress", loScreenLogger, "Log")
BindEvent(loLog, "LogProgress", Thisform, "DisplayProgress")
loPayments.ProcessPayments(loLog)
Now the messages reporting the progress appear on the screen, shown in the Form's Textbox control, and logged to a text file.
Why have we walked you through all this just to explain what a delegate is in the .NET world? Besides learning how to do something useful in VFP, you can also relate the LogProgress object you created here to what a delegate is in .NET. Because VFP doesn't have a pure delegate, we've sort of created one by defining a class that does nothing but declare a method we call generically and link to other methods in other objects. Now we'll show you how to implement the same scenario in .NET.
Delegates in .NET
In .NET, a delegate is defined like this (in C#; the main difference in VB is the semi-colon):
public delegate void LogProgress(string Progress);
That looks a lot like defining a method, other than the fact that you don't create a method's body, and you use the delegate keyword to indicate it's a delegate. The main difference between this and VFP is that you don't have to create a whole class, then define a method within it. You just create the method's signature. Under the hood, a delegate is an object, but you don't have to worry about that.
You can define the PaymentsBizObj like this:
public class PaymentsBizObj
{
public void ProcessPayments(LogProgress oLog)
{
oLog("Starting...");
// Run some code here...
System.Threading.Thread.Sleep(3000);
oLog("Making progress...");
// More code
System.Threading.Thread.Sleep(3000);
oLog("Done.");
}
}
Like in VFP, you define the ProcessPayments method, making it receive the LogProgress delegate as a parameter. Within the method, we used the Sleep method to make the thread pause for 3 seconds. In the ProcessPayments method, we call oLog(ProgressMessage) just like we call a normal function.
Next is the ScreenLogger class:
public class ScreenLogger
{
public void Log(string progress)
{
System.Console.WriteLine(progress);
}
}
Because .NET doesn't have a _Screen object like VFP does, we use the Console.WriteLine method to print a message to the console. Other than that, the class looks fairly simple, with the Log method matching the delegate's signature.
The TextFileLogger class looks a lot more complicated from the perspective of a VFP developer:
public class TextFileLogger
{
public void Log(string progress)
{
FileStream oFs = new FileStream(@"c:\temp\LogDotNet.txt",
FileMode.OpenOrCreate,FileAccess.ReadWrite);
StreamWriter oWriter = new StreamWriter(oFs);
oWriter.BaseStream.Seek(0, SeekOrigin.End);
oWriter.Write("\r\n" + DateTime.Now.ToString() +
": " + progress);
oWriter.Flush();
oWriter.Close();
oFs.Close();
}
}
This class looks so complicated because .NET doesn't have a StrToFile function like VFP does; therefore, writing some text to a text file requires a bit more code. Other than that, the concept for the class is still the same.
We also created a form in .NET that looks and behaves just like the one we created in VFP (figure 1). This form also has a DisplayProgress method that contains this code:
public void DisplayProgress(string progress)
{
this.txtProgress.Text = progress;
this.txtProgress.Refresh();
}
The OnClick method for the button that fires the process has this code:
PaymentsBizObj oPayments = new PaymentsBizObj();
LogProgress oLog = new LogProgress(this.DisplayProgress);
TextFileLogger tfl = new TextFileLogger();
oLog += new LogProgress(tfl.Log);
ScreenLogger sl = new ScreenLogger();
oLog += new LogProgress(sl.Log);
oPayments.ProcessPayments(oLog);
That's all it takes for the .NET implementation to work just like its counterpart in VFP. But you don't see a BindEvent function, and things look a bit different in the code above. We'll dissect it so you better understand what's going on.
Instantiating the PaymentsBizObj class isn't a mystery:
PaymentsBizObj oPayments = new PaymentsBizObj();
In .NET, you don't have to instantiate the delegate, then bind its method to a method on another object using the BindEvent function. Instead, you instantiate the delegate, and pass in the object.method you're going to point to. For example, to point it to the form's DisplayProgress method, you do this:
LogProgress oLog = new LogProgress(this.DisplayProgress);
But you also want to use the TextFileLogger and ScreenLogger classes. First you have to instantiate them:
TextFileLogger tfl = new TextFileLogger();
ScreenLogger sl = new ScreenLogger();
In VFP, you make calls to BindEvent, linking to the different methods. In .NET, you add delegates to other delegates. That means you'll instantiate a new delegate pointing to another specific method, and add the new delegate to the other existing one. In C#, that works like this:
oLog += new LogProgress(tfl.Log);
This is short for:
oLog = oLog + new LogProgress(tfl.Log);
Why do you add one object to another? That isn't an option in VFP. In C#, you can do this through something called operator overloading, which we'll cover in a future article. For now, just think of it as a way to add objects.
VB.NET started supporting operator overloading in version 2003, and will continue support in the next version coming with VS.NET 2005. As a result, you accomplish things slightly differently. One way to do it would be like this:
Dim oLog1 as LogProgress = _
New LogProgress(AddressOf this.DisplayProgress);
Dim tfl as TextFileLogger = New TextFileLogger();
Dim oLog2 as LogProgress = _
New LogProgress(AddressOf tfl.Log)
Dim oLog as LogProgress = _
LogProgress.Combine(oLog1, oLog2);
You can combine multiple delegates in what's known as a "multicast delegate," meaning one delegate can point to many different methods. After the delegate is called, all those different methods are sequentially called. Figure 2 shows a visual representation of the example in this article.
Events
As you may have already suspected, events are similar to multicast delegates. In fact, in .NET, events are multicast delegates. However, there's some special syntax that goes along with the definition of events. Also, events generally follow a pattern regarding the parameters they pass. There's usually a reference to the sender object, as well as an "event args" (event arguments) object. Therefore, the first step toward creating an event is creating a delegate representing that event. You then use that delegate in an event.
Assume you want to automate the above example even a bit further, and make the PaymentsBizObj object expose an event, rather than pass along a delegate. You could do that like this:
public class PaymentsBizObj
{
// Delegate.
public delegate void
ProgressChangedEventHandler(object sender,
ProgressChangedEventArgs e);
// Event.
public event ProgressChangedEventHandler ProgressChanged;
// EventArgs.
public class ProgressChangedEventArgs : EventArgs
{
public string Progress;
public ProgressChangedEventArgs() {}
}
// Method that raises the event.
protected virtual void OnProgressChanged(
ProgressChangedEventArgs e)
{
if (this.ProgressChanged != null)
{
this.ProgressChanged(this, e);
}
}
public void ProcessPayments()
{
ProgressChangedEventArgs args =
new ProgressChangedEventArgs();
args.Progress = "Starting...";
this.OnProgressChanged(args);
// Run some code here...
System.Threading.Thread.Sleep(3000);
args.Progress = "Making progress...";
this.OnProgressChanged(args);
// More code
System.Threading.Thread.Sleep(3000);
args.Progress = "Done.";
this.OnProgressChanged(args);
}
}
As you can see, the event definition is almost trivial after you have the delegate. It's simply a matter of making the multicast delegate available as a member of the class. This looks a bit more complex than it really is because we used complex parameters (objects) rather than just a simple text parameter like we did in the previous examples.
Here's how you can bind to these events:
PaymentsBizObj oPayments = new PaymentsBizObj();
oPayments.ProgressChanged +=
new PaymentsBizObj.ProgressChangedEventHandler(
this.DisplayProgress);
Again, note the += operator lets you add as many event handlers as you want. Also note that when it comes to events, VB.NET can be tricky. For example, there's the special "WithEvents" syntax. You can make the above PaymentsBizObj object a member of a form (this doesn't work with local objects) by adding this line at the top of a class definition:
Public WithEvents oPayments As New PaymentsBizObj()
The WithEvents keyword invokes VB.NET's automatic event notification system. All you have to do now is write a method that handles the events that may occur on that object:
Private Sub oPayments_OnProgressChanged(_
ByVal sender As Object,_
ByVal e As ProgressChangedEventArgs)_
Handles oPayments.OnProgressChanged
MsgBox(e.Text)
End Sub
Voilá! Every time you call the Execute() method on Me.oEx, this invokes the event handler method.
VB.NET also has some special syntax when it comes to defining and, in particular, raising events. First, you have to create the event and the delegate similar to the C# version:
Public Delegate Sub ProgressChangedEventHandler(_
ByVal sender As Object,_
ByVal e As ProgressChangedEventArgs)
Public Event ProgressChangedEventHandler_
As ProgressChanged
Like before, you have to define the second parameter as a class:
Public Class ProgressChangedEventArgs
Inherits System.EventArgs
Private _Progress As String = ""
Public ReadOnly Property Progress()
Get
Return Me._Progress
End Get
End Property
Sub New(ByVal Text as String)
MyBase.New()
Me._Progress = Text
End Sub
End Class
Now, whenever you want to raise the event, you can use VB.NET's special RaiseEvent syntax:
RaiseEvent ProgressChanged(Me,_
New ProgressChangedEventArgs(_
"Some text..."))
It's important to note that even though we've been talking about business objects, events for visual controls (such as buttons and text boxes) work the same way.
Custom events in VFP
VFP doesn't permit the creation of custom events like .NET. It's possible to work around this using the BindEvent function, but it's just a workaround. For example, consider a scenario where you have a Product class that has a UnitInStock property. Every time the UnitInStock property has a value of 0 (zero) assigned to it, an "Out of Stock" event will fire, so other objects can react and take appropriate actions (such as placing a new order for the product or sending e-mail). The Product class could look like this:
Define Class Product as Session
UnitsInStock = 0
ProductName = ""
PK = 0
Function Init()
*-- This should probably come from a database...
This.UnitsInStock = 10
This.PK = 15
EndFunc
Procedure UnitsInStock_Assign(lnValue as Integer)
This.UnitsInStock = lnValue
If lnValue <= 0
RaiseEvent(This, "OutOfStock", This.PK)
EndIf
EndProc
Procedure OutOfStockEvent(lnProductPK as Integer) as VOID
EndProc
EndDefine
An OutOfStockEvent method is defined in the code above. It's sort of a mixture between a delegate and an event, as you've seen previously in this article. On the Assign method for the UnitsInStock property, you check the assigned value, and if it's less than or equal to 0, you raise the OutOfStock "event" using the RaiseEvent function. (You have to keep in mind that it isn't a real event, it's just a simple method in that class.) The basic argument for that event would be the Product PK (it could be a complex object containing more information about the event).
The next step is to create an event handler, like this:
Define Class Handler as Custom
Procedure ProductOutOfStockHandler(;
lnProductPK as Integer) as VOID
? "Product PK " + Transform(lnProductPK) + ;
" ran out of stock." +;
" We are ordering more."
EndProc
EndDefine
The Handler class prints a message about the event when it fires. Another possible handler would be a method on a form that would do something when the user changes the value for UnitsInStock using the visual interface. For that to work, you could use code like this:
*-- Instantiate the Product class and the Handler class.
Local oProduct, oHandler
oProduct = CreateObject("Product")
oHandler = CreateObject("Handler")
*-- Bind the Product's OutOfStock method
*-- to the Handler's ProductOutOfStockHandler method.
BindEvent(oProduct, "OutOfStockEvent", ;
oHandler, "ProductOutOfStockHandler", 1)
*-- Make the product run out of stock.
*-- See message been echoed to the desktop.
oProduct.UnitsInStock = 0
When a 0 value is assigned to the UnitsInStock property, you see a message echoed to the screen. This isn't a pure implementation of a custom event, but it might work if you take the appropriate procautions. For example, nothing would prevent this code from compiling or executing:
oProduct.OutOfStockEvent()
This would be bad. Calling the OutOfStockEvent method directly causes the event handlers to fire. If UnitsInStock is greater than 0, orders would potentially be placed, when they shouldn't. Flagging the method as Protected would prevent the method from being called directly, but it would also prevent the BindEvent function from working because it can only bind to public methods. One possible way to avoid these problems would be to use some sort of naming convention (such as adding the "Event" keyword as a suffix to methods meant to simulate events), and use a Project Hook that would analyze all the code before the application compiles, looking for direct calls to such methods. Implementing this is beyond the scope of this article, but we just wanted to give you something to think about.
Write powerful applications
In this article, we discussed the differences between VFP and .NET in the context of delegates and events. Although you're used to working with events in VFP, they play a much bigger role in .NET and are more powerful because you can write custom events and custom complex arguments you can pass to the events. Understanding how events work in the .NET world will help you write powerful applications.
By Claudio Lassala and Markus Egger