c# wpf - 使用 ValueConverter 时,MVVM 不更新 UI?
c# wpf - When ValueConverter is used, MVVM doesn't update UI?
我只是在学习 WPF,最终我想要完成的是数据网格中的计算列,其中显示的数字是集合中特定 属性 的总和。
经过一番谷歌搜索后,我决定采用的方法是使用 ValueConverter 进行计算,但似乎 UI 中的数字从未更新过。我所做的阅读表明 PropertyChangedEvent 应该冒泡,这应该可以正常工作,但事实并非如此。我错过了一些东西,但我不知道是什么。
我编写了一个简单的演示应用程序来展示我在下面所做的事情。第二个 TextBlock 中的数字在单击按钮之前应该是 10(它是),但在单击之后应该是 6,但它保持在 10。
怎么会?我是不是找错树了?有一个更好的方法吗?任何帮助将不胜感激。
MainWindow.xaml:
<Window x:Class="TestApp.MainWindow"
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"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:BarSumConverter x:Key="BarSumConverter" />
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding ObjFoo.Bars[0].ANumber, Mode=TwoWay}" />
<TextBlock Text="{Binding ObjFoo.Bars, Converter={StaticResource BarSumConverter}, Mode=TwoWay}" />
<Button Content="Click me!" Click="Button_Click" />
</StackPanel>
</Window>
MainWindow.xaml.cs
namespace TestApp
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public Foo ObjFoo { get; set; }
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
ObjFoo = new Foo();
ObjFoo.Bars.Add(new Bar(5));
ObjFoo.Bars.Add(new Bar(5));
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ObjFoo.Bars[0].ANumber = 1;
}
}
}
Foo.cs
public class Foo
{
public Foo()
{
bars = new ObservableCollection<Bar>();
}
ObservableCollection<Bar> bars;
public ObservableCollection<Bar> Bars
{
get
{
return bars;
}
set { bars = value; }
}
}
Bar.cs
public class Bar : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Bar(int number)
{
this.ANumber = number;
}
private int aNumber;
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged("aNumber");
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
BarSumConverter.cs
public class BarSumConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var bars = value as ObservableCollection<Bar>;
if (bars == null) return 0;
decimal total = 0;
foreach (var bar in bars)
{
total += bar.ANumber;
}
return total;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
乍一看,您的代码似乎没问题,除了一个细节:要么让反射对 name
参数进行定值,要么手动指定它(但然后删除该属性)。
在后一种情况下,您应该传递 属性 名称,而不是私有字段。如果名称错误,事件通知将不起作用。绑定机制将仅查找 public 属性。只需利用 nameof
运算符来防止重构拼写错误。
选项 1:
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
选项 2:
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged(nameof(ANumber));
}
}
protected void OnPropertyChanged(string name)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
此外,在这两个选项中,我建议在 属性 set
上添加相等性检查。这是为了防止在替换值与现有值匹配时出现无用的通知:
public int ANumber
{
get { return aNumber; }
set
{
if (aNumber != value)
{
aNumber = value;
OnPropertyChanged( ... );
}
}
}
注意:我没有尝试你的代码,所以它可能隐藏了其他需要修补的东西。
更新:我会在 Foo
class 中做一些根本性的改变,以使事情正常进行。
public class Foo : INotifyPropertyChanged
{
public Foo()
{
bars = new ObservableCollection<Bar>();
bars.CollectionChanged += OnCollectionChanged;
}
ObservableCollection<Bar> bars;
public ObservableCollection<Bar> Bars
{
get
{
return bars;
}
//set { bars = value; }
}
private decimal total;
public decimal Total
{
get { return total; }
private set {
if (total != value)
{
total = value;
OnPropertyChange();
}
}
}
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
decimal t = 0;
foreach (var bar in bars)
{
t += bar.ANumber;
}
this.Total = t;
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
我把总计算移到了这里:转换器不用于业务逻辑。
此外,调整 XAML 第二个 TextBox
:
<TextBlock Text="{Binding ObjFoo.Total}" />
请注意,没有理由进行 TwoWay
此绑定。
所以问题的症结在于我假设更新实现 INotifyPropertyChanged 的 ObservableList 中的项目会触发 CollectionChanged 事件,但事实并非如此。所以这里有更新的代码,包括 Mario 的一些解决问题的建议:
MainWindow.xaml:
<Window x:Class="TestApp.MainWindow"
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"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:BarSumConverter x:Key="BarSumConverter" />
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding ObjFoo.Bars[0].ANumber}" />
<TextBlock Text="{Binding ObjFoo.Total}" />
<Button Content="Click me!" Click="Button_Click" />
</StackPanel>
</Window>
Foo.cs
public class Foo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Foo()
{
bars = new ObservableItemsCollection<Bar>();
bars.CollectionChanged += OnCollectionChanged;
}
private decimal total;
public decimal Total
{
get { return total; }
private set
{
if (total != value)
{
total = value;
OnPropertyChanged();
}
}
}
ObservableItemsCollection<Bar> bars;
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
decimal t = 0;
foreach (var bar in bars)
{
t += bar.ANumber;
}
this.Total = t;
}
public ObservableItemsCollection<Bar> Bars
{
get
{
return bars;
}
set { bars = value; }
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Bar.cs
public class Bar : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Bar(int number)
{
this.ANumber = number;
}
private int aNumber;
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
ObservableItemsCollection.cs
public class ObservableItemsCollection<T> : ObservableCollection<T>
where T: INotifyPropertyChanged
{
private void Handle(object sender, PropertyChangedEventArgs args)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, null));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (object t in e.NewItems)
{
((T)t).PropertyChanged += Handle;
}
}
if (e.OldItems != null)
{
foreach (object t in e.OldItems)
{
((T)t).PropertyChanged -= Handle;
}
}
base.OnCollectionChanged(e);
}
}
我只是在学习 WPF,最终我想要完成的是数据网格中的计算列,其中显示的数字是集合中特定 属性 的总和。
经过一番谷歌搜索后,我决定采用的方法是使用 ValueConverter 进行计算,但似乎 UI 中的数字从未更新过。我所做的阅读表明 PropertyChangedEvent 应该冒泡,这应该可以正常工作,但事实并非如此。我错过了一些东西,但我不知道是什么。
我编写了一个简单的演示应用程序来展示我在下面所做的事情。第二个 TextBlock 中的数字在单击按钮之前应该是 10(它是),但在单击之后应该是 6,但它保持在 10。
怎么会?我是不是找错树了?有一个更好的方法吗?任何帮助将不胜感激。
MainWindow.xaml:
<Window x:Class="TestApp.MainWindow"
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"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:BarSumConverter x:Key="BarSumConverter" />
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding ObjFoo.Bars[0].ANumber, Mode=TwoWay}" />
<TextBlock Text="{Binding ObjFoo.Bars, Converter={StaticResource BarSumConverter}, Mode=TwoWay}" />
<Button Content="Click me!" Click="Button_Click" />
</StackPanel>
</Window>
MainWindow.xaml.cs
namespace TestApp
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public Foo ObjFoo { get; set; }
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
ObjFoo = new Foo();
ObjFoo.Bars.Add(new Bar(5));
ObjFoo.Bars.Add(new Bar(5));
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ObjFoo.Bars[0].ANumber = 1;
}
}
}
Foo.cs
public class Foo
{
public Foo()
{
bars = new ObservableCollection<Bar>();
}
ObservableCollection<Bar> bars;
public ObservableCollection<Bar> Bars
{
get
{
return bars;
}
set { bars = value; }
}
}
Bar.cs
public class Bar : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Bar(int number)
{
this.ANumber = number;
}
private int aNumber;
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged("aNumber");
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
BarSumConverter.cs
public class BarSumConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var bars = value as ObservableCollection<Bar>;
if (bars == null) return 0;
decimal total = 0;
foreach (var bar in bars)
{
total += bar.ANumber;
}
return total;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
乍一看,您的代码似乎没问题,除了一个细节:要么让反射对 name
参数进行定值,要么手动指定它(但然后删除该属性)。
在后一种情况下,您应该传递 属性 名称,而不是私有字段。如果名称错误,事件通知将不起作用。绑定机制将仅查找 public 属性。只需利用 nameof
运算符来防止重构拼写错误。
选项 1:
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
选项 2:
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged(nameof(ANumber));
}
}
protected void OnPropertyChanged(string name)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
此外,在这两个选项中,我建议在 属性 set
上添加相等性检查。这是为了防止在替换值与现有值匹配时出现无用的通知:
public int ANumber
{
get { return aNumber; }
set
{
if (aNumber != value)
{
aNumber = value;
OnPropertyChanged( ... );
}
}
}
注意:我没有尝试你的代码,所以它可能隐藏了其他需要修补的东西。
更新:我会在 Foo
class 中做一些根本性的改变,以使事情正常进行。
public class Foo : INotifyPropertyChanged
{
public Foo()
{
bars = new ObservableCollection<Bar>();
bars.CollectionChanged += OnCollectionChanged;
}
ObservableCollection<Bar> bars;
public ObservableCollection<Bar> Bars
{
get
{
return bars;
}
//set { bars = value; }
}
private decimal total;
public decimal Total
{
get { return total; }
private set {
if (total != value)
{
total = value;
OnPropertyChange();
}
}
}
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
decimal t = 0;
foreach (var bar in bars)
{
t += bar.ANumber;
}
this.Total = t;
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
我把总计算移到了这里:转换器不用于业务逻辑。
此外,调整 XAML 第二个 TextBox
:
<TextBlock Text="{Binding ObjFoo.Total}" />
请注意,没有理由进行 TwoWay
此绑定。
所以问题的症结在于我假设更新实现 INotifyPropertyChanged 的 ObservableList 中的项目会触发 CollectionChanged 事件,但事实并非如此。所以这里有更新的代码,包括 Mario 的一些解决问题的建议:
MainWindow.xaml:
<Window x:Class="TestApp.MainWindow"
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"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:BarSumConverter x:Key="BarSumConverter" />
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding ObjFoo.Bars[0].ANumber}" />
<TextBlock Text="{Binding ObjFoo.Total}" />
<Button Content="Click me!" Click="Button_Click" />
</StackPanel>
</Window>
Foo.cs
public class Foo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Foo()
{
bars = new ObservableItemsCollection<Bar>();
bars.CollectionChanged += OnCollectionChanged;
}
private decimal total;
public decimal Total
{
get { return total; }
private set
{
if (total != value)
{
total = value;
OnPropertyChanged();
}
}
}
ObservableItemsCollection<Bar> bars;
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
decimal t = 0;
foreach (var bar in bars)
{
t += bar.ANumber;
}
this.Total = t;
}
public ObservableItemsCollection<Bar> Bars
{
get
{
return bars;
}
set { bars = value; }
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Bar.cs
public class Bar : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Bar(int number)
{
this.ANumber = number;
}
private int aNumber;
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
ObservableItemsCollection.cs
public class ObservableItemsCollection<T> : ObservableCollection<T>
where T: INotifyPropertyChanged
{
private void Handle(object sender, PropertyChangedEventArgs args)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, null));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (object t in e.NewItems)
{
((T)t).PropertyChanged += Handle;
}
}
if (e.OldItems != null)
{
foreach (object t in e.OldItems)
{
((T)t).PropertyChanged -= Handle;
}
}
base.OnCollectionChanged(e);
}
}