ただただC#でReactive Extensions(Rx)を使ってみる ~イベント合成~

# はじめに

本記事はRx初学者の筆者がRxをただただ使ってみて学びや気づきをまとめたものです。
なお、本記事は[こちらの記事](https://qiita.com/tat_tt/items/10c5b435d11b70c98403)の続編の位置づけです。

# イベントの合成をしてみる

前回までにRxを使って非同期処理とイベントのフィルタリング(イベント関連機能のうちの1つの利用例)を実現しました。今回はイベント関連機能の代表的な利用例の一つである複数のイベントの合成を使ってみます。
以下に例を示します。

## WPFのGridのマウスイベントを組み合わせてマウスドラッグイベントを作成する例

以下のようにGrid(黄緑色の領域)をマウスでドラッグするとその時点での座標位置を表示するサンプルを考えます。

![マウスドラッグイベントサンプルの動作イメージ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/452292/10782e48-7164-1ae0-745a-19d6629f272e.gif)

このサンプルを実現するためにGridで検知できる以下のイベントを合成してマウスドラッグイベントを作成しています。

- MouseDownイベント
- MouseMoveイベント
- MouseUpイベント

具体的なMainWindow.xamlとコードビハインドのMainWindow.xaml.csは以下です。(サンプルとしての説明をしやすくするためにコードビハインドに記載をしています。MVVMに従った作り方になっていないためご承知ください。)

- MainWindow.xaml

```xml
<Window x:Class="RxEventSyntheticSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="150" Width="300">
    <!-- このGridのマウスドラッグ位置をTextBlockに表示する -->
    <Grid x:Name="Grid" Background="LightGreen">
        <TextBlock x:Name="DragDropInfoForGrid"/>
    </Grid>
</Window>
```

- MainWindow.xaml.cs

```csharp
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    /// <summary>
    /// コンストラクタ
    /// </summary>
    public MainWindow()
    {
        InitializeComponent();

        // ドラッグ中に、マウス位置を通知するイベントを発行する。
        // 2行目でGridに対してのマウス左ボタンの押下イベント(MouseDownイベント)を受け取る。
        // 2行目のマウス押下イベント(MouseDownイベント)を受け取った後に、
        // 3行目でGridに対してのマウス移動イベント(MouseMoveイベント)を受け取ってGrid内での相対的なマウス位置を取得し、変数mouseMovePointで保持する。
        // 4行目で3行目のイベントの受け取りはマウス左ボタンが離されるイベント(MouseUpイベント)が発生するまで行うように設定している
        // 最後にselect句を使って、ドラッグ中のマウス位置をイベント引数(型はPointクラス)として通知する。
        var dragEvent =
            from mouseDown in Observable.FromEventPattern<MouseEventArgs>(Grid, nameof(Grid.MouseDown))
            from mouseMovePoint in Observable.FromEventPattern<MouseEventArgs>(Grid, nameof(Grid.MouseMove)).Select(ev => ev.EventArgs.GetPosition(Grid))
                .TakeUntil(Observable.FromEventPattern<MouseEventArgs>(Grid, nameof(Grid.MouseUp)))
            select mouseMovePoint;

        // 上記で作成したイベント(厳密にはイベント引数)をハンドリングする。
        dragEvent.Subscribe(args => OnDrag(args));
    }

    /// <summary>
    /// ドラッグドロップイベントハンドラ
    /// </summary>
    /// <param name="dragPoint"></param>
    private void OnDrag(Point dragPoint)
    {
        // コントロールを操作するためにUIスレッドに処理を委譲する。
        Dispatcher.Invoke(() =>
        {
            // ドラッグ中のマウス位置をテキストブロックに表示する。
            var stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("ドラッグ中のマウス位置");
            stringBuilder.AppendLine($" X: {dragPoint.X} Y: {dragPoint.Y}");
            DragDropInfoForGrid.Text = stringBuilder.ToString();
        });
    }
}
```

大事なポイントはMainWindow.xaml.csのMainWindowメソッド(コンストラクタ)内の以下の部分です。
2行目の部分で、GridのMouseDownイベント(Grid内でマウスの左ボタンを押下した際に発行されるイベント)が発行されるのを待ちます。MouseDownイベントがあったのちに3行目の部分で、GridのMouseMoveイベント(Grid内でマウスを移動した際に発行されるイベント)が発行されるのを待ちます。なお、MouseMoveイベントが発行された場合にはGrid内でのマウスの相対位置をPointクラスに変形して変数mouseMovePointに保持します。このままだとMouseDownイベントが発行され後はずっとMouseMoveイベントを補足し続けることになってしまうため、4行目でMouseMoveイベントを、GridのMouseUpイベント(Grid内でマウスの左ボタンを離した際に発行されるイベント)が発行されるまで補足し続けるという設定をします。最後にselect句を使って保持している変数mouseMovePointをイベント引数として通知します。
上記で作成したイベントをOnDragメソッドでハンドリングします。先述したようにOnDragメソッドにはマウスドラッグ中の座標位置(厳密にはGrid内でのマウスの相対位置)がイベント引数として渡されます。そのため、OnDragメソッド内でその座標情報をTextBlockのTextプロパティに設定することで、座標情報を表示しています。

```csharp
// ドラッグ中に、マウス位置を通知するイベントを発行する。
// 2行目でGridに対してのマウス左ボタンの押下イベント(MouseDownイベント)を受け取る。
// 2行目のマウス押下イベント(MouseDownイベント)を受け取った後に、
// 3行目でGridに対してのマウス移動イベント(MouseMoveイベント)を受け取ってGrid内での相対的なマウス位置を取得し、変数mouseMovePointで保持する。
// 4行目で3行目のイベントの受け取りはマウス左ボタンが離されるイベント(MouseUpイベント)が発生するまで行うように設定している
// 最後にselect句を使って、ドラッグ中のマウス位置をイベント引数(型はPointクラス)として通知する。
var dragEvent =
    from mouseDown in Observable.FromEventPattern<MouseEventArgs>(Grid, nameof(Grid.MouseDown))
    from mouseMovePoint in Observable.FromEventPattern<MouseEventArgs>(Grid, nameof(Grid.MouseMove)).Select(ev => ev.EventArgs.GetPosition(Grid))
        .TakeUntil(Observable.FromEventPattern<MouseEventArgs>(Grid, nameof(Grid.MouseUp)))
    select mouseMovePoint;

// 上記で作成したイベント(厳密にはイベント引数)をハンドリングする。
dragEvent.Subscribe(args => OnDrag(args));
```

# さいごに

今回はRxを使ってイベントの合成を実現してみました。
イベント合成では、同じ時間軸上で発生した複数イベントを1つのシーケンスとして扱うというRxの考え方が重要になると個人的に思いました。
まだ、Rxの考え方の真髄は理解できていないように思うので、今後もバシバシRxを使って、理解せねばと思いました。