読者です 読者をやめる 読者になる 読者になる

SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

WPF用にシンプルなコレクションエディタを作ってみた

C# WPF

ちょっと必要に迫られて、コレクションエディタ的なものを作ってみました。

extended wpf toolkitのCollectionEditorほどのカッチリしたエディタじゃなくて、「ちょろっとコレクションの編集したいだけ」、って位の時に使えるかと思います。

f:id:minami_SC:20150425170721p:plain
コレクションを任意のテンプレートで表示したり、アイテムの追加/削除を出来るようにしています。
コントロール下部の「+」ボタンを押すと要素の追加。各アイテム右側の「×」ボタンを押すと、要素の削除ができます。

使い方

SimpleCollectionEditorに定義した依存関係プロパティは以下の通りです。

依存関係プロパティ 内容
ItemsSource 編集対象のコレクションを指定するためのプロパティ
ItemTemplate コレクションの表示方法をDataTemplateで設定するためのプロパティ
ItemType コレクションの各アイテムの型を指定するプロパティ


コントロールの使い方のイメージはこんな感じ。
f:id:minami_SC:20150425170913p:plain


ItemsSourceにコレクションのプロパティをバインドし、
ItemTypeにコレクションの各要素の型を指定します。
また、各要素を表示するためのDataTemplateの指定もします。

コード

SimpleCollectionEditor

今回作ったコントロールのコードは以下の通りです。

SimpleCollectionEditor.xaml
<UserControl x:Class="WpfBaseTemplate1.SimpleCollectionEditor"
             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"
             d:DesignHeight="300"
             d:DesignWidth="300"
             mc:Ignorable="d">
    <UserControl.Resources>
        <!-- 既定の表示スタイル。ContentControlでそのまま表示する。 -->
        <DataTemplate x:Key="defaultTemplate">
            <ContentControl Content="{Binding}" />
        </DataTemplate>

        <!-- 「×」ボタン付きで要素を表示するためのテンプレート -->
        <DataTemplate x:Key="removeableTemplate">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <ContentControl Content="{Binding}"
                                ContentTemplate="{Binding ItemTemplate,
                                                          RelativeSource={RelativeSource FindAncestor,
                                                                                         AncestorType={x:Type UserControl}}}" />
                <Button Grid.Column="1"
                        Margin="5"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Command="ApplicationCommands.Delete"
                        CommandParameter="{Binding}"
                        Content="r"
                        FontFamily="Marlett" />
            </Grid>
        </DataTemplate>
    </UserControl.Resources>
    <UserControl.CommandBindings>
        <CommandBinding Command="ApplicationCommands.Delete"
                        Executed="CommandBinding_Executed" />
    </UserControl.CommandBindings>

    <Grid x:Name="root">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ItemsControl ItemTemplate="{StaticResource removeableTemplate}"
                      ItemsSource="{Binding ItemsSource}"
                      ScrollViewer.VerticalScrollBarVisibility="Auto">
            <ItemsControl.Template>
                <ControlTemplate>
                    <ScrollViewer>
                        <ItemsPresenter />
                    </ScrollViewer>
                </ControlTemplate>
            </ItemsControl.Template>
        </ItemsControl>
        <Button Grid.Row="1"
                Width="75"
                Margin="5"
                HorizontalAlignment="Center"
                Click="Button_Click"
                Content="+" />
    </Grid>
</UserControl>
SimpleCollectionEditor.xaml.cs
    /// <summary>
    /// SimpleCollectionEditor.xaml の相互作用ロジック
    /// </summary>
    public partial class SimpleCollectionEditor : UserControl
    {
        public IList ItemsSource
        {
            get { return (IList)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }
        // Using a DependencyProperty as the backing store for ItemsSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(IList), typeof(SimpleCollectionEditor), new FrameworkPropertyMetadata(null));

        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }
        // Using a DependencyProperty as the backing store for ItemTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(SimpleCollectionEditor), new PropertyMetadata(null));

        public Type ItemType
        {
            get { return (Type)GetValue(ItemTypeProperty); }
            set { SetValue(ItemTypeProperty, value); }
        }
        // Using a DependencyProperty as the backing store for ItemType.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemTypeProperty =
            DependencyProperty.Register("ItemType", typeof(Type), typeof(SimpleCollectionEditor), new PropertyMetadata(null));


        public SimpleCollectionEditor()
        {
            InitializeComponent();
            root.DataContext = this;
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if(this.ItemTemplate == SimpleCollectionEditor.ItemTemplateProperty.DefaultMetadata.DefaultValue)
            {
                this.ItemTemplate = this.FindName("defaultTemplate") as DataTemplate;
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if (this.ItemType == null)
            {
                System.Diagnostics.Trace.WriteLine("コレクション要素の型が指定されていません。");
                return;
            }
            if (this.ItemsSource == null)
            {
                System.Diagnostics.Trace.WriteLine("ItemsSourceが空のため、要素を追加できません。");
                return;
            }

            var newItem = Activator.CreateInstance(this.ItemType);
            this.ItemsSource.Add(newItem);
        }

        private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            // CommandParameterで、コレクションの各要素のVMがばいんどされているので、
            // e.ParameterをItemsSourceから削除する。
            this.ItemsSource.Remove(e.Parameter);
        }
    }

