Command Pattern

2017/09/30 WPF::Prism
コマンドパターン
これは古典的なパターン。検索すればいくらでも出てきます。

処理(振る舞い)と処理に必要なパラメータをまとめて一つのオブジェクトにする。

コマンドを呼び出す側と処理する側を切り離すことができる。

魔法使いクラスを作成する際、技・術を魔法使いのメソッドにしたりせず、技のインターフェイス(IArt)をつくり、魔法使いクラスはそのリスト(List<IArt> Arts)を持たせておく感じ?

PrismではCompositeCommandで使われています。
CompositeCommandはICommandのリスト(RegisteredCommands)を保持し、Executeメソッドで順に実行します。

親Viewに設置されたCompositeCommandのRegisteredCommandsプロパティに、子ViewからRegisterCommandメソッドでコマンドを登録します。
Command Pattern

※イメージです。実際とは異なる場合があります…

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

Region Navigation

View-Switching Navigationの続きですが、2つめのWPFにしては複雑すぎるものに手を出してしまった感じ。
コードを読んでPrism6に書き換えるのは今の私の知識ではどう考えても無理。
デザインもxamlの知識が無いので躓いた箇所は後回し。
Prism6公式のサンプルを参照しながら元ネタに近いものを仕上げる方針に変えました。
左側メインメニューにモジュールを切り替えるボタンを追加するのに、Region Navigationを使ってみました。

メニュー用View追加

左側のRegion(MainNavigationRegion)に表示するメニューボタン用Viewを追加します。
テンプレート:Prism UserControl
ファイル名:CalendarNavigationItemView.xaml
viewswich130.png

Viewのテスト用のTextBlockを追加。
CalendarModule.csの修正
public void Initialize()
{
    //_regionManager.RegisterViewWithRegion("MainContentRegion", typeof(Views.CalendarView));
    _regionManager.RegisterViewWithRegion(RegionNames.MainNavigationRegion, typeof(CalendarNavigationItemView));
}
この時点では未だView-Switching Navigationのソースに近い形でいくつもりだったので、CalendarViewをコメントアウトし、CalendarNavigationItemViewを追加。
実行してMainNavigationRegionに表示されていることを確認。
viewswich140.png

ナビゲーション用ボタン作成

サンプルからCalendarNavigationItemView.xamlのソースをそのままコピー。
viewswich150.png

MainWindow.xamlの時と同じくエラーが出るので、ネームスペースとプロジェクトの参照を追加。
viewswich160.png

エラーの出るButton_Clickと不要なStyle属性削除して実行して確認。
Material Designを適用し忘れたら下記のような感じ。
ボタンを揃えるにはモジュール追加する毎にまったく同じことを繰り返すか、ユーザーコントロールを作るか、モジュール用のテンプレートを充実させていくかする必要がありそう。
viewswich210.png

ナビゲーション機能の追加

エラーを潰しながら組み込み始めたのですが、System.ComponentModel.Compositionが見つからずNugetで追加する必要があることが分かり方向転換。
(テンプレートに含まれていないということは今の主流から外れそうなので)
Prism6のサンプルプロジェクトの中で一番近そうな17-BasicRegionNavigationを真似することにしました。
<UserControl x:Class="ViewSwitchingNavigation.Calendar.Views.CalendarNavigationItemView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"             
             prism:ViewModelLocator.AutoWireViewModel="True"
             xmlns:controls="clr-namespace:ViewSwitchingNavigation.Controls;assembly=ViewSwitchingNavigation.Controls"
             >
    <Grid x:Name="LayoutRoot">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Button Command="{Binding NavigateCommand}" CommandParameter="CalendarView" Margin="5">calendar</Button>
        <!--<RadioButton x:Name="NavigateToCalendarRadioButton" GroupName="MainNavigation" IsChecked="{Binding NavigateCommand}" CommandParameter="CalendarView" AutomationProperties.AutomationId="CalendarRadioButton">Calendar</RadioButton>-->
        <controls:InfoTipToggleButton Grid.Column="1">
            <controls:InfoTipToggleButton.Popup>
                <Popup>
                    <Border BorderBrush="Black" BorderThickness="2">
                        <StackPanel MinWidth="100" MinHeight="24" MaxWidth="400" Background="White">
                            <TextBlock TextWrapping="Wrap">This button demonstrates navigation to a view that that supports cross-navigation to another area.</TextBlock>
                        </StackPanel>
                    </Border>
                </Popup>
            </controls:InfoTipToggleButton.Popup>
        </controls:InfoTipToggleButton>
    </Grid>
