Prism公式サンプルのメモ

2017/07/21 WPF::Prism
こちらのサンプルからコピー用のメモ
ブートストラッパーとシェルブートストラッパーとシェルの作成
リージョンリージョンの作成
カスタムリージョンアダプターStackPanel用のリージョンアダプターを作る
View DiscoveryView Discoveryによってviewを自動的に注入する。
View InjectionView Injectionを使って手動でviewを追加したり削除したりする。
View Activation/Deactivationviewを手動でアクティベートしたりディアクティベートする。
Modules with App.configApp.config ファイルを使用してモジュールをロードする。
Modules with Codeコードによってモジュールをロードする。
Modules with Directoryディレクトリ内のモジュールをロード。
Modules loded manuallyIModuleManagerを使って手動でモジュールをロードする。
ViewModelLocatorViewModelLocatorの利用
ViewModelLocator - Change ConventionViewModelLocatorのネーミング規約を変更する。
ViewModelLocator - Custom Registrations特定のviewを手動で登録する。
DelegateCommandDelegateCommand と DelegateCommand<T>を使う。
CompositeCommandsコンポジットコマンドを使って複数のコマンドを一つのコマンドとして呼び出す方法を学ぶ。(「全てを保存」ボタン)
IActiveAwareコマンドアクティブな場合にのみ実行されるようコマンド(CompositeCommands)をIActiveAwareにする
Event AggregatorIEventAggregatorを利用する
Event Aggregator - Filter Eventsイベント(events)に登録する際、受け取るイベントを絞り込む
RegionContextRegionContextを使ってネストしたリージョンにデータを渡す
Region Navigation基本的なリージョンナビゲーション(画面遷移)の実装方法
Navigation Callbackナビゲーションが完了した時に通知を受け取る
Navigation ParticipationINavigationAwareを使ってViewやViewModelを画面遷移に加える方法を学ぶ
既存Viewsへの遷移ナビゲーション時にviewのインスタンスをコントロールする(同じViewを再利用するか新規に作成するか)
Passing ParametersView/ViewModelから別のView/ViewModelにパラメータを渡す
Confirm/cancel NavigationIConfirmNavigationReqestインターフェイスを使って「確認」「取消」のナビゲーションを実装する
Viewの生存期間を制御するIRegionMemberLifetimeを使ってメモリーからviewsを自動的に取り除く
Navigation JournalNavigation Journalの使い方を学ぶ(「戻る」「進む」ボタン)
Interactivity - NotificationRequestInteractionRequestを使ってポップアップを表示する方法を学ぶ
Interactivity - ConfirmationRequestInteractionRequestを使って確認用ダイアログを表示する方法を学ぶ(「OK」「Cancel」ボタン)
Interactivity - Custom ContentInteractionRequestでダイアログに表示されるコンテンツをカスタマイズする
Interactivity - Custom RequestInteractionRequestで使うカスタムリクエストを作成する
Interactivity - InvokeCommandActionイベントに応じてコマンドを呼び出す。

1-BootstrapperShell

BootstrapperとShell
ブートストラッパーとシェルの作成。Prism Template Packのテンプレートを使えば既に作成済みの部分。
(Prism用語的にはShellですが、実際のファイル名はMainWindowです。)

2-Regions

リージョン
上の「1-BootstrapperShell」にregionを追加。
こちらもテンプレートで作成済み。

3-CustomRegions

Custom Region Adapter
StackPanel用のregion adapterを作る。
最初から用意されている3つのregion adapterで足りず自作する時。

4-ViewDiscovery

View Discovery
View Discoveryによってviewを自動的に注入する。
MainWindow.xaml.csのコンストラクタ内で
regionManager.RegisterViewWithRegion("ContentRegion",typeof(ViewA));

5-ViewInjection

View Injection
View Injectionを使って手動でviewを追加したり削除したりする。
sample0500.png

ボタンをクリックするとViewAが表示されます。
private void Button_Click(object sender, RoutedEventArgs e)
{
    var view = _container.Resolve<ViewA>();
    IRegion region = _regionManager.Regions["ContentRegion"];
    region.Add(view);
}

