初めてのWPF 6日目

2019/01/16 WPF::Prism
MenuServiceを作成し、ナビゲーション用Viewの重複を無くします。
見た目、機能は5日目とまったく変わりません。
リファクタリング版です。

TreasurerHelper.Infrastructureを追加

通常のクラスライブラリテンプレートを使用。
メニューモジュールを作る、という方法も考えられますが、Viewの重複を省きたい、という単純な要件なので、共通機能の一つとして作成。

RegionNames.cs追加

MenuServiceとはまったく関係ありませんが、しょうもないTYPOで時間を潰さないように。
MainNavigationRegionが無くなり一つだけになりますが、また増える可能性もあるので。
public static class RegionNames
{
    public const String ContentRegion = "ContentRegion";
}

MenuItem.cs

最低限の項目。サブメニュー追加したり、ロール認証使う場合はここに追加。
public class MenuItem
{
    public string Title { get; set; }
    public string IconName { get; set; }
    public string Description { get; set; }
    public string NavigatePath { get; set; }
}

IMenuService.cs

public interface IMenuService
{
    List<MenuItem> GetMainMenuItems();
    void AddMainMenuItem(MenuItem menuItem);
}

MenuService.cs

public class MenuService : IMenuService
{
    public MenuService()
    {
        MainMenuItems = new List<MenuItem>();
    }

    List<MenuItem> MainMenuItems { get; set; }

    public List<MenuItem> GetMainMenuItems()
    {
        return MainMenuItems;
    }

    public void AddMainMenuItem(MenuItem menuItem)
    {
        MainMenuItems.Add(menuItem);
    }
}
メニューサービスは完成。後は親アプリとモジュール間でサービスを共有させる方法をどうするか。

モジュール側からMenuServiceにアクセス

MenuServiceに登録する場所はドキュメントにあります。実際のサンプルコードはありませんが。
このあたりから感を頼りにいてまえコーディングです。
containerProviderがIRegionManagerをResolveできるのならIMenuServiceもResolveできるでは?と言う感じ(^^;)。
NavigatePathはモジュール外から呼ばれることになるので元のCashCalculatorではまずいのは分かるけど、よく分からないのでとりあえずフルネーム(CashCalculator.Views.CashCalculator)にしてみた。
public void OnInitialized(IContainerProvider containerProvider)
{
    var regionManager = containerProvider.Resolve<IRegionManager>();
    regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(Views.CashCalculator));

    var menuService = containerProvider.Resolve<IMenuService>();
    menuService.AddMainMenuItem(new MenuItem { Title = "現金計算", IconName = "Calculator", Description = "紙幣・硬貨毎の枚数入力", NavigatePath = "CashCalculator.Views.CashCalculator" });
}

モジュール側がIMenuServiceをResolveできるように親側を何とかする

親側がIMenuServiceを関知しなかったらモジュール側がいくらResolveしようとしても無理なはず。どこかに登録する必要あるはず。
MainWindow.xaml.csは無い。MainWindowViewModel.csかApp.xaml.csの2択のはず。
なんとなく元Bootstrapper.csのApp.xaml.csっぽい。
App.xaml.csを見ると中身の無いRegisterTypesが用意されています。
containerRegistryでポチするとインテリセンスでRegisterが出てきたので設定。
(実は動かないバージョン)
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.Register<IMenuService, MenuService>();
}

MainWindow.xamlにデータを渡す

ViewModel側

public DelegateCommand<string> NavigateCommand { get; private set; }
// 上の真似をしてViewに渡すデータの器を用意
public List<MenuItem> MainMenuItems { get; set; }

// IRegionManagerがパラメータとして受け取られていたので、真似してIMenuServiceを追加。エラー出ず!
public MainWindowViewModel(IRegionManager regionManager, IMenuService menuService)
{
    _regionManager = regionManager;

    // サービスからデータ取得
    MainMenuItems = menuService.GetMainMenuItems();
    NavigateCommand = new DelegateCommand<string>(Navigate);
}

View側

モジュール毎に書いていたボタンをテンプレート化する。
(ボタンが動作しないバージョン)
<ItemsControl ItemsSource="{Binding MainMenuItems}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button HorizontalAlignment="Left"
                  Style="{DynamicResource MaterialDesignFlatButton}"
                  Command="{Binding NavigateCommand}" CommandParameter="{Binding NavigatePath}">
                <StackPanel Orientation="Horizontal">
                    <materialDesign:PackIcon x:Name="PackIcon" Kind="{Binding IconName}" />
                    <TextBlock Margin="6 0 0 0" Style="{StaticResource MaterialDesignBody1TextBlock}"
               Text="{Binding Title}" />
                </StackPanel>
            </Button>
        </DataTemplate>
     </ItemsControl.ItemTemplate>
</ItemsControl>

ビルドして実行

なんとエラーは出ず画面がちゃんと立ち上がる(^^;)。
ただしメニューは空。MainWindowが先にNewされメニューを読み込んだ後でモジュール側で追加しても遅いのか?と思いつつデバッグしてみると、Homeモジュールで追加したメニューがCashCalculatorモジュールで追加する際には消え、CashCalculatorのメニューだけが追加される。
MenuServiceが毎回Newされている感じ。
えーっシングルトンってどうやって書くんだっけ?と調べかけた時、以前にRegisterSingletonというメソッドをどこかで見かけて記憶がよみがえりcontainerRegistryのインテリセンスの中を探したらRegisterSingletonが見つかりました!(本当はサービス側をシングルトンにしておいた方がいいのかも知れませんが)
下記のように変えて無事アイコン付きのメニューが表示される。
予想以上に順調でちょっと感動。
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterSingleton<IMenuService, MenuService>();
}
残るはボタンを押した際の画面遷移。
一番苦労したのがここ。下記では動かないことは他のMVVM系フレームワークの経験から分かる。NavigateCommandはここには無く親のスコープにあるので。
Command="{Binding NavigateCommand}" 
Parentを付けるとかもっとシンプルな答えを探したのですが、予想以上に複雑な書き方が見つかり試行錯誤の結果動いたのが下記の書き方。
ダサい。もう少し何とかならないかと思いますが、NavigateCommandのブレークポイントを通った時の感動はひとしお。
NavigatePathの書き方も間違っていなかったようで、ちゃんとViewが切り替わりました。
Command="{Binding Path=DataContext.NavigateCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
OK キャンセル 確認 その他