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);
        }
    }