2024年10月26日土曜日

Prism + DryIoc 入門③(ダイアログ編)

前回から少し時間が空きましたが、Prism+DryIoc入門の第3弾です。一応これで一区切りのつもりです。 

前回はPrism+DryIocでプロジェクトを立ち上げるお作法を紹介しました。これに加えてWPFの基礎知識がある皆さんはそれなりのソフトを作ることができると思います。ですが、ひとつ足りないことがありました。それはダイアログの表示です。今回はそのダイアログの表示の方法について説明していきます。 

ダイアログの実装

さて、通常、ダイアログを開くとなるとウィンドウを作ることになりますが、PrismではUserControlを作ることになります。ちょうどウィンドウ内でRegionを作るのと同じような感じですね。

順に説明していきます。

1. Viewの作成

「追加」→「新しい項目」からPrism UserControl (WPF)を追加します。

出来上がったXAMLファイルに前回の記事に書いたとおりのお作法でIgnorebleなどを設定します。

<UserControl x:Class="PrismTest.Views.TestDialog"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:PrismTest.ViewModels"
             mc:Ignorable="d"
             d:DataContext="{d:DesignInstance Type=vm:TestDialogViewModel}"
             prism:ViewModelLocator.AutoWireViewModel="True"
             Height="350" Width="525" >

    <Grid />
</UserControl>

2. VideModelの作成

ViewModelにはIDialogAwareを実装する必要があります。これはPrismにこれはダイアログですよと教えるためのものですね。

public class TestDialogViewModel : BindableBase, IDialogAware
{
    public TestDialogViewModel()
    {
    }

    public string Title => "Test Dialog";

    public event Action<IDialogResult>? RequestClose;

    public bool CanCloseDialog() => true;

    public void OnDialogClosed()
    {    
    }

    public void OnDialogOpened(IDialogParameters parameters)
    {
    }
}

タイトルやいくつかのメソッド、ダイアログを閉じる際に呼ぶイベントなどを実装します。特段説明をしなくても、その名前から何のためのものかはすぐにわかると思います。

3. Viewをダイアログとして登録

App.xaml.csを開き、RegisterTypesメソッド中にて作った型をダイアログとして登録します。これを登録することで、PrismがこのViewがダイアログであることを認識してくれます。

public partial class App
{
    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>();
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterDialog<TestDialog>("TestDialog");
    }
}

これも前回の記事までを読んだ人なら特に困ることは無いと思います。

4. ダイアログを表示

さて、ダイアログの表示も至って簡単です。ライブラリ無しではMVVMのポリシーを保ちつつViewModelから別ウィンドウを表示するのがとても面倒だったので、さすがPrismという感じですね。

public class ContentRegionViewModel : BindableBase
{
    public ContentRegionViewModel(IDialogService dialog)
    {
        ShowDialogCommand = new DelegateCommand(() => dialog.ShowDialog("TestDialog"));
    }

    public DelegateCommand ShowDialogCommand { get; }
}

適当なViewModelでIDialogServiceを受け取るようにすると、DryIoc経由でダイアログ関係の機能にアクセスできるようになります。ダイアログを表示するにはShowDialogメソッドを呼ぶだけです。引数はApp.xaml.csで設定した名前ですね。

このコマンドを呼び出すと、無事ダイアログが表示されました。

ダイアログを作りこむ

1. ウィンドウスタイル

さて、ダイアログが表示されましたが、よく見ると最大化/最小化ボタンなどが表示されていますね。Windows11のデザインではわかりにくいですが、ウィンドウもリサイズ可能なものになっています。さらにタスクバーを見るとダイアログが単体のアイコンを持っています。イケてないですね。


端的に、普通のウィンドウが表示されたと言えます。ダイアログはもっとダイアログっぽいスタイルでウィンドウを表示したいですね。

これに関しては、UserControlにprism:Dialog.WindowStyle添付プロパティを設定してやれば良いです。

<UserControl x:Class="PrismTest.Views.TestDialog"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:PrismTest.ViewModels"
             mc:Ignorable="d"
             d:DataContext="{d:DesignInstance Type=vm:TestDialogViewModel}"
             prism:ViewModelLocator.AutoWireViewModel="True"
             Height="350" Width="525" >

    <prism:Dialog.WindowStyle>
        <Style TargetType="Window">
            <Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterOwner" />
            <Setter Property="ResizeMode" Value="NoResize"/>
            <Setter Property="ShowInTaskbar" Value="False"/>
            <Setter Property="SizeToContent" Value="WidthAndHeight"/>
        </Style>
    </prism:Dialog.WindowStyle>

    <Grid />
