トップ回答者
DataGridの値がコンバーターを通したところだけ表示が更新されない

質問
-
DataGridでコンバーターを通した列だけ値の表示が更新されずに困っています。
グリッドには「名前」「年齢」「名前と年齢をコンバーターで結合した値」を表示しており、Updateボタンを押すと年齢が更新されるようになっています。
Updateボタンを押すと年齢の列は正常に更新されるのですが、コンバーターを使用した列だけが更新されません。
しかし、RefreshボタンでDataContextをリフレッシュすると更新された値が表示されます。
わかりづらいかもしれませんが、ご教示いただければ幸いです。
よろしくお願いいたします。
MainWindow.xaml
<Window x:Class="test2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:test2" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <local:NameAgeConverter x:Key="NameAgeConverter" /> </Window.Resources> <StackPanel> <Button x:Name="btnUpdate" Content="Update" Click="btnUpdate_Click" /> <Button x:Name="btnRefresh" Content="Refresh" Click="btnRefresh_Click"/> <DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" CanUserAddRows="False"> <DataGrid.Columns> <DataGridTextColumn Header="Name" Binding="{Binding Name}"/> <DataGridTextColumn Header="Age" Binding="{Binding Age}"/> <DataGridTextColumn Header="Name+Age" Binding="{Binding Converter={StaticResource NameAgeConverter}, Mode=OneWay}"/> </DataGrid.Columns> </DataGrid> </StackPanel> </Window>
MainWindow.xaml.cs
using System.Collections.ObjectModel; using System.Windows; namespace test2 { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { public static ObservableCollection<Person> _persons = new ObservableCollection<Person>(); public MainWindow() { InitializeComponent(); DataContext = _persons; _persons.Add(new Person() { Name = "名前0", Age = 10 }); _persons.Add(new Person() { Name = "名前1", Age = 11 }); _persons.Add(new Person() { Name = "名前2", Age = 12 }); _persons.Add(new Person() { Name = "名前3", Age = 13 }); _persons.Add(new Person() { Name = "名前4", Age = 14 }); _persons.Add(new Person() { Name = "名前5", Age = 15 }); } private void btnUpdate_Click(object sender, RoutedEventArgs e) { foreach (var p in _persons) { p.Age++; } } private void btnRefresh_Click(object sender, RoutedEventArgs e) { DataContext = null; DataContext = _persons; } } }
Person.cs
using System.ComponentModel; namespace test2 { public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } private string _Name; public string Name { get { return _Name; } set { if (_Name == value) return; _Name = value; OnPropertyChanged("Name"); } } private int _Age; public int Age { get { return _Age; } set { if (_Age == value) return; _Age = value; OnPropertyChanged("Age"); } } } }
NameAgeConverter.cs
using System; using System.Windows.Data; namespace test2 { class NameAgeConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { var person = (Person)value; return string.Format("{0}({1})", person.Name, person.Age); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } }
回答
-
{Binding Converter={StaticResource NameAgeConverter}, Mode=OneWay}
このBindingは、バインディングしているのはPersonオブジェクトであって、AgeプロパティでもNameプロパティでもないため、Ageプロパティの変更通知が来ようがNameプロパティの変更通知が来ようが自身を更新することはありません。
BindingとIValueConverterの代わりに、MultiBindingとIMultiValueConverterを使うのはどうでしょうか。
- 回答の候補に設定 いわさ Tak1waMVP, Moderator 2016年2月29日 2:39
- 回答としてマーク たけぞぅ 2016年2月29日 15:33
-
DataGridTemplateColumnでItemsControlを使ってやればいいです。
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" CanUserAddRows="False"> <DataGrid.Columns> <DataGridTextColumn Header="Name" Binding="{Binding Name}"/> <DataGridTextColumn Header="Age" Binding="{Binding Age}"/> <DataGridTextColumn Header="Name+Age" IsReadOnly="true" > <DataGridTextColumn.Binding> <MultiBinding StringFormat="{}{0}({1})" Mode="OneWay"> <Binding Path="Name" /> <Binding Path="Age" /> </MultiBinding> </DataGridTextColumn.Binding> </DataGridTextColumn> <DataGridTemplateColumn Header="Children" IsReadOnly="true" > <DataGridTemplateColumn.CellTemplate> <DataTemplate> <ItemsControl ItemsSource="{Binding Path=Children}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="," x:Name="commma" /> <TextBlock Margin="0,0,3,0" Text="{Binding Path=Name}" /> </StackPanel> <DataTemplate.Triggers> <DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}"> <Setter TargetName="commma" Property="Visibility" Value="Collapsed" /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid>
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
- 回答としてマーク たけぞぅ 2016年2月29日 15:33
すべての返信
-
{Binding Converter={StaticResource NameAgeConverter}, Mode=OneWay}
このBindingは、バインディングしているのはPersonオブジェクトであって、AgeプロパティでもNameプロパティでもないため、Ageプロパティの変更通知が来ようがNameプロパティの変更通知が来ようが自身を更新することはありません。
BindingとIValueConverterの代わりに、MultiBindingとIMultiValueConverterを使うのはどうでしょうか。
- 回答の候補に設定 いわさ Tak1waMVP, Moderator 2016年2月29日 2:39
- 回答としてマーク たけぞぅ 2016年2月29日 15:33
-
Hongliangさんの回答がベストだと思います。
以下、私の興味がてら違う方法で実装してみました。あまり、以下のコードはお勧めしませんが、何かの折に使えるかもしれません。
なお、以下のページを参考にしました。ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)
http://stackoverflow.com/questions/1427471/observablecollection-not-noticing-when-item-in-it-changes-even-with-inotifyproppublic partial class MainWindow : Window, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string name) { if(PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } //public static ObservableCollection<Person> _Persons = new ObservableCollection<Person>(); public MainWindow() { InitializeComponent(); Persons = new TrulyObservableCollection<Person>(); Persons.Add(new Person() { Name = "名前0", Age = 10 }); Persons.Add(new Person() { Name = "名前1", Age = 11 }); Persons.Add(new Person() { Name = "名前2", Age = 12 }); Persons.Add(new Person() { Name = "名前3", Age = 13 }); Persons.Add(new Person() { Name = "名前4", Age = 14 }); Persons.Add(new Person() { Name = "名前5", Age = 15 }); Persons.Add(new Person() { Name = "名前0", Age = 10 }); this.DataContext = Persons; } public TrulyObservableCollection<Person> Persons {get; set;} private void btnUpdate_Click(object sender, RoutedEventArgs e) { foreach(var p in Persons) { p.Age++; } OnPropertyChanged("Persons"); } private void btnRefresh_Click(object sender, RoutedEventArgs e) { DataContext = null; DataContext = Persons; } public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string name) { if(PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } private string _Name; public string Name { get { return _Name; } set { if(_Name == value) return; _Name = value; OnPropertyChanged("Name"); } } private int _Age; public int Age { get { return _Age; } set { if(_Age == value) return; _Age = value; OnPropertyChanged("Age"); } } } } class NameAgeConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { var Person = (Person)value; return string.Format("{0}({1})", Person.Name, Person.Age); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } public sealed class TrulyObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged { public TrulyObservableCollection() { CollectionChanged += FullObservableCollectionCollectionChanged; } public TrulyObservableCollection(IEnumerable<T> pItems) : this() { foreach(var item in pItems) { this.Add(item); } } private void FullObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if(e.NewItems != null) { foreach(Object item in e.NewItems) { ((INotifyPropertyChanged)item).PropertyChanged += ItemPropertyChanged; } } if(e.OldItems != null) { foreach(Object item in e.OldItems) { ((INotifyPropertyChanged)item).PropertyChanged -= ItemPropertyChanged; } } } private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e) { //NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender)); NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); OnCollectionChanged(args); } }
★良い回答には回答済みマークを付けよう! MVP - .NET http://d.hatena.ne.jp/trapemiya/
-
using System.ComponentModel; namespace test2 { public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } private string _Name; public string Name { get { return _Name; } set { if (_Name == value) return; _Name = value; OnPropertyChanged("Name"); OnPropertyChanged("NameAndAge"); } } private int _Age; public int Age { get { return _Age; } set { if (_Age == value) return; _Age = value; OnPropertyChanged("Age"); OnPropertyChanged("NameAndAge"); } } public string NameAndAge { get { return string.Format("{0}({1})", Name, Age); } } } }
として、NameAndAgeをバインドしてください。
-
StringFormatで書式を適用できる程度であれば、IValueConverterやIMultiValueConverterを作らなくてもいけます。
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" CanUserAddRows="False"> <DataGrid.Columns> <DataGridTextColumn Header="Name" Binding="{Binding Name}"/> <DataGridTextColumn Header="Age" Binding="{Binding Age}"/> <DataGridTextColumn Header="Name+Age" IsReadOnly="true" > <DataGridTextColumn.Binding> <MultiBinding StringFormat="{}{0}({1})" Mode="OneWay"> <Binding Path="Name" /> <Binding Path="Age" /> </MultiBinding> </DataGridTextColumn.Binding> </DataGridTextColumn> </DataGrid.Columns> </DataGrid>
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
-
回答有り難うございます。
Hongliangさんの方法で、以下のように修正したら正常に動作するようになりました。
<DataGridTextColumn Header="Name+Age" > <DataGridTextColumn.Binding> <MultiBinding Converter="{StaticResource NameAgeConverter}"> <Binding Path="Name" /> <Binding Path="Age" /> </MultiBinding> </DataGridTextColumn.Binding> </DataGridTextColumn>
public class NameAgeConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) { string name = values[0] as string; int? age = values[1] as int?; return string.Format("{0}({1})", name, age); } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }
おっしゃっているように、Personオブジェクトをバインディングしておけば、プロパティ変更通知が来ると勘違いしておりました。
しかし、次のケースではどのように処理すればよろしいでしょうか?
1.Personクラスと同じ内容のChildという名前のクラスを作成。
2.PersonクラスにChildrenプロパティをObservableCollection<Child>で追加。
3.xamlのグリッドにChildren列を追加し、Childrenの名前を文字列結合して返して表示したい。
この場合だとChildrenオブジェクトの長さが可変な為、MultiBindingでは対応できないと思われます。(やってみましたが当然コンバーターで連結した名前は更新されません)
みなさんどの様に実装されているのか気になります。
度々申し訳ありませんが、よろしくお願いいたします。
- 編集済み たけぞぅ 2016年2月29日 14:59
-
DataGridTemplateColumnでItemsControlを使ってやればいいです。
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" CanUserAddRows="False"> <DataGrid.Columns> <DataGridTextColumn Header="Name" Binding="{Binding Name}"/> <DataGridTextColumn Header="Age" Binding="{Binding Age}"/> <DataGridTextColumn Header="Name+Age" IsReadOnly="true" > <DataGridTextColumn.Binding> <MultiBinding StringFormat="{}{0}({1})" Mode="OneWay"> <Binding Path="Name" /> <Binding Path="Age" /> </MultiBinding> </DataGridTextColumn.Binding> </DataGridTextColumn> <DataGridTemplateColumn Header="Children" IsReadOnly="true" > <DataGridTemplateColumn.CellTemplate> <DataTemplate> <ItemsControl ItemsSource="{Binding Path=Children}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="," x:Name="commma" /> <TextBlock Margin="0,0,3,0" Text="{Binding Path=Name}" /> </StackPanel> <DataTemplate.Triggers> <DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}"> <Setter TargetName="commma" Property="Visibility" Value="Collapsed" /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid>
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
- 回答としてマーク たけぞぅ 2016年2月29日 15:33