简体   繁体   English

c# wpf - 使用 ValueConverter 时,MVVM 不更新 UI?

[英]c# wpf - When ValueConverter is used, MVVM doesn't update UI?

I'm just learning WPF, and ultimately what I'm trying to accomplish is a calculated column in a data grid where the number displayed there is the sum of a particular property in a collection.我只是在学习 WPF,最终我想要完成的是数据网格中的计算列,其中显示的数字是集合中特定属性的总和。

After a bit of googling, the approach I decided to take was to use a ValueConverter to do the calculation, but it seems that the number is never updated in the UI.经过一番谷歌搜索后,我决定采用的方法是使用 ValueConverter 进行计算,但似乎从未在 UI 中更新该数字。 The reading I've done suggests that the PropertyChangedEvent should bubble up and this should just work but it doesn't.我所做的阅读表明 PropertyChangedEvent 应该冒泡,这应该可以正常工作,但没有。 I'm missing something, but I don't know what.我错过了一些东西,但我不知道是什么。

I wrote up a simple demo app to show what I'm doing below.我编写了一个简单的演示应用程序来展示我在下面所做的事情。 The number in the second TextBlock should be 10 before clicking the button (it is), but 6 after clicking, but it stays at 10.第二个 TextBlock 中的数字在单击按钮之前应该是 10(它是),但在单击之后是 6,但它保持在 10。

How come?怎么来的? Am I barking up the wrong tree?我是不是叫错了树? Is there a better way to do this?有一个更好的方法吗? Any help would be appreciated.任何帮助,将不胜感激。

MainWindow.xaml:主窗口.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主窗口.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 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();
        }
    }

At a glance, your code seems okay, except for a detail: either leave the reflection valorizing the name argument, or specify it manually (but remove the attribute, then).乍一看,您的代码似乎没问题,除了一个细节:要么保留反射对name参数进行赋值,要么手动指定它(然后删除该属性)。

In the latter case, you should pass the property name, not the private field.在后一种情况下,您应该传递属性名称,而不是私有字段。 If the name is wrong, the event notification won't work.如果名称错误,则事件通知将不起作用。 The binding mechanism will look for public properties only.绑定机制将只查找公共属性。 Just leverage the nameof operator to prevent refactoring typo.只需利用nameof运算符来防止重构错字。

Option 1:选项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));
    }

Option 2:选项 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));
    }

Moreover, in both options, I'd suggest to add an equality check on the property set .此外,在这两个选项中,我建议在属性set上添加一个相等性检查。 That's to prevent useless notifications when the replacing value matches the existent one:这是为了在替换值与现有值匹配时防止无用的通知:

    public int ANumber
    {
        get { return aNumber; }
        set
        { 
            if (aNumber != value)
            {
                aNumber = value;
                OnPropertyChanged( ... );
            }
        }
    }

Note: I didn't try your code, so it might hide something else to patch.注意:我没有尝试你的代码,所以它可能隐藏了其他需要修补的东西。

UPDATE: I'd make some radical change in the Foo class, in order to make the things work.更新:我会在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));
    }
}

I moved the total calculation here: converters aren't for business logic.我将总计算移到此处:转换器不适用于业务逻辑。

Also, adjust the XAML for the second TextBox :此外,调整第二个TextBox的 XAML :

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

Please, notice that there's no reason to make TwoWay this binding.请注意,没有理由让TwoWay绑定。

So it turns out the crux of the issue was that I'd assumed that updating an item inside an ObservableList that implements INotifyPropertyChanged would trigger the CollectionChanged event, but that is not the case.所以事实证明问题的关键是我假设更新实现 INotifyPropertyChanged 的​​ ObservableList 中的项目会触发 CollectionChanged 事件,但事实并非如此。 So here's updated code including some of Mario's suggestions that fixes the issue:所以这里是更新的代码,包括马里奥的一些解决问题的建议:

MainWindow.xaml:主窗口.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 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);
        }
    }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM