WPF Prism episode: 18 ~ Livet が Prism に「IDisposable 呼び出し用」としてゲッツされた件 ~
前回までは Prism 7.2 に新たに追加された IDialogService について紹介しました。
今回は .NET Core 3.0 がリリースされたので、今まで WPF Prism episode シリーズで紹介してきたサンプルアプリを .NET Core 3.0 のアプリケーションとして新規に書き換えることにしました。
2020/8/20 追記
このエントリの内容を新規連載の .NET Core WPF Prism MVVM 入門 2020 step: 5 で書き直したので、出来れば新規記事の方を見てください。
又、このエントリで紹介している Xaml.Behaviors.Wpf についても別記事として詳しく紹介している『System.Windows.Interactivity.dll から Xaml.Behaviors.Wpf へ』を公開しているので、良ければこちらもどうぞ。
加えて、機能別に分割導入が可能になった Livet Ver.3.0.3 以降を利用して Window の VM を Dispose する方法と Prism 7.2 から追加された IDestractible を利用して View(UserControl)を Dispose する方法も併せて紹介します。
尚、この記事は Visual Studio 2019 Community Edition で .NET Core 3.0 以上 と C# + Prism 7.2 + ReactiveProperty + Livet を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。
目次
.NET Core 3.0 で WPF Prism アプリケーションを作成する
2019/9/23 に .NET Core 3.0 が正式にリリースされ、IDE である Visual Studio 2019 Ver. 16.3 も併せてリリースされ、WPF はデザイナまで含んでのリリース(Window Form デザイナは Preview 版)なのでようやく .NET Core での WPF 開発環境が整いました。
そこで今までこの連載(WPF Prism episode シリーズ)の開始時から作成し続けてきたサンプルアプリを .NET Core 3.0 版のアプリとして新規に作成しなおすことにしました!
まあ、フレームワークに .NET Framework、.NET Core 3.0 以降のどちらを使う場合でも記述するコード自体は変わりませんが、作成手順を紹介していきたいと思います。
とは言っても作成手順を 1 から説明すると episode: 3 から公開してきた内容と丸被りになってしまうので全手順を再度紹介するつもりはありません。
ただ、管理人もここ 1 年くらい WPF と Prism、ReactvieProperty をいじってきて慣れてきたこともあり、実装内容はエントリ公開時から少し変わっています。サンプルコードが必要と感じた場合は適宜エントリ内に貼り付けますが、基本は GitHub リポジトリ を見てください。
.NET Core 3.0 用のソリューションを作成する
まず、Visual Studio 2019 のスタートウィンドウから【新しいプロジェクトの作成】を選択して fig. 1 のように【Prism Blank App (.NET Core 3.0)】を選択します。
fig. 1 で選択しているプロジェクトテンプレートは Visual Studio 2019 の拡張機能から【Prism Template Pack】をインストールしていれば表示されます。
※ Prism Template Pack は Ver.2.1.7 以降がインストールされている必要があります
プロジェクトの保存先を選択すると .NET Framework を選択した場合と同じく fig. 2 の DI コンテナ選択画面が表示されるので、【Unity】が選択されているのを確認して【CREATE PROJECT】ボタンをクリックします。
手順は .NET Framework を選択した場合と全く同じなので迷う所は無いと思います。
DI コンテナ選択後、fig. 3 のようなプロジェクトが作成されます。
Prism のテンプレートから【ViewModels】、【Views】フォルダ(名前空間)が自動作成されていますが、管理人は名前空間をアーキテクチャに属する分類名でなくアプリの機能名で分ける方が好きなので、今回はまず fig. 3 の赤枠で囲んだファイルとフォルダを全て削除します。
View と ViewModel を同じ名前空間に配置する
管理人的に【Views】、【ViewModels】、【Controllers】、【DataAccesses】、【Models】等アーキテクチャに分割する場合はプロジェクトを分け、名前空間はアプリの機能単位に分けて論理的にグループ化するのが好みです。
そのため、本来であれば【Views】と【ViewModels】はプロジェクトを分けるべきかもしれませんが、今回は View と VM は同一プロジェクトに置いて作成します。
但し、Prism は View と VM がそれぞれ【Views】と【ViewModels】名前空間配下に置かれている前提で View の DataContext を設定するため、同一名前空間配下に置いた VM は認識されません。
そのため、名前付け規則を変更する必要があり、変更するには App.xaml.cs で ConfigureViewModelLocator メソッドを src. 1 のようにオーバーライドします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | using System; using System.Reflection; using System.Windows; using Prism.Ioc; using Prism.Mvvm; using PrismNetCoreApp.Views; namespace PrismNetCoreApp { /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App { /// <summary>ViewとViewModelの名前付け規則を設定します。</summary> protected override void ConfigureViewModelLocator() { base.ConfigureViewModelLocator(); ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(vt => { var viewName = vt.FullName; var asmName = vt.GetTypeInfo().Assembly.FullName; var vmName = $"{viewName}ViewModel, {asmName}"; return Type.GetType(vmName); }); } ~ 略 ~ } } |
ViewModelLocationProvider へ src. 1 のように指定すると ViewModelLocator は View と同一名前空間内から【View クラス名 + ViewModel】と言う名前のクラスを探し出して View の DataContext へ設定するようになるので、View と VM を同じ名前空間へ置けるようになります。
但し、これまでは Views 名前空間に View を追加すると ViewModels 名前空間に ViewModel も自動的に追加されていましたが、Views フォルダ以外に View を追加しても ViewModel は自動で追加されなくなるので手動で追加することが必要になります。
まあ、src. 1 の設定は必須ではありませんし名前空間に関しては完全に管理人の好みの問題なので標準のままでも問題ありません。
後の手順は .NET Core も .NET Framework も変わらないので episode: 3 で紹介している通りに進めていくだけです。
ただ、.NET Core の WPF はまだ安定してないのか fig. 4 のようにエラーの波線が消えません…
現在(2019/10/23)fig. 4 右側の OnStartup メソッドがビルドエラーになる問題は発生していませんが、fig. 4 左側の WindowStartupLocation が『XDG0062:Object does not match target type.』エラーになる件は絶賛継続中で Developer Community にも報告されていて現在調査中のようです。
赤波線が出ていても実行は問題なくできますし、ファイルを閉じればソリューションエクスプローラーから赤波線は消えるので、そこまでの支障はありませんが赤波線が出ていると不安な気持ちになるので早めに対応して欲しい限りです。
fig. 4 で紹介したエラー:XDG0062 は Visual Studio 2019 Ver.16.4 以降では発生しなくなっています。
2019 年 12 月 3 日にリリースされた Ver.16.4 は LTS(Long-Term Supported)版なので .NET Core の LTS 版である Ver.3.1 と併せて早めにバージョンアップすると意味不明なエラーに悩まされることも減ると思います。
(2019/12/21 追記)
Prism に Livet を導入する
フレームワークである Prism は UserControl を動的に切り替えたり、内包されている DI コンテナ経由で DLL 間のデータ受け渡しが簡単にできるようなメリットはありますが、『あると便利なのに!』的なクラスや Behavior は自分で作ってね!的な海外製ライブラリにはよくあるスタンスのフレームワークです。
一方 Livet は MVVM パターンでアプリを構成するための便利な Behavior やクラスが多く含まれているイメージだったので Prism と Livet を併用すると楽そうだと考えていると現在 Livet のメンテナーをされているかずきさん が Livet を機能単位で分割導入できるパッケージ をリリースしてくれました!
Livet の機能が部分的に導入できるようになったのでここで紹介しているサンプルプログラムにも早速導入してみました!
LivetCask.Behaviors のインストール
まず、【ソリューションの Nuget パッケージの管理】で【livet】を検索すると【LivetCask.Behaviors】が出て来るので、選択してインストールします。
管理人は Livet の機能を全て理解できている訳ではないので、とりあえず理解できた機能から順に紹介していく予定です。ここではまず LivetCask.Behaviors の一部のみ紹介します。
LivetCask.Behaviors のインストールが完了すると fig. 6 のようにソリューションエクスプローラーへ警告(緑波線)が表示されます。
これは fig. 7 のエラー一覧に表示されているように LivetCask.Behaviors へ同梱されている Xaml.Behaviors.Wpf が .NET Core 3.0 に対応していないために表示されています。
Xaml.Behaviors.Wpf はこの連載でもイベントのハンドルに利用している Interaction.Triggers を使うために必要な System.Windows.Interactivity.dll の代替えになる OSS ライブラリです。
Visual Studio 2019 からは Blend SDK が同梱されなくなったため OSS に移行したようで、Prism のメンテナーとしてもお馴染みの Brian Lagunas さんも参加されているみたいです。
このエントリ完成直前(2019/10/16)に .NET Core 3.0 対応版の Ver.1.1.3 がリリースされているので、最新版をインストールすれば警告は表示されませんし、以下の設定は必要ありませんが、現時点では .NET Core 3.0 未対応のライブラリもまだ多いので参考情報として修正せず残しています。
ただ、この Xaml.Behaviors.Wpf は先程も書いた通り .NET Core 3.0 のネイティブパッケージでは無いので、.NET Core 3.0 のプロジェクトへ組み込むと警告が表示されてしまいますが、問題なく使用できます。
ただ、警告が表示され続けるのはウザいので fig. 8 のように警告を抑制することをお勧めします。
.NET Core 3.0 はリリースされて間もないのでネイティブパッケージがリリースされていないライブラリも多く、とりあえず fig. 8 のように NU1701 警告だけでも非表示にしておく方が良いと思います。
.NET Core 3.0 対応の Xaml.Behaviors.Wpf
.NET Core 3.0 対応版の Xaml.Behaviors.Wpf Ver.1.1.3 ですが、GitHub ではリリースされず Nuget のみの公開になるようです。
更新するには【ソリューションの Nuget パッケージの管理】から更新できますが、ReactvieProperty や Livet に同梱されている Xaml.Behaviors.Wpf を使用している場合はインストール済みプロジェクトの一覧には表示されないので、xaml ファイルへ【http://schemas.microsoft.com/xaml/behaviors】をインポートしているプロジェクトを更新対象に指定する必要があるので気を付けてください。
Window Close 時に VM を Disposeする
LivetCask.Behaviors には管理人が最も欲しかった Window Close 時に VM を Dispose してくれる DataContextDisposeAction が含まれているので、src. 2 のように MainWindow.xaml へ追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <Window x:Class="PrismNetCoreApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:bh="http://schemas.microsoft.com/xaml/behaviors" xmlns:prism="http://prismlibrary.com/" xmlns:l="http://schemas.livet-mvvm.net/2011/wpf" prism:ViewModelLocator.AutoWireViewModel="True" WindowStartupLocation="CenterScreen" Width="800" Height="600" Title="{Binding Title.Value}"> <bh:Interaction.Triggers> <bh:EventTrigger EventName="Closed"> <l:DataContextDisposeAction /> </bh:EventTrigger> </bh:Interaction.Triggers> ~ 略 ~ </Window> |
DataContextDisposeAction はその名の通り DataContext(VM)の Dispose を呼び出してくれる Behavior で、使用するにはまず、4、6行目のように Xaml.Behaviors.Wpf と LivetCask.Behaviors の名前空間にエイリアスを付けてインポートします。
インポートの位置やエイリアスは任意なので好きな位置、好きなエイリアスで構いません。
11 ~ 15 行目が実際の呼び出し部で Window の Closed イベントで DataContextDisposeAction が実行されるように指定しています。
DataContextDisposeAction で呼び出される側の VM は src. 3 のように IDisposable を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | using System; using System.Reactive.Disposables; using Prism.Mvvm; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace PrismNetCoreApp { public class MainWindowViewModel : BindableBase, IDisposable { ~ 略 ~ /// <summary>Windowタイトルを取得します。</summary> public ReactivePropertySlim<string> Title { get; } ~ 略 ~ private bool disposedValue = false; // 重複する呼び出しを検出するには protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { this.disposables.Dispose(); } disposedValue = true; } } // このコードは、破棄可能なパターンを正しく実装できるように追加されました。 public void Dispose() { // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。 Dispose(true); } private CompositeDisposable disposables = new CompositeDisposable(); /// <summary>コンストラクタ。</summary> public MainWindowViewModel() { this.Title = new ReactivePropertySlim<string>(".NET Core 3.0 Application") .AddTo(this.disposables); } } } |
EventToReactiveCommand 等を指定する必要もなく VM へ IDisposable を実装するだけです。
実行の確認をしたい場合、Dispose メソッドへブレークポイントを張って実行 → Window の×ボタンから終了すると VM の Dispose が呼ばれることが確認できるので試してみてください。
VM で ReactvieProperty 等を使用していて一括で Dispose したい場合には最適と言えます。
LivetCask.Behaviors には DataContextDisposeAction 以外にも以下のような Behavior が同梱されています。
Behavior 名 | コメント |
---|---|
DataContextDisposeAction | このエントリで紹介した DataContext(VM)の Disposeを呼び出してくれる Behavior。 |
LivetCallMethodAction | 何となくですが、EventToReactiveCommand と同じだと思っているので、現時点では紹介する予定はありません。 |
LivetDataTrigger | どのように使用するかは現時点では不明です。 |
MethodBinder MethodBinderWithArgument | これも何となくEventToReactiveCommand と同じだと思っているので、現時点では紹介しないだろうと思っています。 |
RoutedEventTrigger | これも現時点ではどのように使用するか不明です。 |
SetFocusAction | おそらく VM からフォーカスをセットすることができる Behavior のはずなのでいずれ紹介する予定です。 |
WindowCloseCancelBehavior | どうしても欲しかった Window の Close をキャンセルする Behavior なのでその内紹介する予定です。 |
とりあえず、このエントリで紹介するのは DataContextDisposeAction だけですが、上表で『紹介予定』と書いている Behavior はその内記事のネタに使う予定です。
Prism 7.2 で追加された IDestractible から Prism Module 内の UserControl 用 VM を Dispose する
LivetCask.Behaviors の導入で Window の VM を Dispose できるようになりました。
又、episode: 17 でダイアログウィンドウの VM は IDialogAware.OnDialogClosed でオブジェクトの後始末ができることも紹介しました。(IDisposable を実装すれば Dispose も可能です)
後、残るは Prism Module に含まれる UserControl の Dispose ですが、Prism 7.2 から IDestractible が追加されたことで可能になりました。
IDestractible は元々 Prism.Forms(Xamarin)で提供されていましたが、Prism 7.2 から WPF にも組み込まれました。
IDestractible の実装
IDestractible は Destroy メソッドだけが定義されているシンプルなインタフェースなので実装自体は非常に簡単です。
src. 4 は TreeView を配置している NavigationPanel プロジェクトの NavigationTreeViewModel へ IDestructible を追加した例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | using System; using System.Reactive.Disposables; using Prism.Mvvm; using Prism.Navigation; using Prism.Regions; using PrismNetCoreApp.Helpers; using PrismNetCoreApp.NavigationItems; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace PrismNetCoreApp { /// <summary>画面切り替え用のTreeViewパネルを表します。</summary> public class NavigationTreeViewModel : BindableBase, IDisposable, IDestructible { /// <summary>TreeViewに表示するTreeItemを取得します。</summary> public ReactiveCollection<NavigationItemViewModel> TreeItems { get; } #region IDisposable Support private bool disposedValue = false; // 重複する呼び出しを検出するには protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { this.disposables.Dispose(); } disposedValue = true; } } // このコードは、破棄可能なパターンを正しく実装できるように追加されました。 public void Dispose() { // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。 Dispose(true); // TODO: 上のファイナライザーがオーバーライドされる場合は、次の行のコメントを解除してください。 // GC.SuppressFinalize(this); } /// <summary>ViewModelを破棄します。</summary> public void Destroy() => this.Dispose(); #endregion /// <summary>Prism Navigation用のIRegionManagerを表します。</summary> private IRegionManager regionManager = null; /// <summary>IDisposableなオブジェクトを一括でDisposeします。</summary> private CompositeDisposable disposables = new CompositeDisposable(); /// <summary>デフォルトコンストラクタ。</summary> /// <param name="regionMan">Navigation実行用のIRegionManager。</param> /// <param name="appData">アプリケーションデータを表すIPrismNetCoreData。</param> public NavigationTreeViewModel(IRegionManager regionMan, IPrismNetCoreData appData) { this.regionManager = regionMan; this.TreeItems = new ReactiveCollection<NavigationItemViewModel>() .AddTo(this.disposables); this.TreeItems.Add(TreeViewItemHelper.CreateTreeItem(appData.TargetPerson)); } } } |
IDestructible だけでなく IDisposable も継承に追加して IDestructible.Destroy では IDisposable.Dispose を呼び出すようにしています。
IDisposable の継承は必須ではありませんが、継承しておくと GC からの Dispose にも備えられるはずです。
Destroy される VM 側は src. 4 の通りですが、IDestructible.Destroy は IRegion.Remove を呼び出した時しか実行されないと言う制限があります。
そのため、Window Close 時に Load 済み View を Destroy(Dispose)したい場合は src. 5 のように記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | using System; using System.Reactive.Disposables; using Prism.Mvvm; using Prism.Regions; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace PrismNetCoreApp { /// <summary>アプリケーションのメイン画面を表します。</summary> public class MainWindowViewModel : BindableBase, IDisposable { ~ 略 ~ private bool disposedValue = false; // 重複する呼び出しを検出するには protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { this.disposables.Dispose(); foreach (var region in this.regionManager.Regions) { region.RemoveAll(); } } disposedValue = true; } } ~ 略 ~ private IRegionManager regionManager = null; private CompositeDisposable disposables = new CompositeDisposable(); /// <summary>コンストラクタ。</summary> public MainWindowViewModel(IRegionManager regionMan) { this.regionManager = regionMan; this.Title = new ReactivePropertySlim<string>(".NET Core 3.0 Application") .AddTo(this.disposables); } } } |
Livet の DataContextDisposeAction を追加した際の Dispose 内で 24 ~ 27 行目のように IRegion.RemoveAll を呼び出すと Window Close 時に全 Region から 全 View(UserControl)が削除されるタイミングで各 View の IDestructible.Destroy が実行されます。
VM の Dispose にブレークポイントを張って実行後に Window を×ボタンで終了すると Dispose が呼ばれるのが確認できると思います。
前に書いたように IDestructible.Destroy は Prism Region から Remove される場合だけ呼び出されるので、Window 終了時だけでなく任意のタイミングで Region から Remove すると Dispose することもできます。
但し、Dispose した VM を再度使用したい場合、詳しい説明は省きますが、DI コンテナや CompositeDisposable の挙動等も考慮する必要があるので注意は必要だと思います。
又、Prism の Region Navigation で表示した View は 1 度表示すると Region にキャッシュされる仕組みなので RemoveAll を呼び出せば、その時点で表示している View だけでなく表示したことがある View は全て削除されることを別のサンプルで確認したので全ての View が Dispose されるはずです。
そして IDestructible の追加で Prism が使用する Window(Modal Dialog を含む)View(UserControl)等のコントロールコンテナの全てを Dispose できるようになったので更に使い勝手が良くなったと思います。
(今かよ⁉とは管理人も思っていますw)
今回はここまででサンプルコードはいつものように GitHub リポジトリ にアップしています。
次回はまだ決めていませんが、おそらく Prism からは離れた内容の記事になりそうな予定です。