初めてのWPF 8日目

2019/01/21 WPF::Prism

プロジェクト名を変更

プロジェクト

Homeモジュールは一旦削除し、TreasurerHelper.Homeを新たに作成。
作成されたモデル等のモジュール名が「HomeModule.cs」のままだったのでCashCalculatorはTreasurerHelper.CashCalculatorに名前を変更。
デバッグでエラー潰しながらビルド成功すると作成されたdllはCashCalculator.dllのまま。
プロジェクトのプロパティでアセンブリ名を変更する必要ありました。
アセンブリ名の変更


Homeモジュール上にHomeモジュールへのリンクは不要なので、メニュー一つだけでは寂しいのでもう一つ出納帳モジュール追加。

モジュール側からHomeモジュールのViewに画像を表示

デザインそのままいただきました。それでも画像や余白のせいでしょうか、デザイナーさん入っていない感があります(^^;)。
Homeモジュール

画像の保存場所

自分のメニュー用の画像は自分のプロジェクトに保存したい。
画像とViewの関係

pack://~というMaterialDesignを参照する時によく出てくるやり方で表示できました。
menuService.AddMainMenuItem(new MenuItem { Title = "現金計算", IconName = "Calculator", Description = "紙幣・硬貨毎の枚数入力",
    NavigatePath = "TreasurerHelper.CashCalculator.Views.CashCalculator",
    DisplayOnHome = true,
    ImageSource = "pack://application:,,,/TreasurerHelper.CashCalculator;component/Resources/CashCalculator.jpg"
 });

ビルド後の画像はどこに?

binフォルダの中ではモジュールのdllは同じフォルダ内になるしResourcesのフォルダ名や画像ファイル名が被ったらどうなるのか心配したら、出力されたファイルに.jpgファイルは見つかりません。
それでもexeを実行するとちゃんと画像が表示されます。
ビルド後のファイル

画像のビルドアクション

プロジェクトに画像を追加した際、デフォルトでファイルの「ビルドアクション:リソース、出力ディレクトリにコピー:コピーしない」になっているためのようです。
画像のビルドアクション


ファイルをコピーし忘れる心配がないのはいいですが、「もっといい画像あるからこれ使って」とか言われたらビルドし直す必要が出てきます。
「ビルドアクション:なし、出力ディレクトリにコピー:新しい場合はコピーする」に変えたらちゃんと出力されました。
ところが画像へのパスの指定方法が分からず、一旦元に戻しました。
ビルドアクション:なし

ドロワー用メニュー一覧とホーム用メニュー一覧

ホーム画面にホームモジュールの表示は不要ですが、ドロワーには必要。MenuItemのリストは一つでHomeモジュールのViewModelでLinqで省いて渡せばいいわ、と思ったところLinq使ったとたんHomeでのメニューが消える…。
調査は後回しにし、引いても駄目なら押して見る。
public void AddMainMenuItem(MenuItem menuItem)
{
    MainMenuItems.Add(menuItem);
    if (menuItem.DisplayOnHome)
    {
        HomeMenuItems.Add(menuItem);
    }
}
ドロワー上のメニュー

モジュールのViewからMainWindowViewModelのコマンドを呼ぶ

NavigateCommand

色々心配しましたが、Viewがモジュール間にまたがっていても通常の入れ子になったViewと違いは無い。親のViewのViewModelが継承されている。

1.MainWindow.xaml

通常のバインド
Command="{Binding NavigateCommand}"

2.MainWindow.xaml内のItemTemplateから

Command="{Binding Path=DataContext.NavigateCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"

3.HomeModule内のHomeMenu.xamlから

他にも書き方あるかも知れませんが一番てっとりばやそうなので
Command="{Binding Path=DataContext.NavigateCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"

初めてのWPF 7日目

モジュール側からMenuServiceを参照

またまたいてまえ系コーディング。やってみたら動いたよ…。
HomeModuleのViewAのViewModelで。
public ViewAViewModel(IMenuService menuService)
{
    MainMenuItems = menuService.GetMainMenuItems();
    Message = "Home Module";
}
デバッグしてここで待ち受けているとMainMenuItemsの中は空(HomeModuleのOnInitializedよりも先にこちらを通る)だったのですが、実行するとメニューが表示されました。
Prism MenuService

MaterialDesignのCardを使う

Cardで包んでボタンをAccentButtonに変えたらグッと良くなるはず、と思ってましたがいまいち垢抜けません…。
WrapPanelがWrapしてくれず苦労しました。MenuItemを拡張してグリッドの位置をモデルから渡そうかと思ったくらい。
MaterialDesignのCard(WPF)

<Grid>
    <ItemsControl ItemsSource="{Binding MainMenuItems}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
            <DataTemplate>
            <materialDesign:Card Margin="12" Padding="16" Width="280">
                    <StackPanel>
                    <TextBlock Style="{DynamicResource MaterialDesignTitleTextBlock}" Text="{Binding Title}"></TextBlock>
                    <TextBlock Text="{Binding Description}" Margin="10 8 0 0"></TextBlock>
                    <Button
                        Margin="12 16 12 0"   
                        Style="{StaticResource MaterialDesignRaisedAccentButton}"
                        Command="{Binding Path=DataContext.NavigateCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding NavigatePath}">
                        <TextBlock Margin="6 0 0 0" Style="{StaticResource MaterialDesignBody1TextBlock}"
                           Text="OPEN" />
                    </Button>
                </StackPanel>
                 </materialDesign:Card>
             </DataTemplate>
        </ItemsControl.ItemTemplate>
     </ItemsControl>
</Grid>

