.NET Framework 3.5 SP1で、IEditableCollectionViewというものが追加されている。
これは、何かというとIEditableObjectを実装したクラスに対して色々便利な編集機能とかを提供してくれるらしい。
厳密にいうと、多分(ここから個人の妄想入ります)IEditableCollectionView自体はインターフェースなので、別にIEditableObjectを実装していないものに対しても素敵な編集機能を足すことは出来ると思う。
実装しだいでなんでもござれ。
ただ、現状IEditableCollectionViewを実装しているListCollectionView等は、IEditableObjectを前提としてるチック。
さて、ということで実際どうなってるのかを実験してみようと思う。
記事を書きながら実装してるので、前半と後半で文章のトーンが違ったりしてるかもしれないけど、そこはとりあえずスルーしてください。
WpfEditableObjectEduという名前でプロジェクトを作成して、いつもどおりのPersonクラスを作る。
namespace WpfEditableObjectEdu
{
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
}
}
次に、こいつをラップするIEditableObjectを実装したEditablePersonクラスを作る。
public class EditablePerson : IEditableObject, INotifyPropertyChanged
{
// オリジナル
private Person person;
// 編集中の一時データ保管所
private Person work;
public EditablePerson(Person person)
{
this.person = person;
}
public int ID
{
get
{
if (work != null)
{
return work.ID;
}
return person.ID;
}
set
{
if (work != null)
{
work.ID = value;
}
else
{
person.ID = value;
}
OnPropertyChanged("ID");
}
}
public string Name
{
get
{
if (work != null)
{
return work.Name;
}
return person.Name;
}
set
{
if (work != null)
{
work.Name = value;
}
else
{
person.Name = value;
}
OnPropertyChanged("Name");
}
}
#region IEditableObject メンバ
public void BeginEdit()
{
// 編集開始なので、値の一時保管場所を作ってコピー
work = new Person();
work.ID = person.ID;
work.Name = person.Name;
}
public void CancelEdit()
{
// 一時保管場所を破棄
work = null;
// 値が変わったのでイベント発行
OnPropertyChanged("ID");
OnPropertyChanged("Name");
}
public void EndEdit()
{
if (work == null)
{
// そもそも編集中じゃないので何もしない
return;
}
// 編集が終わったので一時保管場所からオリジナルへ値をコピー
person.ID = work.ID;
person.Name = work.Name;
// 編集終了なので一時保管場所を破棄
work = null;
// 値が変わったのでイベント発行
OnPropertyChanged("ID");
OnPropertyChanged("Name");
}
#endregion
#region INotifyPropertyChanged メンバ
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
ちょっと長いが、こんな感じで編集開始・キャンセル・編集終了に対応できてると思う。
正直かなりめんどくさい。
次に、EditablePersonをWindowsのDataContextにセットする。とりあえず100件程度作っておいた。
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = Enumerable.Range(1, 100).
// Personクラスを作って
Select(i => new Person { ID = i, Name = "田中 太郎" + i }).
// EditablePersonでラップする
Select(p => new EditablePerson(p));
}
}
今回は、このオブジェクトをListBoxで表示してみようと思う。XAMLはさくっと定義。
<Window x:Class="WpfEditableObjectEdu.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfEditableObjectEdu"
Title="EditableCollectionView Sample" Height="300" Width="300">
<Window.Resources>
<DataTemplate x:Key="personTemplate" DataType="local:EditablePerson">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ID}" />
<TextBlock Text=": " />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding}" ItemTemplate="{StaticResource personTemplate}" />
</Grid>
</Window>
この状態で実行してみると下のような画面になります。特に変わったところは無い!!
今度は、こいつに編集機能を付け足して行こうと思う。
イメージとしては、選択中の行が編集状態になって、名前をテキストボックスで編集できるようにしたい。
画面の上部にはキャンセルボタンがあって、それを押すと現在の編集中の内容は破棄されるとか。
ということで、編集中のDataTemplateをこさえる。Window.Resourcesに以下のDataTemplateを1つ追加する。
そして、画面上部にキャンセルボタンを配置する。最後にListBoxの選択行変更のタイミングで編集処理などをしたいのでSelectionChangedイベントも追加する。
<Window x:Class="WpfEditableObjectEdu.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfEditableObjectEdu"
Title="EditableCollectionView Sample" Height="300" Width="300">
<Window.Resources>
<DataTemplate x:Key="personTemplate" DataType="local:EditablePerson">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ID}" />
<TextBlock Text=": " />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="personEditTemplate" DataType="local:EditablePerson">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ID}" />
<TextBlock Text=": " />
<TextBox Text="{Binding Name}" MinWidth="150"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<Button
Name="buttonCancel"
Content="キャンセル"
Margin="5"
Click="buttonCancel_Click"/>
</StackPanel>
<ListBox
Name="listBox"
Grid.Row="1"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource personTemplate}" SelectionChanged="listBox_SelectionChanged"/>
</Grid>
</Window>
後は、IEditableCollectionViewのAPIを使ってしこしこ編集中の状態を制御していく。
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace WpfEditableObjectEdu
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = Enumerable.Range(1, 100).
// Personクラスを作って
Select(i => new Person { ID = i, Name = "田中 太郎" + i }).
// EditablePersonでラップする
Select(p => new EditablePerson(p)).
// List化(ListCollectionViewを使いたいから必須)
ToList();
}
// DataContextに入ってるIEditableCollectionViewを取得
private IEditableCollectionView GetEditableView()
{
return CollectionViewSource.GetDefaultView(DataContext) as IEditableCollectionView;
}
private void buttonCancel_Click(object sender, RoutedEventArgs e)
{
// キャンセル処理をして、ListBoxを未選択状態にする。
Cancel(GetEditableView());
listBox.SelectedIndex = -1;
}
private void Edit(IEditableCollectionView view)
{
// 別の行の編集をする前に、直前の編集をコミット
Commit(view);
object currentItem = listBox.SelectedItem;
if (currentItem == null)
{
return;
}
// 現在の選択行を編集状態にする(テンプレートも差し替え)
view.EditItem(currentItem);
ChangeTemplate(view.CurrentEditItem, "personEditTemplate");
}
private void Commit(IEditableCollectionView view)
{
// 現在の編集をコミット
var currentEditItem = GetCurrentEditItem(view);
if (currentEditItem == null)
{
return;
}
view.CommitEdit();
// テンプレートを表示専用に差し替え
ChangeTemplate(currentEditItem, "personTemplate");
}
private void Cancel(IEditableCollectionView view)
{
// 現在の編集をキャンセル
var currentEditItem = GetCurrentEditItem(view);
if (currentEditItem == null)
{
return;
}
view.CancelEdit();
// テンプレートを表示専用に差し替え
ChangeTemplate(currentEditItem, "personTemplate");
}
// 現在編集中のアイテムを返す
private object GetCurrentEditItem(IEditableCollectionView view)
{
if (view == null)
{
return null;
}
return view.CurrentEditItem;
}
// リストボックスのアイテムのテンプレートを差し替える
private void ChangeTemplate(object currentEditItem, string templateName)
{
var currentListBoxItem = (ListBoxItem) listBox.ItemContainerGenerator.ContainerFromItem(currentEditItem);
currentListBoxItem.ContentTemplate = (DataTemplate)FindResource(templateName);
}
private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// 編集開始
Edit(GetEditableView());
}
}
}
Edit, Cancel, CommitメソッドでIEditableCollectionViewのAPIをメインに使ってる。
EditItem(object)とCanecelEdit(), CommitEdit()が主なメソッドになる。このほかにも行の追加とかもあるけど、ここでは使ってない。
ここら辺は、DataGridに任せてしまったほうがきっと楽できること間違いなし!
実行結果は下のような感じになる。
実行直後
適当な行を選択した状態(テキストボックスになってる)
データを編集して
別の行を選択(6の部分は、編集内容確定)
7の部分を適当に編集して
キャンセルボタンを押すと、元に戻る
簡単に試しただけでなんとなく動くようになったけど、まだまだ実用はできない。
バリデーションやコンバータとの組み合わせや、BindingGroupや入力エラー時の動きとの兼ね合いとかを考えると、やることはいっぱいありそうだ。
多分、今後につづく…。