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;
});
測試一下
————————————————————————————————————————————————————————————-
上面的程式碼是沒問題的,也是最常見的思路,因為直接綁定的類的子屬性,在子屬性修改時,通知UI。
不過我的需求則是:當學生的年齡與學級的差大於6時,則需要在原來的年齡上有超齡提醒。
比如:
那麼修改程式碼,前端
<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();
}
}
開始調試
納尼???,怎麼年齡一直是默認值呢!!!所有的類我都實現了 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"));
});
調試一下
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直接註冊為依賴屬性,是不是就可以了。
結果是不行,而且,因為是註冊成了依賴屬性,依賴屬性是不支援手動通知的,導致不管怎麼修改都不能正確顯示正確結果。
後來,面向了搜索引擎了一下,發現這篇文章 //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 });
});
調試測試符合預期。