No more Death March

あるSEのチラシの裏 C# WPF

WPF ItemsControlをDataGridみたいに使う

 この記事ではItemsControlをDataGridのみたいに扱うため、途中途中で必要な記述をメモして行く。


そもそもなんでDataGridを使わないのか

 WPFでは標準でDataGridというコントロールが用意されており、WindowsFormsのDataGridViewのようにテーブルイメージで簡単にデータを表示させることが出来る。列幅の変更やソートといった機能も標準搭載されているのでわざわざItemsControlをこねくり回して頑張るのはナンセンスなように思えるが、このDataGridクラス、自分的にはかなり扱い難い代物と感じる。

そう思った理由を書いておく。

・プロパティが多くて複雑すぎる

 これはただ難癖を付けているレベルの話だけど、複雑な機能に見合う分だけプロパティがとにかく多い。ただシンプルに一覧を表示したいだけなのだけど、シンプルな機能にするためあちこちプロパティの設定に気を配らなければいけないのがつらい。

・列に対するバインディングの敷居が高い

 自分がDataGridを検証していて引っ掛かったところその1、列名をビューモデルから通知しようとBindingを行ったが上手く行かず・・・

行き着いたのが以下の記事↓
c# - Bind datagrid column visibility MVVM - Stack Overflow

 DataGridColumn自体がVisualTree内に追加されないため、バインディング機能自体が扱えないというもの。回避策としてプロキシーとなるオブジェクトにバインディングを行い、リソース経由でバインディングを行うことが出来る。

・スタイルを拡張するのが困難

 自分がDataGridを検証していて引っ掛かったところその2、DataGridに限らずコントロールの外観をカスタマイズしたければMSDNのサンプルを参考にコントロールテンプレートを宣言すれば良いのだけど標準の部品の中では自分が知る限り一番複雑で難しいコントロールだと思う。これをベースに手を加えて変な不具合を混入させては元も子もないので、可能な限り手を加えずに使いたい。

以下DataGridのテンプレート↓
DataGrid のスタイルとテンプレート | Microsoft Docs

・複雑な要件に対する拡張性がいまいち

 自分がDataGridを検証していて引っ掛かったところその3、DataGridTemplateColumnを使えば多段表記にしたり、標準以外のコントロールを埋め込んだりすることが出来るのだが
DataGridTemplateColumnを使った列はソートが出来ないため、結局自作する必要がある。
 ⇒当然と言えば当然なんだけど・・・

 あと、多段表示でGridなんかを埋め込んだ時の挙動が若干怪しい気がする・・・これは検証していて心が折れた。

じゃあDataGridを使うなということ?

 というわけでは全くなく、目的に応じて使い分けた方が良いということ。本当にシンプルで標準通りに表示したいだけならDataGridをそのまま使った方が良いし、要件が複雑で流動的になる見込みがあるならItemsControlでカスタマイズすることを視野に入れといた方が良いということ。

 どの道WPFいじってるとItemsControlにお世話になることが多いので、引き出しを増やす意味でもItemsControlには慣れておきたい。

とりあえずデータの一覧を表示する

 基本的なところなので細かいところは割愛したいけど忘れそうなので書いておこう。

 疑似個人情報を生成してPersonクラスを作成、これを一行辺りのデータとしてStackPanelに表示する。

疑似個人情報の生成は以下を利用
疑似個人情報データ生成サービス

Personクラスの実装は以下の通り、

using System.Linq;

namespace ItemsControlExample
{
    public sealed class Person
    {
        public Person(string[] values)
        {
            this.Sequence = int.Parse(values.ElementAt(0));
            this.LastName = values.ElementAt(1);
            this.FirstName = values.ElementAt(2);
            this.BirthDay = values.ElementAt(28);
        }
        public int Sequence { get; }
        public string LastName { get; }
        public string FirstName { get; }
        public string BirthDay { get; }
    }
}

