WPF PropertyChanged实现子属性通知

今天用WPF的View绑定了ViewModel的一个属性类,结果在属性类的子属性修改时,没有通知到UI.

如有要显示一个学生信息,采用WPF MVVM的模式,则前端代码

   <StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="姓名:"/>
                <TextBlock Text="{Binding Student.Name}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="年龄:"/>
                <TextBlock Text="{Binding Student.Age}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="学级:"/>
                <TextBlock Text="{Binding Student.Grade}"/>
            </StackPanel>
        </StackPanel> 

Student实体类,外部引入 PropertyChanged.Fody 第三方库作为自动属性通知

   public class Student: INotifyPropertyChanged
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public int Grade { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;
    }

ViewMode

    public ICommand TestCmd => new RelayCommand(() =>
        {
            Student = new Student();
            Random random = new Random();
            Student.Name = "张三";
            Student.Age = random.Next(5,9);
            Student.Grade = 1;
        });

测试一下

WPF PropertyChanged实现子属性通知

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

上面的代码是没问题的,也是最常见的思路,因为直接绑定的类的子属性,在子属性修改时,通知UI。

不过我的需求则是:当学生的年龄与学级的差大于6时,则需要在原来的年龄上有超龄提醒。

比如:

WPF PropertyChanged实现子属性通知

 

 

那么修改代码,前端

   <StackPanel Grid.Row="0">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="姓名:"/>
                <TextBlock Text="{Binding Student.Name }"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="年龄:"/>
                <TextBlock Text="{Binding Student,Converter={StaticResource StudentAgeConverter}}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="学级:"/>
                <TextBlock Text="{Binding Student.Grade }"/>
            </StackPanel>
            <Button Command="{Binding TestCmd}" Content="测试"/>
        </StackPanel>

 

新增加的转换器代码

  public class StudentAgeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
            {
                return null;
            }
            if (value is Student student)
            {
                if (student.Age - student.Grade > 6)
                {
                    return $"超龄,实际年龄为{student.Age}";
                }
                return student.Age;
            }
            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

 

开始调试

WPF PropertyChanged实现子属性通知

 

 纳尼???,怎么年龄一直是默认值呢!!!所有的类我都实现了 INotifyPropertyChanged 接口,为什么这个年龄没有被通知到?为什么姓名和年级就可以正常显示呢?他们看起来并没有什么不一样,不就一个直接绑的对象,一个绑的对象的具体属性吗,为什么绑的对像的这个没有被通知到呢?

仔细想想,前端我Age其实绑定的是Student,之所以显示Age,是因为转换器把Student的Age提了出来。也就是说,当Student的子属性被修改时,并不触发PropertyChanged。

和之前代码的区别就是一个直接绑定的子属性,子属性修改自然会通知UI,而这个则是绑定的类,类的子属性修改,并不会通知UI。

那么解决的思路就有两个了:

 

方法1.前端的Age 改为采用多重绑定来直接绑定 Student 的Age 和Student 的Grade,然后转换器也改为实现IMultiValueConverter,然后转换器的传参用Object数组来接收...,再考虑到VS对Xmal的多重绑定的支持并不太好,我头就更大了,虽然最后还是写出来了,这里就不放出来了。

 

方法2.简单点想,在修改子属性的时候,手动通知Student不就可以了嘛

说干就干,修改下后端命令代码

       public ICommand TestCmd => new RelayCommand(() =>
        {
            Student = new Student();
            Random random = new Random();
            Student.Name = "张三";
            Student.Age = random.Next(5,9);
            Student.Grade = 1;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Student"));
          
        });

调试一下

WPF PropertyChanged实现子属性通知

 

 OK,可以了。不过想了想,还是让Student的Age属性自己触发可能更好点,这样就不用担心如果其他地方修改了Age,导致没有手动触发的尴尬了。

Student的实体类修改为

   public class Student : INotifyPropertyChanged
    {
        public object obj { get; set; }
        public string Name { get; set; }
        private int age;

        public int Age
        {
            get { return age; }
            set
            {
                age = value;
                PropertyChanged?.Invoke(obj, new PropertyChangedEventArgs("Student"));
            }
        }

        public int Grade { get; set; }

        public event PropertyChangedEventHandler PropertyChanged;
    }

生成实体时,当前的VIewmodel赋值到Student的obj上

        public ICommand TestCmd => new RelayCommand(() =>
        {
            Student = new Student();
            Student.obj = this;
            Random random = new Random();
            Student.Name = "张三";
            Student.Age = random.Next(5, 9);
            Student.Grade = 1;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Student"));

        });

测试同样可以正常运行。

 

后来我又想,如果把Viewmodel的Student直接注册为依赖属性,是不是就可以了。

结果是不行,而且,因为是注册成了依赖属性,依赖属性是不支持手动通知的,导致不管怎么修改都不能正确显示正确结果。

后来,面向了搜索引擎了一下,发现这篇文章  https://blog.csdn.net/Liwuqingxin/article/details/81141856 ,

按照他提供的方法,修改代码如下:

添加针对依赖属性的扩展方法

   public static class Dependencyextension
    {
        public static object InvokeInternal<T>(this T caller, string method, object[] parameters)
        {
            MethodInfo methodInfo = typeof(T).GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic);
            return methodInfo?.Invoke(caller, parameters);
        }
    }

 

ViewModel的Student改为依赖属性

      public Student Student
        {
            get { return (Student)GetValue(StudentProperty); }
            set { SetValue(StudentProperty, value); }
        }

        public static readonly DependencyProperty StudentProperty =
            DependencyProperty.Register("Student", typeof(Student), typeof(MainWindowViewModel), null);

TestCmd改为

       public ICommand TestCmd => new RelayCommand(() =>
        {
            Student = new Student();
            Student.obj = this;
            Random random = new Random();
            Student.Name = "张三";
            Student.Age = random.Next(5, 9);
            Student.Grade = 1;
            this.InvokeInternal<DependencyObject>("NotifySubPropertyChange", new object[] { StudentProperty });

        });

调试测试符合预期。

 

上一篇:[WPF] 制作一个彩虹按钮


下一篇:WPF 排版基础