2024年8月31日土曜日

ListView Extensions ver.1.1.0

唐突ですがListView Extensions ver.1.1.0をリリースしました。実に前回の更新から6年ぶりです。

Github:

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

今回の変更点

  • Githubでソースコードを公開
  • ライセンスをMITライセンスに変更
  • ターゲットを.NET Framework 4.5.2 / .NET Core 3.1 / .NET 6に変更
  • ListViewのヘッダーサポートを強化
    • SortableGridViewColumnHeader、SortableGridViewColumnを追加
    • SortedHeaderをObsolete指定にした
  • ISortableObservableCollectionのSortメソッドの引数を変更、古いメソッドはObsolete指定にした
  • コードのリファクタリング、Nullableの有効化、単体テストの追加など

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

Githubで公開 / MITライセンス化

最近久しぶりにこのListView Extensionsをいじろうとしたとき、そういえばはコードを公開していなかったなと思ってGithubで公開することにしました。併せてライセンスもMITに変更しています(今までは明記なし)。すっかり私もオープンソースの人(?)になってきました。

ターゲットを.NET Framework 4.5.2 / .NET Core 3.1 / .NET 6に変更

直前のver.1.0.1 では.NET Framework 4.5のみでした。これが.NET系統にも対応するようにしました。実はこれが今回のアップデートの大きなモチベーションだったりします。

ListViewのヘッダーサポートを強化

ListViewのヘッダーを簡単に作成

今までのListView Extensionsではヘッダーの記述がとても冗長でした。

<ListView ItemsSource="{Binding People}" >
    <ListView.Resources>
        <lv:SortingConditionConverter x:Key="ConditionToDirectionConverter" />
    </ListView.Resources>
    <ListView.View>
        <GridView>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Name" >
                    <lv:SortedHeader Content="Name" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Name'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="150" DisplayMemberBinding="{Binding Pronunciation}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Pronunciation" >
                    <lv:SortedHeader Content="Pronunciation" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Pronunciation'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="70" DisplayMemberBinding="{Binding Age}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Age" >
                    <lv:SortedHeader Content="Age" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Age'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Birthday}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Birthday" >
                    <lv:SortedHeader Content="Birthday" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Birthday'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="100" DisplayMemberBinding="{Binding Height}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Height_cm" >
                    <lv:SortedHeader Content="Height" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Height_cm'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
        </GridView>
    </ListView.View>
    <!-- 中略 -->
</ListView>

これはサンプルコードの抜粋ですが、ListViewで1列作るのに5行もコードが必要でした。そして似たような記述も繰り返し行われとても冗長です。XAMLの構造、というよりもどういう仕組みになっているかはこの状態でわかりやすいんですがね。

これを、今回のバージョンでは以下のように書き換えることができるようになりました。

<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>
    </ListView.View>
    <!-- 中略 -->
</ListView>

だいぶすっきりしました。

このSortableGridViewColumnはGridViewColumnを継承して作られたものですが、少しトリッキーです。上のコードは下のコードとほぼ同等です。

<ListView ItemsSource="{Binding People}" >
    <ListView.Resources>
        <lv:SortingConditionConverter x:Key="ConditionToDirectionConverter" />
    </ListView.Resources>
    <ListView.View>
        <GridView>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
                <lv:SortableGridViewColumnHeader Content="Name" SortingArrowLocation="Right" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Name'}"
                                                 Command="{Binding People.SortByPropertyCommand}" CommandParameter="Name"/>
            </GridViewColumn>
            <GridViewColumn Width="150" DisplayMemberBinding="{Binding Pronunciation}" >
                <lv:SortableGridViewColumnHeader Content="Pronunciation" SortingArrowLocation="Right" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Pronunciation'}"
                                                 Command="{Binding People.SortByPropertyCommand}" CommandParameter="Pronunciation"/>
            </GridViewColumn>
            <GridViewColumn Width="70" DisplayMemberBinding="{Binding Age}" >
                <lv:SortableGridViewColumnHeader Content="Age" SortingArrowLocation="Right" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Age'}"
                                                 Command="{Binding People.SortByPropertyCommand}" CommandParameter="Age"/>
            </GridViewColumn>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Birthday}" >
                <lv:SortableGridViewColumnHeader Content="Birthday" SortingArrowLocation="Right" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Birthday'}"
                                                 Command="{Binding People.SortByPropertyCommand}" CommandParameter="Birthday"/>
            </GridViewColumn>
            <GridViewColumn Width="100" DisplayMemberBinding="{Binding Height}" >
                <lv:SortableGridViewColumnHeader Content="Height" SortingArrowLocation="Right" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Height'}"
                                                 Command="{Binding People.SortByPropertyCommand}" CommandParameter="Height"/>
            </GridViewColumn>
        </GridView>
    </ListView.View>
    <!-- 中略 -->