CSVのカラムを対応する属性に設定しているだけ。

ViewModelはこんな感じ

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Collections.ObjectModel;
using Microsoft.VisualBasic.FileIO;

namespace ItemsControlExample
{
    public sealed class ViewModel
    {
        public ViewModel()
        {
            var list = new ObservableCollection<Person>();
            this.Persons = new ReadOnlyObservableCollection<Person>(list);
            using (var parser = new TextFieldParser(@"personal_infomation.csv"))
            {
                parser.Delimiters = new string[] { "," };
                parser.ReadFields(); // ヘッダを飛ばす
                while(!parser.EndOfData)
                {
                    list.Add(new Person(parser.ReadFields()));
                    if (list.Count == 100) break;
                }
            }
        }

        public ReadOnlyObservableCollection<Person> Persons { get; }
    }
}

 コンストラクタでCSVファイルを読み取ってPersonクラスを生成、ReadOnlyObservableCollectionでViewに対して公開。データは5000件ほど用意したけど重いので100件まで読み取り。

Viewの記述は以下の通り

<Window 
    x:Class="ItemsControlExample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ItemsControlExample"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800"
    >
    <ItemsControl 
        ItemsSource="{Binding Persons}"
        >
        <ItemsControl.Template>
            <ControlTemplate TargetType="{x:Type ItemsControl}">
                <StackPanel
                    IsItemsHost="True"
                    />
            </ControlTemplate>
        </ItemsControl.Template>
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="{x:Type local:Person}">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Column="0" Text="{Binding Sequence}"/>
                    <TextBlock Grid.Column="1" Text="{Binding LastName}"/>
                    <TextBlock Grid.Column="2" Text="{Binding FirstName}"/>
                    <TextBlock Grid.Column="3" Text="{Binding BirthDay}"/>
                </Grid>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Window>

 まずViewModelの内容のバインディングから、コードビハインドのコンストラクタでViewModelを生成してDataContextへ設定しItemsControlのItemsSourceにPersonsをバインディングしている。アイテムの配置はTemplateにStackPanelを配置しItemsTemplateにGridとTextBlockを置いて各属性をバインディングした。

とりあえず起動してみると以下のような感じ。
f:id:nomoredeathmarch:20190121003344p:plain

スクロールバーを表示する

 ただ表示するだけなら簡単で、Template内のStackPanelをScrollViewerでラップしてやれば良い。

この部分を

        <ItemsControl.Template>
            <ControlTemplate TargetType="{x:Type ItemsControl}">
                <StackPanel
                    IsItemsHost="True"
                    />
            </ControlTemplate>
        </ItemsControl.Template>

こうする

       <ItemsControl.Template>
            <ControlTemplate TargetType="{x:Type ItemsControl}">
                <ScrollViewer
                    VerticalScrollBarVisibility="Auto"
                    HorizontalScrollBarVisibility="Auto"
                    >
                    <StackPanel
                        IsItemsHost="True"
                        />
                </ScrollViewer>
            </ControlTemplate>
        </ItemsControl.Template>

 ScrollBarVisibilityは垂直方向、水平方向どっちも必要になったら自動的に表示する。今回はItemsControlのTemplate内にScrollViewerを置いたけど、ItemsControlをラップしても良い。

で、実行するとこんな感じ

f:id:nomoredeathmarch:20190121220048p:plain

 注意しなければいけない点がひとつ、以下のようにAutoで指定されたGridに配置されるとスクロールバーが表示されなくなる。ItemsControlに限らずStackPanelを使っているとよく引っ掛かるので頭に止めておきたい。

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ItemsControl 
            ItemsSource="{Binding Persons}"
            >
            (略)
        </ItemsControl>
    </Grid>


先日の投稿で実験したので以下参考:
nomoredeathmarch.hatenablog.com

