かずきのBlog

C#やJavaやRubyとメモ書き

目次

Blog 利用状況

ニュース

わんくまBlogが不安定になったため、前に書いてたはてなダイアリーにメインを移動します。
かずきのBlog@Hatena
技術的なネタは、こちらにも、はてなへのリンクという形で掲載しますが、雑多ネタははてなダイアリーだけに掲載することが多いと思います。
コメント
プログラマ的自己紹介
お気に入りのツール/IDE
プロフィール
経歴
アクセサリ

書庫

日記カテゴリ

[WPF][C#]IEditableObjectの愚直な実装とIEditableCollectionView

.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>

この状態で実行してみると下のような画面になります。特に変わったところは無い!!
image

今度は、こいつに編集機能を付け足して行こうと思う。
イメージとしては、選択中の行が編集状態になって、名前をテキストボックスで編集できるようにしたい。
画面の上部にはキャンセルボタンがあって、それを押すと現在の編集中の内容は破棄されるとか。

ということで、編集中の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に任せてしまったほうがきっと楽できること間違いなし!

実行結果は下のような感じになる。

実行直後
image

適当な行を選択した状態(テキストボックスになってる)
image

データを編集して
image

別の行を選択(6の部分は、編集内容確定)
image

7の部分を適当に編集して
image

キャンセルボタンを押すと、元に戻る
image 

 

簡単に試しただけでなんとなく動くようになったけど、まだまだ実用はできない。
バリデーションやコンバータとの組み合わせや、BindingGroupや入力エラー時の動きとの兼ね合いとかを考えると、やることはいっぱいありそうだ。

多分、今後につづく…。

投稿日時 : 2008年12月14日 23:31

Feedback

# [C#][WPF]IEditableCollectionViewの動きを見てみよう 2008/12/24 22:34 かずきのBlog

[C#][WPF]IEditableCollectionViewの動きを見てみよう

タイトル
名前
Url
コメント