BootstrapperとShell
ブートストラッパーとシェルの作成。Prism Template Pack のテンプレートを使えば既に作成済みの部分。
(Prism用語的にはShellですが、実際のファイル名はMainWindowです。)
リージョン
上の「1-BootstrapperShell」にregionを追加。
こちらもテンプレートで作成済み。
View Discovery
View Discoveryによってviewを自動的に注入する。
MainWindow.xaml.csのコンストラクタ内で
regionManager.RegisterViewWithRegion("ContentRegion",typeof(ViewA));
View Injection
View Injectionを使って手動でviewを追加したり削除したりする。
ボタンをクリックするとViewAが表示されます。
private void Button_Click(object sender, RoutedEventArgs e)
{
var view = _container.Resolve<ViewA>();
IRegion region = _regionManager.Regions["ContentRegion"];
region.Add(view);
}
View Activation/Deactivation
viewを手動でアクティベートしたりディアクティベートする。
MainWindow_LoadedイベントでViewAとViewBは追加済み。
private void Button_Click(object sender, RoutedEventArgs e)
{
//activate view a
_region.Activate(_viewA);
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
//deactivate view a
_region.Deactivate(_viewA);
}
Modules with App.config
App.config ファイルを使用してモジュールをロードする。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf" />
</configSections>
<startup>
</startup>
<modules>
<module assemblyFile="ModuleA.dll" moduleType="ModuleA.ModuleAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleAModule" startupLoaded="True" />
</modules>
</configuration>
Modules with Code
コードによってモジュールをロードする。
Bootstrapper.cs内でカタログにモジュール追加。
protected override void ConfigureModuleCatalog()
{
var catalog = (ModuleCatalog)ModuleCatalog;
catalog.AddModule(typeof(ModuleAModule));
}
コードで追加するのはモジュール化のメリット無いように思えましたが、モジュール変更の際ソリューション丸ごとビルドし直す必要無くなりますね。
Modules with Directory
ディレクトリ内のモジュールをロード。
protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
}
Modules loded manually
IModuleManagerを使って手動でモジュールをロードする。
Bootstrapper.cs内でカタログに追加。
protected override void ConfigureModuleCatalog()
{
var moduleAType = typeof(ModuleAModule);
ModuleCatalog.AddModule(new ModuleInfo()
{
ModuleName = moduleAType.Name,
ModuleType = moduleAType.AssemblyQualifiedName,
InitializationMode = InitializationMode.OnDemand
});
}
ロードはMainWindowのクリックイベントで。
private void Button_Click(object sender, RoutedEventArgs e)
{
_moduleManager.LoadModule("ModuleAModule");
}
ViewModelLocatorの利用
Viewと対応するViewModelを紐付けるのに使われている規約(convention)。
同一アセンブリ内で、ViewはViews配下のネームスペースを持つ、View ModelはViewModels配下のネームスペースを持つ。View Modelの名前はViewと同じ名前の後ろにViewModelが付く。
規約通りのフォルダ名とネーミングにしておけば勝手に紐付けてくれる。
ViewModelLocator - Change Convention
ViewModelLocatorのネーミング規約を変更する。
Bootstrapper.cs内でConfigureViewModelLocatorを上書き。
protected override void ConfigureViewModelLocator()
{
base.ConfigureViewModelLocator();
ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
{
var viewName = viewType.FullName;
var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
var viewModelName = $"{viewName}ViewModel, {viewAssemblyName}";
return Type.GetType(viewModelName);
});
}
同一フォルダ内(ネームスペースが同じ)でクラス名にViewModelが追加されたものを探す。
ViewModelLocator - Custom Registrations
特定のviewを手動で登録する。
これもBootstrapper.cs内でConfigureViewModelLocatorを上書き。
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>();
}
デリゲートコマンド
DelegateCommand と DelegateCommand<T>
を使う。
一番下の「DelegateCommand Generic」ボタンのみXAMLから受け取ったテキストを表示。上3つはいずれも現在時刻を表示。ボタンを活性化/非活性化するbool値の監視方法の書き方が違うだけで動きは同じようです。
CanExecuteはboolを返すメソッド、IsEnabledはプロパティ。
送信ボタン等、入力フォームをまとめてチェックする場合はメソッドの方が使いやすそう。
パラメータを受け取るDelegateCommand Genericの方はRegion Navigation ではViewのパスを渡すのに使われています。
public MainWindowViewModel()
{
ExecuteDelegateCommand = new DelegateCommand(Execute, CanExecute);
DelegateCommandObservesProperty = new DelegateCommand(Execute, CanExecute).ObservesProperty(() => IsEnabled);
DelegateCommandObservesCanExecute = new DelegateCommand(Execute).ObservesCanExecute((vm) => IsEnabled);
ExecuteGenericDelegateCommand = new DelegateCommand<string>(ExecuteGeneric).ObservesCanExecute((vm) => IsEnabled);
}
private void Execute()
{
UpdateText = $"Updated: {DateTime.Now}";
}
private void ExecuteGeneric(string parameter)
{
UpdateText = parameter;
}
実際に使ってみました 。
コンポジットコマンド
CompositeCommandsを使って複数のコマンドを一つのコマンドとして呼び出す方法を学ぶ。
11のサンプルのボタンをタブ内に配置し、CompositeCommandsのボタンを付けた感じ。
一番上の「Save」ボタン(CompositeCommands)を押すと、全てのタブのボタン(DelegateCommand)が実行される(更新時刻が同時になってる)。
どれかのタブの「Can Execute」のチェックを外すと、CompositeCommandsは押せなくなる。サンプルのコードのどの部分で書かれているのかよく分からなかったので、ドキュメント を確認したら、CompositeCommands側で実装されていました。
CompositeCommandクラスはチャイルドコマンド(DelegateCommandインスタンス)のリストを保持します。CompositeCommandクラスのExecuteメソッドはチャイルドコマンドのExecuteメソッドを順番に呼び出すだけです。CanExecuteメソッドも同様にチャイルドコマンドのCanExecuteメソッドを呼び出しますが、チャイルドコマンドが一つでも実行できない場合はfalseを返します。言い換えればデフォルトでCompositeCommandはチャイルドコマンドが全て実行可能な場合にのみ実行できるようになっています。
IActiveAware Commands
アクティブなコマンドのみ呼び出すようにcommands(CompositeCommands)をIActiveAwareにする
実行後の見た目は上と全く同じ。今度は全てのタブの「Can Execute」にチェックが付いていても現在表示されているタブのコマンドだけが実行される(タブの切り替えによってコマンドのIsActiveが切り替わるためアクティブでないタブのコマンドは実行されない)。
ViewModelでIActiveAwareを実装
public class TabViewModel : BindableBase, IActiveAware
{
…
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;
}
イベントアグリゲーター
イベントアグリゲーターを利用する
モジュール間でイベントのpublish/subscribe機能を提供する。
(.NET Frameworkのeventsはモジュール内でのみ使い、モジュール間のやり取りはIEventAggregatorを使うこと。.NET Frameworkのeventsを使いunsubscribe忘れたりすると参照残ったままでガベージコレクトされずメモリリークに繋がることがある)
マルチキャストにも対応している(publisher:N、subscriber:N)。
詳しくはドキュメント 参照。
publisherとsubscriberを結びつけるのはPubSubEventクラス。
using Prism.Events;
namespace UsingEventAggregator.Core
{
public class MessageSentEvent : PubSubEvent<string>
{
}
}
イベントのpublish(ModuleA側)
_ea.GetEvent<MessageSentEvent>().Publish(Message);
イベントへのsubscribe(ModuleB側)
_ea.GetEvent<MessageSentEvent>().Subscribe(MessageReceived);
イベントアグリゲーター - イベントのフィルター
イベント(events)に登録する際、受け取るイベントを絞り込む
イベントへのsubscribe(ModuleB側)
_ea.GetEvent<MessageSentEvent>().Subscribe(MessageReceived, ThreadOption.PublisherThread, false, (filter) => filter.Contains("Brian"));
メッセージにBrianを付けないと一覧に表示されない。
リージョンコンテキスト
RegionContextを使ってネストしたリージョンにデータを渡す
上に表示されている一覧のどれかをクリックすると、下にその詳細が表示される。よくあるmaster/detailのパターン。
外枠のContentRegionはMainWindowで定義され、内枠のPersonDetailsRegionはモジュール内のPersonList.xamlで定義されている(Regionってモジュール側でも使えるんですね)。
ドキュメントはこちら
親ViewのPersonListからそのリージョン内の子Viewにデータを渡すのにRegionContextを使う。
渡す側(PersonList.xaml)
<ListBox x:Name="_listOfPeople" ItemsSource="{Binding People}"/>
<ContentControl Grid.Row="1" Margin="10"
prism:RegionManager.RegionName="PersonDetailsRegion"
prism:RegionManager.RegionContext="{Binding SelectedItem, ElementName=_listOfPeople}"/>
受け取る側(PersonDetail.xaml.cs)
private void PersonDetail_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var context = (ObservableObject<object>)sender;
var selectedPerson = (Person)context.Value;
(DataContext as PersonDetailViewModel).SelectedPerson = selectedPerson;
}
Region Navigation
基本的なリージョンナビゲーションの実装方法
view injectionやview discoveryを使わないURIベースのナビゲーション。Prism4.0から使えるようになった。
Viewを直接参照するかINavigateAsyncインターフェイスのRequestNavigateメソッドを使う。
ドキュメントはこちら
ボタンによってViewを切り替える。どちらのViewも同じモジュール内。
MainWindow.xaml
<DockPanel LastChildFill="True">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Margin="5" >
<Button Command="{Binding NavigateCommand}" CommandParameter="ViewA" Margin="5">Navigate to View A</Button>
<Button Command="{Binding NavigateCommand}" CommandParameter="ViewB" Margin="5">Navigate to View B</Button>
</StackPanel>
<ContentControl prism:RegionManager.RegionName="ContentRegion" Margin="5" />
</DockPanel>
MainWindowViewModel.cs
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);
}
実際に使ってみました 。
ナビゲーションコールバック
ナビゲーションが完了した時に通知を受け取る
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate("ContentRegion", navigatePath, NavigationComplete);
}
private void NavigationComplete(NavigationResult result)
{
System.Windows.MessageBox.Show(String.Format("Navigation to {0} complete. ", result.Context.Uri));
}
RequestNavigateメソッドにナビゲーションが完了した時に呼ばれるコールバックメソッドあるいはデリゲートを指定できる。
NavigationResultクラスにはナビゲーション操作に関するプロパティが定義されている。操作が成功した場合はResultプロパティがtrueになる。Resultがfalseの場合Errorプロパティが操作中に発生した例外への参照を渡してくれる。
詳しくはこちらの下の方
実際に使ってみました 。
Navigationへの参加
INavigationAwareを使ってViewやViewModelをナビゲーションに参加させる方法を学ぶ
元々ボタンのみ表示。ボタンを押すとそれぞれのViewが表示され、押すたびにカウントアップ。画像を見てもよく分かりませんが、記憶の手がかりとして。
INavigationAware
public interface INavigationAware
{
// ViewやViewModelがリクエストに応えられるかどうかを示す
// 同じViewやViewModelを使いまわす時に利用
// 20-NavigateToExistingViews参照
bool IsNavigationTarget(NavigationContext navigationContext);
// ナビゲーション前に実行される
// 状態やデータの保存等、Viewが閉じられても大丈夫なようにしておくのに利用
void OnNavigatedTo(NavigationContext navigationContext);
// ナビゲーション完了後に実行される
// 新しく表示されるViewにデータを渡すのに利用
// 21-PassingParameters参照
void OnNavigatedFrom(NavigationContext navigationContext);
}
ViewあるいはViewModelで実装。ViewModelの方が一般的。サンプルもViewModel側。
コメント部分は自信なし。詳しくはドキュメント 参照。
既存のViewsへのナビゲーション
ナビゲーション時にviewのインスタンスをコントロールする
19のサンプルでは何度押しても同じタブでカウントアップしていましたが、3を超えると新たなタブが追加されます。
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return PageViews / 3 != 1;
}
IsNavigationTargetがfalseの時に新たなタブViewが追加されます。
詳しくはこちら 。
パラメータを渡す
View/ViewModelから別のView/ViewModelにパラメータを渡す
渡す側(PersonListViewModel.cs)
private void PersonSelected(Person person)
{
var parameters = new NavigationParameters();
// パラメータとしてIDだけでなくオブジェクト丸ごと渡すこともできる
parameters.Add("person", person);
if (person != null)
_regionManager.RequestNavigate("PersonDetailsRegion", "PersonDetail", parameters);
}
受け取る側(PersonDetailViewModel.cs)
public void OnNavigatedTo(NavigationContext navigationContext)
{
var person = navigationContext.Parameters["person"] as Person;
if (person != null)
SelectedPerson = person;
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
// 一覧で選択されているのと同じLastNameのViewだとtrueになって再利用
// 同じデータ(Person)を複数開かない→複数のPersonを同時に編集させる時に便利
// IsNavigationTargetと言うネーミングがやっと理解できた…
var person = navigationContext.Parameters["person"] as Person;
if (person != null)
return SelectedPerson != null && SelectedPerson.LastName == person.LastName;
else
return true;
}
サンプルのOnNavigatedFromでは使われていませんが、同様に使用可能です。
詳しくはこちら 。
コンファーム/キャンセル ナビゲーション
IConfirmNavigationReqestインターフェイスを使って「確認」「取消」のナビゲーションを実装する
public class ViewAViewModel : BindableBase, IConfirmNavigationRequest
{
public 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);
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
…
}
IConfirmNavigationRequestはINavigationAwareにConfirmNavigationRequestを追加したもの。
ドキュメントはこちら 。
Viewの生存期間をコントロールする
IRegionMemberLifetimeを使ってメモリーからviewsを自動的に取り除く
ViewBは次のボタンが押されても残るがViewAは消える。違いはViewAのViewModelがIRegionMemberLifetimeを実装してKeepAliveをfalseにしている。
public class ViewAViewModel : BindableBase, INavigationAware, IRegionMemberLifetime
{
public ViewAViewModel()
{
}
public bool KeepAlive
{
get
{
return false;
}
}
…
}
Navigation Journal
Navigation Journalの使い方を学ぶ
最初はボタンが押せない状態。
どれかを選択すると詳細へ。
戻るとボタンが押せるように。押すと同じ詳細へ。
詳しくはこちら 。
インタラクティビティ - NotificationRequest
InteractionRequestを使ってポップアップを表示する方法を学ぶ。
オブザーバーパターン の一種だそうです。
Prism.Interactivity.InteractionRequestのInteractionRequest利用
public MainWindowViewModel()
{
NotificationRequest = new InteractionRequest<INotification>();
NotificationCommand = new DelegateCommand(RaiseNotification);
}
void RaiseNotification()
{
NotificationRequest.Raise(new Notification { Content = "Notification Message", Title = "Notification" }, r => Title = "Notified");
}
インタラクティビティ - ConfirmationRequest
InteractionRequestを使って確認用ダイアログを表示する方法を学ぶ
public MainWindowViewModel()
{
…
ConfirmationRequest = new InteractionRequest<IConfirmation>();
ConfirmationCommand = new DelegateCommand(RaiseConfirmation);
}
…
void RaiseConfirmation()
{
ConfirmationRequest.Raise(new Confirmation {
Title = "Confirmation",
Content = "Confirmation Message" },
r => Title = r.Confirmed ? "Confirmed" : "Not Confirmed");
}
インタラクティビティ - カスタムコンテント
InteractionRequestでダイアログに表示されるコンテンツをカスタマイズする
ViewModel側は25とほぼ同じ。
ポップアップ画面のUserControlを作って、
<StackPanel>
<TextBlock Text="{Binding Title}" FontSize="24" HorizontalAlignment="Center" />
<TextBlock Text="{Binding Content}" Margin="10"/>
<Button Margin="25" Click="Button_Click">Accept</Button>
</StackPanel>
MainWindowから呼び出す
<prism:InteractionRequestTrigger SourceObject="{Binding CustomPopupRequest}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True">
<prism:PopupWindowAction.WindowContent>
<views:CustomPopupView />
</prism:PopupWindowAction.WindowContent>
</prism:PopupWindowAction>
</prism:InteractionRequestTrigger>
実際に使ってみました 。
インタラクティビティ - Custom Request
InteractionRequestで使うカスタムリクエストを作成する
リストからの選択を促すダイアログ
インタラクティビティ - InvokeCommandAction
イベントに応じてコマンドを呼び出す。