WPF:DataGridやListViewなどにデータをソートして表示するには?[XAML、C#、VB].NET TIPS

WPFアプリでDataGridコントロールやListViewコントロールに表示されるデータを、プログラムコードの側からソートして表示する方法を解説する。

» 2014年10月21日 12時48分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Platform Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 4.0以降


 WPFには、データのコレクションを表示するためのコントロールが用意されている。DataGridコントロールやListViewコントロールなどがそうだ(いずれもSystem.Windows.Controls名前空間)。そこに表示するデータをソートしておくにはどうしたらよいだろうか? もちろん、与えるデータをソートしておけば可能なのだが、ソート順(昇順/降順)やソート対象の項目を変更するたびにデータを作り直すのは非効率である。CollectionViewSourceクラス(System.Windows.Data名前空間)を利用すれば、元のデータを並べ替えたり削除したりすることなく、表示だけをソート/フィルタリング/グルーピングできる。本稿では、CollectionViewSourceクラスを使って、ソートして表示する方法を解説する。

 なお、本稿のプログラミングには、無償のVisual Studio Express 2012 for Windows Desktop(以降、VS 2012)を使用した。Visual Studio 2013でも手順は同じである。

事前準備

 簡単なデータを表示/編集できるプログラムを用意する(次の画像)。画面の左側にはDataGridコントロールを、右側にはListViewコントロールを、画面下部にはデータを追加/編集するための簡単なUIを配置する。

ベースとなるプログラムを起動した画面(Windows 7) ベースとなるプログラムを起動した画面(Windows 7)
この画面で、例えば[VALUE]の昇順([Blue]→[Green]→[Red])に並べ替えて表示したい。どうしたらよいだろうか?

 少々長くなるが、以下の手順に従ってコーディングしてほしい。SampleDataクラスの新設、MainWindowクラスへのUIの追加(XAML)、そしてMainWindowクラスのコードビハインドへのコードの追加(C#またはVB)を行う。

 まず、Visual StudioでWPFアプリのプロジェクトを新しく作成したら、そこに新しくクラスのファイルを追加し、その名前を「SampleData.cs/.vb」とする。ファイルの内容を次のコードのように書き換える。

using System.Collections.ObjectModel;
using System.ComponentModel;

namespace dotNetTips1082CS //←ここはプロジェクトによって異なる
{
  // 表示する個々のデータ(データバインド可能)
  public class SampleData : INotifyPropertyChanged
  {
    // Indexプロパティ
    string _index;
    public string Index
    {
      get { return _index; }
      set{_index = value; OnPropertyChanged("Index");}
    }

    // Valueプロパティ
    string _value;
    public string Value
    {
      get { return _value; }
      set { _value = value; OnPropertyChanged("Value"); }
    }

    // INotifyPropertyChangedインターフェースの実装
    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged(string pName)
    {
      var handler = this.PropertyChanged;
      if (handler != null)
        handler(this, new PropertyChangedEventArgs(pName));
    }
  }

  // 表示するデータのコレクション(データバインド可能)
  public class SampleDataCollection : ObservableCollection<SampleData>
  {
    public SampleDataCollection()
    { 
      // 初期データ
      this.Add(new SampleData(){ Index="1", Value="Red"});
      this.Add(new SampleData(){ Index="2", Value="Green"});
      this.Add(new SampleData(){ Index="3", Value="Blue"});
    }
  }
}

Imports System.ComponentModel
Imports System.Collections.ObjectModel

' 表示する個々のデータ(データバインド可能)
Public Class SampleData
  Implements INotifyPropertyChanged

  ' Indexプロパティ
  Private _index As String
  Public Property Index As String
    Get
      Return _index
    End Get
    Set(value As String)
      _index = value : OnPropertyChanged("Index")
    End Set
  End Property

  ' Valueプロパティ
  Private _value As String
  Public Property Value As String
    Get
      Return _value
    End Get
    Set(value As String)
      _value = value : OnPropertyChanged("Value")
    End Set
  End Property

  ' INotifyPropertyChangedインターフェースの実装
  Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) _
    Implements INotifyPropertyChanged.PropertyChanged
  Private Sub OnPropertyChanged(pName As String)
    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(pName))
  End Sub