列幅を自動的に調整する

 表イメージでデータを表示する時、各属性の表示位置は揃える必要がある。GridのWidthに固定の幅を指定すれば簡単に実現出来るのだけどここはやはり登録されている内容に応じて必要な幅を自動的に確保してほしい。

じゃあ、Gridには幅を自動的に調整するAutoを指定する。

                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>

実行してみると以下の通り、残念な見え方になってしまう。

f:id:nomoredeathmarch:20190127144546p:plain

 Autoで調整されてはいるのだが調整されるのは 行毎に宣言されたGridの幅 であって 行を跨ぐ列毎の幅 は調整してくれない。じゃあどうするかというとSharedSizeGroupを設定してやれば良い。

まずは以下の通り幅を調整するグループごとに名前を付けてやる。

                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" SharedSizeGroup="Sequence"/>
                        <ColumnDefinition Width="Auto" SharedSizeGroup="LastName"/>
                        <ColumnDefinition Width="Auto" SharedSizeGroup="FirstName"/>
                        <ColumnDefinition Width="Auto" SharedSizeGroup="BirthDay"/>
                    </Grid.ColumnDefinitions>

 SharedSizeGroupは同じグループ名を指定されたもの同士、VisualTreeを跨いでサイズを共有するというもので、Sequenceと名前を付けてやれば他にSequenceと名付けられた要素を探し、互いにサイズを共有してくれる。このままだと実行しても先の例と結果は変わらないので、加えてItemsControlのGrid.IsSharedSizeScope添付プロパティにtrueを指定してやる。

    <ItemsControl 
        ItemsSource="{Binding Persons}"
        Grid.IsSharedSizeScope="True"
        >


これで実行すると行を跨いで幅が統一される。

f:id:nomoredeathmarch:20190127150006p:plain

 Grid.IsSharedSizeScope添付プロパティにTrueを指定するとそのUI要素がSharedSizeGroupの名前を検出するルート要素となり、下位の要素で同じ名前が指定されているもの同士サイズ調整を行ってくれる。

列名を表示する

 列幅を調整する時と同じ要領で対応するヘッダ部分にSharedSizeGroupを指定してやればよい。あと、Grid.IsSharedSizeScopeの設定先をItemsControlからヘッダとなるGridとItemsControlを分割するGridに変更してある。大分長くなってしまうがこの時点でXAML全文は以下の通りになっている。

<Window 
    x:Class="ItemsControlExample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ItemsControlExample"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800"
    >
    <Grid
        Grid.IsSharedSizeScope="True"
        >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid
            Grid.Row="0"
            >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Sequence"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="LastName"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="FirstName"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="BirthDay"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Text="連番"/>
            <TextBlock Grid.Column="1" Text="氏"/>
            <TextBlock Grid.Column="2" Text="名"/>
            <TextBlock Grid.Column="3" Text="生年月日"/>
        </Grid>
        <ItemsControl 
            Grid.Row="1"
            Focusable="False"
            ItemsSource="{Binding Persons}"
            >
            <ItemsControl.Template>
                <ControlTemplate TargetType="{x:Type ItemsControl}">
                    <ScrollViewer
                        Focusable="False"
                        VerticalScrollBarVisibility="Auto"
                        HorizontalScrollBarVisibility="Auto"
                        >
                        <StackPanel
                            IsItemsHost="True"
                            />
                    </ScrollViewer>
                </ControlTemplate>
            </ItemsControl.Template>
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="{x:Type local:Person}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="Sequence"/>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="LastName"/>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="FirstName"/>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="BirthDay"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Column="0" Text="{Binding Sequence}"/>
                        <TextBlock Grid.Column="1" Text="{Binding LastName}"/>
                        <TextBlock Grid.Column="2" Text="{Binding FirstName}"/>
                        <TextBlock Grid.Column="3" Text="{Binding BirthDay}"/>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

実行結果はこの通り

f:id:nomoredeathmarch:20190128222601p:plain

 マージンの指定や罫線の表示が無いので味気ないが、ヘッダも含めて幅の調整が出来ていることがわかる。

