次の方法で共有


DataGrid上で列の新規作成や編集がしたい

質問

2015年11月2日月曜日 11:54

中断していたWPFの勉強を再開したのですが、DataGridの使い方がよくわかりません。

編集用のダイアログを出したりするのではなく、DataGrid上で直接、新規行の追加や既存データの編集を試したくて、まずはシンプルなサンプルを作って新規行の入力を試そうとしました。

実行すると、空行は表示されるのですがセルが編集状態にならないし、キーボードから何か入力しようとすると落ちてしまいます。

ソースは以下のようなものなのですが、どこが足りないのでしょうか?(おそらくすごく初歩的な知識不足だと思うのですが…)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Microsoft.Practices.Prism.Mvvm;

namespace DataGridTest02
{
    class Person:BindableBase   
    {
        public int Id{get;set;}
        
        private string name;
        public string Name
        {
            get { return name; }
            set { this.SetProperty(ref this.name, value); }
        }

        private string phone;
        public string Phone
        {
            get { return phone; }
            set { this.SetProperty(ref this.phone, value); }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Microsoft.Practices.Prism.Mvvm;
using System.Collections.ObjectModel;

namespace DataGridTest02
{
    class MainWindowViewModel:BindableBase
    {
        private ObservableCollection<Person> items = new ObservableCollection<Person>();
        public ObservableCollection<Person> Items
        {
            get { return items; }
            set { this.SetProperty(ref this.items, value); }
        }

        private Person item;
        public Person Item
        {
            get { return item; }
            set { this.SetProperty(ref this.item, value); }
        }
    }
}
<Window x:Class="DataGridTest02.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataGridTest02"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <DataGrid AutoGenerateColumns="False"
                  ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Path=Item}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="名前" Binding="{Binding Path=Name}"/>
                <DataGridTextColumn Header="電話" Binding="{Binding Path=Phone}"/>
            </DataGrid.Columns>            
        </DataGrid>
    </Grid>
</Window>

すべての返信 (13)

2015年11月2日月曜日 19:08 ✅回答済み | 1 票

新規行はPerson型ではないのでPerson型のMainWindowViewModel.Itemとのやりとりが失敗します。
まずobject型で受けてPerson型の時だけ受け取るプロパティを別に作るといいです

using System.Windows;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace DataGridTest02
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

    class Person : BindableBase
    {
        public int Id { get; set; }

        private string name;
        public string Name
        {
            get { return name; }
            set { this.SetProperty( ref this.name, value ); }
        }

        private string phone;
        public string Phone
        {
            get { return phone; }
            set { this.SetProperty( ref this.phone, value ); }
        }
    }

    class MainWindowViewModel : BindableBase
    {
        private ObservableCollection<Person> items = new ObservableCollection<Person>();
        public ObservableCollection<Person> Items
        {
            get { return items; }
            set { this.SetProperty( ref this.items, value ); }
        }

        private object item;
        public object Item
        {
            get { return item as Person; }
            set
            {
                this.SetProperty( ref this.item, value );
                SelectedPerson = item as Person;
                IsNewRow = item != SelectedPerson;
            }
        }
        public bool IsNewRow
        {
            get
            {
                return item != null && !(item is Person);
            }
            private set
            {
                this.SetProperty( ref this._IsNewRow, value );            
            }
        }
        private bool _IsNewRow;

        public Person SelectedPerson
        {
            get { return _SelectedPerson; }
            private set { this.SetProperty( ref this._SelectedPerson, value ); }
        }
        private Person _SelectedPerson;
    }

    /// <summary> Prismを入れるのが面倒だったので代用</summary>
    class BindableBase : System.ComponentModel.INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropeprtyChanged(string name)
        {
            var pc = PropertyChanged;
            if (pc != null) { pc( this, new PropertyChangedEventArgs( name ) ); }
        }
        public void SetProperty<T>(ref T oldvalue, T newvalue, [System.Runtime.CompilerServices.CallerMemberName] string name = "")
        {
            oldvalue = newvalue;
            OnPropeprtyChanged( name );
        }
    }
}

個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)


