WPF用にシンプルなコレクションエディタを作ってみた
ちょっと必要に迫られて、コレクションエディタ的なものを作ってみました。
extended wpf toolkitのCollectionEditorほどのカッチリしたエディタじゃなくて、「ちょろっとコレクションの編集したいだけ」、って位の時に使えるかと思います。
コレクションを任意のテンプレートで表示したり、アイテムの追加/削除を出来るようにしています。
コントロール下部の「+」ボタンを押すと要素の追加。各アイテム右側の「×」ボタンを押すと、要素の削除ができます。
使い方
SimpleCollectionEditorに定義した依存関係プロパティは以下の通りです。
依存関係プロパティ | 内容 |
---|---|
ItemsSource | 編集対象のコレクションを指定するためのプロパティ |
ItemTemplate | コレクションの表示方法をDataTemplateで設定するためのプロパティ |
ItemType | コレクションの各アイテムの型を指定するプロパティ |
コントロールの使い方のイメージはこんな感じ。
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として作ってテンプレート切り替えとか出来るようにしておくと、もっと汎用的に使えるかも。