</ListView>

SortableGridViewColumnHeaderはGridViewColumnHeaderを継承して作られたもので、GridViewColumnHeaderに並び替えのアイコン「▲」「▼」を表示する機能を追加したものです。SortingDirectionプロパティに並び替えの向きを指定することでアイコンが表示されます。

SortableGridViewColumnは、HeaderプロパティがSortableGridViewColumnHeaderではない場合に自動的にSortableGridViewColumnHeaderを作ってそのインスタンスにHeaderプロパティに入っていたものを渡しますます。そして、そのSortableGridViewColumnHeaderのSortingDirectionプロパティ、Command / CommandParameterプロパティにSortableSourceプロパティにバインディングされたオブジェクトのプロパティを自動的にバインディングします。ソートのキーにはDisplayMemberBindingの値を使うので、万が一DisplayMemberBindingの値と異なるプロパティをソートのキーにする必要がある場合はGridViewColumnHeaderを直接触る必要があります。

これらの機能が追加されたことによって、SortedHeaderはObsolete扱いとなりました。

ListViewのヘッダーをカスタマイズ① - 並び替え矢印の位置を変更

SortableGridViewColumnはSortingArrowLocationプロパティを持っています。これはDock列挙型になっているので、上下左右好きな場所に配置することができます。直接プロパティを触っても良いのですが、GridView.ColumnHeaderContainerStyleプロパティによる一括スタイル指定をするのが良いでしょう。

<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>

↓Top

Top

↓Right

Right

画像ではTopとRightの例を出していますが、LeftとBottomを指定することもできます。あまり馴染みのないデザインかとは思いますが。

あとはSortingArrowMarginプロパティを使えばArrowの周囲のMarginを設定することができるので、「Name」などのヘッダーテキストの間隔を調整したいときに使えます。

ListViewのヘッダーをカスタマイズ② - 並び替え矢印をカスタマイズ

ListViewのヘッダーの矢印は専用のクラス「AscendingArrow」「DescendingArrow」で表現されています。ですので、このクラスのTemplateを丸々置き換えてしまうことで矢印の見た目をカスタマイズすることができます。

<ListView ItemsSource="{Binding People}" >
    <ListView.Resources>
        <Style TargetType="lv:AscendingArrow">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Polyline Points="0,5 5,0,10,5" StrokeThickness="1" Stroke="Black" />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style TargetType="lv:DescendingArrow">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Polyline Points="0,0 5,5,10,0" StrokeThickness="1" Stroke="Black" />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListView.Resources>
    <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>

このように全く違う見た目になりました。これで、仮に私のデザインした▲が気に入らない人がいたとしても、クレームを受けることなく「自分で好きに変えてください」と言えるようになりました。こういった表現力の豊かさがさすがWPFという感じですね。

ISortableObservableCollectionのSortメソッドの引数を変更

ISortableObservableCollectionのSortメソッドの形が少し変わっています。破壊的変更です。

public interface ISortableObservableCollection<T> : IList<T>, INotifyPropertyChanged, INotifyCollectionChanged
{
    // 中略

    [Obsolete]
    void Sort(string propertyName, SortingDirection direction);

    /// <summary>
    /// 自身をソートするメソッド
    /// </summary>
    /// <param name="direction">ソート方向。Noneの場合は何もしない。</param>
    /// <param name="propertyName">ソートに使用するプロパティ名。Nullの場合は要素自身をキーとしてソート。</param>
    void Sort(SortingDirection direction, string? propertyName = null);
}

まあ、直接SortableObservableCollectionからソートを手動でかけていない限り影響は無いとは思いますが、注意してください。Obsoleteの警告が出てもパラメーターの順序を入れ替えるだけでOKです。

この変更は、propertyNameをnullでも受け取れるようにしたところにあります。nullにすると要素自身をキーにして並び替えることができます。SortableObservableCollection<int>などの場合を想定しています。

***

以上で今回の変更分の説明は終わりです。あまり使用面で影響のない修正は割愛させてもらいます。

0 件のコメント:

コメントを投稿