🐕

WPFでイベントを使用して、子ViewModelから親ViewModelへバインドする方法

2025/01/28に公開

イベントを使用して、子ViewModelから親ViewModelへバインドする方法

1. はじめに

WPF の MVVM アプリケーションでは、通常は「View <-> ViewModel 間の双方向バインディング」や「Command」を用いて画面とデータをやり取りします。しかし、画面を構成する上で 親ViewModel と子ViewModel を分けることがあります。その際、

  • 子ViewModel の状態が変わったときに、親ViewModel に何らかの通知を行いたい
  • 子ViewModel によるアクションで、親ViewModel のプロパティを更新したい

といった要件が発生することがしばしばあります。

MVVM Toolkit には「Messenger」と呼ばれるメッセージング機能も用意されていますが、本記事では 「イベント」を使って子ViewModel から親ViewModel へ通知する方法 について解説します。これは MVVM Toolkit 以外でも一般的に使える手法ですが、MVVM Toolkit の基本的なクラス(ObservableObjectRelayCommand など)を使用して例示します。


2. 全体像

図解

  1. ChildViewModel 側で何かしらの操作や状態変化が起こった際に、カスタムイベントを発火します
  2. ParentViewModel 側では、そのイベントを購読(ハンドリング)し、必要に応じて自身のプロパティを更新します
  3. 更新したプロパティが画面(View)にバインドされていれば、UI に反映されます

3. 実装例

3.1. ChildViewModel の実装

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;

public class ChildViewModel : ObservableObject
{
    // 子ViewModelから親ViewModelへ通知したいイベントを定義
    public event EventHandler? SomethingHappened;

    private string _childData;
    public string ChildData
    {
        get => _childData;
        set => SetProperty(ref _childData, value);
    }

    public IRelayCommand UpdateDataCommand { get; }

    public ChildViewModel()
    {
        UpdateDataCommand = new RelayCommand(UpdateData);
    }

    private void UpdateData()
    {
        // 子ViewModelのデータを更新
        ChildData = $"Updated at {DateTime.Now:HH:mm:ss}";

        // イベントを発火し、親ViewModelに通知
        SomethingHappened?.Invoke(this, EventArgs.Empty);
    }
}

ポイント:

  • ObservableObject を継承し、SetProperty メソッドでプロパティ変更を通知
  • SomethingHappened イベントを用意し、 子ViewModel内で状態変化があったときに Invoke して親に知らせる
  • コマンド (RelayCommand) を用意し、ボタン押下などで UpdateData() を呼び出せるようにする

3.2. ParentViewModel の実装

using CommunityToolkit.Mvvm.ComponentModel;
using System;

public class ParentViewModel : ObservableObject
{
    private ChildViewModel _child;
    public ChildViewModel Child
    {
        get => _child;
        set => SetProperty(ref _child, value);
    }

    private string _receivedData;
    public string ReceivedData
    {
        get => _receivedData;
        set => SetProperty(ref _receivedData, value);
    }

    public ParentViewModel()
    {
        // 子ViewModelを生成
        Child = new ChildViewModel();
        
        // 子ViewModelのイベントを購読
        Child.SomethingHappened += OnChildSomethingHappened;
    }

    // 子ViewModelでイベントが発火したら呼ばれる
    private void OnChildSomethingHappened(object? sender, EventArgs e)
    {
        // 子ViewModelのデータを読み取り、親ViewModelのプロパティを更新
        ReceivedData = Child.ChildData;
    }
}

ポイント:

  • ParentViewModelChildViewModel のインスタンスを保持し、 SomethingHappened イベントを購読
  • OnChildSomethingHappened では、 ChildViewModel の最新のデータを取得し、自身の ReceivedData を更新している

4. XAML 側の設定例

4.1. View (例えば Window や UserControl)

<Window x:Class="SampleApp.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"
        xmlns:local="clr-namespace:SampleApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="300">

    <Window.DataContext>
        <!-- 親ViewModelをデータコンテキストとしてセット -->
        <local:ParentViewModel />
    </Window.DataContext>

    <StackPanel Margin="10">
        <!-- 子ViewModelのコマンドをボタンにバインド -->
        <Button Content="Update Child Data" 
                Command="{Binding Child.UpdateDataCommand}"
                Width="150" Height="30" />

        <!-- 親ViewModelが受け取ったデータを表示 -->
        <TextBlock Text="{Binding ReceivedData}"
                   Margin="0,10,0,0"
                   FontSize="16" />
    </StackPanel>