6-ViewActivationDeactivation

View Activation/Deactivation
viewを手動でアクティベートしたりディアクティベートする。
sample0600.png

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);
}

7-Modules

7-Modules - AppConfig

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>

7-Modules - Code

Modules with Code
コードによってモジュールをロードする。
Bootstrapper.cs内でカタログにモジュール追加。
protected override void ConfigureModuleCatalog()
{
    var catalog = (ModuleCatalog)ModuleCatalog;
    catalog.AddModule(typeof(ModuleAModule));
}
コードで追加するのはモジュール化のメリット無いように思えましたが、モジュール変更の際ソリューション丸ごとビルドし直す必要無くなりますね。

7-Modules - Directory

Modules with Directory
ディレクトリ内のモジュールをロード。
protected override IModuleCatalog CreateModuleCatalog()
{
  return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
}

7-Modules - LoadManual

Modules loded manually
IModuleManagerを使って手動でモジュールをロードする。
sample0700.png

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");
}

8-ViewModelLocator

ViewModelLocatorの利用
Viewと対応するViewModelを紐付けるのに使われている規約(convention)。
同一アセンブリ内で、ViewはViews配下のネームスペースを持つ、View ModelはViewModels配下のネームスペースを持つ。View Modelの名前はViewと同じ名前の後ろにViewModelが付く。
sample0800.png

規約通りのフォルダ名とネーミングにしておけば勝手に紐付けてくれる。

9-ChangeConvention

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が追加されたものを探す。

10-CustomRegistrations

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>();
}

11-UsingDelegateCommands

デリゲートコマンド
DelegateCommand と DelegateCommand<T>を使う。
sample1100.png

一番下の「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;
}
実際に使ってみました

12-UsingCompositeCommands

コンポジットコマンド
CompositeCommandsを使って複数のコマンドを一つのコマンドとして呼び出す方法を学ぶ。
sample1200.png

11のサンプルのボタンをタブ内に配置し、CompositeCommandsのボタンを付けた感じ。
一番上の「Save」ボタン(CompositeCommands)を押すと、全てのタブのボタン(DelegateCommand)が実行される(更新時刻が同時になってる)。
どれかのタブの「Can Execute」のチェックを外すと、CompositeCommandsは押せなくなる。サンプルのコードのどの部分で書かれているのかよく分からなかったので、ドキュメントを確認したら、CompositeCommands側で実装されていました。
CompositeCommandクラスはチャイルドコマンド(DelegateCommandインスタンス)のリストを保持します。CompositeCommandクラスのExecuteメソッドはチャイルドコマンドのExecuteメソッドを順番に呼び出すだけです。CanExecuteメソッドも同様にチャイルドコマンドのCanExecuteメソッドを呼び出しますが、チャイルドコマンドが一つでも実行できない場合はfalseを返します。言い換えればデフォルトでCompositeCommandはチャイルドコマンドが全て実行可能な場合にのみ実行できるようになっています。

13-IActiveAwareCommands

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;
}

14-UsingEventAggregator

イベントアグリゲーター
イベントアグリゲーターを利用する
モジュール間でイベントのpublish/subscribe機能を提供する。
(.NET Frameworkのeventsはモジュール内でのみ使い、モジュール間のやり取りはIEventAggregatorを使うこと。.NET Frameworkのeventsを使いunsubscribe忘れたりすると参照残ったままでガベージコレクトされずメモリリークに繋がることがある)
マルチキャストにも対応している(publisher:N、subscriber:N)。
詳しくはドキュメント参照。
sample1400.png

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);

15-FilteringEvents

イベントアグリゲーター - イベントのフィルター
イベント(events)に登録する際、受け取るイベントを絞り込む
イベントへのsubscribe(ModuleB側)
_ea.GetEvent<MessageSentEvent>().Subscribe(MessageReceived, ThreadOption.PublisherThread, false, (filter) => filter.Contains("Brian"));
sample1500.png

メッセージにBrianを付けないと一覧に表示されない。

16-RegionContext