SimpleCollectionEditorの使い方

SimpleCollectionEditorを使う側は以下のようになります。

MainWindow.xaml
<Window x:Class="WpfBaseTemplate1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfBaseTemplate1"
        Title="MainWindow"
        Width="525"
        Height="350">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <local:SimpleCollectionEditor ItemType="{x:Type local:PersonViewModel}"
                                      ItemsSource="{Binding List}">
            <local:SimpleCollectionEditor.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition />
                        </Grid.ColumnDefinitions>
                        <TextBox Height="23"
                                 Margin="5"
                                 VerticalAlignment="Center"
                                 Text="{Binding Name}"
                                 TextWrapping="Wrap" />
                        <TextBox Grid.Column="1"
                                 Height="23"
                                 Margin="5"
                                 VerticalAlignment="Center"
                                 Text="{Binding Age}"
                                 TextWrapping="Wrap" />
                    </Grid>
                </DataTemplate>
            </local:SimpleCollectionEditor.ItemTemplate>
        </local:SimpleCollectionEditor>
    </Grid>
</Window>
MainWindowViewModel.cs
    class MainWindowViewModel : BindableBase
    {
        private ObservableCollection<PersonViewModel> list;
        public ObservableCollection<PersonViewModel> List
        {
            get { return list; }
            set { this.SetProperty(ref this.list, value); }
        }

        public MainWindowViewModel()
        {
            this.List = new ObservableCollection<PersonViewModel>();
        }
    }
PersonViewModel.cs
    class PersonViewModel : BindableBase
    {
        private string name;
        public string Name
        {
            get { return name; }
            set { this.SetProperty(ref this.name, value); }
        }

        private int age;
        public int Age
        {
            get { return age; }
            set { this.SetProperty(ref this.age, value); }
        }

        public PersonViewModel()
        {
            this.Name = "hogeさん";
            this.Age = 30;
        }
    }

使い方 その2・UserControlで各要素を表示

各アイテムの表示内容が複雑になってきたら、各アイテムを表示するためのUserControlを作って、表示用のDataTemplateに指定することもできます。


まず、以下のようなPersonクラス編集用のコントロールをつくります。
この手のプロパティの編集を行うコントロールでは、データバインディング時には基本的にTwoWayバインディングをしてほしくなります。
なので依存関係プロパティの定義では、FrameworkPropertyMetadataOptions.BindsTwoWayByDefaultを指定しています。

PersonEditor.xaml
<UserControl x:Class="WpfBaseTemplate1.PersonEditor"
             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"
             d:DesignHeight="300"
             d:DesignWidth="300"
             mc:Ignorable="d">
    <Grid x:Name="root">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <TextBox Height="23"
                 Margin="5"
                 VerticalAlignment="Center"
                 Text="{Binding PersonName}"
                 TextWrapping="Wrap" />
        <TextBox Grid.Column="1"
                 Height="23"
                 Margin="5"
                 VerticalAlignment="Center"
                 Text="{Binding Age}"
                 TextWrapping="Wrap" />
    </Grid>
</UserControl>
PersonEditor.xaml.cs
    /// <summary>
    /// PersonEditor.xaml の相互作用ロジック
    /// </summary>
    public partial class PersonEditor : UserControl
    {
        public string PersonName
        {
            get { return (string)GetValue(PersonNameProperty); }
            set { SetValue(PersonNameProperty, value); }
        }
        // Using a DependencyProperty as the backing store for PersonName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PersonNameProperty =
            DependencyProperty.Register("PersonName",
                                        typeof(string),
                                        typeof(PersonEditor),
                                        new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public int Age
        {
            get { return (int)GetValue(AgeProperty); }
            set { SetValue(AgeProperty, value); }
        }
        // Using a DependencyProperty as the backing store for Age.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AgeProperty =
            DependencyProperty.Register("Age",
                                        typeof(int),
                                        typeof(PersonEditor),
                                        new FrameworkPropertyMetadata(20, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public PersonEditor()
        {
            InitializeComponent();
            root.DataContext = this;
        }
    }
MainWindow.xaml

DataTemplate内でUserControlを使うように、コレクションエディタを定義します。

    <Grid>
        <local:SimpleCollectionEditor ItemType="{x:Type local:PersonViewModel}"
                                      ItemsSource="{Binding List}">
            <local:SimpleCollectionEditor.ItemTemplate>
                <DataTemplate>
                    <local:PersonEditor Age="{Binding Age}"
                                        PersonName="{Binding Name}" />
                </DataTemplate>
            </local:SimpleCollectionEditor.ItemTemplate>
        </local:SimpleCollectionEditor>
    </Grid>


今回はとりあえずUserControlとして作ったけど、CustomControlとして作ってテンプレート切り替えとか出来るようにしておくと、もっと汎用的に使えるかも。