</UserControl>

Windows11のデザインではわかりにくいですが、ウィンドウ枠が細枠(サイズ変更不可)になっており、最小化/最大化のボタンもなくなっています。もちろんタスクバーにも表示はされていません。

2. データの受け渡しをする

さて、ダイアログを表示するときは何かしらのデータを引き渡したいし、ダイアログを閉じた時には入力されたデータを受け取りたいことがあります。安心してください、Prismではそのような機能もサポートしています。

public class ContentRegionViewModel : BindableBase
{
    public ContentRegionViewModel(IDialogService dialog)
    {
        ShowDialogCommand = new DelegateCommand(() => {
            var param = new DialogParameters() { { "Text", Text }, };

            dialog.ShowDialog("TestDialog", param, result => {
                if(result.Result == ButtonResult.OK)
                    Text = result.Parameters.GetValue<string>("Text");
            });
        });
    }

    public DelegateCommand ShowDialogCommand { get; }

    public string Text
    {
        get => _Text;
        set => SetProperty(ref _Text, value);
    }
    string _Text = "";
}

呼び出し側のViewModelはこんな感じです。ShowDialogの第2引数にDialogParametersを渡します。名前こそDialogParametersですが、単純なディクショナリ型ですので、任意のデータを入れ込むことができます。

第3引数はダイアログから制御が返ってきたときに呼び出されるデリゲートです。下のダイアログ側のViewModelを見てからのほうがわかりやすいと思いますので、ひとまずそちらを見てから解説します。

public class TestDialogViewModel : BindableBase, IDialogAware
{
    public TestDialogViewModel()
    {
        OkButtonCommand = new DelegateCommand(() => RaiseRequestClose(ButtonResult.OK, new DialogParameters() { { "Text", Text } }));
        CancelButtonCommand = new DelegateCommand(() => RaiseRequestClose(ButtonResult.Cancel));
    }

    public string Title => "Test Dialog";

    public void OnDialogOpened(IDialogParameters parameters)
    {
        Text = parameters.GetValue<string>("Text");
    }

    public void OnDialogClosed()
    {
    }

    public bool CanCloseDialog() => true;

    protected void RaiseRequestClose(ButtonResult btnres, IDialogParameters param)
    {
        RequestClose?.Invoke(new DialogResult(btnres, param));
    }
    protected void RaiseRequestClose(ButtonResult btnres)
    {
        RequestClose?.Invoke(new DialogResult(btnres));
    }
    public event Action<IDialogResult>? RequestClose;


    public DelegateCommand OkButtonCommand { get; }
    public DelegateCommand CancelButtonCommand { get; }

    public string Text
    {
        get => _Text;
        set => SetProperty(ref _Text, value);
    }
    string _Text = "";
}

OnDialogOpenedメソッドはIDialogAwareインターフェースのメンバーの一つですが、ダイアログが開いたときにPrismがこのメソッドを呼んでくれます。この引数が、呼び出し元がパラメーターとして指定したものになります。今回は呼び出し物が指定したTextを読み取って、Textプロパティに代入しています。 

OKボタン/Cancelボタンが押されたときにそれぞれRequestCloseイベントを発動させています。その引数にはDialogResultクラスで、どのボタンが押されたかとDialogParametersをコンストラクタに渡してあげることで、その値を呼び出し元に返すことができます。

そして、呼び出し元のコードでは、OKボタンが押されたことを受けて、ShowDialogメソッドの第3引数のデリゲートが呼ばれますので、そこでButtonResultやDialogParametersを読み取って、適宜何かしらの処理を追加してやれば良いです。

ちなみに、ShowDialogメソッドは同期的に呼ばれます。すなわち、第3引数のデリゲートが制御を返した後にShowDialogメソッドは制御を返します。じゃあなんでShowDialogメソッドの戻り値をIDialogResultにしてくれなかったんだ…。

番外編:標準ダイアログを表示する