2015年11月5日木曜日 2:19 ✅回答済み | 1 票

以下のようにしてみて下さい。

class MyConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value != null && string.Equals("{NewItemPlaceholder}", value.ToString(), StringComparison.Ordinal))
        {
            return DependencyProperty.UnsetValue;
        }

        if (value == null)
        {
            return DependencyProperty.UnsetValue;
        }

        return value;
    }
}

>DataGridのセルのフォーカス移動とかはXAMLのコードビハインドでやっても不調法にはならないでしょうか。

Modelが関係ないんで、そこは良いと思いますよ。Viewだけで全て完結していますので、MVVMから外れているということはないと思います。
ViewModelから操作するならビヘイビアを用意すると思います。Prismは経験が無いのでよく知りませんが、Prismにそういった目的に使えるようなものが用意されているのかもしれません。

★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/


2015年11月2日月曜日 15:11

こんにちは。

概ね問題なくて、以下のせいで落ちてますね。

SelectedItem="{Binding Path=Item}"

SelectedIndexでintプロパティをバインドさせた場合はうまくいくんですが、
SelectedItemをバインドさせる場合だと何かやり方があるのかな…。調べ中です。


2015年11月4日水曜日 2:47

以下のようにConverterを使っても良いようです。

WPF DataGrid SelectedItem
http://stackoverflow.com/questions/9109103/wpf-datagrid-selecteditem

ただ、ViewModelを使用しているのであれば、DataGridの機能で新規行を作成するのではなく、Model側で新規行を追加するのが良いように思います。つまり、新規行と言っても動作としては変更になります。何らかの処理をする際に、NameもPhoneも空であれば、その行を無視すれば良いだけです。このように実装すれば、新規行を複数行表示することも可能になります。
そもそもObservableCollectionを使用されていますので、このような要求にはぴったりです。
私がよくやるのは、新規行を追加するボタンを設置して、そのボタンを押したらModelに空の行を追加して、Viewに表示するということです。

★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/


2015年11月4日水曜日 9:39

そのようです。新規行がPersonにならなかったとは。


2015年11月4日水曜日 9:43

ありがとうございます。エラーが発生しなくなりました。

選択行の取得や新規行の判定なども出来て参考になりました。しかし、いったんObjectで受けてからPersonかどうか判定するって自分ではなかなか気づかなかったと思います。


2015年11月4日水曜日 10:16

ありがとうございます。

Converterを使うと言うことは、ViewModelのItemはPerson型のままで良いとして、MainWindow.xamlを

<Window x:Class="DataGridTest03.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataGridTest03"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <local:MyConverter x:Key="myConverter"/>
    </Window.Resources>
    
    <Grid>
        <DataGrid AutoGenerateColumns="False"
                  ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Path=Item,Converter={StaticResource myConverter}}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="名前" Binding="{Binding Path=Name}"/>
                <DataGridTextColumn Header="電話" Binding="{Binding Path=Phone}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

リソースにConverterをつけて、DataGridのSelectedItemでConverterを動作

そのConverterはこんな感じで

using System;
using System.Windows.Data;

namespace DataGridTest03
{
    class MyConverter:IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value != null && string.Equals("{NewItemPlaceholder}", value.ToString(), StringComparison.Ordinal))
            {
                //ここでなにしよう?
            }
            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }
    }
}

新規行が出来るとNewItemPlaceholderが渡ってくるのですが、これをPersonに変換するということでしょうか? 変換と言っても空っぽの行なので New Person()でも返せばいいのかと試しても上手くいかないし、nullを返しても同様で…

なにを返すべきなのでしょうか?

あと、Model側で新規行追加は学習中なので後で試してみたいと思います。だた、スプレッドシートに入力する時のようにEnterで新たな新規行に移って、そのまま追加を続けるというやり方も試しておきたいのでしばらくそちらを学習してみます。

DataGridのセルのフォーカス移動とかはXAMLのコードビハインドでやっても不調法にはならないでしょうか。


2015年11月5日木曜日 2:28

trapemiyaさん>

Viewの都合をViewだけで吸収できているので確かに綺麗な感じがしますね。
とても参考になりました。


