Delphi/VCLの大きな特徴のひとつである"PMEモデル(Properties, Methods and Events model)"の"E"、イベントハンドラは基本的に関数ポインタ(実際には
TMethod = データポインタ + 関数ポインタ)で実装されており、結果としてイベントはユニキャスト、つまりある事象の通知を受け取れるのは一つのイベントハンドラのみ、ということになっています。しかし状況によってはマルチキャスト、つまり複数のイベントハンドラに事象の通知を送りたい、ということがあります。デザインパターンでいうと
オブザーバパターンとなるような状況です。デザインパターンの定石に従って、たとえば
Branko StojakovicさんのGoF Patterns in Delphiの
Observerや
Nick Hodgesさんの
More Coding in Delphiにあるように、IObserverとISubjectを実装して…というやりかたも考えましたが、マルチキャストで通知したい事象が何種類もある状況だと、何らかの形でジェネリックス化されている実装でないと厳しそうです。そこで探してみると、
Spring4DというDI(dependency injection)フレームワークで
マルチキャストイベントが実装されているということだったので、これを試してみることにします。なおSpring4Dのライセンスは
Apache License 2.0です(
日本語参考訳)。またSpring4Dそのもの(DIコンテナとしてのSpring4D)については第28回エンバカデロ・デベロッパーキャンプのLTの
岡野さんの
プレゼンテーション資料が非常に参考になります。
まずSpring4Dのなかでマルチキャストを実現しているのは
Event<T>レコード型になります。これを使用するのに必要なユニットですが、Source\Baseフォルダにある
- Spring.pas
- Spring.inc
- Spring.ResourceStrings.pas
- Spring.Events.Base.pas
- Spring.Events.pas
- jedi.inc
です。もちろんSpring4Dをインストールして検索パスにSpring4D関係のフォルダを追加しても構いませんが、マルチキャストだけを試すのであればこれらのファイルをコピーして.pasファイルをプロジェクトに追加するだけでも大丈夫です。
では実際にEvent<T>でマルチキャストを試してみます。今回はメインフォームからモードレスなサブフォームをいくつか表示して、フォーム上のボタンクリックの発生と、その累計回数を各フォームに通知します。
まず通知のためのイベントと、イベントを登録/削除するインタフェースを定義します。
type
TNotifyNumber = procedure (Sender: TObject; Number: Integer) of object;
ISubject = interface
procedure Add(AEvent: TNotifyNumber);
procedure Remove(AEvent: TNotifyNumber);
end;
TNotifyNumberが通知のためのイベントハンドラの型、ISubjectがそのイベントハンドラを登録/削除する機能を持たせたインタフェースです。メインフォーム、サブフォームの両方で使用するので別ユニットで定義しておきます。次はメインフォームです。
uses
Spring, ...
type
TForm1 = class(TForm,ISubject)
...
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FEvent: Event<TNotifyNumber>
public
procedure Add(AEvent: TNotifyNumber);
procedure Remove(AEvent: TNotifyNumber);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FEvent := Event<TNotifyNumber>.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FEvent.Clear;
end;
procedure TForm1.Add(AEvent: TNotifyNumber);
begin
FEvent.Add(AEvent);
end;
procedure TForm1.Remove(AEvent: TNotifyNumber);
begin
FEvent.Remove(AEvent);
end;
メインフォームはISubjectインタフェースを継承するようにして、AddとRemoveを実装します。Event<T>はレコード型ですが、内部の適切な初期化のために
class function Createを呼び出しておく必要があります。AddとRemoveでは単にEvent<T>の
Addと
Removeを呼び出すだけです。一方サブフォームでは
type
TForm2 = class(TForm)
...
private
FSubject: ISubject;
public
property Subject: ISubject
read FSubject
write FSubject;
end;
メインフォームをISubjectとして保持するためのプロパティを用意します。これでメインフォームからサブフォームを生成、表示する処理は
procedure TForm1.Button1Click(Sender: TObject);
begin
with TForm2.Create(Self) do
begin
Subject := Self;
Show;
end;
end;
となります。サブフォーム側ではボタンクリックで任意にマルチキャストイベントを登録、削除できるようにしてみます、
type
TForm2 = class(TForm)
Button1: TButton;
Button2: TButton;
Label1: TLabel;
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
...
procedure DoNotifyNumber(Sender: TObject; Number: Integer);
public
...
end;
procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Subject.Remove(DoNotifyNumber);
Action := caFree;
end;
procedure TForm2.Button1Click(Sender: TObject);
begin
Subject.Add(DoNotifyNumber);
end;
procedure TForm2.Button2Click(Sender: TObject);
begin
Subject.Remove(DoNotifyNumber);
end;
procedure TForm2.DoNotifyNumber(Sender: TObject; Number: Integer);
begin
Label1.Caption := 'Notified number is ' + IntToStr(Number);
end;
procedure DoNotifyNumberがマルチキャストイベントのイベントハンドラで、これをButton1/Button2のクリックイベントでSubjectにAdd/Removeすることで登録、削除します。最後にメインフォーム側からのマルチキャストイベントの呼び出しです。
type
TForm1 = class(TForm,ISubject)
...
Label1: TLabel;
Button2: TButton;
procedure Button2Click(Sender: TObject);
private
FCount: Integer;
...
public
...
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
FCount := FCount + 1;
Label1.Caption := IntToSTr(FCount);
FEvent.Invoke(Self,FCount);
end;
Selfと1ずつ増える整数値(FCount)でEvent<T>のInvokeメソッドを呼び出すだけです(正確には
Invokeはイベントハンドラ型(<T> = <TNotifyNumber>)のプロパティで、Invoke(...)とすることでメソッドの呼び出しになる)。
Spring4DのEventがよくできていることもあってそれほど難しくはないと思います(気をつけなければならないのはclass function CreateによるEvent<T>の明示的な初期化くらい)。ただEvent<T>のTには通常のイベントハンドラ(procedure ... of object)だけでなく、無名メソッド(reference to ...)も入れられるのですが、この場合Event<T>で保持されるものが自動的に生成される無名メソッド値(TInterfacedObjectの派生クラスのインスタンス)であり、そのポインタをAddの呼び出し側(TForm2)では知ることができないためRemoveできない、という点には注意が必要です。