リージョンコンテキスト
RegionContextを使ってネストしたリージョンにデータを渡す
上に表示されている一覧のどれかをクリックすると、下にその詳細が表示される。よくあるmaster/detailのパターン。
外枠のContentRegionはMainWindowで定義され、内枠のPersonDetailsRegionはモジュール内のPersonList.xamlで定義されている(Regionってモジュール側でも使えるんですね)。
ドキュメントはこちら
sample1600.png

親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;
}

17-BasicRegionNavigation

Region Navigation
基本的なリージョンナビゲーションの実装方法
view injectionやview discoveryを使わないURIベースのナビゲーション。Prism4.0から使えるようになった。
Viewを直接参照するかINavigateAsyncインターフェイスのRequestNavigateメソッドを使う。
ドキュメントはこちら
sample1700.png

ボタンによって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);
}
実際に使ってみました

18-NavigationCallback

ナビゲーションコールバック
ナビゲーションが完了した時に通知を受け取る

sample1800.png

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プロパティが操作中に発生した例外への参照を渡してくれる。
詳しくはこちらの下の方
実際に使ってみました

19-NavigationParticipation

Navigationへの参加
INavigationAwareを使ってViewやViewModelをナビゲーションに参加させる方法を学ぶ
sample1900.png

元々ボタンのみ表示。ボタンを押すとそれぞれの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側。
コメント部分は自信なし。詳しくはドキュメント参照。

20-NavigateToExistingViews

既存のViewsへのナビゲーション
ナビゲーション時にviewのインスタンスをコントロールする
sample2000.png

19のサンプルでは何度押しても同じタブでカウントアップしていましたが、3を超えると新たなタブが追加されます。
public bool IsNavigationTarget(NavigationContext navigationContext)
{
    return PageViews / 3 != 1;
}
IsNavigationTargetがfalseの時に新たなタブViewが追加されます。
詳しくはこちら

21-PassingParameters

パラメータを渡す
View/ViewModelから別のView/ViewModelにパラメータを渡す
sample2100.png


渡す側(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では使われていませんが、同様に使用可能です。
詳しくはこちら

22-ConfirmCancelNavigation

コンファーム/キャンセル ナビゲーション
IConfirmNavigationReqestインターフェイスを使って「確認」「取消」のナビゲーションを実装する
sample2200.png

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を追加したもの。
ドキュメントはこちら

23-RegionMemberLifetime

Viewの生存期間をコントロールする
IRegionMemberLifetimeを使ってメモリーからviewsを自動的に取り除く
sample2300.png

ViewBは次のボタンが押されても残るがViewAは消える。違いはViewAのViewModelがIRegionMemberLifetimeを実装してKeepAliveをfalseにしている。
public class ViewAViewModel : BindableBase, INavigationAware, IRegionMemberLifetime
{
    public ViewAViewModel()
    {
    }
    public bool KeepAlive
    {
        get
        {
            return false;
        }
    }
…
}

24-NavigationJournal

Navigation Journal
Navigation Journalの使い方を学ぶ
最初はボタンが押せない状態。
sample2400.png

どれかを選択すると詳細へ。
sample2410.png

戻るとボタンが押せるように。押すと同じ詳細へ。
sample2420.png


詳しくはこちら

25-NotificationRequest

インタラクティビティ - NotificationRequest
InteractionRequestを使ってポップアップを表示する方法を学ぶ。
オブザーバーパターンの一種だそうです。
sample2500.png

sample2510.png

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");
}

26-ConfirmationRequest

インタラクティビティ - ConfirmationRequest
InteractionRequestを使って確認用ダイアログを表示する方法を学ぶ
sample2600.png

sample2610.png

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");
}

27-CustomContent

インタラクティビティ - カスタムコンテント
InteractionRequestでダイアログに表示されるコンテンツをカスタマイズする
sample2700.png

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>
実際に使ってみました

28-CustomRequest

インタラクティビティ - Custom Request
InteractionRequestで使うカスタムリクエストを作成する
リストからの選択を促すダイアログ
sample2800.png

sample2810.png

29-InvokeCommandAction

インタラクティビティ - InvokeCommandAction
イベントに応じてコマンドを呼び出す。
sample2900.png

OK キャンセル 確認 その他