WPF:DataGridやListViewなどにデータをソートして表示するには?[XAML、C#、VB]:.NET TIPS
WPFアプリでDataGridコントロールやListViewコントロールに表示されるデータを、プログラムコードの側からソートして表示する方法を解説する。
対象:.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を配置する。
少々長くなるが、以下の手順に従ってコーディングしてほしい。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
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>
冒頭の<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
このVBのコードでは、Visual Basic 2008から利用できるようになった単一行のラムダ式やオブジェクト初期化子を使用している。
以上で、プログラムの準備は完了だ。実行すると、冒頭の画像のようにデータが表示される([VALUE]が[Red]→[Green]→[Blue]の順になっている)。これを[VALUE]の昇順にソートして表示([Blue]→[Green]→[Red])にしたいのだ。
DataGridコントロールのソート機能
ここでちょっと寄り道をして、DataGridコントロールに備わっているソート機能を見ておこう。
DataGridコントロールのヘッダー部分をクリックすると、その列のデータで昇順/降順に並べ替えることができる(次の画像)。
DataGridコントロールのソート機能(Windows 7)
ヘッダー部分(赤丸)をクリックすると、その列のデータで昇順にソートされる(上)。
もう一度クリックすると、降順に切り替わる(下)。
なお、DataGridコントロール上で行った並べ替えは、データバインディングによって元データに反映され、それが再びデータバインディングによって右側のListViewコントロールにも反映される。
また、ソートした状態であれば、プログラムからデータを追加したときに、正しくソートされた位置に挿入される(次の画像)。
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
DataGridコントロールのヘッダーにソートの印を表示するためのSortDirectionプロパティは、.NET Framework 4で導入されたものである。それを除けば、このコードはWPFの最初のバージョンから利用できるはずだ。
また、別解として、DataGridコントロールが内部的に自動生成している既定のビュー(これによって前述した手動によるソートが実現されている)を利用しても同じ結果が得られる。すなわち、view.SortDescriptionsではなく、DataGrid1.Items.SortDescriptionsにソート指定を行ってもよい(CollectionViewSourceクラスで既定のビューを取り出す必要はない)。どちらの方法を採るかは自由だが、1つのプログラムの中では統一しておいた方がよいだろう。
なお、このVBのコードでは、Visual Basic 2010から利用できるようになった「暗黙の行連結」を使用している。
このようにすれば、プログラムを起動したときからソートされた状態で表示される(次の画像)。同様にして、実行中にプログラムからソート順を切り替えることも可能である。
利用可能バージョン:.NET Framework 4.0以降
カテゴリ:WPF 処理対象:DataGridコントロール、ListViewコントロール
使用ライブラリ:CollectionViewSourceクラス(System.Windows.Data名前空間)
Copyright© Digital Advantage Corp. All Rights Reserved.