さて、ダイアログの表示のしかたは分かったけど、MessageBoxやファイルを開くダイアログなどのWindows標準ダイアログを表示させるにはどうしたら良いでしょう?

結論から言うと、Prismではサポートしていません。今まで説明してきた通り、ダイアログを実装するための支援が充実しているから、MessageBoxやファイルを開くダイアログは自作してねということのようです。

確かに、最近そういったダイアログも独自実装されたアプリをしばしば見かけますが、個人的にはソウジャナイ感をどうしても感じてしまいます。そういう標準ダイアログは、アプリを作る側の省力化だけでなく、使う側としてもどのアプリを使っても同じデザインだからOSとして統一感があり使いやすいわけです。それをわざわざ「自分で実装しろ」というのはいかがなものか…。

というわけで、標準ダイアログを表示するためにはLivetを使いましょう。大丈夫、PrismとLivetも共存できます。

まずは NugetからLivet.Messagingを入れます。

public class ContentRegionViewModel : BindableBase
{
    public ContentRegionViewModel()
    {
        Messenger = new InteractionMessenger();

        ShowMessageBoxCommand = new DelegateCommand(() => Messenger.Raise(new InformationMessage("MessageBox Text", "Caption", System.Windows.MessageBoxImage.Information, "InformationMessageBox")));
    }

    public InteractionMessenger Messenger { get; }

    public DelegateCommand ShowMessageBoxCommand { get; }
}

その後、ViewModelにInteractionMessengerクラスのプロパティを一つ用意してあげます。Livet標準のViewModel基底型ならもとからこのプロパティを持っているのですが、当然ながらPrismには無いので後付けしてやる必要があります。

<UserControl x:Class="PrismTest.Views.ContentRegion"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
             xmlns:vm="clr-namespace:PrismTest.ViewModels"
             mc:Ignorable="d"
             d:DataContext="{d:DesignInstance Type=vm:ContentRegionViewModel}"
             prism:ViewModelLocator.AutoWireViewModel="True"
             d:Height="350" d:Width="525" >

    <i:Interaction.Triggers>
        <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="InformationMessageBox">
            <l:InformationDialogInteractionMessageAction InvokeActionOnlyWhenWindowIsActive="False" />
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>
    
    <Grid>
        <Button Content="Show MessageBox" Command="{Binding ShowMessageBoxCommand}" Width="150" Height="40" />
    </Grid>
</UserControl>

