Prism-Samples-Wpf の勉強メモ
個人の勉強メモです。
経緯
本業は組み込みなのですが、業務補助ツールなどでWPFアプリを作成することがあります。
MVVMインフラには Livet を使っているのですが、規模の大きいアプリでは、Viewの遅延読み込みやDIコンテナを使いたいことがあり、少し物足りないと感じることがありました。
そこでPrismの習得に向けて、公式のWPFサンプルを順に勉強していきます。
タイトル横の記号は理解度の印象で、深い意味はありません。
01-BootstrapperShell ○
実行
真っ白なウィンドウが表示されるだけ。
内容
次の 02-Regions と同じ内容っぽいし、こちらは IntelliSense に旧型式って表示されるので、重要でなさげ。
02-Regions ◎
Prismを使って真っ白なウィンドウを表示するサンプル
実行
真っ白なウィンドウが表示されるだけ。
内容
App.xaml のコードビハインド CreateShell() にて、コンテナを使って MainWindowクラスを探して起動してる感じ。
全てのソリューションで必要そう。
Appの継承元が、System.Windows.Application でなく、Prism.Unity.PrismApplication になっている点がポイントで、App.xaml のクラスも通常とは異なっている。
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
<prism:PrismApplication
x:Class="Regions.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>
</Application.Resources>
</prism:PrismApplication>
03-CustomRegions ×
何のサンプルか分からない…
実行
真っ白なウィンドウが表示されるだけ。
内容
これで何が出来ているのかが分からない。
新たに PrismApplicationBase.ConfigureRegionAdapterMappings() や、class RegionAdapterBase<T> が出てきた。
04-ViewDiscovery ◎
コードビハインドから xaml で命名した Region に 自作UserControl を表示するサンプル。
実行
ViewAが表示される。
内容
- MainWindowのxaml
Region名を付けているだけ
<Window x:Class="ViewDiscovery.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
Title="Shell" Height="350" Width="525">
<Grid>
<ContentControl prism:RegionManager.RegionName="ContentRegion" />
</Grid>
</Window>
- MainWindowのコードビハインド
ViewAを登録するために、MainWindowコンストラクタに IRegionManager が渡されている。
明示的にMainWindowのコンストラクタをコールしている記述はなく、コンテナがええ感じでインターフェスを渡していると思われる。インジェクションと言われるやつ?
シンプルなコードであっても、コードビハインドにコードを書くことに抵抗がある…
public partial class MainWindow : Window
{
public MainWindow(IRegionManager regionManager)
{
InitializeComponent();
// RegionにViewを読み出す ★重要
regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA));
}
}
05-ViewInjection ◎
Button の Clickイベントで、自作UserControl を読み出すサンプル
UserControl の登録の実装が 04-ViewDiscovery と異なる。
実行
ボタンが1つ配置されており、クリックするとViewAが読み込まれる。
ボタンを複数回クリックしても View の見た目は変わらないが、内部では ViewA が複数追加されていると思われイマイチ。
内容
MainWindow のボタンClickイベントにより、コードビハインドから UserControl を読み出している。
これは View の遅延読み込みに使えそう。
private void Button_Click(object sender, RoutedEventArgs e)
{
// RegionにViewを読み出す ★重要
var view = _container.Resolve<ViewA>();
_regionManager.Regions["ContentRegion"].Add(view);
}
上記対応のために、MainWindowのコンストラクタに引数 IContainerExtension が増えた。
やはり明示的にMainWindowのコンストラクタをコールしている記述はなく、コンテナがええ感じで解決していると思われる。
public MainWindow(IContainerExtension container, IRegionManager regionManager)
{
InitializeComponent();
_container = container;
_regionManager = regionManager;
}
06-ViewActivationDeactivation ○
自作UserControl のアクティブ/非アクティブを切り替えるサンプル。(破棄ではない)
実行
ボタンが4つ配置されており、ViewAが表示されている。
各ボタンは、ViewA/ViewBの読み込みと破棄に割り当てられており、ボタンに応じてViewの見た目が変化する。
内容
MainWindow の ContentControl に対して、登録済み UserControl の表示/非表示を行う。
表示の切り替えなので、05-ViewInjection のように View が複数個登録されることはない。
Viewの表示は、先にRegionに登録した方(ViewA)が、優先して表示されるルールっぽい。
MainWindow のコードビハインドに UserControl の実体を定義してることに違和感を感じる。
ViewA _viewA;
ViewB _viewB;
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_viewA = _container.Resolve<ViewA>();
_viewB = _container.Resolve<ViewB>();
_region = _regionManager.Regions["ContentRegion"];
_region.Add(_viewA);
_region.Add(_viewB);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
_region.Activate(_viewA);
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
_region.Deactivate(_viewA);
}
07-Modules - AppConfig △
07-Modules は子モジュールを読み込むサンプル。
AppConfig / Code / Directory / LoadManual の4項目が用意されている。
2つめの Code が最も広く使われているっぽい。
実行
ViewAが表示される 。 (07-Modules共通)
内容
親モジュール 07-Modules 共通
ContentRegionの名前で ContentControl を配置しておく。
<Grid>
<ContentControl prism:RegionManager.RegionName="ContentRegion" />
</Grid>
子モジュール側
子モジュールのProject直下に ModuleAModule.cs を追加して、親モジュールの Region名を string で指定する。(07-Modules共通)
public class ModuleAModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
親モジュール側
親モジュール側のコードに、ModuleA を直接記述している箇所は App.configのみ。
<modules>
<module assemblyFile="ModuleA.dll"
moduleType="ModuleA.ModuleAModule,
ModuleA,
Version=1.0.0.0,
Culture=neutral,
PublicKeyToken=null"
moduleName="ModuleAModule"
startupLoaded="True" />
</modules>
App.xamlのコードビハインド CreateModuleCatalog() にて、App.configに記載されたジュールを自動で配置しているっぽい。
protected override IModuleCatalog CreateModuleCatalog()
{
return new ConfigurationModuleCatalog();
}
07-Modules - Code ◎
07-Modules は子モジュールを読み込むサンプルの2つめ。
見たことあるパターンで、以降のサンプルでも度々登場する。
実行
ViewAが表示される 。 (07-Modules共通)
内容
子モジュール側
07-Modules - AppConfig と同内容なので割愛(07-Modules共通)
親モジュール側
App.xaml のコードビハインド ConfigureModuleCatalog() で、ModuleAプロジェクトのクラス ModuleAModule を直接指定して、Viewに配置している。
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<ModuleA.ModuleAModule>();
}
07-Modules - Directory ◎
07-Modules は子モジュールを読み込むサンプルの3つめ。
dllが含まれるディレクトリを指定する。機能拡張などに使えるかも。
実行
ViewAが表示される 。 (07-Modules共通)
内容
子モジュール側
07-Modules - AppConfig と同内容なので割愛(07-Modules共通)
親モジュール側
App.xaml のコードビハインドで、子モジュール.dll が格納された DirectoryPATH を指定して読み出している。
dll が存在しなければ何も読み込まれないので、機能拡張などに使えるかも。
protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() {
ModulePath = @"C:\source\Prism-Samples-Wpf\07-Modules - Directory\Modules\bin\Debug\Modules"
};
}
07-Modules - LoadManual ◎
07-Modules は子モジュールを読み込むサンプルの4つめ。
実行
ViewAが表示される 。 (07-Modules共通)
内容
子モジュール側
07-Modules - AppConfig と同内容なので割愛(07-Modules共通)
親モジュール側
親モジュールのUIイベント(Windowのコードビハインド)で子モジュールを読み出している。
UserControl を読み出してたやつ(05-ViewInjection)のモジュール版。
こちらも遅延読み込みに使えそう。
子モジュールの指定は、子モジュールクラス名の文字列により行われているが、nameof() 演算子を使った方が安全そう。
private void Button_Click(object sender, RoutedEventArgs e)
{
_moduleManager.LoadModule("ModuleAModule");
}
08-ViewModelLocator ◎
Viewに対応する ViewModel を自動で配置するサンプル。
ViewModel が初登場!
実行
真っ白なウィンドウが表示されるだけだが、Windowタイトル が ViewModel で指定された文字列になっている。
内容
xaml に1行書いとけば、対応する ViewModel が自動で読み込まれる。
DataContext で指定する必要がない。
<Window x:Class="ViewModelLocator.Views.MainWindow"
~中略~
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
ViewModel
Prism.Mvvm.BindableBase を継承しており、BindableBase は INotifyPropertyChanged を継承している。
ViewModel は必ず BindableBase を継承しておけば良さげ。
09-ChangeConvention ○
Viewのxamlファイルに ViewModel を含めたような記述ができるっぽい。
実行
08-ViewModelLocatorと同じ
真っ白なウィンドウが表示されるだけだが、Windowタイトル が ViewModel で指定された文字列になっている。
内容
Viewのxamlファイルに ViewModel を含めたような記述ができるっぽい。(xamlに2つの*.csがぶらさった構成を初めて見た)
MVVM記法から外れている気がするので使わなさそう。
10-CustomRegistrations ○
08-ViewModelLocator で出てきた、自動で名前解決される ViewModel のクラス名を指定できるっぽい。
サンプルでは、View の MainWindow.xaml に対して、ViewModel の CustomViewModel.cs を割り当てている。
どのような場面で使うかが想像できないので使わなさそう。
実行
08-ViewModelLocatorと同じ。
真っ白なウィンドウが表示されるだけだが、Windowタイトル が ViewModel で指定された文字列になっている。
内容
App.xaml.cs で View に対応する ViewModel を指定している。
いくつかの方法があるっぽく、サンプル時点でコメントアウトされていた。
protected override void ConfigureViewModelLocator()
{
base.ConfigureViewModelLocator();
// type / type
//ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), typeof(CustomViewModel));
// type / factory
//ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), () => Container.Resolve<CustomViewModel>());
// generic factory
//ViewModelLocationProvider.Register<MainWindow>(() => Container.Resolve<CustomViewModel>());
// generic type
ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();
}
11-UsingDelegateCommands ○
xaml の Button.Command に応じた ViewModel 処理。
実行
チェックボックスを有効にすると各ボタンが有効になり、各ボタンを押すと下部に現在時間が表示される。
複雑で文字で説明できなくなってしまったので、図を貼ってみる。
内容
ICommand を継承した Prism.Commands.DelegateCommand を使って、Action / CanExecute を指定してる。
4つのボタンそれぞれで実装が異なる。
使い慣れた ReactiveCommand で代替できそうなので使わなさそう。
最後までサンプルをやった結果、DelegateCommand がゴリゴリ出てくるので、ちゃんと使えるようになるべきと考えを改めました。
public DelegateCommand ExecuteDelegateCommand { get; }
public DelegateCommand<string> ExecuteGenericDelegateCommand { get; }
public DelegateCommand DelegateCommandObservesProperty { get; }
public DelegateCommand DelegateCommandObservesCanExecute { get; }
public MainWindowViewModel()
{
ExecuteDelegateCommand =
new DelegateCommand(Execute, CanExecute);
DelegateCommandObservesProperty =
new DelegateCommand(Execute, CanExecute).ObservesProperty(() => IsEnabled);
DelegateCommandObservesCanExecute =
new DelegateCommand(Execute).ObservesCanExecute(() => IsEnabled);
ExecuteGenericDelegateCommand =
new DelegateCommand<string>(ExecuteGeneric).ObservesCanExecute(() => IsEnabled);
}
12-UsingCompositeCommands △
複数の DelegateCommand を1つに集約するサンプル。
集約されたCommandを実行したら、各Commandを全実行した動作になる。
実行
コントロールのEnable
-
3つのTabが存在し、各Tabがチェックボックスを持っている。
-
各Tabのチェックボックスが有効なら、自Tabのボタンが有効になる。
-
全てのTabのボタンが有効なら、上部の Saveボタンが有効になる。
ボタンクリック
各TabのSaveボタンを押下すると、自タブに現在時刻が表示される。
上部のSaveボタンを押下すると、全てのタブに現在時刻が表示される。(=全タブのボタンを押した動作)
内容
この辺りから理解が難しくなってきた。
TabControlへの子要素の登録方法
-
親Moduleの xaml で TabControl を定義し、Region名を付けておく
<TabControl prism:RegionManager.RegionName="ContentRegion" />
-
UserControl をベースに TabItem を作成する(TabView.xaml)
-
子モジュールの fogeModule.OnInitialized() にて、作成した TabItem を登録していく。
TabView から TabViewModel の取得法が力業で引いた…
ViewModelのコンストラクタにTitleを渡すような実装はできないのでしょうか。public class ModuleAModule : IModule { public void OnInitialized(IContainerProvider containerProvider) { var regionManager = containerProvider.Resolve<IRegionManager>(); IRegion region = regionManager.Regions["ContentRegion"]; var tabA = containerProvider.Resolve<TabView>(); SetTitle(tabA, "Tab A"); region.Add(tabA); /* tabB,tabC は同じなので割愛 */ } } private void SetTitle(TabView tab, string title) { (tab.DataContext as TabViewModel).Title = title; }
CompositeCommand
各Tab が保持する ICommand を結合して、上部のSaveボタンに割り当てているっぽい。
登録された全ての ICommand が CanExecute == true なら、統合された CompositeCommand が実行可能となる。
CompositeCommand が実行されると、登録されたすべての ICommand が実行される。
-
親と子モジュールが参照できるところに(サンプルでは別プロジェクト)、ApplicationCommands クラスを定義する。
ApplicationCommands の中身は Prism.Command.CompositeCommand のみ。public interface IApplicationCommands { CompositeCommand SaveCommand { get; } } public class ApplicationCommands : IApplicationCommands { public CompositeCommand SaveCommand { get; } = new CompositeCommand(); }
-
親Module の App.xaml.cs にて、上記自作クラスのインターフェースと実装をコンテナに登録する。
サンプルでは Singleton で登録。protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterSingleton<IApplicationCommands, ApplicationCommands>(); }
-
MainWindow の ViewModel のコンストラクタ引数に対して、上記自作クラスが自動で注入されるので受け取る。
public MainWindowViewModel(IApplicationCommands applicationCommands) { ApplicationCommands = applicationCommands; }
-
各TabItem の ViewModel も同様にコンストラクタで自作クラスを受け取り、自クラスの UpdateCommand(ICommand) を登録していく。
自クラスの UpdateCommand は自クラスのチェックボックスにバインドされたフラグを監視している。(2行目)public TabViewModel(IApplicationCommands applicationCommands) { _applicationCommands = applicationCommands; UpdateCommand = new DelegateCommand(Update).ObservesCanExecute(() => CanUpdate); _applicationCommands.SaveCommand.RegisterCommand(UpdateCommand); }
13-IActiveAwareCommands △
12-UsingCompositeCommands と同様に複数の DelegateCommand を1つに集約するが、アクティブなコントロールを参照するサンプル。
実行
12-UsingCompositeCommands と同じ View。
動作は少し違って、こちらはActiveなTabの チェックボックスに応じて上部ボタンの IsEnable を切り替えている。
コマンドが実行されるのもActiveなタブのみ。
各Tabのチェックボックス状態と上部ボタン状態
TabA: CheckBox.IsEnable=True (Viewで選択中のTab)
TabB: CheckBox.IsEnable=False
TabC: CheckBox.IsEnable=False
12-UsingCompositeCommands ⇒ TopButton.IsEnable=False
13-IActiveAwareCommands ⇒ TopButton.IsEnable=True
内容
CompositeCommand が Active な Control を判別するために、下記対応をしてるっぽい(抜けてるかも)
-
Prism.Commands.CompositeCommand のコンストラクタ引数を true にする。
// 概要: // Initializes a new instance of Prism.Commands.CompositeCommand. // // パラメーター: // monitorCommandActivity: // Indicates when the command activity is going to be monitored. public CompositeCommand(bool monitorCommandActivity);
-
TabItem の ViewModel にて、IActiveAware インターフェースを継承する。
public interface IActiveAware { //Gets or sets a value indicating whether the object is active. bool IsActive { get; set; } //Notifies that the value for Prism.IActiveAware.IsActive property has changed. event EventHandler IsActiveChanged; }
-
継承した IActiveAware に対応するコードを書き、IsActiveプロパティを更新する。
bool _isActive; public bool IsActive { get { return _isActive; } set { _isActive = value; OnIsActiveChanged(); } } private void OnIsActiveChanged() { UpdateCommand.IsActive = IsActive; IsActiveChanged?.Invoke(this, new EventArgs()); } public event EventHandler IsActiveChanged;
サンプルには出てこないが、IsActiveChanged にイベントを登録しておけば、IsActiveの変化時に処理ができる。
public ctor()
{
IsActiveChanged += (object sender, EventArgs e) =>
{
if (e is DataEventArgs<bool> e2)
Console.WriteLine($"ActiveChanged: {e2.Value}");
};
}
private bool _isActive;
public bool IsActive
{
get => _isActive;
set
{
if (SetProperty(ref _isActive, value))
IsActiveChanged?.Invoke(this, new DataEventArgs<bool>(value));
}
}
public event EventHandler IsActiveChanged;
14-UsingEventAggregator ○
モジュール間で情報の受け渡しをするサンプル
実行
ボタンをクリックすると、テキストボックスに入力された文字列が、右のリストボックスに表示される
内容
左右でModule(プロジェクト)が分かれている。
共通部
各プロジェクトが参照できるところに MessageSentEvent.cs を定義する。(サンプルでは別プロジェクト)
MessageSentEvent.cs は、Prism.Events.PubSubEvent<TPayload> を継承してるだけの空クラス。
定義しなくても対応できると思うが、使用時にジェネリックの型パラメータのネストが深くなり見辛いのかも。
public class MessageSentEvent : PubSubEvent<string>
{
}
親モジュール
MainWindowを左右に分割して、リージョン名を付けてるだけ。
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ContentControl prism:RegionManager.RegionName="LeftRegion" />
<ContentControl Grid.Column="1"
prism:RegionManager.RegionName="RightRegion" />
</Grid>
子モジュール(左:情報発行側)
親モジュールへの配置は、07-Modules と同じ手法なので割愛。
ViewModelのコンストラクタで IEventAggregator を受けており、ボタンクリック時に DelegateCommand で SendMessage() がコールされている。
public class MessageViewModel : BindableBase
{
IEventAggregator _ea;
private string _message = "Message to Send";
public string Message
{
get { return _message; }
set { SetProperty(ref _message, value); }
}
public DelegateCommand SendMessageCommand { get; }
public MessageViewModel(IEventAggregator ea)
{
_ea = ea;
SendMessageCommand = new DelegateCommand(SendMessage);
}
private void SendMessage()
{
_ea.GetEvent<MessageSentEvent>().Publish(Message);
}
}
情報発行は、デリゲートコマンド引数のActionがポイント
_ea.GetEvent<MessageSentEvent>().Publish(Message);
子モジュール(右:情報取得側)
左側の子モジュールと同様に、コンストラクタで IEventAggregator を受ける。
public class MessageListViewModel : BindableBase
{
IEventAggregator _ea;
private ObservableCollection<string> _messages;
public ObservableCollection<string> Messages
{
get { return _messages; }
set { SetProperty(ref _messages, value); }
}
public MessageListViewModel(IEventAggregator ea)
{
_ea = ea;
Messages = new ObservableCollection<string>();
_ea.GetEvent<MessageSentEvent>().Subscribe(MessageReceived);
}
private void MessageReceived(string message)
{
Messages.Add(message);
}
}
情報取得は、コンストラクタ内のメソッドがポイント
_ea.GetEvent<MessageSentEvent>().Subscribe(MessageReceived);
15-FilteringEvents ○
EventAggregator にて、情報取得側が取得する情報のフィルタリングを行うサンプル
実行
14-UsingEventAggregator と同じView。
機能差分は情報の取得側のフィルタリングで、文字列に "Brian" が含まれる場合のみ情報を取得している。
※情報の発行側は、14-UsingEventAggregator と同じで、文字列に関わらず発行する。
内容
14-UsingEventAggregator からの変化点である 右側モジュール(ModuleB)についてのみまとめる。
子モジュール(右:情報取得側)
フィルタリングは、ModuleB の ViewModel に実装されており、判定は Predicate<T> なので、色々と差し替えできそう。
引数の " ThreadOption threadOption" と "bool keepSubscriberReferenceAlive" については未調査。
// PubSubEvent<TPayload>.Subscribe(
// Action<TPayload> action,
// ThreadOption threadOption,
// bool keepSubscriberReferenceAlive,
// Predicate<TPayload> filter)
_ea.GetEvent<MessageSentEvent>()
.Subscribe(MessageReceived, ThreadOption.PublisherThread,
false, (filter) => filter.Contains("Brian"));
16-RegionContext ○
初の複数Region。上下で分かれている。
実行
上下に分かれており、上部リストボックスで選択した情報が、下部テキストブロックに表示される。
内容
リッチUIアプリの頻出パターン。
Prism特有のクラスとかは使ってなさげで、特別難しいことはないと思う。
親モジュール
特別なことはしてない。子モジュールを読み込むだけ。(07-Modules - Code)
子モジュール
ListBox の選択情報を SelectedItem で取得し表示している。
下部コントロールの配置をリージョン名から行っており、こーゆー雰囲気でUIコントロールを細分化して管理するのかなと思った。
prism:RegionManager.RegionName="PersonDetailsRegion"
17-BasicRegionNavigation ○
Navitgation を使って、表示する自作UserControl を切り替えるサンプル
実行
ボタンが二つ配置されており、各ボタンを押すと、ウィンドウ下部に ViewA もしくは ViewB が読み込まれる。
起動時は ViewA も ViewB も読み込まれていない状態。
内容
子モジュール
コンテナに ViewA/ViewB を登録している。
public class ModuleAModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<ViewA>();
containerRegistry.RegisterForNavigation<ViewB>();
}
}
親モジュール
重要なのは親モジュール。
親モジュールでは、View(xaml)のButtonの CommandParameter で 子モジュールの UserControl名 (ViewAorViewB) を指定している。
<Button Command="{Binding NavigateCommand}"
CommandParameter="ViewA"
Content="Navigate to View A" />
<Button Command="{Binding NavigateCommand}"
CommandParameter="ViewB"
Content="Navigate to View B" />
ViewModel の DelegateCommand で、子モジュールの UserControl を読み込んでいる。
public DelegateCommand<string> NavigateCommand { get; private set; }
public MainWindowViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
NavigateCommand = new DelegateCommand<string>(Navigate);
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate("ContentRegion", navigatePath);
}
試しに存在しない UserControl名 (ViewC) を指定したところ、画面に "System.Object" と表示された。例外は発生しなかった。
18-NavigationCallback ○
Navigation の完了時に Callback(Action) を行うサンプル。
実行
17-BasicRegionNavigation と同じ見た目。
差分としては、ボタンのクリックにより、子モジュールの View が読み込まれたあとに、ポップアップウィンドウが表示される。
内容
モジュール読み出し完了のタイミングで処理を行っている。
親モジュール
UserControl の呼び出し時に、Callbackとして Action<NavigationResult> を登録している。
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate("ContentRegion", navigatePath, NavigationComplete);
}
private void NavigationComplete(NavigationResult result)
{
MessageBox.Show(String.Format("Navigation to {0} complete. ", result.Context.Uri));
}
先ほどと同様に存在しない UserControl の指定を試したところ、読み込めていないのに complete. と表示された。
NavigationResult には、Errorプロパティ が存在するので、読み込めない場合は、Errorに何か情報が入るかと期待したが、モジュールが見つからない場合でも Error は null だった。(いつErrroが入るか謎…)
19-NavigationParticipation ○
自作UserControl に INavigationAware を継承することで、Navigationの遷移時に処理を行うサンプル
実行
ボタンが二つ配置されており、ボタンを押すと各ボタンに対応した UserControl がタブに読み込まれる。
ボタンをクリックするたびに、UserControl 内のカウンタがインクリメントされていく。
また、ボタンクリック時に対応する UserControl が表示されていなければ、対応するタブにフォーカスが移動する。
内容
親モジュール
17-BasicRegionNavigation と同じ。
xaml で UserControl名 を指定して、DelegateCommand で読み込み。
子モジュール
17-BasicRegionNavigation からの差分として、ViewModel が Prism.Regions.INavigationAware を継承している。
INavigationAware には 3つのメソッドが存在し、カウントアップなどを実現している。
下記コメントは勝手に書いています。
public interface INavigationAware
{
// ナビゲーションが移る前にコールされる。
// trueを返すと、このインスタンスが使いまわされる。
// falseを返すと、別のインスタンスが作成される。
// サンプルの動作だとボタンクリックの度にタブが増えていく。
bool IsNavigationTarget(NavigationContext navigationContext);
// ナビゲーションが他に移る時にコールされる。
void OnNavigatedFrom(NavigationContext navigationContext);
// ナビゲーションが移ってきた時にコールされる。
// これでカウンタをインクリメントしていた。
void OnNavigatedTo(NavigationContext navigationContext);
}
20-NavigateToExistingViews ○
19-NavigationParticipation の応用サンプル。
特定条件で 自作View を使い回さずに新規追加する。
実行
19-NavigationParticipation と同じ見た目。
差分としては、ボタンを一定回数クリックすると、新規タブが作成されて増えていく。
内容
先ほど少し出てきた INavigationAware.IsNavigationTarget() の戻り値をカウンタに応じて変えているだけ。
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return PageViews / 6 != 1;
}
21-PassingParameters △
実行
16-RegionContext とほぼ同じ見た目。
差分としては、リストアイテムのクリックにより、下部にタブが追加されて情報が表示される。
内容
複雑で少し付いていけない…
実現してることに比べて実装が難しすぎる気がする…
親モジュール
特に目新しいことはしてない
子モジュール
-
ListBox の SelectionChanged イベントで 選択されている Personインスタンス を DelegateCommand の Action<Person> に渡す
-
Action<Person> では、Tabのリージョン("PersonDetailsRegion") に PersonDetail.xaml を登録する
private void PersonSelected(Person person) { var parameters = new NavigationParameters { { "person", person } }; if (person != null) _regionManager.RequestNavigate( "PersonDetailsRegion", "PersonDetail", parameters); }
-
初回の PersonDetailViewModel では、インスタンスが作成されて、OnNavigatedTo() で SelectedPerson が更新される
public void OnNavigatedTo(NavigationContext navigationContext) { if (navigationContext.Parameters["person"] is Person person) SelectedPerson = person; } public bool IsNavigationTarget(NavigationContext navigationContext) { if (navigationContext.Parameters["person"] is Person person) return SelectedPerson != null && SelectedPerson.LastName == person.LastName; else return true; }
-
初回以降は、IsNavigationTarget() で 選択された Person に対応するViewModelインスタンスが存在するかがチェックされて、存在しなければ新たなインスタンスが作成され増えていく。
22-ConfirmCancelNavigation ○
Navigation に遷移するかどうかをダイアログで切り替えるサンプル
実行
17-BasicRegionNavigation と同じ見た目で、機能もほぼ同じ。
差分としては、ViewAを表示中にボタンBを押下すると、Navigate するかどうかを Yes/No 形式のメッセージボックスで質問される。
Yes を選択すると ViewB の表示に切り替わる。
No を選択すると ViewA の表示のまま変化しない。
内容
親モジュール
17-BasicRegionNavigation と同じで、目新しいことはない
子モジュール
ViewA の ViewModel のみ Prism.Regions.IConfirmNavigationRequest を継承している。
- IConfirmNavigationRequest が継承してるインターフェースは既出(19-NavigationParticipation)
- IConfirmNavigationRequest のメソッドは 以下の 1つのみで『このインスタンスからの移動を受け入れるかどうか』を選択できる。
public interface IConfirmNavigationRequest : INavigationAware
{
void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback);
}
ViewAViewModel の実装
public void ConfirmNavigationRequest(
NavigationContext navigationContext, Action<bool> continuationCallback)
{
bool result = true;
if (MessageBox.Show("Do you to navigate?", "Navigate?",
MessageBoxButton.YesNo) == MessageBoxResult.No)
result = false;
continuationCallback(result);
}
23-RegionMemberLifetime ○
Viewの非アクティブ時にインスタンスを保持するかどうかを IRegionMemberLifetime により指定するサンプル
実行
17-BasicRegionNavigation と同じ見た目。
- ViewA/ViewB ボタンをクリックすると、各Viewの名前が表示された 水色のパネルが表示される。
- ViewBはクリックするたびにパネルが増えていく。
- ViewAは何度クリックしても 1つのパネルしかできず、ViewBをクリックするとViewAパネルは消える。
動作だけ見ても意味が分からないが、コードを見るに、水色パネルはViewのインスタンスを可視化している。
ViewAは非アクティブ時にインスタンスを保持しない造りにしているので、ViewA→ViewB と操作すると、ViewAパネル(インスタンス)が消える。
内容
親モジュール
水色パネルは、ObservableCollection<object> を ItemsControl により表示してる。
ObservableCollection<object> だが string しか格納されないので、ObservableCollection<string> でも問題ない。
RegionにViewが追加された際の処理
CollectionChangedイベントにより実現されている。サンプルで初めて出てきた。
public MainWindowViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
_regionManager.Regions.CollectionChanged += Regions_CollectionChanged;
NavigateCommand = new DelegateCommand<string>(Navigate);
}
// こちらはアプリ起動直後に1回しか来ない (xaml "ContentRegion" の追加)
private void Regions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
var region = (IRegion)e.NewItems[0];
region.Views.CollectionChanged += Views_CollectionChanged;
}
}
// こちらはボタンクリック(ViewA/B)追加の度にAddが来る
// ViewAインスタンスが消える際にはRemoveが来る
private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
ViewPanels.Add(e.NewItems[0].GetType().Name);
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
ViewPanels.Remove(e.OldItems[0].GetType().Name);
}
}
子モジュール
ほぼ 19-NavigationParticipation と同じ。
差分は、ViewAViewModel が Prism.Regions.IRegionMemberLifetime を継承している点だけ。
public interface IRegionMemberLifetime
{
// インスタンスを非アクティブ化時に保持するか?
bool KeepAlive { get; }
}
ViewAViewModel の実装(非アクティブ時は常にインスタンスを保持しない)
public bool KeepAlive { get => false; }
24-NavigationJournal ○
ブラウザの戻る進むのように画面遷移するサンプル
実行
16-RegionContext と同じ Person選択系。
リストボックスが表示されており、選択すると詳細表示に画面が遷移する。
GoBackボタンで戻れて、GoForwardボタンで進める。画面遷移が初めて出てきた。
内容
親モジュール
特に目新しい点はなし
子モジュール
-
ListBoxのSelectionChangedイベントで、ViewModelからUserControlのPersonDetailを読み出す。
<i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <prism:InvokeCommandAction Command="{Binding PersonSelectedCommand}" CommandParameter="{Binding SelectedItem, ElementName=_listOfPeople}" /> </i:EventTrigger> </i:Interaction.Triggers>
-
PersonDetailViewModel は INavigationAware を継承しており、ナビゲートが移った際に IRegionNavigationJournal を保持する。
この Prism.Regions.IRegionNavigationJournal を使って画面遷移を制御する。public DelegateCommand GoBackCommand { get; } public PersonDetailViewModel() { GoBackCommand = new DelegateCommand(GoBack); } private void GoBack() => _journal.GoBack(); public void OnNavigatedTo(NavigationContext navigationContext) { _journal = navigationContext.NavigationService.Journal; if (navigationContext.Parameters["person"] is Person person) SelectedPerson = person; }
-
Go Backボタンが押されたら、IRegionNavigationJournal.GoBack() により、最初のViewModelに戻る。
ここでも INavigationAware.OnNavigatedTo() で IRegionNavigationJournal を取得し、Forward に使用する。public DelegateCommand GoForwardCommand { get; } public PersonListViewModel(IRegionManager regionManager) { _regionManager = regionManager; PersonSelectedCommand = new DelegateCommand<Person>(PersonSelected); CreatePeople(); GoForwardCommand = new DelegateCommand(GoForward, CanGoForward); } private void GoForward() => _journal.GoForward(); private bool CanGoForward() => _journal != null && _journal.CanGoForward; public void OnNavigatedTo(NavigationContext navigationContext) { _journal = navigationContext.NavigationService.Journal; GoForwardCommand.RaiseCanExecuteChanged(); }
25-NotificationRequest ○
ポップアップウィンドウ(OKのみ)を表示するサンプル
実行
ボタンが1つ配置されており、クリックするとポップアップウィンドウが表示される。
ポップアップウィンドウの OK ボタンを押すと、タイトル文字列が変化する。
内容
ポップアップウィンドウ(OKボタンのみ)を出す方法
-
ViewModel に InteractionRequest<INotification> プロパティを定義し、Buttonコマンドなどをトリガにして Raise() する。
Raise() は Action<T> で callback を指定できるので、OKボタン押下後の処理はそちらで行う。public InteractionRequest<INotification> NotificationRequest { get; } = new InteractionRequest<INotification>(); public DelegateCommand NotificationCommand { get; } public MainWindowViewModel() { NotificationCommand = new DelegateCommand(() => { NotificationRequest.Raise( new Notification { Content = "Notification Message", Title = "Notification" }, r => Title = "Notified"); }); }
-
xaml側にも下記対応が必要
- IsModal:ポップアップ表示後に親ウィンドウの操作を禁止する
- CenterOverAssociatedObject:ポップアップウィンドウを親ウィンドウの中央に表示する
<i:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding NotificationRequest}"> <prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True" /> </prism:InteractionRequestTrigger> </i:Interaction.Triggers>
26-ConfirmationRequest ○
ポップアップウィンドウ(OK/Cancel)を表示するサンプル
実行
25-NotificationRequest に、OK/Cancel 形式のポップアップウィンドウを表示するボタンが追加されたサンプル。
内容
ポップアップウィンドウ(OK/Cancelボタン)を出す方法
-
ViewModel対応
25-NotificationRequest とほぼ同じ。OK/Cancelは Confirmed(bool型) で取得できる。public InteractionRequest<IConfirmation> ConfirmationRequest { get; } = new InteractionRequest<IConfirmation>(); public DelegateCommand ConfirmationCommand { get; } public MainWindowViewModel() { ConfirmationCommand = new DelegateCommand(() => ConfirmationRequest.Raise( new Confirmation { Title = "Confirmation", Content = "Confirmation Message" }, r => Title = r.Confirmed ? "Confirmed" : "Not Confirmed")); }
-
xaml対応
先ほどと全く同じ。SourceObjectの指定を変えているだけ。<i:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding ConfirmationRequest}"> <prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True" /> </prism:InteractionRequestTrigger> </i:Interaction.Triggers>
27-CustomContent ○
ポップアップウィンドウ(自作View)を表示するサンプル
実行
26-ConfirmationRequest に、カスタムポップアップを表示するボタンが追加されたサンプル。
内容
カスタムポップアップを出す方法
-
ViewModel対応
26-ConfirmationRequest とほぼ同じ。public InteractionRequest<INotification> CustomPopupRequest { get; } = new InteractionRequest<INotification>(); public DelegateCommand CustomPopupCommand { get; } public MainWindowViewModel() { CustomPopupCommand = new DelegateCommand(() => CustomPopupRequest.Raise( new Notification { Title = "Custom Popup", Content = "Custom Popup Message " }, r => Title = "Good to go")); }
-
xaml対応
PopupWindowAction.WindowContent に、自作UserControl (CustomPopupView) を指定する。
ポップアップウィンドウの初期サイズなどは、PopupWindowAction.WindowStyle から指定できます。<prism:InteractionRequestTrigger SourceObject="{Binding CustomPopupRequest}"> <prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"> <prism:PopupWindowAction.WindowStyle> <Style TargetType="Window"> <Setter Property="Width" Value="400" /> <Setter Property="Height" Value="200" /> </Style> </prism:PopupWindowAction.WindowStyle> <prism:PopupWindowAction.WindowContent> <views:CustomPopupView /> </prism:PopupWindowAction.WindowContent> </prism:PopupWindowAction> </prism:InteractionRequestTrigger>
28-CustomRequest ○
ポップアップウィンドウ(自作Viewで結果を戻す)を表示するサンプル
実行
ポップアップウィンドウで選択した情報を親ウィンドウに戻している。
Cancel(非選択)も判別できる。
アイテム選択後に ×ボタンでウィンドウを閉じた場合も、ちゃんと Cancel として扱われる。
内容
-
親ウィンドウのViewModel対応
public InteractionRequest<ICustomNotification> CustomNotificationRequest { get; } = new InteractionRequest<ICustomNotification>(); public DelegateCommand CustomNotificationCommand { get; } public MainWindowViewModel() { CustomNotificationCommand = new DelegateCommand(() => CustomNotificationRequest.Raise( new CustomNotification { Title = "Custom Notification" }, r => { if (r.Confirmed && r.SelectedItem != null) Title = $"User selected: {r.SelectedItem}"; else Title = "User cancelled or didn't select an item"; })); }
InteractionRequest<T> の ICustomNotification がポイント。
ICustomNotification は 自作クラスで(Prism名前空間でない)、26-ConfirmationRequest で出てきた Prism.Interactivity.InteractionRequest.IConfirmation を継承している。
SelectedItemプロパティだけを持っており、ポップアップウィンドウで選択されたアイテムを設定/取得できる。
using Prism.Interactivity.InteractionRequest; namespace UsingPopupWindowAction.Notifications { public interface ICustomNotification : IConfirmation { string SelectedItem { get; set; } } }
-
親ウィンドウのView対応
27-CustomContent と同じ。ポップアップウィンドウの View 指定が異なるだけ
<i:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding CustomNotificationRequest}"> <prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"> <prism:PopupWindowAction.WindowContent> <views:ItemSelectionView /> </prism:PopupWindowAction.WindowContent> </prism:PopupWindowAction> </prism:InteractionRequestTrigger> </i:Interaction.Triggers>
-
子ウィンドウのViewModel対応
ViewModel は、Prism.Interactivity.InteractionRequest.IInteractionRequestAware を継承している。
ポイントは、IInteractionRequestAware の Notificationプロパティを定義しつつ、バックフィールド(?) ではINotificationを継承した自作の ICustomNotification で受けている点やと思う。
プロパティとバックフィールドの型が異なる実装を初めて見たので勉強になった。
private ICustomNotification _notification; public INotification Notification { get { return _notification; } set { SetProperty(ref _notification, (ICustomNotification)value); } }
以下は、各ボタン(Select/Cancel)に応じたコマンド
情報の設定後に IInteractionRequestAware インターフェースの Actionである FinishInteraction を実行することで、ポップアップウィンドウを閉じている。
public ItemSelectionViewModel() { SelectItemCommand = new DelegateCommand(() => { _notification.SelectedItem = SelectedItem; _notification.Confirmed = true; FinishInteraction?.Invoke(); }); CancelCommand = new DelegateCommand(() => { _notification.SelectedItem = null; _notification.Confirmed = false; FinishInteraction?.Invoke(); }); }
-
子ウィンドウのView対応
見た目通りの実装なので割愛。
AutomationProperties.AutomationId
Viewでちらっと出てきたのでメモ。
AutomationProperties.AutomationIdは、UIコントロールを一意に識別するためのIDのようです。
検索すると色々ヒットしましたが、Microsoft Docs以外の日本語解説が見つからず…
Microsoft Docsって少し理解してる状態で読まないと、理解できないイメージがあり敬遠しちゃう…Prism機能ではないので今回は置いときます。
<Button AutomationProperties.AutomationId="ItemsSelectButton" Command="{Binding SelectItemCommand}" Content="Select Item" /> <Button AutomationProperties.AutomationId="ItemsCancelButton" Command="{Binding CancelCommand}" Content="Cancel" />
29-InvokeCommandAction ○
UserControlのEventトリガでコマンドを実行するサンプル
実行
ListBoxが表示されており、選択したアイテム名が下部に表示される。
内容
InvokeCommandAction
View の InvokeCommandAction によって、ListBox の SelectionChanged 時に ViewModelのコマンドを実行している。
<ListBox ItemsSource="{Binding Items}"
SelectionMode="Single" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<prism:InvokeCommandAction Command="{Binding SelectedCommand}"
TriggerParameterPath="AddedItems" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
TriggerParameterPath で指定されている AddedItems は、SelectionChangedEventArgsクラスのプロパティで、選択された項目を含むリストが格納されている。
サンプルでは ListBox の SelectionMode が Single なので、AddedItems は長さ1の配列となるが、SelectionMode を Extended にして Shiftボタン+クリックなどで複数アイテムを選択すると、選択分のアイテム配列となる。
ViewModel の DelegateCommand、配列先頭の要素名を表示している。
// This command will be executed when the selection of the ListBox in the view changes.
SelectedCommand = new DelegateCommand<object[]>(x =>
{
if (x != null && x.Count() > 0)
SelectedItemText = x.FirstOrDefault().ToString();
});
最後に
全部で32項目もあったが『Prismは任せろ!』までの理解度にはならなかった。
学習開始前から、モジュールのDisposeについて知りたいと思っていたが、サンプルでは出てこなかった。
基本的な操作(子モジュールの配置、画面遷移、ポップアップ)については知れたので、何となくは使えそう。
出力しつつ勉強していこうと思う。
Qiitaのおかげで、Markdownの書き方を学べた。