画像を追加する前に発行後のフォルダ構成を確認

発行方法は3種類あるようですが、一番簡単そうなXCOPYでいくことに。
ところがその方法を書いてくれていない。
普通に「発行」するとexeが出来ずにインストーラーが出来てました。
ビルドをリリースに変えてbinフォルダの中にできたものをデスクトップに移動しexeをクリックしたらちゃんと動きました。
WPF XCOPY


現在は親からモジュールへ参照を張っているのでモジュールのdllも一緒にリリースビルドに含まれています。
dllの並び見ているとモジュールのプロジェクト名を変えたくなってきて一旦終了。

初めての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}}"

初めてのWPF 5日目

起動時はHomeモジュール表示
BasicRegionNavigation 1


ハンバーガートグルボタンでメインメニュー表示
BasicRegionNavigation 2


ドロワー上のボタンをクリックしてモジュール切り替え
BasicRegionNavigation 3

モジュール間の切り替えをBasicRegionNavigationに変更

Prism6版を確認したら、NavigateCommandを親プロジェクトの既存のViewModelに移動しても問題なく動いたとあった(記憶には無かった)のでモジュール側のメニューはViewだけを残して他はバッサリ削除。
「動かん!?」と思ったらNavigate先のRegion名を間違えてました(Typo)。
エラー出してよ、と思いました。

Viewもテンプレート化すれば重複減らせそうですが、それは次の課題。

メインメニューをMaterialDesignのDrawerに変更

デスクトップアプリでメインメニューにDrawer使うとか他人様にやられると「はぁ?」という感じですが、ものは試しで(^^;)。

MainWindowViewModelにメニュー開閉用のプロパティを追加

setの見慣れない書き方はよく分かってませんが、Titleの真似で。
private bool _mainMenuIsOpen = false;
public bool MainMenuIsOpen
{
    get { return _mainMenuIsOpen; }
    set { SetProperty(ref _mainMenuIsOpen, value); }
}

MainWindow.xamlのトグルボタンにバインド

2箇所あるので要注意
<materialDesign:DrawerHost IsLeftDrawerOpen="{Binding ElementName=MenuToggleButton, Path=IsChecked}">
    <materialDesign:DrawerHost.LeftDrawerContent>
        <DockPanel MinWidth="212">
            <ToggleButton Style="{StaticResource MaterialDesignHamburgerToggleButton}" 
                                DockPanel.Dock="Top"
                                HorizontalAlignment="Right" Margin="16"
                                IsChecked="{Binding MainMenuIsOpen}" />
            <StackPanel Margin="0">
                <ItemsControl x:Name="NavigationItemsControl" prism:RegionManager.RegionName="MainNavigationRegion" Margin="0" Padding="0" />
            </StackPanel>
        </DockPanel>
    </materialDesign:DrawerHost.LeftDrawerContent>
    <DockPanel>
        <materialDesign:ColorZone Padding="16" materialDesign:ShadowAssist.ShadowDepth="Depth2"
                                Mode="PrimaryMid" DockPanel.Dock="Top">
            <DockPanel>
                <ToggleButton Style="{StaticResource MaterialDesignHamburgerToggleButton}" IsChecked="{Binding MainMenuIsOpen}"
                                    x:Name="MenuToggleButton"/>
                <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="22" Text="{Binding Title}"></TextBlock>
            </DockPanel>
        </materialDesign:ColorZone>
・・・・

NavigateCommandで遷移時にfalseに

private void Navigate(string navigatePath)
{
    MainMenuIsOpen = false;

    if (navigatePath != null)
      _regionManager.RequestNavigate("ContentRegion", navigatePath);
       
}

初めてのWPF 4日目

2019/01/12 WPF::Prism
モジュール間の切り替えを行うメインのナビゲーション
TreasurerHelper080.png

TreasurerHelper090.png

MainWindow.xamlにメニュー用Region追加

Gridで画面を左右に分割し、左側にMainNavigationRegionを追加
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Border Grid.Column="0" Grid.Row="2" MinWidth="100" 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="ContentRegion" 
              Grid.Column="1" Grid.Row="2" Margin="5,8,5,5" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
</Grid>

モジュール側にナビゲーション用View追加

Prism UserControlのテンプレート使用
<Button x:Name="Root" ToolTip="紙幣・硬貨毎の枚数入力" ToolTipService.InitialShowDelay="0" 
                        Style="{DynamicResource MaterialDesignFlatButton}"
                        Click="Button_Click" >
    <StackPanel Orientation="Horizontal">
        <materialDesign:PackIcon x:Name="PackIcon" Kind="Calculator" />
        <TextBlock Margin="6 0 0 0" Style="{StaticResource MaterialDesignBody1TextBlock}"
                                   Text="現金計算" />
    </StackPanel>
</Button>

ModuleのOnInitializedにナビゲーション用Viewの登録を追記

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

ボタンクリックでViewを切り替える

View InjectionView Activation/Deactivationの合わせ技
IContainerExtension _container;
IRegionManager _regionManager;

public CashCalculatorNavigationItemView(IContainerExtension container, IRegionManager regionManager)
{
    InitializeComponent();
    _container = container;
    _regionManager = regionManager;
}

private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
    var view = _container.Resolve<CashCalculator>();
    IRegion region = _regionManager.Regions["ContentRegion"];
    region.Add(view);
    region.Activate(view);
}
ナビゲーションがちゃんと動作しているか確認するためにもう一つModuleを作成しましたが、同じような記述を繰り返すことになり、ちょっとダサい。
何とかする前に動いた状態のものを一旦コミット
OK キャンセル 確認 その他