End Class

' 表示するデータのコレクション(データバインド可能)
Public Class SampleDataCollection
  Inherits ObservableCollection(Of SampleData)

  Public Sub New()
    ' 初期データ
    Me.Add(New SampleData() With {.Index = "1", .Value = "Red"})
    Me.Add(New SampleData() With {.Index = "2", .Value = "Green"})
    Me.Add(New SampleData() With {.Index = "3", .Value = "Blue"})
  End Sub
End Class

「SampleData.cs/.vb」ファイルの内容(上:C#、下:VB)
C#のコードで「namespace」に指定されている名前空間は自動生成されたもの。これは作成したプロジェクトの名前によって変化する。
なお、このVBのコードでは、マルチステートメント(1行に複数のステートメントを記述する)や、Visual Basic 2008から利用できるようになったオブジェクト初期化子を使用している。

 次に、「MainWindow.xaml」ファイルを開き、ファイルの内容を次のコードのように書き換える。

<Window x:Class="dotNetTips1082CS.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"
        mc:Ignorable="d"
        Title=".NET TIPS #1082" Height="300" Width="350"
        MinHeight="200" MinWidth="300"
        Loaded="Window_Loaded"
        >
  <d:DesignProperties.DataContext>
    <SampleDataCollection xmlns="clr-namespace:dotNetTips1082CS" />
    <!-- 上の行の名前空間の指定(=dotNetTips1082CS)はプロジェクトによって異なる。
         前述のSampleDataCollectionクラスの名前空間にする -->
  </d:DesignProperties.DataContext>
  
  <Grid x:Name="RootGrid" Background="LightGray">
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <!-- DataGrid(画面左側):双方向バインド(UI上での変更がデータに反映される) -->
    <DataGrid x:Name="DataGrid1"
              AutoGenerateColumns="False" HorizontalAlignment="Left" 
              ItemsSource="{Binding DataContext, Mode=TwoWay, RelativeSource={RelativeSource Self}}" 
              SelectionChanged="DataGrid1_SelectionChanged"
              >
      <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Index}" Header="INDEX" MinWidth="50" />
        <DataGridTextColumn Binding="{Binding Value}" Header="VALUE" MinWidth="150" />
      </DataGrid.Columns>
    </DataGrid>

    <!-- ListView(画面右側):一方向バインド(UI上での変更はデータに反映されない) -->
    <ListView Grid.Column="1" Margin="10,0,0,0"
              ItemsSource="{Binding DataContext, Mode=OneWay, RelativeSource={RelativeSource Self}}"
              DisplayMemberPath="Value" />

    <!-- 画面下部のデータ編集用UI -->
    <StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Horizontal">
      <TextBlock VerticalAlignment="Center">INDEX</TextBlock>
      <TextBox x:Name="TextBoxIndex" Width="30" />
      <TextBlock VerticalAlignment="Center" Margin="10,0,0,0">VALUE</TextBlock>
      <TextBox x:Name="TextBoxValue" Width="70" />
      <Button Click="Button_Click" Content="Add/Renew" Margin="10,0,0,0" />
    </StackPanel>
  </Grid>
</Window>

「MainWindow.xaml」ファイルにUIを記述する(XAML)
冒頭の<Windows>タグの中身にも追加/変更があるので注意。特に太字にした部分を忘れないよう。また、<Windows>タグの「x:Class」属性の値は、プロジェクトごとに異なる。
なお、データとコントロールを結び付けるために「データバインディング」の仕組みを利用している。詳しくは「連載:WPF入門:第5回 WPFの「データ・バインディング」を理解する」などを参照してほしい。

 最後に、「MainWindow.xaml.cs/.vb」ファイルを開き、ファイルの内容を次のコードのように書き換える。

