もくじ
https://tera1707.com/entry/2022/02/06/144447
やりたいこと
画面(View/ViewModel)側に、例えば何かのセンサの値を取ってきてくれる奴ら(Model)が値を渡すときに、そのModelのプロパティを定期的に見るようなやり方ではなく、ViewModelが用意したメソッドを、Model側が、値が変化したタイミングで呼んでくれるようなそういうカッコイイ(?)のをやってみたい。
やったこと
C#のデリゲート(delegate)/イベント(event)を使う。
下記のページを参考にさせて頂き、いろいろと試した。
参考ページ(++C++)
- イベント
- デリゲート
- イベントの注意点など
実験コード
画面は、ボタンが一個だけ配置されたものを作って、押したときにButton_Click
を通るようにした。
上に挙げた参考ページの方でも書かれているが、イベントが発生するほうを「発生側」、そのイベントのデータをもらうほうを「受取側」と呼ぶようにする。
下のサンプルでいうと、
- TestClass:イベントの「発生側」
- MainWindow:イベントの「受取側」
という位置づけになる。
using System;
using System.Diagnostics;
using System.Windows;
namespace WpfApp62
{
public partial class MainWindow : Window
{
public MainWindow() => InitializeComponent();
// 「呼出側」クラスの中の、実際に呼び出すヤツ
private void Button_Click(object sender, RoutedEventArgs e)
{
var c = new TestClass();
c.myDelegate += func1; // デリゲートの登録
c.myAction += func1; // アクション(デリゲート)
c.myEvent += func1; // イベントの登録
c.doFunc(null);
}
// 呼出側が、発生側のクラスに登録するメソッド
private void func1(string txt) => Debug.WriteLine(txt);
}
// 「発生側」のクラス
public class TestClass
{
// デリゲートを登録
public delegate void TestDelegate(string txt);
public TestDelegate myDelegate;
// イベントを登録
public event TestDelegate myEvent;
// Actionもデリゲートなので、eventにできる
public event Action<string> myAction;
// 登録したイベント/デリゲートを読んでみるメソッド
public void doFunc(TestDelegate func)
{
myDelegate?.Invoke("デリゲートです");
myAction?.Invoke("アクションです");
myEvent?.Invoke("イベントです");
}
}
}
今回は簡単に実験するために、発生側のデリゲートやイベントを呼ぶメソッドを、画面側で直接呼んでしまっているが、本当は、発生側のクラスで、例えばセンサの値が取れた!というタイミングでデリゲートやイベントを呼ぶはず。
概要
デリゲートやイベントは、受取側(ViewとかViewModel)側が、発生側クラス(Model)でイベントが起きたときに、そのイベントの値を使って、自分のメソッドを実行できるようにするもの、と理解した。
例えば、センサの値が変化したときに、Viewが画面上のテキストボックスに出した数字を更新したい、という場合は、
- View/ViewModelは、引数で受けた値を使ってテキストボックスの値を更新する、というメソッドを作っておく
- View/ViewModelは、Modelのイベントorデリゲートに、そのメソッドを登録する
- Modelは、所望の事象が発生したときに、所望の値を引数に渡して、View/ViewModelのメソッドを呼ぶ
ということをする。
例えば、温度センサの値を取ってくれるModelに、温度の値を表示するView/ViewModelのメソッドを登録する場合だと、
// View/ViewModelが用意するメソッド
private void OnSensorDataReceived(int temperature)
{
textbox1.Text = temperature.ToString();
}
というメソッドをView/ViewModelが用意したら、そいつを
// 温度更新時に読んでほしいメソッドをModelに登録
sensorModel.OnSensorDataReceived += OnSensorDataReceived;
みたいな感じで登録しておくと、データが更新されたらOnSensorDataReceived
を呼んでもらえるイメージ。
メモ
いろいろやってみて、自分でこうなんだなと理解した内容をメモ。
〇 イベント(event)は・・・
- 「=」で登録(代入)はできない。
- 「+=」を使って、登録ができる。
- 「-=」を使って、登録した処理を解除できる。
- 登録した数だけ解除すると、イベントはnullに戻る。
- ただし、解除には、登録したときと同じもの(メソッド)を渡さないといけないので、ラムダ式が使えない。
- 解除しないまま受け取り側の方が破棄などされた場合、発生側が受取側への参照をもったままになり、メモリリークになってしまう。
- ただ、そうなる可能性がある、という話で、それ自体が悪い、イベントの仕組みが悪い、と言っているわけではない。ちゃんと解除できるのであればOK。
- 「+=」をした回数以上に「-=」をしても、例外になったりはしない。
- むしろ、登録の際、まず一旦「-=」をしてから「+=」する、というコードをよく見る。これは、間違って2回登録されたら困るときにそのようにしてる様子。
ただそんなことするならeventを使わずdelegateにすれば良いのではと思う。(発生側を作った人は別に何個登録されてもいいが、使う側(受取側)の都合でそうしているのだろうと思う) - 受取側の方で「-=」と「+=」以外の操作はできない。例えば、イベントにすでに+=で登録済みかどうかをチェックしたくなっても、
if (c.myEvent != null)
とかはできない。(コンパイルエラーになる)
〇 デリゲート(delegate)は・・・
- 「=」で登録(代入)ができる。
- 「+=」を使って、登録ができる。
- 「-=」を使って、登録した処理を解除できる。
- 「=」で代入したら、それまでに「+=」したものは全部消えて代入したものだけになる。
- 登録した数だけ解除すると、デリゲートはnullに戻る。
- 「-=」しすぎても別にいい、「-=」には同じものを渡す必要あり、などはイベントと同じ。
- イベントと異なり、受取側の方で**「-=」と「+=」以外の操作もできる。**
if (c.myEvent != null)
とかもできる。 - 個人的によく使う**「Action」とか「Func」とかは、実はdelegateのジェネリクス版**。
「namespace System」の中で、public delegate void Action(T1 arg1, T2 arg2); で定義されてる。- だから、Actionにも「+=」「-=」で登録/解除することができる。