[Prism]UserControlの削除時にView/ViewModelをDisposeする

# はじめに

Prism にて、Region に 登録されている UserControl を削除する際に、ViewModel 及び View の Disposeメソッド をコールする方法についてまとめます。

国産MVVMインフラのLivetであれば、DataContextDisposeAction が用意されており、xaml から手軽に ViewModel を Dispose できますが、Prism には対応する機能がなさそうです。

MVVMパターンで、View が IDispose を継承する機会はめったにないと思いますがついでに紹介します。



# 下準備

View/ViewModel の Dispose を紹介するため、下準備としてDisposeがない状態の Prismアプリ を作成していきます。

## アプリの動作仕様

- Add ボタンで 下部Region に 自作UserControl を読み出します
- UserControl はボタンを持っており、自身を削除できます
- 削除ボタンは2つありますが、どちらも動作は同じです（削除者が異なる View/ViewModel）

![capture.gif](https://qiita-image-store.s3.amazonaws.com/0/249816/237eaaff-7b6e-acc1-733f-f7f3ba90bc2c.gif)


## ソースコードの紹介

- App.xaml

	Prismの一般的な実装で、特に何もしていません。

	```xml
	<prism:PrismApplication
	    x:Class="PrismDispose.App"
	    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	    xmlns:prism="http://prismlibrary.com/" >
	    <Application.Resources/>
	</prism:PrismApplication>
	```

- App.xaml.cs

	Prismの一般的な実装で、特に何もしていません。

	```C#
	public partial class App : PrismApplication
	{
	    protected override Window CreateShell() => Container.Resolve<MainWindow>();
	    protected override void RegisterTypes(IContainerRegistry containerRegistry) { }
	    protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) {}
	}
	```

- MainWindow.xaml

	追加ボタンと UserControl を読み出すための Region を持っています。

	```xml
	<Window x:Class="PrismDispose.Views.MainWindow"
	        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
	        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
	        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
	        xmlns:prism="http://prismlibrary.com/"
	        prism:ViewModelLocator.AutoWireViewModel="True"
	        mc:Ignorable="d"
	        Title="PrismDispose"
	        Height="250" Width="400">
	    <i:Interaction.Triggers>
	        <i:EventTrigger EventName="Closed">
	            <prism:InvokeCommandAction Command="{Binding ClosedCommand}" />
	        </i:EventTrigger>
	    </i:Interaction.Triggers>
	    <Grid Margin="0,10,0,0">
	        <Grid.RowDefinitions>
	            <RowDefinition Height="Auto" />
	            <RowDefinition Height="*" />
	        </Grid.RowDefinitions>
	        <Button Content="Add ViewA"
	                Command="{Binding AddCommand, Mode=OneWay}" />
	        <ContentControl Grid.Row="1"
	                        prism:RegionManager.RegionName="ContentRegion" />
	    </Grid>
	</Window>
	```

- MainWindow.xaml.cs

	変更はないので割愛します。

- MainWindowViewModel.cs

	ボタンコマンドにより、ViewAを追加しています。

	```C#
	class MainWindowViewModel : BindableBase
	{
	    private readonly IContainerExtension _container;
	    private readonly IRegionManager _regionManager;

	    public DelegateCommand AddCommand { get; }
	    public DelegateCommand ClosedCommand { get; }

	    public MainWindowViewModel(IContainerExtension container, IRegionManager regionManager)
	    {
	        _container = container;
	        _regionManager = regionManager;

	        AddCommand = new DelegateCommand(() => AddModule<ViewA>("ContentRegion"));
	    }

	    // 指定リージョンにモジュールを追加
	    private void AddModule<T>(string regionName) where T : UserControl
	    {
	        var name = typeof(T).Name;

                // 重複チェック対策
	        var viewTarget = _regionManager.Regions[regionName].Views
	            .FirstOrDefault(x => x.GetType().Name == name);

	        if (viewTarget == null)
	        {
	            var view = _container.Resolve<T>();
	            _regionManager.Regions[regionName].Add(view, name);
	        }
	    }
	}
	```

- ViewA.xaml

	2つの自爆ボタンを持っています。

	上ボタンは コードビハインド(View) で処理しており、

	下ボタンは DelegateCommand(ViewModel) で処理しています。

	```xml
	<UserControl x:Class="PrismDispose.Views.ViewA"
	             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
	             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
	             mc:Ignorable="d" 
	             d:DesignHeight="300" d:DesignWidth="300"
	             xmlns:prism="http://prismlibrary.com/"
	             prism:ViewModelLocator.AutoWireViewModel="True" >
	    <StackPanel Margin="10" Background="LightGreen" >
	        <TextBlock Text="View A from Module" />
	        <Button Content="ViewA Close from View"
	                Click="Button_Click" />
	        <Button Content="ViewA Close from ViewModel"
	                Command="{Binding CloseCommand, Mode=OneWay}" />
	    </StackPanel>
	</UserControl>
	```

- ViewA.xaml.cs

	ボタンクリックイベントで自身を所属リージョンから削除しています。

	また、ViewModelからの削除に対応するため、自身の名前をプロパティで公開しています。

	```C#
	public partial class ViewA : UserControl
	{
	    private IRegionManager _regionManager;

	    public ViewA(IRegionManager regionManager)
	    {
	        InitializeComponent();
	        _regionManager = regionManager;
	    }

	    private void Button_Click(object sender, RoutedEventArgs e)
	    {
	        _regionManager.Regions["ContentRegion"].Remove(this);
	    }
	}
	```

- ViewAViewModel.cs

	削除したいViewの型名から Region に登録されている View を探索し削除しています。

	```C#
	public class ViewAViewModel : BindableBase
	{
	    private IRegionManager _regionManager;
	    public DelegateCommand CloseCommand { get; }

	    public ViewAViewModel(IRegionManager regionManager)
	    {
	        _regionManager = regionManager;
	        CloseCommand = new DelegateCommand(() =>
	            RemoveModule<ViewA>("ContentRegion"));
	    }

	    // 指定リージョンからモジュールを削除
	    private void RemoveModule<T>(string regionName) where T : UserControl
	    {
	        var viewToRemove = _regionManager.Regions[regionName].Views
	            .FirstOrDefault(x => x.GetType().Name == typeof(T).Name);
	        if (viewToRemove != null)
	            _regionManager.Regions[regionName].Remove(viewToRemove);
	    }
	}
	```

下準備は以上です。

# Dispose対応

下準備が済んだので、Dispose部分を実装していきます。

## DisposeBehaviorの作成

Prism.Regions.IRegionBehavior を継承したクラスを作成し、Region.Views の変化を購読します。

Views_CollectionChanged() では Remove が行われた時に、ViewとViewModelの Disposeメソッドを呼び出しています。

以下は、参考にさせて戴いた Okazuki さんのコードそのものです。毎度お世話になっておりますm(_ _)m

```C#
class DisposeBehavior : IRegionBehavior
{
    public const string Key = nameof(DisposeBehavior);
    public IRegion Region { get; set; }

    public void Attach()
    {
        Region.Views.CollectionChanged += Views_CollectionChanged;
    }

    private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            Action<IDisposable> callDispose = d => d.Dispose();
            foreach (var o in e.OldItems)
            {
                MvvmHelpers.ViewAndViewModelAction(o, callDispose);
            }
        }
    }
}
```



## DisposeBehaviorの登録

RegionBehaviorへの登録は App.xaml.cs にて行います。

以下、App.xaml.cs の追加箇所のみ

```C#
protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors)
{
    regionBehaviors.AddIfMissing(DisposeBehavior.Key, typeof(DisposeBehavior));
    base.ConfigureDefaultRegionBehaviors(regionBehaviors);
}
```



## View/ViewModelのIDispose対応

UserControl の View/ViewModelが IDisposeを継承していなければ始まりませんので追加します。

- ViewA.xaml.cs

	```C#
	public partial class ViewA : UserControl, IDisposable
	{
	    public void Dispose() => Debug.WriteLine("View.Dispose");
	```

- ViewAViewModel.cs

	```C#
	public class ViewAViewModel : BindableBase, IDisposable
	{
	    public void Dispose() => Debug.WriteLine("ViewModel.Dispose");
	```

以上の対応により、UserControlの削除ボタンにより View/ViewModel の Disposeが行われます。



# ×ボタン終了時のDispose

実はここまでの説明だと、アプリ×ボタンによる終了時に Dispose が行われません。

今回のサンプルでは動作に問題はありませんが、『UserControl が削除された際にログファイルを出力する』のような要件だった場合、×ボタンで Dispose が呼ばれないと不都合があるかと思います。

以下で、×ボタン終了時の Dispose に対応していきます。

- MainWindow.xaml

	終了イベントを補足し、ViewModelのコマンドを実行します。

	```xml
	<i:Interaction.Triggers>
	    <i:EventTrigger EventName="Closed">
	        <prism:InvokeCommandAction Command="{Binding ClosedCommand}" />
	    </i:EventTrigger>
	</i:Interaction.Triggers>
	```

- MainWindow.xaml.cs

	終了時のコマンドで、当該リージョンのすべてのViewを削除します。

	UserControlのViewModelと違って、削除対象の指定が不要なのでスッキリ書けます。

	```C#
	public DelegateCommand ClosedCommand { get; }

	public MainWindowViewModel(IContainerExtension container, IRegionManager regionManager)
	{
	    ClosedCommand = new DelegateCommand(() =>
	        _regionManager.Regions["ContentRegion"].RemoveAll());
	    
	    /* 先ほどと同じなので省略 */
	```


以上で、×ボタン押下時にも View/ViewModel の Dispose が呼ばれるようになりました。



# まとめ

Prism にて、Region に 登録されている UserControl を削除する際に、ViewModel 及び View の Disposeメソッド をコールする方法についてまとめました。

普段は何か調査をしても Qiita 投稿にまとめることはないのですが、今回はインフルによる外出禁止期間のおかげで投稿する時間が持てて、良い経験になりました。



# 環境

Visual Studio Community 2017 15.9.5
.NET Framework 4.6.1
Prism.Wpf v7.2.0.708-pre



# 参考にさせて頂いたページ

https://github.com/runceel/PrismEdu/tree/master/09.RegionBehavior

[\[stackoverflow\] Is there any way to remove a view (by name) from a Prism region when the view was added using the RegionManager.RequestNavigate method?](https://stackoverflow.com/questions/18086195/is-there-any-way-to-remove-a-view-by-name-from-a-prism-region-when-the-view-wa)