c# wpf - 使用 ValueConverter 时,MVVM 不更新 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,CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

解决方法

乍一看,您的代码似乎没问题,除了一个细节:要么保留反射对 name 参数进行赋值,要么手动指定它(然后删除该属性)。

在后一种情况下,您应该传递属性名称,而不是私有字段。如果名称错误,则事件通知将不起作用。绑定机制将只查找公共属性。只需利用 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 类中进行一些根本性的更改,以使事情正常运行。

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

我将总计算移到此处:转换器不适用于业务逻辑。

另外,调整第二个 TextBox 的 XAML:

    <TextBlock Text="{Binding ObjFoo.Total}" />

请注意,没有理由使 TwoWay 绑定。

,

所以事实证明问题的关键是我假设更新实现 INotifyPropertyChanged 的​​ ObservableList 中的项目会触发 CollectionChanged 事件,但事实并非如此。所以这里是更新的代码,包括马里奥的一些解决问题的建议:

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...