using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace dotNetTips1082CS
{
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();
    }

    // 表示するデータ
    private SampleDataCollection _data = new SampleDataCollection();

    // 画面が表示されるとき、データを画面にセットする
    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
      // データをそのままセットする
      this.RootGrid.DataContext = _data;
    }

    // DataGrid1で選択行が移動したら、下端のテキストボックスにその値を反映させる
    private void DataGrid1_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
      var selectedData = this.DataGrid1.SelectedItem as SampleData;
      if (selectedData == null)
      {
        this.TextBoxIndex.Text = string.Empty;
        this.TextBoxValue.Text = string.Empty;
      }
      else
      {
        this.TextBoxIndex.Text = selectedData.Index;
        this.TextBoxValue.Text = selectedData.Value;
      }
    }

    // ボタンがクリックされたら、下端のテキストボックスの内容をデータに反映させる
    private void Button_Click(object sender, RoutedEventArgs e)
    {
      string index = this.TextBoxIndex.Text;
      if (string.IsNullOrWhiteSpace(index))
        return;

      string value = this.TextBoxValue.Text;

      var currentData = _data.FirstOrDefault(d => d.Index == index);
      if (currentData == null)
        _data.Add(new SampleData() { Index = index, Value = value });
      else
        currentData.Value = value;
    }
  }
}

Class MainWindow 

  ' 表示するデータ
  Private _data As SampleDataCollection = New SampleDataCollection()

  ' 画面が表示されるとき、データを画面にセットする
  Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
    ' データをそのままセットする
    Me.RootGrid.DataContext = _data
  End Sub

  ' DataGrid1で選択行が移動したら、下端のテキストボックスにその値を反映させる
  Private Sub DataGrid1_SelectionChanged(sender As Object, e As SelectionChangedEventArgs)
    Dim selectedData = TryCast(Me.DataGrid1.SelectedItem, SampleData)
    If (selectedData Is Nothing) Then
      Me.TextBoxIndex.Text = String.Empty
      Me.TextBoxValue.Text = String.Empty
    Else
      Me.TextBoxIndex.Text = selectedData.Index
      Me.TextBoxValue.Text = selectedData.Value
    End If
  End Sub

  ' ボタンがクリックされたら、下端のテキストボックスの内容をデータに反映させる
  Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
    Dim index As String = Me.TextBoxIndex.Text
    If (String.IsNullOrWhiteSpace(index)) Then
      Return
    End If

    Dim value As String = Me.TextBoxValue.Text

    Dim currentData = _data.FirstOrDefault(Function(d) d.Index = index)
    If (currentData Is Nothing) Then
      _data.Add(New SampleData() With {.Index = index, .Value = value})
    Else
      currentData.Value = value
    End If
  End Sub
End Class

