Delegates, single-cast events, and multicast events are names for language constructs that you have been using for quite some time now.
Although there are some differences between C# and Delphi events, delegates in all three languages — C#, Delphi for Win32, and Delphi for .NET — are simply methods that are called in response to an event.
Single-cast events are events that call only one event handler in response to themselves. They are used by the VCL. Delphi for Win32 only supports single-cast events.
Multicast events are events that can call multiple event handlers. Windows.Forms controls use multicast events. Delphi for .NET language supports both single-cast and multicast events.
A delegate in Delphi is a procedure of object like, for instance, the TNotifyEvent and TMouseEvent types, which looks like this:
type TNotifyEvent = procedure (Sender: TObject) of object; type TMouseEvent = procedure (Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer) of object;
Single-cast events are created with the read and write reserved words:
FOnClick: TNotifyEvent; ... property OnClick: TNotifyEvent read FOnClick write FOnClick;
Now it's time to see how to create and use a custom delegate in Delphi for Win32. We are going to derive a new component from TGraphicControl, draw randomly colored rectangles on the component's surface, and fire a custom event of type TSelectColor when the user clicks on the component, as shown in Figure 30-3.
Figure 30-3: Using a custom delegate in Delphi for Win32
Besides the Sender parameter, which tells us the component that called the method, the TSelectColor delegate has another parameter, SelectedColor, which tells us what color is selected. Here's the TSelectColor delegate:
type TSelectColor = procedure(Sender: TObject; SelectedColor: TColor) of object;
The TGraphicControl descendant, the TColorMix component, needs to override two methods of the TGraphicControl to do its job: Paint and MouseUp. It needs to override the Paint method to paint on the component's surface and it needs to override the TGraphicControl's MouseUp method to fire the custom OnSelectColor event when the user releases a mouse button over the component.
Listing 30-12 shows the source of the TColorMix component and the source of the main form that uses the component.
Listing 30-12: Using a custom delegate in Delphi for Win32
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs; type TSelectColor = procedure(Sender: TObject; SelectedColor: TColor) of object; TColorMix = class(TGraphicControl) private FOnSelectColor: TSelectColor; public constructor Create(AOwner: TComponent); override; procedure Paint; override; procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; property OnSelectColor: TSelectColor read FOnSelectColor write FOnSelectColor; end; TMainForm = class(TForm) procedure FormCreate(Sender: TObject); private { Private declarations } Mix: TColorMix; public { Public declarations } procedure SelectColorDelegate(Sender: TObject; SelectedColor: TColor); end; var MainForm: TMainForm; implementation {$R *.dfm} constructor TColorMix.Create(AOwner: TComponent); begin inherited; Align := alClient; // automatically fill the form end; { Paint 100 rectangles on the component's surface } procedure TColorMix.Paint; var i, j: Integer; rcWidth: Integer; rcHeight: Integer; begin inherited; rcWidth := Width div 10; rcHeight := Height div 10; // paint random rects for i := 0 to 10 do for j := 0 to 10 do begin Canvas.Brush.Color := RGB(Random(255), Random(255), Random(255)); Canvas.Rectangle(i * rcWidth, j * rcHeight, (i + 1)* rcWidth, (j + 1) * rcHeight); end; end; procedure TColorMix.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var selectedColor: TColor; begin inherited; if Assigned(FOnSelectColor) then begin selectedColor := Canvas.Pixels[X, Y]; // Fire the OnSelectColor event, and pass Self as the Sender // parameter and the selected color as the SelectedColor param. FOnSelectColor(Self, selectedColor); end; end; // -------------------------------------------------------- procedure TMainForm.FormCreate(Sender: TObject); begin // display the TColorMix component on the form Mix := TColorMix.Create(Self); Mix.Parent := Self; Mix.OnSelectColor := SelectColorDelegate; end; // this is called by the OnSelectColor event procedure TMainForm.SelectColorDelegate(Sender: TObject; SelectedColor: TColor); var Frm: TForm; begin // if user didn't click on border (black) if SelectedColor <> clBlack then begin Frm := TForm.Create(Self); try Frm.Caption := 'Selected Color'; Frm.Position := poScreenCenter; Frm.Color := SelectedColor; Frm.ShowModal; finally Frm.Free; end; end; end; end.
Multicast events can be implemented with great ease in Delphi. The only thing that you have to do is use the reserved words add and remove, instead of read and write:
property OnClick: TNotifyEvent add FCustomOnClick remove FCustomOnClick;
The above OnClick event is from a custom button class, displayed in Listing 30-13. In order to create a multicast version of the OnClick event, you have to override the TButton's Click procedure and, instead of the inherited behavior, call FCustomOnClick. Figure 30-4 shows what happens when you click on a TMultiClickButton.
Figure 30-4: Using a TMultiClickButton
Listing 30-13: A TButton descendant with a multicast OnClick event
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, System.ComponentModel, Borland.Vcl.StdCtrls, Borland.Vcl.XPMan; type TMultiClickButton = class(TButton) private FCustomOnClick: TNotifyEvent; public procedure Click; override; published property OnClick: TNotifyEvent add FCustomOnClick remove FCustomOnClick; end; TMainForm = class(TForm) XPManifest: TXPManifest; procedure FormCreate(Sender: TObject); private { Private declarations } mcb: TMultiClickButton; public { Public declarations } procedure ChangeCaption(Sender: TObject); procedure ShowMyMessage(Sender: TObject); end; var MainForm: TMainForm; implementation {$R *.nfm} procedure TMultiClickButton.Click; begin if Assigned(FCustomOnClick) then FCustomOnClick(Self); end; // ---------------------------------------------------------------- procedure TMainForm.FormCreate(Sender: TObject); begin mcb := TMultiClickButton.Create(Self); mcb.Parent := Self; mcb.SetBounds(10, 10, 200, 25); mcb.Caption := 'Click to call two handlers!'; // add TWO methods to the OnClick event Include(mcb.OnClick, ChangeCaption); Include(mcb.OnClick, ShowMyMessage); end; // another sample method procedure TMainForm.ChangeCaption(Sender: TObject); begin Caption := 'Hello from method #1!'; end; // another sample method procedure TMainForm.ShowMyMessage(Sender: TObject); begin MessageDlg('Hello from method #2!', mtInformation, [mbOK], 0); end; end.
Delegates can be used to anonymously call methods and to implement events in C# classes. To create a delegate in C# (procedure of object equivalent), you have to use the following syntax:
public delegate ReturnType DelegateName(Parameters)
To use a delegate, you then have to create a method with the same return type and the same parameter list. When you have both the delegate and a method with the appropriate return type and parameter list, you can call the method through the delegate, using the following syntax:
delegate InstanceName = new delegate(MethodName); delegate(MethodParameters);
Listing 30-14 illustrates how to use the above syntax to declare, instantiate, and call a method through a delegate.
Listing 30-14: Using delegates
using System; namespace Wordware.Delegates { public delegate void MathOperation(int value); class DelegateUser { public static int x = 0; public static void Add(int value) { x += value; } public static void Subtract(int value) { x -= value; } public static void Show() { Console.WriteLine("x = {0}", x); } [STAThread] static void Main(string[] args) { MathOperation a = new MathOperation(Add); a(100); Show(); // 100 MathOperation b = new MathOperation(Subtract); b(50); Show(); // 50 Console.ReadLine(); } } }
Now that you know the syntax involved in the creation and usage of a delegate, it's time to see how to implement events in C# classes.
Once you've created a delegate, you declare an event using the reserved word event:
public event DelegateType EventName;
Events in C# are fired like events in Delphi: you first check whether an event has event handlers assigned to it and then call the handlers:
if(Event != null) Event(ParameterList);
Finally, you need to assign an event handler to an event, using the following syntax:
Object.Event += new DelegateType(EventHandler);
Listing 30-15 shows the MyFileReader class that can read text files. The class has four events: OnNoFile (error), OnOpen, OnReadLine, and OnClose. The OnReadLine event is the most complex and the most important event because it allows other classes to do whatever they want with the strings read from the file. In this example, the DelegateUser class's OnReadLine event handler only reads the interface part of a Delphi unit and displays it in the console window. The OnReadLine event handler of the FormUser class shows how to use the same MyFileReader class and read the file into a ListBox.
Figure 30-5 shows how the event handlers of the DelegateUser class work.
Figure 30-5: Reading a text file using delegates and events
Listing 30-15: Delegates and events in C#
using System; using System.IO; using System.Windows.Forms; namespace Wordware.Delegates { /* three different delegates */ public delegate void Notification(); public delegate void ErrorNotification(string file); public delegate void BreakEvent(string line, ref bool Break); class MyFileReader { /* events */ public event Notification OnOpen; public event Notification OnClose; public event ErrorNotification OnNoFile; public event BreakEvent OnReadLine; /* this method fires all four events */ public void ReadFile(string FileName) { if(!File.Exists(FileName) && (OnNoFile != null)) { OnNoFile(FileName); return; } StreamReader sr = new StreamReader(FileName); try { if(OnOpen != null) OnOpen(); string line; bool Break = false; while((line = sr.ReadLine()) != null) { if(OnReadLine != null) { OnReadLine(line, ref Break); } /* if Break == true, the caller wants to stop reading from the file */ if(Break == true) return; } } finally { sr.Close(); if(OnClose != null) OnClose(); } } } // MyFileReader class // -------------------------------------------------------------- class DelegateUser { public static void OpenHandler() { Console.WriteLine("File opened..."); Console.WriteLine("--------------"); } public static void CloseHandler() { Console.WriteLine("--------------"); Console.WriteLine("File closed..."); } public static void ErrorHandler(string file) { Console.WriteLine("Cannot load file \"{0}\".", file); } public static void ReadInterfaceOnly(string line, ref bool Break) { if(line == "implementation") Break = true; else Console.WriteLine(line); } [STAThread] static void Main(string[] args) { string fileName = @"C:\My Components\StringsCache.pas"; MyFileReader mfr = new MyFileReader(); /* Passing handlers to all four events */ mfr.OnOpen += new Notification(OpenHandler); mfr.OnClose += new Notification(CloseHandler); mfr.OnNoFile += new ErrorNotification(ErrorHandler); mfr.OnReadLine += new BreakEvent(ReadInterfaceOnly); // displays the file and fires the events mfr.ReadFile(fileName); // display the file on a form by using a // different OnReadLine event handler FormUser fu = new FormUser(); fu.DisplayFile(); Console.ReadLine(); } } // -------------------------------------------------------------- class FormUser { private string fileName = @"C:\My Components\StringsCache.pas"; private Form f; private ListBox fileList; private MyFileReader fr; public void ReadFileToList(string line, ref bool Break) { fileList.Items.Add(line); // after the unit's name has been displayed, // remove the display unit name handler; this // works because the OnOpen event first assigns // the DisplayUnitName handler, and then the // main code assigns the ReadFileToList handler // to the OnReadLine event fr.OnReadLine -= new BreakEvent(DisplayUnitName); } public void OpenHandler() { // add another handler to the OnReadLine event; // this handler needs to execute only once and its // purpose is to display the unit name on the form fr.OnReadLine += new BreakEvent(DisplayUnitName); } public void DisplayUnitName(string line, ref bool Break) { f.Text = line; } // display the file in a listbox public void DisplayFile() { f = new Form(); try { fileList = new ListBox(); try { f.Controls.Add(fileList); fileList.Dock = DockStyle.Fill; fr = new MyFileReader(); // assign the handler that reads the unit name fr.OnOpen += new Notification(OpenHandler); // assign the main handler that reads the // file into the list box fr.OnReadLine += new BreakEvent(ReadFileToList); fr.ReadFile(fileName); f.ShowDialog(); } finally { fileList.Dispose(); } } finally { f.Dispose(); } } } }