パネルを仮想化する

 この時点で一覧に表示しているデータは百件なので、一覧の表示を遅いと感じることはない。しかし、パネルを仮想化していない状態で大量のデータを表示すると数千件程度のデータの表示ですら体感出来るほど表示速度が落ちてしまう。この表示速度の低下がかなり顕著で、ごくごく小規模のアプリケーションであれば大して問題にならないと思うが仕事の一環として作っているようなシステムでは正直お話にならないレベルで遅くなってしまう。なので、WPFで大量(になりえる)データを表示するなら必ずパネルを仮想化することが大前提と言っても良い。それくらいパネル仮想化というものはWPFを使ったアプリケーションを構築するうえで基本的かつ重要な概念と言える。

 というか、もうVMからコレクションをバインディングするならとりあえず仮想化しておく。程度の認識でも良いと思う。

 仮想化の中身に入る前に実際どれくらい遅くなるのかということを書いておく、今のところCSVファイルの読み込みは100件までに留めておりこの状態では実行速度になにも問題無いと思える。

    public sealed class ViewModel
    {
        public ViewModel()
        {
            var list = new ObservableCollection<Person>();
            this.Persons = new ReadOnlyObservableCollection<Person>(list);
            using (var parser = new TextFieldParser(@"personal_infomation.csv"))
            {
                parser.Delimiters = new string[] { "," };
                parser.ReadFields(); // ヘッダを飛ばす
                while(!parser.EndOfData)
                {
                    list.Add(new Person(parser.ReadFields()));
                    if (list.Count == 100) break;
                }
            }
        }

        public ReadOnlyObservableCollection<Person> Persons { get; }
    }

 アプリケーションを起動し、起動が終わったら終了したパフォーマンスプロファイラの結果は以下の通り、リリースビルドじゃないのであまり正確なものじゃないが、起動分の時間も含めまぁ及第点だと思う。

f:id:nomoredeathmarch:20190128224323p:plain

 ここで、先のVMでCSVの読み取りを100件目で止めるという処理を外し、CSVファイル全件(5000件)を取り込めるようにした時のパフォーマンスプロファイラを見てみる。

f:id:nomoredeathmarch:20190128225550p:plain

 100件取り込んだ場合と比較して見事に真っ赤になっており、レイアウト処理に時間がかかっていることがわかる。100件の時は1秒弱だったのと比較し5000件取り込んだ場合だと20秒強という結果である。計測している端末がどうとか、試行回数が云々以前の問題でたかだか5000件のデータを処理するのにこの状況では使い物にならない。

 仮想化せずに大量にデータを表示しても遅いというのが確認出来たところで実際にパネルを仮想化する。必要最低限の設定で仮想化したXAMLは以下の通り。

<Window 
    x:Class="ItemsControlExample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ItemsControlExample"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800"
    >
    <Grid
        Grid.IsSharedSizeScope="True"
        >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid
            Grid.Row="0"
            >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Sequence"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="LastName"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="FirstName"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="BirthDay"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Text="連番"/>
            <TextBlock Grid.Column="1" Text="氏"/>
            <TextBlock Grid.Column="2" Text="名"/>
            <TextBlock Grid.Column="3" Text="生年月日"/>
        </Grid>
        <ItemsControl 
            Grid.Row="1"
            Focusable="False"
            ItemsSource="{Binding Persons}"
            ScrollViewer.CanContentScroll="True"
            VirtualizingPanel.IsVirtualizing="True"
            ScrollViewer.VerticalScrollBarVisibility="Auto"
            ScrollViewer.HorizontalScrollBarVisibility="Auto"
            >
            <ItemsControl.Template>
                <ControlTemplate TargetType="{x:Type ItemsControl}">
                    <ScrollViewer
                        Focusable="False"
                        >
                        <VirtualizingStackPanel
                            IsItemsHost="True"
                            />
                    </ScrollViewer>
                </ControlTemplate>
            </ItemsControl.Template>
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="{x:Type local:Person}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="Sequence"/>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="LastName"/>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="FirstName"/>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="BirthDay"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Column="0" Text="{Binding Sequence}"/>
                        <TextBlock Grid.Column="1" Text="{Binding LastName}"/>
                        <TextBlock Grid.Column="2" Text="{Binding FirstName}"/>
                        <TextBlock Grid.Column="3" Text="{Binding BirthDay}"/>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