2015年11月5日木曜日 9:27

ありがとうございます。Viewが非常にすっきりしました。

DependencyProperty.Unsetvalue は、「プロパティは存在するが、その値がプロパティ システムによって設定されていないことを示すために、WPF プロパティ システムが null の代わりに使用する静的な値を指定します。」って言うことは、新規行は値が設定されていないから

ConvertBackの方に設定するのはBindingがTwoWayなのでViewModelからModelの方向に、ViewModelのPreson型のプロパティが反映されるけどその先は未設定プロパティだから落ちた…という理解でいいんでしょうか? 試しにDataGridのItemのバインディングをOneWayにしてコンバーターのConvertBackを空にしてもエラーが出なかったのでそんな感じかなと思ったのですが…、勉強不足ですね。

>Modelが関係ないんで、そこは良いと思いますよ。Viewだけで全て完結していますので、MVVMから外れているということはないと思います。

セル移動はまた調べながらですがViewの中でこねくりまわしたいと思います。


2015年11月6日金曜日 0:50

>Viewの都合をViewだけで吸収できているので確かに綺麗な感じがしますね。

こういったトリック的なことにConverterは良いツールになりますよね。
{NewItemPlaceholder}という、マジックナンバー的なハードコーディングがありますので、私が紹介したページに記載がありますが、代わりに、

if (value != null && value.GetType().Name == "NamedObject")

を使った方が良いかもしれませんね。コードも短くなりますし、より綺麗になるように思います。
いずれにしても、どちらも公式に公開されているものではないと思いますので、その点ではあまり変わりないのですが・・・

★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/


2015年11月6日金曜日 0:56

>ConvertBackの方に設定するのはBindingがTwoWayなのでViewModelからModelの方向に、ViewModelのPreson型のプロパティが反映されるけどその先は未設定プロパティだから落ちた…という理解でいいんでしょうか?

落ちるのはnullが渡ってくるためです。そのため、

        if (value == null) {
            return DependencyProperty.UnsetValue;
        }

を追加しています。

        if (value != null && string.Equals("{NewItemPlaceholder}", value.ToString(), StringComparison.Ordinal))
        {
            return DependencyProperty.UnsetValue;
        }

が無いと、DataGridの枠が赤くなります。これは、NamedObject型をPerson型にキャストできないからだと思います。

★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/


2015年11月6日金曜日 6:24

ふむふむ…、落ちるのは(ViewModelからViewに)nullが渡ってくるけれど、参照しようとしたらnullだから、確かエラーメッセージもそんな感じだったような。 もう少し理解が進むまで「そういうものだ」と心に留めておきます。ありがとうございました。


2015年11月6日金曜日 6:35 | 1 票

>ただ、ViewModelを使用しているのであれば、DataGridの機能で新規行を作成するのではなく、Model側で新規行を追加するのが良いように思います。つまり、新規行と言っても動作としては変更になります。何らかの処理をする際に、NameもPhoneも空であれば、その行を無視すれば良いだけです。このように実装すれば、新規行を複数行表示することも可能になります

Model側で新規行を追加する方法を考えて試してみました。今回はPersonではなくてOrderになってます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Microsoft.Practices.Prism.Mvvm;

namespace DataGridTest05
{
    class Order:BindableBase
    {
        public int Id { get; set; }

        //品名
        private string name;
        public string Name
        {
            get { return name; }
            set { this.SetProperty(ref this.name, value); }
        }

        //単価
        private decimal unitPrice;
        public decimal UnitPrice
        {
            get { return unitPrice; }
            set 
            {
                this.SetProperty(ref this.unitPrice, value);
                SubTotal = UnitPrice * Quantity;
                OnPropertyChanged("SubTotal");
            }
        }

        //数量
        private decimal quantity;
        public decimal Quantity
        {
            get { return quantity; }
            set 
            { 
                this.SetProperty(ref this.quantity, value);
                SubTotal = UnitPrice * Quantity;
                OnPropertyChanged("SubTotal");
            }
        }

        //小計
        public decimal SubTotal { get; private set; }
    }
}