</Window>

ポイント:

  • Window.DataContext には ParentViewModel を設定
  • 親ViewModel から辿れる ChildViewModelUpdateDataCommand を使ってボタンを押下
  • 親ViewModel の ReceivedData を画面に表示している

5. 解説

  1. イベントの定義

    • MVVM とは直接関係しない通常の .NET のイベント定義と同様です。
    • SomethingHappened などの任意の名前で event を宣言し、Invoke して通知を行います。
  2. イベントの購読

    • 親ViewModel で ChildViewModel のインスタンスを生成後に event += handler の形で購読
    • 子ViewModel がイベントを発火すると、購読しているメソッドが呼び出されます
  3. データの反映

    • OnChildSomethingHappened が呼ばれるタイミングで、 子ViewModel から必要なデータ を取得して 親ViewModel のプロパティ に反映
    • 親ViewModel のプロパティを介して 画面にバインドされていれば UI に更新内容が表示されます
  4. MVVM Toolkit のメリット

    • ObservableObject により INotifyPropertyChanged の実装が簡単
    • RelayCommand によりシンプルにコマンド実装が可能
    • (今回は触れていませんが)MessengerIMessenger を使えば、より疎結合な Pub/Sub 形式で通信可能

6. 応用:イベント引数で値を渡す

実際の現場では、単に「子ViewModelのデータを全取得する」のではなく、イベント時に特定の値を渡したい場合もあります。
その場合は、カスタムEventArgs を用意して値を渡す方法があります。

6.1. カスタムEventArgs

public class ChildDataEventArgs : EventArgs
{
    public string UpdatedValue { get; }

    public ChildDataEventArgs(string value)
    {
        UpdatedValue = value;
    }
}

6.2. ChildViewModel での実装

public event EventHandler<ChildDataEventArgs>? SomethingHappened;

private void UpdateData()
{
    ChildData = $"Updated at {DateTime.Now:HH:mm:ss}";
    SomethingHappened?.Invoke(
        this, 
        new ChildDataEventArgs(ChildData) // ここで必要な値を渡す
    );
}

6.3. ParentViewModel でのハンドリング

private void OnChildSomethingHappened(object? sender, ChildDataEventArgs e)
{
    // イベント引数から取得
    ReceivedData = e.UpdatedValue;
}

7. 注意点・ベストプラクティス

  1. 依存方向と疎結合

    • イベントを使うと、子ViewModel が直接「親ViewModel」クラスを参照するわけではありません
    • あくまで「イベントを起点にして、親ViewModel が子ViewModel に定義されたイベントを購読する」形になっているため、依存が逆転しているわけではありません。
    • 親ViewModel が子ViewModel のライフサイクルを管理する設計となり、明確になる利点がありますが、スコープが大きくなる可能性もあります。
  2. Messenger の活用

    • MVVM Toolkit には Messenger(Pub/Sub)という非同期メッセージング仕組みが用意されており、こちらを使うと複数の ViewModel 間通信をさらに疎結合にできます。
    • ただしシンプルなケースでは、上記のようにイベントやプロパティを直接参照した方が分かりやすい場合もあります。
  3. テスト

    • ParentViewModel のユニットテストを行う場合、ChildViewModel をモックやスタブに差し替えることも検討できます。
    • イベントを上げる動作をテスト観点で簡単に再現できるよう、インターフェイス設計やDependency Injection を取り入れるのも一案です。

8. まとめ

本記事では、MVVM Toolkit を使用した WPF 開発において、子ViewModel からイベントを使って親ViewModel を更新する方法を解説しました。以下が本手法のポイントです。

  • ChildViewModel でイベントを定義し、必要なタイミングで Invoke
  • ParentViewModel がイベントを購読し、子ViewModel の最新情報を取得して自身のプロパティを更新
  • 更新されたプロパティを View でバインドすれば、UI にも反映される

MVVM Toolkit では Messenger を用いたやり取りも推奨されていますが、イベントのほうがシンプルな実装に向いているケースもあります。アプリケーション規模や設計ポリシーに合わせて選択するとよいでしょう。

Discussion