オルタナティブ・ブログ > IT's my business >

IT業界のコメントマニアが始めるブログ。いつまで続くのか?

(XAML#21)「DataGrid と詳細表示」

»

WPF の DataGrid は、単一のデータをグリッド形式で表示するだけでなく、行に対応する詳細データを表示する機能も持っています。ここでは簡略化のため、テスト用のデータをプログラムコードですべて準備します。以下のコードでは、それぞれの人物情報の中に、業績リストを持たせています。

[コード]
public partial class MainWindow : Window
{
    private SampleViewModel vm = new SampleViewModel();

    public MainWindow()
    {
        InitializeComponent();

        DataContext = vm;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        var sb = new StringBuilder();
        foreach (var info in vm.Persons)
        {
            sb.AppendFormat("{0:D4}: {1,-8} ... {2:yyyy/MM/dd}({3})\n", info.ID, info.Name, info.BirthDate, info.Gender);
            if (info.Works != null)
            {
                foreach (var work in info.Works)
                    sb.AppendFormat(" ---> {0:D4}: {1,-8}\n", work.WorkID, work.Title);
            }
        }
        MessageBox.Show(sb.ToString());
    }
}

public class SampleViewModel
{
    public ObservableCollection<PersonInfo> Persons { get; private set; }

    public SampleViewModel()
    {
        Persons = new ObservableCollection<PersonInfo>()
        {
            new PersonInfo() { ID = 1, Name = "福澤諭吉", BirthDate = new DateTime(1835, 1, 10), Gender = gender_t.Male,
            Works = new ObservableCollection<WorkInfo>() {
                new WorkInfo() { WorkID = 1, Title = "学問のすすめ" },
                new WorkInfo() { WorkID = 2, Title = "西洋事情" },
                }
            },
            new PersonInfo() { ID = 2, Name = "樋口一葉", BirthDate = new DateTime(1872, 5, 2), Gender = gender_t.Female,
                Works = new ObservableCollection<WorkInfo>() {
                    new WorkInfo() { WorkID = 3, Title = "たけくらべ" },
                    new WorkInfo() { WorkID = 4, Title = "大つごもり" },
                }
            },
            new PersonInfo() { ID = 3, Name = "野口英世", BirthDate = new DateTime(1928, 5, 21), Gender = gender_t.Male,
                Works = new ObservableCollection<WorkInfo>() {
                    new WorkInfo() { WorkID = 3, Title = "黄熱ワクチン" },
                }
            },
        };
    }
}

public enum gender_t
{
    Unknown, Male, Female,
}

public class PersonInfo : INotifyPropertyChanged
{
    private int _id;
    public int ID {
        get { return _id; }
        set
        {
            _id = value;
            OnPropertyChanged("ID");
        }
    }

    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            OnPropertyChanged("Name");
        }
    }

    private DateTime _birthdate;
    public DateTime BirthDate
    {
        get { return _birthdate; }
        set
        {
            _birthdate = value;
            OnPropertyChanged("BirthDate");
        }
    }

    private gender_t _gender;
    public gender_t Gender
    {
        get { return _gender; }
        set
        {
            _gender = value;
            OnPropertyChanged("Gender");
        }
    }

    public ObservableCollection<WorkInfo> Works { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
}

public class WorkInfo : INotifyPropertyChanged
{
    private int _workid;
    public int WorkID
    {
        get { return _workid; }
        set
        {
            _workid = value;
            OnPropertyChanged("WorkID");
        }
    }

