【问题标题】:c# wpf - When ValueConverter is used, MVVM doesn't update UI?c# wpf - 使用ValueConverter时,MVVM不更新UI?
【发布时间】:2021-06-26 07:44:08
【问题描述】:

我只是在学习 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();
        }
    }

【问题讨论】:

    标签: c# wpf mvvm ivalueconverter


    【解决方案1】:

    乍一看,您的代码似乎没问题,除了一个细节:要么让反射为 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 成为此绑定。

    【讨论】:

    • 啊,你是对的,这是演示应用程序中的一个失误。我确实尝试过改变它,但仍然没有运气。只是要注意第一个 TextBlock 确实会正确更新。
    • 另外,值得一提的是 Foo 类。要么将 Bars 属性标记为只读(即删除集合),要么遵循 Bar 类中的 INotifyPropertyChange 模式。否则,如果您替换 Bars 集合,则不会通知框架有关更改。
    • 是的,缺少更新只是因为 Bars 属性。如果你改变集合的内容,那么什么都不会改变。您的绑定设置在 Bars 属性上。你必须改变方法。
    • 所有好的建议,但都没有真正解决问题。不过,您确实让我走上了正确的道路,所以+1,谢谢。我回答了我的最终解决方案。
    【解决方案2】:

    所以问题的症结在于我假设更新实现 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);
            }
        }
    

    【讨论】:

      猜你喜欢
      • 2014-11-23
      • 1970-01-01
      • 2023-03-08
      • 2021-12-20
      • 2016-12-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多