ViewではInteractionMessageTriggerを使い、ViewModelからのメッセージを受け取ってやれば良いです。今回はOKボタンのみのMessageBoxを表示させていますが、その他の種類のMessageBoxだったりファイルを開くダイアログだったりは、対応する***Message型/***InteractionMessageAction型に差し替えれば表示することができます。ここらへんはLivetの使い方ですので、だいぶ昔に書いたこちらの記事を見ていただければ良いかと思います。

Prismの入門記事でPrismではなく別のライブラリを使えというのも変な話ですが、ライブラリによって当然得手不得手というものはありますから、それによって組み合わせが発生するのはやむを得ないことと思います。

***

以上で、3回にわたって連載(?)してきたPrism+DryIocの入門編ですが、ひとまず今回で終わりにしたいと思います。また何か思いついたテーマがあれば随時記事にします。

2024年10月20日日曜日

.NET時代のWPFでMessageBoxにビジュアルスタイルを適用する

WPFには一応標準のMessageBoxが付いています。Windows 95以降(Win32APIにて)標準で装備されているMessageBoxをWPFから呼び出す機能ですね。Livetなどではちゃんとそれを使うための機能が備わっているため、未だにお世話になります。

ですが、実はこのMessageBox、そのままではあまりイケた見た目になりません。

MessageBoxの見た目 

早速MessageBoxを表示させてみましょう。
<Button HorizontalAlignment="Center" VerticalAlignment="Center" Height="30" Width="150" Content="Show MessageBox" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <l:InformationDialogInteractionMessageAction InvokeActionOnlyWhenWindowIsActive="False" >
                <l:DirectInteractionMessage>
                    <l:InformationMessage Caption="Test" Text="Test MessageBox" Image="Information" />
                </l:DirectInteractionMessage>
            </l:InformationDialogInteractionMessageAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

見てくださいこのボタン。四角くて段付きのボタン!25年前のデザインです。

まじめな話をすると、Windows Me及びWindows 2000まではこのデザインでした。いずれも2000年発売のOSです。その後、Windows XPの発売に合わせて「ビジュアルスタイル」と呼ばれる見た目が導入されました。それを有効にすると、ボタンが少しフラットな見た目になります。

良いですね!ボタンがフラットなデザインになりましたし、アイコンも少しモダンな感じななりました。 

余談ですが、WPFが正式にリリースされたのは(=WPFが初めて組み込まれた.NET Framework 3.0がリリースされたのは)2006年のことですから、これらビジュアルスタイルはそれよりもだいぶ古い技術の話になります。WPFでは見た目を非常に高い柔軟性でいじることができますが、それよりも前はコントロールの見た目もそんなに柔軟に変えられるものではありませんでした。MicrosoftがWin32APIの中で提供するコントロールを使って自分のアプリを作成しなければならない時代では、やはりこういった全体的な見た目にかかわる機能というのはやはり当時は注目度が高かったわけです。

.NET時代のビジュアルスタイル有効化

さて、ネットで少し調べると、まあ古い技術であるため、Win32APIの話(C++のアプリ作成の話)だったり、Windows Formsの話だったり、.NET Framework時代の話だったり、昔の記事がたくさん見つかります。.NET時代の設定はどうしたら良いのでしょうか。

結論から言えば簡単です。

まず、ソリューション エクスプローラーからプロジェクトを右クリックし、「追加」→「新しい項目」を選択します。

つづいて「アプリケーション マニフェスト ファイル」を選択し、そのまま「追加」ボタンを押します。


そうすると、プロジェクト直下にapp.manifestが追加されるので、それを開きます。下のほうを見ると「Windows のコモン コントロールとダイアログのテーマを有効にします (Windows XP 以降)」と書いてあるブロックがあるので、その部分のコメントを外します。

最後にこれをビルドすれば、ビジュアルスタイルが適用されたMessageBoxが表示されます。

--------------

以上です。

2024年10月13日日曜日

WPFでReactivePropertyを使う際のお作法

今回はWPFでReactivePropertyを使う際のお作法を備忘録的に紹介していこうと思います。

ReactivePropertyとはReactive Extensions (Rx)ベースで作られたプロパティを提供するライブラリです。INotifyPropertyChangedを実装しているためViewModelにそのまま使用することができ、また、LINQを使って値の伝搬を表現することができるため、Statefulなアプリを簡単に実装することができる、非常に強力なライブラリです。

1. 導入

Nugetからダウンロードできます。

ReactiveProperty.WPFは入れなくても一応使えないことは無いのですが、あとでハマることになりますので、WPFアプリなら思考停止でとりあえず入れてしまいましょう。

2. UIスレッドへの転送設定

WPFには単一のスレッドからしかUI要素にアクセスできないという仕様があります。ですので、ViewModelにてイベントをそのスレッドに転送してやらねばならないのですが、ReactivePropertyには便利なことに特定のスレッドでイベントを発生させる機能があります。

さて、転送するにあたって、当然ながらどのスレッドがUIスレッドかということをライブラリに教え込まなければなりません。それは、App.xaml/App.xaml.csに以下のコードを追加することで行います。

<Application x:Class="WpfTest.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfTest"
             StartupUri="MainWindow.xaml"
             Startup="Application_Startup">
    
    <Application.Resources>
    </Application.Resources>
</Application>
public partial class App : Application
{        
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        ReactivePropertyScheduler.SetDefault(new ReactivePropertyWpfScheduler(Dispatcher));
    }
}

アプリのスタートアップ時に、Application.DispatcherをReactivePropertySchedulerとして登録するという作業になります。ReactivePropertyWpfSchedulerはReactiveProperty.WPFに入っているクラスで、WPF向けのスケジューラーです。

3. ReactivePropertyとReactivePropertySlim

さて、これで準備が整いましたので、実際に使用していきます。

ReactiveProperty(ライブラリ)には、ReactiveProperty(クラス)とReactivePropertySlim(クラス)があります。ReactivePropertyは色々な機能を持っているが故にパフォーマンスが悪めなのですが、ReactivePropertySlimは機能を絞ってパフォーマンスをかなり改善しています。そのReactivePropertySlimで削られた機能として、主に以下の2つがあります。

  • UIスレッドへの自動転送機能
  • 入力値のバリデーション機能

いずれも、ViewModelとしては必要な機能です。ですので、基本的にViewModelではReactivePropertyModelではReactivePropertySlimを使用すると良いでしょう。

public class MainWindowViewModel : BindableBase
{
    readonly IMainWindowModel model;
    public MainWindowViewModel(IMainWindowModel model)
    {
        this.model = model;

        Text = model.Text.ToReactivePropertyAsSynchronized(p => p.Value);
    }

    public ReactiveProperty<string> Text { get; }
}
public interface IMainWindowModel
{
    IReactiveProperty<string> Text { get; }
}
public class MainWindowModel : IMainWindowModel
{
    public MainWindowModel()
    {
        Text = new ReactivePropertySlim<string>();
        Text.Subscribe(p => System.Diagnostics.Debug.WriteLine(p));
    }

    public ReactivePropertySlim<string> Text { get; }
    IReactiveProperty<string> IMainWindowModel.Text => Text;
}

このコードでは、Prism前提でModelとViewModelを疎結合にするため、間にIMainWindowModelを挟んでいます。そこまでの抽象化が必要ない場合は適宜コードを読み替えてください。

ReactivePropertyとReactivePropertySlimを双方向に同期させるためにはToReactivePropertyAsSynchronized()を使います。片方向(Model→ViewModel)の場合はToReadOnlyReactiveProperty()で良いでしょう。

--------------

以上です。その他の細かい使い方は公式ドキュメントを参照ください。

2024年9月29日日曜日

ListView Extensions ver.1.3.1

ListView Extensions ver.1.3.1をリリースしました。

Github:

Nuget:
https://www.nuget.org/packages/ListViewExtensions/

今回の変更点

  • ソート対象のプロパティ名を入れ子(プロパティのプロパティの…のプロパティ)にできるようにした。
  • SortableGridViewColumnにSortingMemberPathを追加した。この項目に何も設定していない場合(nullの場合)は今まで通りDisplayMemberBindingsのパスをソートのキーとして扱うが、この項目を設定した場合はこれをキーとしてソートされる。
  • ListViewViewModel (ReadOnlyUIObservableCollectionを継承しているクラス)でIndexerとCountのPropertyChangedイベントが発生しなかった不具合を修正
  • ListViewの子要素にComboBoxなどがあると、その選択が変化したときに例外が発生する不具合を修正

下2つはバグ修正ですので、上2つについて説明していきます。

ソート対象のプロパティ名を入れ子(プロパティのプロパティの…のプロパティ)にできるようにした

今までは、ソートのキーにするプロパティ名は、SortableObservableCollectionの要素のプロパティしかできませんでした。ですので、少々わざとらしいですが、例えば以下のようなクラスがあったとしたら、person.Name.Spellなどのいわゆる「プロパティのプロパティ」はソートキーに指定することができませんでした(こちらのコードはgithubに公開しているサンプルコードの抜粋です)。

public class PersonViewModel : ViewModel
{
    // 中略
    
    public NameViewModel? Name
    {
        get => _Name;
        set => RaisePropertyChangedIfSet(ref _Name, value);
    }
    NameViewModel? _Name;

    public string Age => $"{model.Age}歳";

    public string Birthday => model.Birthday.ToShortDateString();

    public string Height => $"{model.Height_cm}cm";

    // 中略
}

public class NameViewModel : ViewModel
{
    // 中略
    
    public string? Spell
    {
        get => model.Spell;
        set => model.Spell = value;
    }

    public string? Pronunciation
    {
        get => model.Pronunciation;
        set => model.Pronunciation = value;
    }
}

それをできるようにしたという変更です。

これの真価を発揮するのは、ReactivePropertyを使ってViewModelを作ったときです。ReactivePropertyでは、person.Name.Valueなどの形でプロパティにアクセスする必要があるため、必ず2段以上入れ子になります。

実は今までもListVIewViewModelのコンストラクタでプロパティの読み替え用のディクショナリを、SortableObservableCollectionの引数でIComparerのディクショナリを渡すことで無理やり使うことはできなくはなかったのですが、回りくどい方法でソースコードの負荷が高まってしまうためあまりイケている方法ではありませんでした。ですが、今回のアップデートで、DisplayMemberBindingsに表現したとおりのパスを辿るようになったので、ViewModelやModelでは何も書かずに入れ子プロパティを参照できるようになりました。

SortableGridViewColumnにSortingMemberPathを追加した

ListViewのヘッダーはSortableGridViewColumnで作ることができます。ここで、それぞれのヘッダーに対応するソートのキーとするプロパティは、そのままDisplayMemberBindingsのパスを用いていましたが、それを任意のプロパティに設定できるようにしました。

通常は別の名前のプロパティにする必要は無いとは思いますが、例えば、ViewModelとViewでプロパティ名が異なる場合は、実際はソート操作自体はModel(SortableObservableCollection)にて行っているためプロパティ名の読み替えが必要になっていました。前述した通り、ViewModelにはプロパティ名読み替え用にディクショナリを受け取るコンストラクタがありますが、SortingMemberPathで直接設定できるようになりました。

<GridView>
    <lv:SortableGridViewColumn Width="120" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Name.Spell}" Header="Name" />
    <lv:SortableGridViewColumn Width="150" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Name.Pronunciation}" Header="Pronunciation" />
    <lv:SortableGridViewColumn Width="70"  SortableSource="{Binding People}" DisplayMemberBinding="{Binding Age}" Header="Age" />
    <lv:SortableGridViewColumn Width="120" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Birthday}" Header="Birthday" />
    <lv:SortableGridViewColumn Width="120" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Height}" SortingMemberPath="Height_cm" Header="Height" />
    <GridView.ColumnHeaderContainerStyle>
        <Style TargetType="lv:SortableGridViewColumnHeader">
            <Setter Property="SortingArrowLocation" Value="Top" />
        </Style>
    </GridView.ColumnHeaderContainerStyle>
</GridView>

このコードでは、5つ目の「Height」のプロパティ名を「Height_cm」と指定しています。

ちなみにもう一点この機能が必須のところがあって、SortableGridViewColumnにてCellTemplateを指定する場合です。セルの中身をDataTemplateで表現したい場合に使うものですが、DisplayMemberBindingsを設定しているとそちらのほうが優先されてしまいCellTemplateが働きません。その際もこのSortingMemberPathを指定することで、DataTemplateを使いながらソート用のプロパティも指定できるようになりました。

ちなみに、DisplayMemberBindingsはBindingBase型のプロパティですが、SortingMemberPathはString型です。ですので、Visual StudioでIntelliSenseは働きませんのでご注意ください。

--------------

変更点の説明は以上です 。

実際自分でこのライブラリを使い込んでいくといろいろと見つかりますね。まあ、WPFの全貌は10年以上触っていてもよくわからないので、こうやってブラッシュアップしていくしかないですね…。

2024年9月23日月曜日

ListView Extensions ver.1.3.0

ListView Extensions ver.1.3.0をリリースしました。

Github:

Nuget:
https://www.nuget.org/packages/ListViewExtensions/

今回の変更点

  • 選択項目の同期をListViewSelectedItemsActionからSelectedItemsSync.Source添付プロパティ経由で行うようにした。
  • Obsolete指定していたSortedHeaderを削除

後者は古い機能が削除されただけなので、前者について説明します。

選択項目の同期をListViewSelectedItemsActionからSelectedItemsSync.Source添付プロパティ経由で行うようにした。

ListViewにはSelectedItemsプロパティがあり、複数項目を選択したときはここから選択項目をすべて取得することができますし、このリストをプログラムから操作することで選択項目を変更することができます。しかし、これはViewModelをバインディングできないのです。

というのも、見ての通りこれはget専用プロパティであり、ViewModelのにIListのプロパティを作ってバインディングしようとしてもsetすることができないのです。Mode=OneWayToSourceにしてみてもやはり上手くいきません。BinadbleAttributeが付いているプロパティなのに一体どういうことなんでしょうね。

ということで、ver.1.2.0までのListViewExtensionsではListViewSelectedItemsActionというものを用意していました。

<ListView ItemsSource="{Binding People}" >
    <ListView.View>
        <GridView>
            <lv:SortableGridViewColumn Width="120" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Name}" Header="Name" />
            <lv:SortableGridViewColumn Width="150" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Pronunciation}" Header="Pronunciation" />
            <lv:SortableGridViewColumn Width="70"  SortableSource="{Binding People}" DisplayMemberBinding="{Binding Age}" Header="Age" />
            <lv:SortableGridViewColumn Width="120" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Birthday}" Header="Birthday" />
            <lv:SortableGridViewColumn Width="120" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Height}" Header="Height" />
            <GridView.ColumnHeaderContainerStyle>
                <Style TargetType="lv:SortableGridViewColumnHeader">
                    <Setter Property="SortingArrowLocation" Value="Top" />
                </Style>
            </GridView.ColumnHeaderContainerStyle>
        </GridView>
    </ListView.View>
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Header="Increment the age" Command="{Binding IncrementAgeCommand}" />
                        <MenuItem Header="Decrement the age" Command="{Binding DecrementAgeCommand}" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
            <!--<Setter Property="lv:DoubleClickBehavior.Command" Value="{Binding DoubleClickCommand}" />-->
            <Setter Property="lv:DoubleClickBehavior.MethodTarget" Value="{Binding}" />
            <Setter Property="lv:DoubleClickBehavior.MethodName" Value="DoubleClicked" />
        </Style>
    </ListView.ItemContainerStyle>
    <i:Interaction.Triggers>
        <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="SelectedItemsMirroring" >
            <lv:ListViewSelectedItemsAction Source="{Binding People.SelectedItemsSetter}" />
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>
</ListView>

ただし、これを使うのには癖がありすぎました。先日これを使おうとしたところ自分でもめちゃめちゃハマりましたし、ハマった方も多かったのではないでしょうか。

まず、これは適当なタイミングでViewModelからこのアクションを発動させないと同期しません。

public void Initialize()
{
    model = MainWindowModel.GetInstance();

    People = new ListViewViewModel<PersonViewModel, PersonModel>(model.People, person => new PersonViewModel(person), new Dictionary<string, string>() { { nameof(PersonModel.Height_cm), nameof(PersonViewModel.Height) } }, DispatcherHelper.UIDispatcher);
    Messenger.Raise(new InteractionMessage("SelectedItemsMirroring"));
}

忘れずにそのコードを入れたとして、このアクションを発動させるタイミングもとても重要です。このアクションはListViewのSelectedItemsをListViewSelectedItemsAction.Sourceにコピーする操作をするので、ListViewがインスタンス化されているタイミングでなければなりません。Loadedイベントなどで発動するようにしてもその前なので上手くいかないようです。Window.ContentRenderedイベントに合わせて使えば上手くいきますが、例えばUserControl内での使用などではこのイベントが使えないので一苦労します。

こんな癖つよシステムは使っていられないとのことで、試行錯誤のすえ、今回のバージョンでは以下のような形になりました。

<ListView ItemsSource="{Binding People}" lv:SelectedItemsSync.Source="{Binding People.SelectedItemsSetter}" >
    <ListView.View>
        <GridView>
            <lv:SortableGridViewColumn Width="120" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Name}" Header="Name" />
            <lv:SortableGridViewColumn Width="150" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Pronunciation}" Header="Pronunciation" />
            <lv:SortableGridViewColumn Width="70"  SortableSource="{Binding People}" DisplayMemberBinding="{Binding Age}" Header="Age" />
            <lv:SortableGridViewColumn Width="120" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Birthday}" Header="Birthday" />
            <lv:SortableGridViewColumn Width="120" SortableSource="{Binding People}" DisplayMemberBinding="{Binding Height}" Header="Height" />
            <GridView.ColumnHeaderContainerStyle>
                <Style TargetType="lv:SortableGridViewColumnHeader">
                    <Setter Property="SortingArrowLocation" Value="Top" />
                </Style>
            </GridView.ColumnHeaderContainerStyle>
        </GridView>
    </ListView.View>
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Header="Increment the age" Command="{Binding IncrementAgeCommand}" />
                        <MenuItem Header="Decrement the age" Command="{Binding DecrementAgeCommand}" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
            <!--<Setter Property="lv:DoubleClickBehavior.Command" Value="{Binding DoubleClickCommand}" />-->
            <Setter Property="lv:DoubleClickBehavior.MethodTarget" Value="{Binding}" />
            <Setter Property="lv:DoubleClickBehavior.MethodName" Value="DoubleClicked" />
        </Style>
    </ListView.ItemContainerStyle>
</ListView>

はい、超シンプルです。ListViewに添付プロパティでSelectedItemsSetterを登録するだけです。わかりやすいし、今までのようにViewModelで特別な処理を入れる必要もありませんし、タイミングを選ぶなどといったこともありません。

中身的にはWPFシステムのバインディングではなく、独自のバインディングシステムを使っています。すなわち、ListView.SelectedItemsとPeople.SelectedItemsSetterは別インスタンスで、裏で中身を同期する仕組みを作って動かしています。そのため、今までのSelectedItemsSetterとはプロパティの形態が変わり、以前バージョンとの互換性はなくなっています。

それ以外の使い方は今までのバージョンと合わせています。

2024年9月3日火曜日

ListView Extensions ver.1.2.0

ListView Extensions ver.1.2.0をリリースしました。

Github:

Nuget:
https://www.nuget.org/packages/ListViewExtensions/

今回の変更点

  • IReadOnlySortableObservableCollectionインターフェースとReadOnlySortableObservableCollectionクラスを追加
    • ISortableObservableCollectionインターフェースはIReadOnlySortableObservableCollectionインターフェースを継承するようにした
    • ListViewViewModelのコンストラクタに与えるソースコレクションをIReadOnlySortableObservableCollectionにした
  • ListViewViewModelに単一の型引数を取るオーバーロードを追加

主な変更点を説明していきます。

IReadOnlySortableObservableCollectionインターフェースとReadOnlySortableObservableCollectionクラスを追加

もともとSortableObservableCollectionはMVVMのModelで使うことを想定していますが、今まではReadOnlyがありませんでした。MVVMパターンでは、Modelでは読み取り専用のコレクションを公開し、書き換えは別のメソッドなどを介してやることが多いので、今までのReadOnlyが無い環境では少し不便でした。

そこで、読み取り専用のソート可能ObservableCollectionとしてReadOnlySortableObservableCollectionを追加しました。ReadOnlyですがソートはできるので、SortやMoveなどのメソッドも動きます。そうした場合、ソースコレクションに対してSort / Moveを行うという形になり、結果的にソースコレクションに影響を与えることができます。ReadOnlyなのにそれはどうなのかと少し思いましたが、まあ、Sortableと言ってるから割り切ってくれということで。

これに伴って、ISortableObservableCollectionインターフェイスはIReadOnlySortableObservableCollectionを継承する形にしました。この辺で破壊的変更をしているのでバージョンの2桁目を上げています。 

また、ListViewViewModelもIReadOnlySortableObservableCollectionを受け取るようにしています。SortableObservableCollectionもIReadOnlySortableObservableCollectionを実装していますので、今までと使い勝手は変わりないでしょう。ただし、ListVIewViewModelのRemoveSelectedItemCommandはReadOnlyだと失敗します(InvalidOperationExceptionを吐きます)。

ListViewViewModelに単一の型引数を取るオーバーロードを追加

ListViewViewModelは、要素の型をModelとViewModelで変換できるよう2つの型引数を取るものしか今までありませんでしたが、変換が不要な際は記述が冗長だったので、1つの型引数を取るオーバーロードを追加しています。

ただし、ListViewViewModelは同じ参照の要素を2つ以上設定すると例外を吐きます。これは、同じ参照の要素が2つ以上あるとSelectedItemsでどちらが選択された項目か区別がつかないからです。そういうことになりうる場合は、少し面倒ですが、ラッピングするViewModel型を作ってnewするようにしてください。あくまでもお作法です。

***

以上です。最近投稿頻度が上がっている気がする…。

2024年9月1日日曜日

Tategakiのダウンロード数

ふとNugetのTategakiのダウンロード数を確認したところ、次の画像のようになっていました。

過去6週間のダウンロード数ということで、最新のver.3.2.2が出てからすでに6週間以上たっていますが、いまだにver.2.1.1のダウンロード数が最も多いようです。

確かにver.2.1.1からver.3.0.0ではメジャーバージョンアップに相応しいほどガラリと変えていますので、敢えて古いバージョンを使いたいという人もいるのかもしれませんが、実際はどうなんでしょうね。何か知っている人いたら教えてください。