[C#/WPF]ビューモデルからビューのメソッドを呼ぶ

# やりたいこと
Model/ViewModel/Viewがあるパターンで、
通常はViewModel(VM)をView(V)のDataContextに設定して、VMのプロパティをVにバインドして画面をコントロールしているが、どうしてもVMからは呼ぶことのできないコントロールのクラスメソッドを呼びたいときなどに、VMからVのメソッドを呼び出したい。

# やり方
下記のようにする。

- VM側で、V側のメソッドを登録するためのActionのプロパティを公開する。
- V側で、DataContextの変化のタイミングで、そのActionに登録したいメソッドを登録する。

# やり方(コード)

### 画面(V)側

```MainWindow.xaml
<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel>
            <TextBlock Name="MyTextBlock">ABC</TextBlock>
            <Button Command="{Binding MyCommand}">ボタン</Button>
        </StackPanel>
    </Grid>
</Window>
```

```MainWindow.xaml.cs
using System.Windows;

namespace WpfApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // ★データコンテキスト登録(変化)時、VM側のActionに自分(View側)のメソッドを登録する
            this.DataContextChanged += (s, e) =>
            {
                if (e.NewValue is ViewModel)
                {
                    var vm = e.NewValue as ViewModel;
                    vm.ViewsideAction = this.ViewsideAction;    // ★スライダーの範囲表示更新処理をViewModelに登録
                }
            };

            // DataContextを登録
            this.DataContext = new ViewModel();
        }

        private void ViewsideAction()
        {
            MyTextBlock.Text = "ビュー側のメソッドが呼ばれました。";
        }
    }
}
```

### ビューモデル(VM)側

```ViewModel.cs
using System;

namespace WpfApp1
{
    class ViewModel : BindingBase
    {
        // ★ビュー側のメソッドを登録するためのAction
        public Action ViewsideAction { get; set; }

        // ボタンを押したときの処理
        public DelegateCommand MyCommand { get; private set; }

        public ViewModel()
        {
            // ボタンを押したときの処理
            MyCommand = new DelegateCommand(
                () =>
                {
                    // ★登録したメソッドの呼び出し
                    ViewsideAction?.Invoke();
                },
                () => { return true; });
        }
    }
}
```

# 要点

### VM側で、Actionを公開する

VM側で、V側のメソッドを登録するためのActionを公開する。
登録は、V側で実施する。

```cs
public Action ViewsideAction { get; set; }
```

### V側で、VMのActionを登録する

VMとVが紐づけられた時 = V側のDataContextにVMへの参照が入れられた時に、
VM側で公開したActionに、Vのメソッドを登録する。
(DataContextChangedは、this.DataContext = new ViewModel();でDataContextが変化したときに呼ばれるイメージ。)

```cs
// データコンテキスト登録(変化)時、VM側のActionに自分(View側)のメソッドを登録する
this.DataContextChanged += (s, e) =>
{
    if (e.NewValue is ViewModel)
    {
        var vm = e.NewValue as ViewModel;
        vm.ViewsideAction = this.ViewsideAction;    // ★スライダーの範囲表示更新処理をViewModelに登録
    }
};
```

### 呼び出す

あとは、VM側で、実際に登録したActionを呼び出せばV側のメソッドが呼ばれる。

```cs
ViewsideAction?.Invoke();
```

# 考察

本当は、VMのプロパティをバインドすることで画面表示を制御したいが、どうしてもVMからは呼ぶことのできないコントロールのクラスメソッドを呼びたい時に、この方法で回避した。
※詳しくは忘れたが、確かListBoxの中の〇番目のitemを表示したい、というときに、ListBox.ScrollIntoView(Object)のメソッドでできるようなことをしたかったのだが、VM側からそれがどうしてもできなかったので、この方法でScrollIntoViewを呼んでやることで回避した記憶がある。

こういう抜け道的なことをした時、自分はMVVMできてないなぁと思ってしまう。
(正しい解決方法はあるのか？？)

# 参考

MVVMパターンでVMからVを操作する方法　再考
https://blog.okazuki.jp/entry/20100925/1285426626