PrismWPFSample(1)モジュール追加方法

Prismを使用したWPFアプリケーション開発で役に立つと思われる項目を一つのアプリケーションにまとめたものを作りました。モジュールの追加方法、タブコントロールへの組み込み、モジュール間でのデータ共有方法等について書いています。

動作環境:Win10, Visual Studio Community 2017, Prism V7.1.0.431, .NET4.5.2, Prism Template Pack, TraceListeners, WPFLocalizeExtension, OxyPlot

アプリの外観はこんな感じです。

f:id:feynman911:20190625213109j:plain

今回は、アプリ全体の構成をなしているPrismの使い方についてです。
Prism Template Pack がないとモジュールを追加するのが大変なので、是非機能拡張からインストールしておいてください。

ソリューションの説明

ソリューションの構成は次の図のようになっています。

f:id:feynman911:20190626213024j:plain

CommonModels にはアプリで共有するデータ&機能をまとめています。

  • 言語化の為のファイル Resources.resx
  • アプリで共有する設定 Settings.settings
  • 共有モデルの CModel

各モデルが参照するオブジェクトは CommonModels のみです。
CommonModels に記述した共有クラス CModel はモジュールでインスタンス化することなく、Prism にシングルトンでインスタンス化して各モジュールの ViewModel の引数として渡してもらいます。
各モジュールのDLLはビルド時に PrismWPFSample の実行フォルダーの下の Modules フォルダーに自動的にコピーされるようにビルド後イベントのコマンドラインを記述しておきます。

モジュールの組み込み方法 App.xaml.cs

Prism は登録したViewを、Region 指定したメインアプリのエリアに自動で組み込んでくれます。
モジュールを一つずつ指定するのは面倒なので、特定のフォルダーに入れてあるDLLをすべて読み込む設定で十分ではないでしょうか。
という事で、メインアプリの App.xaml.cs に下記のような設定を行い、実行ファイルがあるフォルダ下のModulesフォルダーの中のDLLを読み込んでもらいます。

protected override IModuleCatalog CreateModuleCatalog()
{
    //Modulesフォルダーにあるモジュールを読み込む。
    //読み込まれる先はモジュール側に書かれたRegionNameの場所
    return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
}


次のように、共有したいクラスを登録すると、ViewModel の引数として、モジュールで同じインスタンスを受け取ることができます。

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    //アプリ全体で共有したいオブジェクトをシングルトンで生成する。
    //モジュール側では ViewModel のコンストラクターの引数で受け取る
    containerRegistry.RegisterSingleton<CommonModels.CModel>();
}

  

メインアプリの View

View にモジュールを読み込む ContentRegion を TabControl に設定しています。
読み込まれた Module は TabControl に TabItem として追加されていきます。 

<TabControl prism:RegionManager.RegionName="ContentRegion" />

これだけでは TabItem の Header に何も表示されない為、モジュールの ViewModel にある Title プロパティーを TabItem の Header にバインドするためのスタイルを xaml に追加しておきます。

<Window.Resources>
    <Style TargetType="TabItem">
        <Setter Property="Header" Value="{Binding DataContext.Title}" />
    </Style>
</Window.Resources>

  

M2Module.cs

モジュール側は PrismModule ( WPF ) のテンプレートで自動生成して ***Module.csに次のような行を追加します。

public void OnInitialized(IContainerProvider containerProvider)
{
    var regionManager = containerProvider.Resolve<IRegionManager>();
    regionManager.RegisterViewWithRegion("ContentRegion", typeof(M2));
}

M2はこのモジュールの View のクラス名です。この View が Main アプリの ContentRegion に読み込まれます。

Module は、Build後に「Modules」フォルダーへ下記の設定でコピーします。
xcopy "$(TargetDir)$(TargetName)*$(TargetExt)" "$(SolutionDir)$(SolutionName)\$(OutDir)Modules" /Y /S /I

モジュールの ViewModel コンストラクター