ポイントは以下の通り
・StackPanelではなく、VirtualizingStackPanelを使う。
・ItemsControlのScrollViewer.CancontetScroll添付プロパティにtrueを指定する。
・ItemsControlのVirtualizingPanel.IsVirtualizing添付プロパティにtrueを指定する。

 仮想化した後のパフォーマンスプロファイルは以下の通りで、起動までの速度が大幅に改善されていることが分かる。

f:id:nomoredeathmarch:20190128231452p:plain

表示する列を増やす

 より多くの情報を表示するためにXAMLを修正する。以下ヘッダ部分の定義を抜粋したものでデータの部分にも同じ要領で定義を修正してやる。

        <Grid
            Grid.Row="0"
            >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Sequence"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="LastName"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="FirstName"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="LastNameKana"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="FirstNameKana"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="LastNameAlpha"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="FirstNameAlpha"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Gender"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="PhoneNumber"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="FaxNumber"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="MobilePhoneNumber"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="MailAddress"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="PostCode"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Address1"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Address2"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Address3"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Address4"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Address5"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressKana1"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressKana2"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressKana3"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressKana4"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressKana5"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressAlpha1"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressAlpha2"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressAlpha3"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressAlpha4"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="AddressAlpha5"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="BirthDay"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="Age"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="BirthPlace"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="BloodType"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Text="連番"/>
            <TextBlock Grid.Column="1" Text="氏"/>
            <TextBlock Grid.Column="2" Text="名"/>
            <TextBlock Grid.Column="3" Text="姓(カタカナ)"/>
            <TextBlock Grid.Column="4" Text="名(カタカナ)"/>
            <TextBlock Grid.Column="5" Text="姓(ローマ字)"/>
            <TextBlock Grid.Column="6" Text="名(ローマ字)"/>
            <TextBlock Grid.Column="7" Text="性別"/>
            <TextBlock Grid.Column="8" Text="電話番号"/>
            <TextBlock Grid.Column="9" Text="FAX"/>
            <TextBlock Grid.Column="10" Text="携帯電話"/>
            <TextBlock Grid.Column="11" Text="メールアドレス"/>
            <TextBlock Grid.Column="12" Text="郵便番号"/>
            <TextBlock Grid.Column="13" Text="住所1"/>
            <TextBlock Grid.Column="14" Text="住所2"/>
            <TextBlock Grid.Column="15" Text="住所3"/>
            <TextBlock Grid.Column="16" Text="住所4"/>
            <TextBlock Grid.Column="17" Text="住所5"/>
            <TextBlock Grid.Column="18" Text="住所(カタカナ)1"/>
            <TextBlock Grid.Column="19" Text="住所(カタカナ)2"/>
            <TextBlock Grid.Column="20" Text="住所(カタカナ)3"/>
            <TextBlock Grid.Column="21" Text="住所(カタカナ)4"/>
            <TextBlock Grid.Column="22" Text="住所(カタカナ)5"/>
            <TextBlock Grid.Column="23" Text="住所(ローマ字)1"/>
            <TextBlock Grid.Column="24" Text="住所(ローマ字)2"/>
            <TextBlock Grid.Column="25" Text="住所(ローマ字)3"/>
            <TextBlock Grid.Column="26" Text="住所(ローマ字)4"/>
            <TextBlock Grid.Column="27" Text="住所(ローマ字)5"/>
            <TextBlock Grid.Column="28" Text="生年月日"/>
            <TextBlock Grid.Column="29" Text="年齢"/>
            <TextBlock Grid.Column="30" Text="出身地"/>
            <TextBlock Grid.Column="31" Text="血液型"/>
        </Grid>

 が、この状態で実行してやると一つ問題が起きる。レイアウト処理に膨大な時間がかかってしまう。SharedSizeGroupの仕様なのかどうなのか、単一のスコープ内でサイズを共有する要素が増えると加速度的にパフォーマンスが劣化するらしい。仕方ないのでSharedSizeGroupを高速化したビヘイビアを自前で用意した。ここまで書いて大分シンプルさが損なわれて企画倒れした感じがするのだがめげずに続けてみようと思う・・・・ビヘイビアの内容はかなりこってりしているのでこの記事では要点だけ記述しておく。(そこそこ濃い処理をしているので別の記事でまとめようか・・・・)