ViewModelですべてやってもよさそうですが、学習のためAppModelというのをモデル層として挟みました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Microsoft.Practices.Prism.Mvvm;
using System.Collections.ObjectModel;

namespace DataGridTest05
{
    class AppModel:BindableBase
    {
        private ObservableCollection<Order> items = new ObservableCollection<Order>();
        public ObservableCollection<Order> Items
        {
            get { return items; }
            set { this.SetProperty(ref this.items, value); }
        }

        public void AddNewItem()
        {
            Items.Add(new Order());
        }
    }
}

ViewModelではCollectionChangedを見張ってます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Microsoft.Practices.Prism.Mvvm;
using Microsoft.Practices.Prism.Commands;
using System.Collections.ObjectModel;

namespace DataGridTest05
{
    class MainWindowViewModel:BindableBase
    {
        AppModel model = new AppModel();

        public MainWindowViewModel()
        {
           model.Items.CollectionChanged ;= Items_CollectionChanged;
        }

        void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            this.Items = model.Items;
            TotalPrice = model.Items.Sum(odr => odr.SubTotal);
        }

        ~ MainWindowViewModel()
        {
            model.Items.CollectionChanged -= Items_CollectionChanged;
        }

        private ObservableCollection<Order> items;
        public ObservableCollection<Order> Items
        {
            get { return items; }
            set 
            { 
                this.SetProperty(ref this.items, value);
                model.Items = this.Items;
            }
        }

        private Order item;
        public Order Item
        {
            get { return item; }
            set 
            {
                this.SetProperty(ref this.item, value);
            }
        }

        private decimal totalPrice;
        public decimal TotalPrice
        {
            get { return totalPrice; }
            set { this.SetProperty(ref this.totalPrice, value); }
        }



        private DelegateCommand addCommand;
        public DelegateCommand AddCommand
        {
            get
            {
                if (addCommand == null) addCommand = new DelegateCommand(ExecuteAddCommand);
                return addCommand;
            }
        }
        private void ExecuteAddCommand()
        {
            model.AddNewItem();
        }
    }
}

MyConverterは NamedObjectで調べる方法に書き換えて、

using System;
using System.Windows;
using System.Windows.Data;

namespace DataGridTest05
{
    class MyConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value != null && value.GetType().Name == "NamedObject")
            {
                return DependencyProperty.UnsetValue;
            }

            if (value == null)
            {
                return DependencyProperty.UnsetValue;
            }

            return value;
        }
    }
}

Viewではボタンで追加するのでCanUserAddRows="False"に、

<Window x:Class="DataGridTest05.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataGridTest05"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <local:MyConverter x:Key="myConverter"/>
    </Window.Resources>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="32"/>
        </Grid.RowDefinitions>
        <DataGrid AutoGenerateColumns="False" CanUserAddRows="False"
                  ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Path=Item,Converter={StaticResource myConverter}}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="品名" Binding="{Binding Path=Name}"/>
                <DataGridTextColumn Header="単価" Binding="{Binding Path=UnitPrice, UpdateSourceTrigger=PropertyChanged}"/>
                <DataGridTextColumn Header="数量" Binding="{Binding Path=Quantity, UpdateSourceTrigger=PropertyChanged}"/>
                <DataGridTextColumn Header="小計" Binding="{Binding Path=SubTotal}" IsReadOnly="True"/>
            </DataGrid.Columns>
        </DataGrid>
        <StackPanel Orientation="Horizontal" Grid.Row="1">
            <Button Content="新規" Width="80" Command="{Binding Path=AddCommand}"/>
            <TextBlock Width="32"/>
            <TextBlock Width="32" Text="合計:"/>
            <TextBlock Text="{Binding Path=TotalPrice}"/>
        </StackPanel>
    </Grid>
</Window>

動作することは動作するのですが、

1) デストラクタで解除しているけれど、これはちゃんと動作するのだろうか?

2)合計が新規行を追加するタイミングで合計されているので、最後の一行が加算されない。どこでやればいいのか?

などなど疑問がでてきました。

その他、「自分ならこんなやり方はしない」と思われる点があれば皆様ご指摘ください。よろしくお願いします。