昨今のデスクトップアプリはモーダルウィンドウなどを出さずにウィンドウ一つだけで完結するように作られることが増えてきたのではないでしょうか。
しかし、WPF+MVVMなアプリににおいて、そのような設計のサポートをするコントール等はNavigationWindow+Pageくらいしかなくあまり使い勝手がよくありません。
PrismのRegionを使うという選択肢もありますが、DIコンテナの利用やIModuleの実装など気軽に使うにはちょっとハードルが高かったり面倒だったりすると思われます。
もしくは自前でDatatemplateをResoucesに登録することになりますが、画面が増えたり記述が冗長になったり管理が面倒になりがちです。
そこで、設定より規約なアプローチで"XXXView"と"XXXViewModel"を自動的に紐づけるライブラリを作りました。
サンプルアプリもあるので是非確認してみてください。
Hyosya/WpfSimpleNavi: Easy and simple navigation library for WPF.
NuGet Gallery | WpfSimpleNavi 1.0.0
対応環境は.NET Framework 4.5と.NET Core 3.1です。
#使い方
ContentControlのContentTemplateSelectorにライブラリで提供するDataTemplateSelectorを設定するだけです。
以下のようなWindowを用意します。
<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"
xmlns:wsn="clr-namespace:WpfSimpleNavi;assembly=WpfSimpleNavi"
Title="MainWindow"
Width="800"
Height="450"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.DataContext>
<local:MainWindwoViewModel />
</Window.DataContext>
<Window.Resources>
<wsn:CustomDataTemplateSelector x:Key="CustomSelector" />
</Window.Resources>
<ContentControl Content="{Binding Path=MainContent}"
ContentTemplateSelector="{StaticResource CustomSelector}" />
</Window>
VMはこんな感じ。
public class MainWindwoViewModel : INotifyPropertyChanged
{
public MainWindwoViewModel()
{
MainContent = new SomeViewModel());
}
public event PropertyChangedEventHandler PropertyChanged;
private INotifyPropertyChanged _MainContent;
public INotifyPropertyChanged MainContent
{
get => _MainContent;
set
{
if (_MainContent == value) return;
_MainContent = value;
RaisePropertyChanged();
}
}
private void RaisePropertyChanged([CallerMemberName]string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
上記の例であればSomeViewModelに対してSomeViewというUserContorolがあればそれが表示されます。
CustomDataTemplateクラスでは渡されたオブジェクトの名前が"ViewModel"で終わる場合(e.g. HogeViewModel)、"View"で終わる同名のオブジェクト(e.g. HogeView)をDataTemplateにしたものをApplication.Current.Resourcesに登録したうえで返します。
クラス名のViewModelとViewという部分のVとMは必ず大文字にしてください。
あとはMainWindowViewModelのMainContentプロパティを別のViewModelに差し替えるような処理を書けば、お手軽に画面遷移が実現できます。
#更なる使い方
このままでは、上記の例でいうSomeView+SomeViewModel側から画面遷移をしたい場合MainWindowViewModelへの参照が必要になり、安直な実装だと相互参照になってしまいます。そのような状況を厳密に避けたい場合、PrismのようなDIを使った実装がおすすめですが、それをやめたいのがこのライブラリの趣旨です。なので、簡易な実装になりますが、Manager的な機能を用意しました。
public MainWindwoViewModel()
{
//MainContent = new SomeViewModel());
MainContent = NavigationManager.PublishArea("MainArea", new SampleAViewModel());
}
NavigationManagerクラスはWpfSimpleNavi.Navigation名前空間に定義されています。
PublishAreaメソッドはAreaクラスのインスタンスを返します。
AreaクラスのプロパティにViewModelプロパティがあるので、それをContentContorolのContentプロパティのバインディングソースにします。
画面遷移をしたい場合、AreaクラスのViewModelプロパティを変更することになります。ですがsetterはprivateになっており、直接変更することはできません。
NavigatinManagerクラスにNavigateメソッドとNavigateSameAreaメソッドがあるのでそれを利用します。
NavigateメソッドはどのAreaのVMを変更するのか特定するために、PublishAreaメソッドを呼んだ際のID文字列を要求します。(上記例だと"MainArea")。
NavigateSameAreaメソッドはArea特定のために、変更したいAreaのViewModelプロパティが参照しているインスタンスを要求します。上記の例でSampleAViewModel内からNavigateSameAreaを呼ぶ際はthisを引数にすると"MainArea"のViewModelプロパティが変更されます。
詳しくは、サンプルアプリがありますので是非ソースコードをダウンロードして確認してみてください。
#Pros and Cons
####Pros
- 設定より規約アプローチによるお手軽さ。
- 素朴な実装。サードパーティーライブラリへの依存無し。
####Cons
- 素朴すぎる実装。
- DataTemplateSelectorがほかの用途に利用できない。
#その他
AreaをObservableCollectionに詰めてItemsControl派生なコントールでバインディングすると(例えばTabControlで自由にTabの内容を変えられるような)リッチなアプリが作れたりします。夢が広がりますね。
ライブラリを公開するのが初めてなので、何か不手際があるかもしれません。コメント等歓迎します。