</UserControl>
RadioButtonに Command="Binding NavigateCommand" を追加する方法が分からなかったので、Prism6のサンプルにあわせボタンに変更。現在アクティブなボタンの色を変えるのは一通り動いた後余裕があれば。
ボタンのxamlをモジュール側で記述するように変えたので、NavigateCommandをどこに記述するか悩み最初はViewSwitchingNavigation.CalendarにViewModelを追加して動作確認できましたが、同じことを他のモジュールでも行うのが面倒なのと、上記のxamlがNavigateCommand追加前でもエラーにならなかったので親プロジェクトの既存のViewModelに移動したところ、問題なく動きました。
viewswich220.png

カタログにモジュール追加する順序を変えてみた

viewswich230.png

保証されているかどうか確認はしていませんが、上下を変えたら変わり、戻したら戻りました。

View-Switching Navigation

Prism5.0のQuickstarts View-Switching NavigationをVS2017でMaterial Design In XAML Toolkitを使って最後までたどり着けるかな?

ソリューション&親プロジェクト作成

  • テンプレート:Prism Unity App(WPF)(QuickstartsではMEFが使われてました…。ここからUnity版を選んでダウンロードしたはずなのですが。とりあえずこのままUnityで続けます)
  • プロジェクト名:ViewSwitchingNavigation
※Prismのテンプレートが見つからない場合は、Hello World(2017版)を参照してテンプレートをインストールしてください。
viewswich010.png

共有コード用プロジェクト追加

  • テンプレート:通常のクラスライブラリ
  • プロジェクト名:ViewSwitchingNavigation.Infrastructure
viewswich020.png

ユーザーコントロール用プロジェクト追加

  • テンプレート:WPFユーザーコントロールライブラリ(.NET Framework)…なんと「Windowsクラシック デスクトップ」の下に。始めたばかりだと言うのにクラシック扱いされてます。
  • プロジェクト名:ViewSwitchingNavigation.Controls
viewswich025.png

