自分でクラスを作っていると呼び出し元のFormなどに処理の完了を伝えたい時が出てくるかと思います。
特に非同期での処理を行っているときなど、処理が終わったら教えてほしいという場面が出てきます。
そこでdelegateを使って処理完了などの特定の条件下で発動するイベント関数を設定する方法を今回は紹介しようと思います。
delegate
delegateとは、簡単に言ってしまえば関数の変数です。
つまり、宣言してある関数を格納し、利用できます。C++では関数ポインタと似ています。
イベントハンドラーは、delegateを介して呼び出している関数です。
サンプル
ソケット通信のサンプルです。ここで通信を非同期で行っており、通信処理が完了すると終了イベントを発生する仕組みでdelegateを利用しています。
delegateの宣言
delegateのイベントハンドラーを宣言します。
public delegate void SocketEventHandller(object sendor, SocketEventArgs e);
ここではSocketEventHandllerという名前で宣言しました。
このSocketEventHandllerでイベントを作成します。
引数に必要なパラメータを設定します。ここではSocketEventArgsクラスを指定していますがこれは独自クラスです。
SocketEventArgsの宣言はこちらです。これはただのデータ格納用クラスですので、渡したいデータの寄せ集めです。
public class SocketEventArgs
{
public string RemoteIPorHost { set; get; } = "";
public string LocalIPorHost { set; get; } = "";
public int RemotePort { set; get; } = -1;
public int LocalPort { set; get; } = -1;
public byte[] SendBytes { set; get; } = null;
public byte[] RecvBytes { set; get; } = null;
public System.Text.Encoding Encoding { set; get; } = System.Text.Encoding.UTF8;
public Socket WorkSocket { set; get; } = null;
//文字列の取得
public string GetSendString()
{
return Encoding.GetString(SendBytes, 0, SendBytes.Length).Trim('\0');
}
public string GetRecvString()
{
return Encoding.GetString(RecvBytes, 0, RecvBytes.Length).Trim('\0');
}
}
イベントを宣言/イベント発生
イベントを宣言します。
ソケット通信の非同期通信で行ったときにイベントを起こします。
例としてここではデータ受信をしたときにイベント発生させます。
public event SocketEventHandller OnRecved = null;
これでこのクラスにはイベントが新しく宣言されました。
しかし、このままでは何も呼び出されません。データ受信が終わったら呼び出してみます。
割り当てされてない場合は、nullになっているのでnullじゃない場合のみ呼び出しを行う。
//受信のコールバック関数
private void RecvCallback(IAsyncResult ar)
{
・・・途中は省略・・・
//受信したデータを取得する
SocketStateClass state = (SocketStateClass)ar.AsyncState;
//受信イベント用のパラメータを作成
SocketEventArgs args = new SocketEventArgs()
{
RemoteIPorHost = ((IPEndPoint)state.WorkSocket.RemoteEndPoint).Address.ToString(),
LocalIPorHost = ((IPEndPoint)state.WorkSocket.LocalEndPoint).Address.ToString(),
RemotePort = ((IPEndPoint)state.WorkSocket.RemoteEndPoint).Port,
LocalPort = ((IPEndPoint)state.WorkSocket.LocalEndPoint).Port,
SendBytes = null,
RecvBytes = state.Buffer,
Encoding = state.Encoding,
WorkSocket = state.WorkSocket
};
//受信完了イベントを発生させる
if (this.OnRecved != null)
{
this.OnRecved(this, args);
}
・・・途中は省略・・・
}
イベントを割り当てる
サンプルでは受信したら受信した内容をフォームで呼び出したいため、イベント関数をフォームに作成しています。
ここで注意ですが、今回のソケット通信は非同期です。非同期ということは別のスレッドで処理が行われます。フォームが動いているスレッドとは違うスレッドなのでフォーム上のコントロールを操作しようとするとエラーになります。
これを回避するための方法はいくつかあります。ここでは一時的に変数によけておいて、メインスレッドで動いているタイマーイベントで処理をさせるようにしています。
private static Queue<string> _que_log = new Queue<string>(); //ログのキュー(先入れ先出し)
public void OnRecved(object sendor, SocketEventArgs e)
{
//ログ表示
_que_log.Enqueue(e.GetRecvString());
}
たとえばこんなイベントを作成します。Queueクラスに通信ログを格納して、タイマーイベントでテキストボックスに記載します。
最後にクラスで作ったイベントに紐づけします。
_client.OnRecved += OnRecved;
これでデータ受信に成功したときに、フォームのOnRecvイベント関数に飛んできます。
図にしてみました。
![](https://csharp.nomux2.net/wp-content/uploads/2023/07/20230701_socket_recv_rev3.png)
まとめ
Textboxにデータを入れる処理にしても、Textboxのインスタンスを他のクラスに渡せば同スレッド内で処理できます。
ダメではないですが、私はフォームコントロールの操作はそのフォーム内で行うというルールで開発しています。
他のところに渡すと、あとでコントロールの更新箇所がわからなくなり修正が困難になる可能性があるからです。
この記事が皆様のお役に立てたら幸いです。