首页 > 解决方案 > 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>

主窗口.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; }
        }
    }

酒吧.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();
        }
    }

标签: c#wpfmvvmivalueconverter

解决方案


乍一看,您的代码似乎没问题,除了一个细节:要么让反射对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));
    }
}

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

此外,为第二个调整 XAML TextBox

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

请注意,没有理由进行TwoWay此绑定。


推荐阅读