【WPF】DataGridに編集可能なComboBoxを表示するには?
- 2014.03.01 Saturday
- 14:06
-----------------------------------------------------------------------------------
久々にWPFネタを書いてみようと思います。今回は「DataGridに編集可能なComboBoxを表示するには?」というタイトルです。ターゲットは.NET Framework 4.5.1です。大昔にWPFTookit(最新版ではないと思います)で同じことを実現するコードは紹介したような気がしまうが、正式に.NET FrameworkがDataGridをサポートするようになってからは、技術情報を載せた記憶がないので、これが初めてだと思います。そんなことはどうでもいいのですが、重要な要点は、「過去の記事は古いから参考にならないよ」ってところですかね。まるで他人事みたいな言い方でごめんなさい。
前置きは、これぐらいにしておいて、今回実現することは、以下の要件を満たすことです。
1.DataGridで値を表示する。
2.セルは編集可能であること。
3.編集中のセルはComboBox(ドロップダウンリストが表示されるコントロール)であること。
4.編集中のセルは手入力可能であること。
この要件を満たすにあたり技術的な問題が発生します。いや、無知だと発生するというだけです。何かしらの解決策をご存知の方は発生しません。いや、本当。無知な私は以下の問題が発生しました。
いろいろ試行錯誤したんですが、「いろいろ」なのでとりあえず、シンプルに書いたコードだけ紹介します。
<Window x:Class="ComboBoxDataGrid20140301_001.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:y="clr-namespace:ComboBoxDataGrid20140301_001"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<!-- 既定のDataGridのスタイル -->
<Style TargetType="DataGrid">
<Setter Property="AutoGenerateColumns" Value="False" />
<Setter Property="CanUserAddRows" Value="False" />
<Setter Property="CanUserDeleteRows" Value="False" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DataGrid x:Name="SampleDataGrid">
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Column1"
TextBinding="{Binding Column1, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
/>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Click="OnContentChanged">メモリ上のデータを表示する。</Button>
</Grid>
</Window>
using System.Data;
using System.Linq;
using System.Text;
using System.Windows;
namespace ComboBoxDataGrid20140301_001
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
var dataTable = new DataTable() { TableName = "SampleData" };
dataTable.Columns.Add("Column1");
dataTable.Columns.Add("Column2");
dataTable.Columns.Add("Column3");
Enumerable.Range(0, 10)
.Select(n => new object[] { "A" + n, "B" + n, "C" + n })
.ToList()
.ForEach(item => dataTable.Rows.Add(item));
SampleDataGrid.ItemsSource = dataTable.DefaultView;
}
private void OnContentChanged(object sender, RoutedEventArgs e)
{
string message = SampleDataGrid
.ItemsSource
.OfType<DataRowView>()
.Select(rowView => string.Join(", ", rowView.Row.ItemArray))
.Aggregate(new StringBuilder(), (sb, line) => sb.AppendLine(line))
.ToString();
MessageBox.Show(message);
}
}
}
上記のコードを実行してみると、要件を全然満たせていないことがわかります。
1.参照モードで値が表示されていない。
2.編集開始時に値が表示さていない。
3.編集中のComboBoxコントロールは手入力できない。
唯一編集中はセルがComboBoxになるという点だけ要件を満たせています。うん。どうしたものかといところです。この問題を解決するには、大きく分けて二つ手があると思います。DataGridComoBoxColumnのEditingElementStyleおよびElementStyleからStyleを変更し、要件を満たす。もう一つはDataGridComboBoxColumnの利用をやめて、DataGridTemplateColumnを利用する。今回はStyleを編集する方向で、実現することにしました。
変更したソースは以下です。
<Window x:Class="ComboBoxDataGrid20140301_001.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:y="clr-namespace:ComboBoxDataGrid20140301_001"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<y:TextBlockComboBoxItemsSourceConverter x:Key="TextBlockComboBoxItemsSourceConverterKey" />
<!-- 編集モード時のComboBox -->
<Style TargetType="ComboBox" x:Key="EditingElementStyle">
<Setter Property="IsEditable" Value="True" />
</Style>
<!-- 参照モード時のComboBox -->
<Style TargetType="ComboBox" x:Key="TextBlockComboBoxStyle">
<!--<Setter Property="Text" Value="{Binding Column1}" />-->
<Setter Property="ItemsSource"
Value="{Binding .,
Converter={StaticResource TextBlockComboBoxItemsSourceConverterKey},
ConverterParameter=Column1}" />
</Style>
<!-- 既定のDataGridのスタイル -->
<Style TargetType="DataGrid">
<Setter Property="AutoGenerateColumns" Value="False" />
<Setter Property="CanUserAddRows" Value="False" />
<Setter Property="CanUserDeleteRows" Value="False" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DataGrid x:Name="SampleDataGrid">
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Column1"
TextBinding="{Binding Column1, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
EditingElementStyle="{StaticResource EditingElementStyle}"
ElementStyle="{StaticResource TextBlockComboBoxStyle}"
/>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Click="OnContentChanged">メモリ上のデータを表示する。</Button>
</Grid>
</Window>
using System;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;
namespace ComboBoxDataGrid20140301_001
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
var dataTable = new DataTable { TableName = "SampleData" };
dataTable.Columns.Add("Column1");
dataTable.Columns.Add("Column2");
dataTable.Columns.Add("Column3");
Enumerable.Range(0, 10)
.Select(n => new object[] { "A" + n, "B" + n, "C" + n })
.ToList()
.ForEach(item => dataTable.Rows.Add(item));
SampleDataGrid.ItemsSource = dataTable.DefaultView;
}
private void OnContentChanged(object sender, RoutedEventArgs e)
{
string message = SampleDataGrid
.ItemsSource
.OfType<DataRowView>()
.Select(rowView => string.Join(", ", rowView.Row.ItemArray))
.Aggregate(new StringBuilder(), (sb, line) => sb.AppendLine(line))
.ToString();
MessageBox.Show(message);
}
}
public class TextBlockComboBoxItemsSourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string columnName = (parameter as string) ?? string.Empty;
var dataRowView = value as DataRowView;
if (dataRowView == null
|| dataRowView.Row == null
|| !dataRowView.Row.Table.Columns.Contains(columnName))
{
return DependencyProperty.UnsetValue;
}
return new[] { dataRowView.Row[columnName] };
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
すべてのを説明するのは面倒なので、要点だけ書くと
1.ComboBoxを手入力可能な設定にした。IsEditable=True
2.参照モードのComboBoxに値を表示できる値は、候補の値に限られているため
参照モードのみ候補に値を一件仕込んだ。
<Setter Property="ItemsSource" ... />
上記のことをすれば、参照モードでは、値が表示され、編集中は手入力可能なComboBoxが表示されるようになります。補足としては、データソースにDataTableを利用しています。それぞれのカラムにプリミティブな値しか設定しませんが、データソース側に多少の努力を加えると、IValueConverterあたりの下りは不要になるかもしれません。この辺は開発・保守の観点から費用対効果で結論を出すべきところですかね。今回は、知ってる知識で最短で組める方法でがんばってみました。
今回はComboBoxに候補を出すということはしていませんが、これに関しても色々方法があり、満たしたい要件で実装方法、難易度(知ってれば難易度なんてあがりませんが)が上がります。
以上で、今回は以上で終わります。久々にBlogを書くこと書き方忘れますね。
-----------------------------
(追記 2014/03/05)
Styleに不要なSetterがあったため修正しました。
誤)
<Setter Property="Text" Value="{Binding Column1}" />
正)
<!--<Setter Property="Text" Value="{Binding Column1}" />-->
TextBindingで上書きされる項目なので指定しても無効となります。
datagridは主に表データを表示するものだと思うのですが、行ごとにdataGridComboBoxに違う属性のデータを選択させるという感じで作りたいと思っています。そして選ばれたら横の列にtext反映させるというやり方で作りたいと思ってます
よろしくお願いします
↑
こういうところに質問したら親切におしえてくれますよ。