「MainWindow.xaml.cs/.vb」ファイルを変更する(上:C#、下:VB)
このVBのコードでは、Visual Basic 2008から利用できるようになった単一行のラムダ式オブジェクト初期化子を使用している。

 以上で、プログラムの準備は完了だ。実行すると、冒頭の画像のようにデータが表示される([VALUE]が[Red]→[Green]→[Blue]の順になっている)。これを[VALUE]の昇順にソートして表示([Blue]→[Green]→[Red])にしたいのだ。

DataGridコントロールのソート機能

 ここでちょっと寄り道をして、DataGridコントロールに備わっているソート機能を見ておこう。

 DataGridコントロールのヘッダー部分をクリックすると、その列のデータで昇順/降順に並べ替えることができる(次の画像)。

ヘッダー部分(赤丸)をクリック
ヘッダー部分(赤丸)をクリック
DataGridコントロールのソート機能(Windows 7) DataGridコントロールのソート機能(Windows 7)
ヘッダー部分(赤丸)をクリックすると、その列のデータで昇順にソートされる(上)。
もう一度クリックすると、降順に切り替わる(下)。
なお、DataGridコントロール上で行った並べ替えは、データバインディングによって元データに反映され、それが再びデータバインディングによって右側のListViewコントロールにも反映される。

 また、ソートした状態であれば、プログラムからデータを追加したときに、正しくソートされた位置に挿入される(次の画像)。

[INDEX]テキストボックスと[VALUE]テキストボックスに値を入力して[Add/Renew]ボタンをクリック
[INDEX]テキストボックスと[VALUE]テキストボックスに値を入力して[Add/Renew]ボタンをクリック
DataGridコントロールでソートが有効のときにデータを追加する(Windows 7) DataGridコントロールでソートが有効のときにデータを追加する(Windows 7)
ヘッダーで[VALUE]の昇順にソートしておき、新しいデータとして「Orange」を追加してみよう。画面下端のUIで[INDEX]テキストボックスに「4」、[VALUE]テキストボックスに「Orange」と入力して、[Add/Renew]ボタン(赤丸)をクリックすると、コードビハインドのプログラムで元データのコレクションにそれが追加される(上)。
DataGridコントロールでソートが有効になっていると、正しくソートされた位置に新しいデータが挿入される(下、赤枠内)。
なお、画像にはないが、DataGridコントロール上でエンドユーザーがデータを書き換えた場合も、正しくソートされた位置にデータが移動される。

 エンドユーザーが操作してソートできればよいのであれば、上記のプログラムでも十分だろう。ただし、ListViewコントロール(画面の右側)には、この機能はない。また、DataGridコントロールであっても、表示したときに(エンドユーザーの操作なしで)ソートしたいのであれば、やはりプログラムで対応する必要がある。

データをソートして表示するには?

 ソートした状態で表示するには、CollectionViewSourceクラスを使えばよい。

 正確には、CollectionViewSourceクラスを使って、元のデータコレクションから既定のビューを取り出し、それをコントロールにバインドする。そして、既定のビューに対して並べ替えを設定するのである。具体的には、次のコードのように、「MainWindow.xaml.cs/.vb」ファイルのWindow_Loadedメソッドを変更する。

// 画面が表示されるとき、データを画面にセットする
private void Window_Loaded(object sender, RoutedEventArgs e)
{
  // データをそのままセットする
  //this.RootGrid.DataContext = _data;
  // 上の行を削除し、以下のように書き換える

  // 既定のビューを取り出してセットする
  var view = CollectionViewSource.GetDefaultView(_data);
  this.RootGrid.DataContext = view;

  // 既定のビューにソートを指定する
  view.SortDescriptions.Add(
    new System.ComponentModel.SortDescription(
          "Value",
          System.ComponentModel.ListSortDirection.Ascending)
        );
  // DataGridコントロールのヘッダーにソートの印(三角のマーク)を表示する
  this.DataGrid1.Columns[1].SortDirection 
    = System.ComponentModel.ListSortDirection.Ascending;
}

' 画面が表示されるとき、データを画面にセットする
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
  ' データをそのままセットする
  'Me.RootGrid.DataContext = _data
  ' 上の行を削除し、以下のように書き換える

  ' 既定のビューを取り出してセットする
  Dim view = CollectionViewSource.GetDefaultView(_data)
  Me.RootGrid.DataContext = view

  ' 既定のビューにソートを指定する
  view.SortDescriptions.Add(
    New System.ComponentModel.SortDescription(
          "Value",
          System.ComponentModel.ListSortDirection.Ascending)
        )
  ' DataGridコントロールのヘッダーにソートの印(三角のマーク)を表示する
  Me.DataGrid1.Columns(1).SortDirection _
    = System.ComponentModel.ListSortDirection.Ascending
End Sub

データの既定のビューをバインドし、ソートを設定するコード(上:C#、下:VB)
DataGridコントロールのヘッダーにソートの印を表示するためのSortDirectionプロパティは、.NET Framework 4で導入されたものである。それを除けば、このコードはWPFの最初のバージョンから利用できるはずだ。
また、別解として、DataGridコントロールが内部的に自動生成している既定のビュー(これによって前述した手動によるソートが実現されている)を利用しても同じ結果が得られる。すなわち、view.SortDescriptionsではなく、DataGrid1.Items.SortDescriptionsにソート指定を行ってもよい(CollectionViewSourceクラスで既定のビューを取り出す必要はない)。どちらの方法を採るかは自由だが、1つのプログラムの中では統一しておいた方がよいだろう。
なお、このVBのコードでは、Visual Basic 2010から利用できるようになった「暗黙の行連結」を使用している。

 このようにすれば、プログラムを起動したときからソートされた状態で表示される(次の画像)。同様にして、実行中にプログラムからソート順を切り替えることも可能である。

起動したときからソートされている(Windows 7) 起動したときからソートされている(Windows 7)

利用可能バージョン:.NET Framework 4.0以降
カテゴリ:WPF 処理対象:DataGridコントロール、ListViewコントロール
使用ライブラリ:CollectionViewSourceクラス(System.Windows.Data名前空間)


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。