モジュール側プロジェクト追加

  • テンプレート:Prism Module(WPF)
  • プロジェクト名:ViewSwitchingNavigation.Calendar,ViewSwitchingNavigation.Contacts,ViewSwitchingNavigation.Emailの3つ(実際に仕事で使う場合は一つだけ作ってテンプレート化した方が楽でしょう。練習兼ねて3回同じこと繰り返します(まだそのつもり段階ですが)
viewswich030.png

NuGetでパッケージ追加

パッケージ管理画面起動

viewswich035.png

MaterialDesignThemesを追加

XAMLのViewを追加しそうなプロジェクトにはチェックを入れる。
viewswich036.png

Prism関連を追加

不要かもしれませんが、ついでに。
viewswich037.png

モジュール側View追加

ViewSwitchingNavigation.CalendarプロジェクトのViewsフォルダにCalendarView.xamlを追加
Prism UserControlのテンプレートを使うとxamlにPrism関連のネームスペースが追加されています。
viewswich040.png

親プロジェクトから他のプロジェクトへ参照追加

ViewSwitchingNavigationプロジェクトの「参照」を右クリックして「参照の追加」を選択。
viewswich060.png

プロジェクトの下のソリューションを選択して一応全てにチェックを入れる(自信なし)。
viewswich050.png

Hello Woldのおさらい

Hello World(2017版)のViewSwitchingNavigation.Calendar版。
これまで順調に進んでいることを確認。
viewswich070.png

親プロジェクトのMainWindow.xamlを修正

タイトル追加、MainNavigationRegionを追加。
ユーザーコントロール(カスタムコントロール?まだ違いが分かりません)が使われていてエラーになる部分はカット。
viewswich080.png

ユーザーコントロールの追加

ViewSwitchingNavigation.ControlsプロジェクトにユーザーコントロールInfoTipToggleButton.xamlを追加。
追加→既存項目でダウンロードしたものそのまま移植するかコードをコピペ。
新規追加してコードを追加する場合は赤枠あたりに注意。InfoTipToggleButton.xaml.csファイルの修正も忘れずに。
viewswich090.png

親プロジェクトのMainWindow.xamlの上でカットした部分を追加
viewswich100.png

実行してInfoTipToggleButtonの動作確認。
viewswich110.png

Material Designを組み込む

InfoTipToggleButtonが動かない…。画像をButtonに変えたらボタンのクリックイベントが先に走る…。状態が取れない…。標準のボタンでいいような気がして今は無視。
viewswich120.png

親プロジェクトのApp.xaml(一部)色の指定はこちらで。
<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.DeepPurple.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Accent/MaterialDesignColor.Lime.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>
親プロジェクトのMainWindow.xaml(ボタン動かないバージョン)
<Window x:Class="ViewSwitchingNavigation.Views.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"
        mc:Ignorable="d"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
        xmlns:controls="clr-namespace:ViewSwitchingNavigation.Controls;assembly=ViewSwitchingNavigation.Controls"
        TextElement.Foreground="{DynamicResource MaterialDesignBody}"
        Background="{DynamicResource MaterialDesignPaper}"
        TextElement.FontWeight="Medium"
        TextElement.FontSize="14"
        FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
        Title="{Binding Title}"
        d:DesignHeight="300" d:DesignWidth="400">

    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Button.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Shadows.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ToggleButton.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <DockPanel>
        <materialDesign:ColorZone Padding="16" materialDesign:ShadowAssist.ShadowDepth="Depth2"
                                    Mode="PrimaryMid" DockPanel.Dock="Top">
            <DockPanel>
                <TextBlock HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="22">View-Switching Navigation</TextBlock>
                <controls:InfoTipToggleButton DockPanel.Dock="Right">
                    <controls:InfoTipToggleButton.Popup>
                        <Popup>
                            <StackPanel MinWidth="100" MinHeight="24" MaxWidth="500" Background="White">
                                <TextBlock TextWrapping="Wrap" Style="{StaticResource MaterialDesignBody2TextBlock}" Padding="6">This Navigation Quickstart demonstrates navigation within Prism's Regions to show new views, move between existing views, and how to pass context to views during navigaton.</TextBlock>
                            </StackPanel>
                        </Popup>
                    </controls:InfoTipToggleButton.Popup>
                </controls:InfoTipToggleButton>
            </DockPanel>
        </materialDesign:ColorZone>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>

            <Border Grid.Column="0" Grid.Row="2" MinWidth="250" Margin="5,0,0,5">
                <ItemsControl x:Name="NavigationItemsControl" prism:RegionManager.RegionName="MainNavigationRegion" Grid.Column="0" Margin="5" Padding="5" />
            </Border>
            <ContentControl prism:RegionManager.RegionName="MainContentRegion" 
                Grid.Column="1" Grid.Row="2" Margin="5,0,5,5" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>

        </Grid>
    </DockPanel>
</Window>

Application Controller Pattern

2017/06/21 WPF::Prism
"アプリケーション コントローラ パターン"で検索したらMSDNしかヒットしない。
"Application Controller Pattern"だとJavaやPHPもヒットする。
マイクロソフトのオフィシャルな説明はこちら

MVCのC(Controller)みたいなもだと思ったら、少し違うとのこと。このあたりのことでしょうか。MVC間の調整も行うコントローラの親玉みたいな感じ?

application controllerの仕事

  • viewのインスタンス作成
  • UI上の適当なコンテナへの配置
  • 同じコンテナ上のviewの切り替え
  • viewやviewモデル間のコミュニケーションの調整
パターン名は「application」controllerだが、applicationの一部と紐付けられることも多く、一つのapplicationに複数のapplication controllerが存在しうる。

Prismを使ったアプリケーションでのコントローラ実装例
OK キャンセル 確認 その他