列幅を統一するビヘイビアの要点

・SharedSizeGroupの様にスコープを指定する添付プロパティ(bool)を用意する。
・幅を統一するGridに対象であることをマークする添付プロパティ(bool)を用意する。
・スコープを指定したUI要素のLayoutUpdatedを購読するメソッド内で以下の通り処理を行う。
 1.VisualTreeの配下からマークがついているGridのインスタンスを収集する。
 2.各グリッドの列毎のActualWidthを計測し、列毎の最大値を算出する。
 3.全てのグリッドに2.で計測した結果を書き込む。

 かなり強引な処理になってしまったが、仮想化しているおかげで実際に処理の対象となるグリッドの件数はそこまで多くならない、がスクロールバーをスクロールさせた時にかなりひっかかりを感じるのでIsDeferredScrollingEnabledプロパティを有効にした。これでぱっと見はストレスなく操作出来ているように見える。IsDeferredScrollingEnabledを有効にした時にスクロール操作自体がちょっといまいちに感じるのだが、ビヘイビアのつくりがもっと良くなればこのあたりのプロパティに頼らなくてもスムーズに動くようになると思う。

実行結果は以下の通り、各行の幅を検証し、列毎にぴったりと幅が統一される。

f:id:nomoredeathmarch:20190217163015p:plain

横スクロールを同期させる。

 列数を増やしたことで画面に収まらなくなった。スクロールバーを表示させることで横スクロールが出来るのだが、今のままでは行の部分しか横スクロールに対応しておらず、列名はスクロールさせることが出来ない。ためしに右方向にスクロールさせると以下のように列名が取り残され、データの部分のみ右方向にスクロールしてしまう。

f:id:nomoredeathmarch:20190217163816p:plain

 じゃあどうするか、列名の部分にスクロールバーを置いて、スクロールバーを表示せずにスクロール位置のみ同期してやれば良い。一つ注意が必要で、垂直方向のスクロールバーの表示をAutoに指定した場合、データ量によって水平方向のスクロールバーを表示する領域が必要か否かが実行時に代わってしまう。イメージしやすいように列名の部分にスクロールビューアーを配置した画像を載せて置く。

f:id:nomoredeathmarch:20190217164737p:plain


 この画像では列名の部分においたスクロールビューアーとデータ部分においたスクロールビューアー、どちらもスクロールバーを常に表示するようにしてある。しかし、必要に応じてスクロールバーが表示されるようにしたいとなれば、データ部分のスクロールビューアーは垂直方向、水平方向ともにVisibilityをAutoに指定するだろう。このとき困るのが、データ部分の垂直スクロールバーの表示状況に応じて列名部分も全く同じ幅の余白を取ってやる必要があることだ、そうしないと水平方向のスクロール位置を同期させたとしても、スクロールバーの幅の分だけ列名とデータの表示がずれてしまうことになる。


とりあえず今日はここまで、おもったより大分複雑になってしまった・・・