    private string _title;
    public string Title
    {
        get { return _title; }
        set
        {
            _title = value;
            OnPropertyChanged("Title");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
}

従来通り、人物情報のみを DataGrid で表示する場合は、次のようになります。

[XAML]
<Window x:Class="WpfApp1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="320" Width="480">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <DataGrid Grid.Row="0"
                ItemsSource="{Binding Persons}"
                AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="ID" Binding="{Binding ID}" IsReadOnly="True" />
                <DataGridTextColumn Header="Name" Binding="{Binding Name}" />
                <DataGridTextColumn Header="BirthDate" Binding="{Binding BirthDate, StringFormat=yyyy/MM/dd}" />
            </DataGrid.Columns>
        </DataGrid>
        <StackPanel Grid.Row="1" Orientation="Horizontal">
            <Button Content="Show Data" Click="Button_Click" />
        </StackPanel>
    </Grid>
</Window>

グリッド中に詳細情報を表示する場合は、LoadingRowDetails イベントや RowDetailsTemplate プロパティを使います。上記の例では、次のように修正できます。

[XAML]
<DataGrid Grid.Row="0"
        ItemsSource="{Binding Persons}"
        AutoGenerateColumns="False"
        LoadingRowDetails="DataGrid_LoadingRowDetails">
    <DataGrid.Columns>
        <DataGridTextColumn Header="ID" Binding="{Binding ID}" IsReadOnly="True" />
        <DataGridTextColumn Header="Name" Binding="{Binding Name}" />
        <DataGridTextColumn Header="BirthDate" Binding="{Binding BirthDate, StringFormat=yyyy/MM/dd}" />
    </DataGrid.Columns>
    <DataGrid.RowDetailsTemplate>
        <DataTemplate>
            <DataGrid />
        </DataTemplate>
    </DataGrid.RowDetailsTemplate>
</DataGrid>

[コード]
private void DataGrid_LoadingRowDetails(object sender, DataGridRowDetailsEventArgs e)
{
    var dg = e.DetailsElement as DataGrid;
    if (dg != null)
    {
        var person = e.Row.DataContext as PersonInfo;
        if (person != null && person.Works != null)
            dg.ItemsSource = person.Works;
    }
}

RowDetailsTemplate プロパティが定義されている DataGrid で、データ行をクリックすると、LoadingRowDetails イベントが発生します。このイベントハンドラーの第2引数(DataGridRowDetailsEventArgs 型)の DetailsElement プロパティには、RowDetailsTemplate プロパティに割り当てられている要素(ここでは DataGrid)が渡されるため、この要素にデータを割り当てることができます。ここでは、行のデータコンテキスト(PersonInfo 型のデータが割り当てられている)に対応する詳細情報(Works)をそのまま DataGrid の ItemsSource に割り当てています。詳細表示のための DataGrid は何の設定もしていないものですが、すべてデフォルトの設定のまま詳細情報を表示するようになります。もちろん、内側の DataGrid に対しても AutoGenerateColumns を False にして、個別のカラムを設定したり、さまざまなスタイルを割り当てることができます。

行の内容によって、詳細情報を表示する形式を変えたい場合には RowDetailsTemplateSelector を使います。ここでは(あまり意味はありませんが)レコードの性別によって、形式を変えることを考えてみます。テンプレートセレクターは値コンバーターと同じようにクラスとして実装します。

[XAML]
<DataGrid Grid.Row="0"
        ItemsSource="{Binding Persons}"
        AutoGenerateColumns="False"
        LoadingRowDetails="DataGrid_LoadingRowDetails">
    <DataGrid.Columns>
        <DataGridTextColumn Header="ID" Binding="{Binding ID}" IsReadOnly="True" />
        <DataGridTextColumn Header="Name" Binding="{Binding Name}" />
        <DataGridTextColumn Header="BirthDate" Binding="{Binding BirthDate, StringFormat=yyyy/MM/dd}" />
    </DataGrid.Columns>
    <DataGrid.RowDetailsTemplateSelector>
        <local:DetailsTemplateSelector />
    </DataGrid.RowDetailsTemplateSelector>
</DataGrid>

[コード]
public class DetailsTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var dt = new DataTemplate(typeof(DataGrid));
        var factory = new FrameworkElementFactory(typeof(DataGrid));
        if (item != null && item is PersonInfo)
        {
            switch (((PersonInfo)item).Gender) {
            case gender_t.Male:
                factory.SetValue(DataGrid.ForegroundProperty, Brushes.Blue);
                break;
            case gender_t.Female:
                factory.SetValue(DataGrid.ForegroundProperty, Brushes.Red);
                break;
            }
        }
        dt.VisualTree = factory;
        return dt;
    }
}

DataTemplate は必要に応じてされる「テンプレート」であり、実体(オブジェクト)を返すものではないことに注意してください。呼び出し元は、この SelectTemplate メソッドが返すテンプレートを使ってオブジェクトを生成します。また、このような単純なものであれば、このようにプログラムで書くこともできますが、より複雑なテンプレートを作ろうとすると難しくなります。そこで、テンプレートを XAML でリソースとして定義し、これを返す方法についても紹介します。

以下は、DataGrid のリソース(Resources)として Gender 項目に対応する3種類のテンプレートを定義しています。Window のリソースとして定義しないのは、これらのテンプレートは、この DataGrid のためだけに使われるものであり、テンプレートセレクターを使って見つけやすくするためです。

[XAML]
<DataGrid Grid.Row="0"
        ItemsSource="{Binding Persons}"
        AutoGenerateColumns="False"
        LoadingRowDetails="DataGrid_LoadingRowDetails">
    <DataGrid.Resources>
        <DataTemplate x:Key="DetailsTemplateUnknown">
            <DataGrid />
        </DataTemplate>
        <DataTemplate x:Key="DetailsTemplateMale">
            <DataGrid Foreground="Blue" />
        </DataTemplate>
        <DataTemplate x:Key="DetailsTemplateFemale">
            <DataGrid Foreground="Red" />
        </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTextColumn Header="ID" Binding="{Binding ID}" IsReadOnly="True" />
        <DataGridTextColumn Header="Name" Binding="{Binding Name}" />
        <DataGridTextColumn Header="BirthDate" Binding="{Binding BirthDate, StringFormat=yyyy/MM/dd}" />
    </DataGrid.Columns>
    <DataGrid.RowDetailsTemplateSelector>
        <local:DetailsTemplateSelector />
    </DataGrid.RowDetailsTemplateSelector>
    </DataGrid>

リソースとして定義されたテンプレートを、条件に応じて切り替えるためには次のようなコードを書きます。container は詳細データを持つコンテナコントロールをあらわしますが、DataGrid そのものではないことに注意してください。ここには DataGridDetailsPresenter というオブジェクトが渡されるため、その親をたどって、リソースを保持する DataGrid を見つけ出す必要があります。

[コード]
public class DetailsTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item != null && item is PersonInfo)
        {
            var element = container as UIElement;
            while (element != null)
            {
                if (element is DataGrid)
                {
                    // DataGrid が見つかったら、リソース中から対応するテンプレートを返す
                    var dg = (DataGrid)element;
                    switch (((PersonInfo)item).Gender)
                    {
                    case gender_t.Male:
                        return dg.Resources["DetailsTemplateMale"] as DataTemplate;
                    case gender_t.Female:
                        return dg.Resources["DetailsTemplateFemale"] as DataTemplate;
                    default:
                        return dg.Resources["DetailsTemplateUnknown"] as DataTemplate;
                    }
                }
                element = VisualTreeHelper.GetParent(element) as UIElement;
            }
        }
        return null;
    }
}

Xaml21

Comment(0)