モジュール間で共有されるオブジェクトはモジュールの ViewModel のコンストラクターの引数で渡してもらえるので、引数付きのコンストラクターを記述しておきます。イベントアグリゲータも渡してもらえるので、追加して使用する事ができます。
引数無しのコンストラクターも Blend ( Xaml Designer ) の為に残しておきます。
View と ViewModel の関連付けはPrismの Auto 機能に任せましょう。くれぐれも自分で ViewModel のインスタンスは作らないようにします。
ですが、そのままでは Xaml Designer での GUI を使ってのバインド設定ができないので、View を表示している時の書式メニューにある、「デザイン時のDataContextの設定」を行って、ViewModel がGUIから見えるようにします。

public ViewAViewModel() { }
public ViewAViewModel(CModel cModel, IEventAggregator ea)
{
    MyCModel = cModel;
    _ea = ea;

    //モデルの生成
    MyViewAModel = new ViewAModel(myCommonData, _ea);
    //イベントの購読 (言語の変更)
     ea.GetEvent<LanguageChangeEvent>().Subscribe(ChangedLang);

    //TabItemのHeaderになる言葉を多言語設定の為Resoucesから取り出す
    Title = CommonData.GetLocalizedValue<string>("TITLEM2");
}

IEventAggregator _ea;

private CModel myCModel;
public CModel MyCModel
{
    get { return myCModel; }
    set { SetProperty(ref myCModel, value); }
}

イベントアグリゲータ

viewModel のコンストラクターで受け取った IEventAggregator を使用して、モジュール間で通信を行う事ができます。

受信側は

    ea.GetEvent<LanguageChangeEvent>().Subscribe(ChangedLang);

の様に書くことで、LanguageChangeEvent を受信した時に ChangedLang メソッドが実行される様に設定できます。
イベントは CommonModels の SentEvent.cs に定義されています。

    //言語設定が変わったことの通知
    //XAMLに{lex:Loc MESSAGE}で埋め込まれた言葉は即変わるが
    //codeで設定されたところは変わらないので、このイベントで再設定する
    public class LanguageChangeEvent : PubSubEvent
    {    }

各モジュールの ViewModel には ChangedLang を書いておき、言語切り替えが行われるようにします。

    /// <summary>
    /// 言語設定が変わった時の処理
    /// </summary>
    public void ChangedLang()
    {
        Title = CModel.GetLocalizedValue<string>("TITLEM9");
        HeaderMenu1 = CModel.GetLocalizedValue<string>("M9MENU1");
        HeaderMenu2 = CModel.GetLocalizedValue<string>("M9MENU2");
    }

発行側は

    _ea.GetEvent<LanguageChangeEvent>().Publish();

となります。
  

モジュール組み込み順番 M2.xaml.cs

タブコントロール等複数のモジュールを読み込む時の順番は、View に以下の様に

[Prism.Regions.ViewSortHint("1000")]
public partial class M2 : UserControl
{ ・・・

と Attribute を付けることでコントロール可能です。

モジュールの遷移

ViewModelのコンストラクターでIRegionManagerを受け取れるので、_regionManager.RequestNavigate("ContentRegion", ”M2");
の様にして画面遷移ができます。
ViewModelを
public class M2ViewModel : BindableBase, INavigationAware
の様にINavigationAwareを付けることで、遷移の対象となるか、遷移の前後での処理を記述する事ができます。

public void OnNavigatedTo(NavigationContext navigationContext)
{
    //ナビゲーションで遷移して来た時の処理
}

public bool IsNavigationTarget(NavigationContext navigationContext)
{
    //ナビゲーションの対象となるかどうか
    return true;
}

public void OnNavigatedFrom(NavigationContext navigationContext)
{
    //ナビゲーションで移動する前の処理
}

忘れやすい事

  • モジュールを修正した時にはリビルドして DLL を Modules フォルダーにコピーしないと反映されない。
  • モジュールで参照されているライブラリーはメインアプリでも参照するか、メインアプリのフォルダーにコピーする。


作成したサンプルは次の場所に置いてあります。
github.com


次回は、2. モジュールから MenuItem を追加する方法 について記述したいと思います。