C# 使用属性更改通知和继承
我有一个实现属性更改通知的基类。为了清晰起见,我省略了实现技术细节C# 使用属性更改通知和继承,c#,mvvm,C#,Mvvm,我有一个实现属性更改通知的基类。为了清晰起见,我省略了实现技术细节 public class PersonDTO : INotifyPropertyChanged { // in real these all have the backing field + equality check in setter + OnChanged call implementations public string Name { get; set; } public int Age { get;
public class PersonDTO : INotifyPropertyChanged {
// in real these all have the backing field + equality check in setter + OnChanged call implementations
public string Name { get; set; }
public int Age { get; set; }
public Gender Gender { get; set; }
public PersonDTO() {
// initialize default values
// this invoke OnChanged so the object state can be maintained
Name = "New person";
Age = 30;
Gender = Gender.Female;
}
protected virtual void OnChanged(string propertyName) {
// raise PropertyChanged
}
}
我有另一个类,它继承自PersonDTO
,并添加了一些属性
public class PersonEditorModel : PersonDTO {
public BindingList<string> Titles { get; private set; }
private readonly IRepository _repository;
public PersonEditorModel(IRepository repository) {
_repository = repository;
}
protected override void OnChanged(string propertyname) {
if (propertyName == "Gender") {
// Here is a NullReferenceException
Titles.Clear();
if (Gender == Gender.Female) {
Titles.AddRange(new[] {"Ms", "Mrs"});
else
Titles.AddRange(new[] {"Mr", "Sir"});
}
// do some other things perhaps using the _repository (which would raise a NullReferenceException again)
}
}
公共类PersonEditorModel:PersonDTO{
公共绑定列表标题{get;private set;}
专用只读IRepository存储库;
公共PersonEditorModel(IRepository存储库){
_存储库=存储库;
}
已更改受保护的重写void(字符串propertyname){
如果(propertyName==“性别”){
//这里有一个NullReferenceException
标题。清除();
如果(性别==性别。女性){
Titles.AddRange(新[]{“Ms”、“Mrs”});
其他的
Titles.AddRange(新[]{“先生”、“先生”});
}
//使用_存储库做一些其他事情(这会再次引发NullReferenceException)
}
}
此模型的问题在于,在基本构造函数中,设置属性会调用更改通知,并且子类中的OnChanged
方法在尚未构造子类时执行(Titles
list为null)
我一直在想的几种方法。
性别
和标题
应该同步OnChanged
中检查该标志。这对于类似这样的简单情况是有效的,但如果我有一个3级层次结构会怎么样。我需要确保在最底层的构造函数完成运行时设置该标志,这一点并不简单StartChangeTracking()的东西
。这是有问题的,因为子类可以有不同的构造函数参数,就像在本例中的IRepository
服务一样。此外,工厂方法模式会使例如Json序列化/反序列化变得相当困难(我指的是那些没有构造函数参数的类)这里有两种选择。首先,您可以直接分配
标题
,如下所示,假设您永远不会重新分配绑定列表
(通常在MVVM中,我们使用可观察集合
)我已将其设置为只读。这确保了标题
永远不会为空,并且您不必执行空
-检查
// C# 6.0, read only property
public BindingList<string> Titles { get; } = new BindingList<string>();
// C# 5.0 and older
private readonly BindingList<string> titles = new BindingList<string>();
public BindingList<string> Titles { get { return titles; } }
不太理想,因为根据执行顺序,您可能会进入另一个分支,其中标题
可能为空,您必须添加额外的空检查和赋值。此外,当您的父构造函数执行完毕时,它将执行您的子构造函数,并且那里的标题
将已经被赋值。如果再次在此处重新分配,将覆盖在OnChanged
中完成的分配
最后但并非最不重要的一点是,您可以进行惰性实例化
private BindingList<string> titles;
public BindingList<string> Titles {
get
{
if(titles == null)
{
titles = new BindingList<string>();
}
return titles;
}
}
在构造函数中调用Gender=Gender.Female
,如果代码如下所示,它可能不会触发OnChange
方法
public Gender
{
get { return gender; }
set
{
if(gender!=value)
{
gender = value;
OnChange("Gender");
}
}
}
我终于意识到,要解决这个问题,我必须解决间接导致这种行为的根本问题。根本问题是虚成员调用构造函数(在本例中是以间接方式) 因此,我决定根本不使用虚拟的
OnChanged
方法,而是自行订阅对象自己的PropertyChanged
事件。自行订阅应始终安全,无需取消订阅。解决方案如下所示
public class PersonEditorModel : PersonDTO {
public BindingList<string> Titles { get; private set; }
private readonly IRepository _repository;
public PersonEditorModel(IRepository repository) {
_repository = repository;
Titles = new BindingList<string>();
UpdateTitles();
PropertyChanged += OnPropertyChanged;
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (e.PropertyName == "Gender") {
UpdateTitles();
}
}
private void UpdateTitles() {
Titles.Clear();
if (Gender == Gender.Female) {
Titles.AddRange(new[] {"Ms", "Mrs"});
else
Titles.AddRange(new[] {"Mr", "Sir"});
}
}
公共类PersonEditorModel:PersonDTO{
公共绑定列表标题{get;private set;}
专用只读IRepository存储库;
公共PersonEditorModel(IRepository存储库){
_存储库=存储库;
Titles=新绑定列表();
updatetiles();
PropertyChanged+=OnPropertyChanged;
}
私有void OnPropertyChanged(对象发送方,PropertyChangedEventArgs e){
如果(例如,PropertyName==“性别”){
updatetiles();
}
}
私有void updatetiles(){
标题。清除();
如果(性别==性别。女性){
Titles.AddRange(新[]{“Ms”、“Mrs”});
其他的
Titles.AddRange(新[]{“先生”、“先生”});
}
}
提供的解决方案是可行的,但我认为架构甚至视图模型本身都可以改进,如下所示:
1)数据模型与服务模型/视图模型的分离-已经在评论中提到,但这里用一个示例进行了说明。视图模型(您的“编辑器”类)应该尽可能松散地耦合到数据模型,因此继承是不允许的。此外,DI建议也可以,尽管可能会使用一些
2)与魔法字符串相比,更喜欢静态类型。例如E.PropertyName==“Gender”
容易受到通常通过重构(自动)完成的属性名称更改的影响,但无法更改这些字符串
public enum Gender
{
Male,
Female
};
// this should be a simple class (POCO), persistence agnostic
public class PersonDTO
{
public string Name { get; set; }
public int Age { get; set; }
public Gender Gender { get; set; }
}
// repository interface
public interface IRepository<T>
{
IQueryable<T> GetAll();
T GetById(int id);
}
// this is responsible for delivering person related information without exposing fetching details
public class PersonService
{
private IRepository<PersonDTO> _Repository;
public PersonService(IRepository<PersonDTO> repository)
{
_Repository = repository;
}
// normally, service should return service models that are view agnostic, but this requires extra mapping
// so, for convenience service returns the view model
public PersonEditorModel GetPerson(int id)
{
var ret = AutoMapper.Mapper.Map<PersonEditorModel>(_Repository.GetById(id));
return ret;
}
}
// base editor model (or view model)
public class BaseEditorModel : INotifyPropertyChanged
{
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises the PropertyChanged event
/// </summary>
/// <param name="propertyName">Name of the property</param>
protected void OnPropertyChanged(string propertyName)
{
var ev = PropertyChanged;
if (ev != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
// base editor model (or view model) that allow statically-typed property changed notifications
public abstract class BaseEditorModel<TVm> : BaseEditorModel
where TVm : BaseEditorModel<TVm>
{
/// <summary>
/// Raises the PropertyChanged event
/// </summary>
/// <param name="expr">Lambda expression that identifies the updated property</param>
protected void OnPropertyChanged<TProp>(Expression<Func<TVm, TProp>> expr)
{
var prop = (MemberExpression)expr.Body;
OnPropertyChanged(prop.Member.Name);
}
}
// the actual editor
// notice that property changed is done directly on setters and without magic strings
public class PersonEditorModel : BaseEditorModel<PersonEditorModel>
{
public BindingList<string> Titles { get; private set; }
public PersonEditorModel()
{
Titles = new BindingList<string>();
UpdateTitles();
}
private Gender _Gender;
public Gender Gender
{
get { return _Gender; }
set
{
_Gender = value;
UpdateTitles();
OnPropertyChanged(m => m.Gender);
}
}
private void UpdateTitles()
{
Titles = Gender == Gender.Female ?
new BindingList<string>(new[] { "Ms", "Mrs" }) :
new BindingList<string>(new[] { "Mr", "Sir" });
OnPropertyChanged(m => m.Titles);
}
}
// just an example
class Program
{
static void Main(string[] args)
{
// this should be performed one per application run
// obsolete in a newer version
AutoMapper.Mapper.CreateMap<PersonDTO, PersonEditorModel>();
var service = new PersonService(null); // get service using DI
// work on a dummy/mock person
var somePerson = service.GetPerson(30);
// bind and do stuff with person view model
}
}
公共枚举性别
{
男,,
女性
};
//这应该是一个简单类(POCO),持久性不可知
公共类个人
{
公共字符串名称{get;set;}
公共整数{get;set;}
公共性别{get;set;}
}
//存储库接口
公共接口假定
{
IQueryable GetAll();
T GetById(int-id);
}
//它负责传递与人员相关的信息,而不公开获取细节
公共类人员服务
{
私人IRepository_存储库;
公共人员服务(IRepository存储库)
{
_存储库=存储库;
}
//通常,服务应该返回视图不可知的服务模型,但这需要额外的映射
//所以
public class PersonEditorModel : PersonDTO {
public BindingList<string> Titles { get; private set; }
private readonly IRepository _repository;
public PersonEditorModel(IRepository repository) {
_repository = repository;
Titles = new BindingList<string>();
UpdateTitles();
PropertyChanged += OnPropertyChanged;
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (e.PropertyName == "Gender") {
UpdateTitles();
}
}
private void UpdateTitles() {
Titles.Clear();
if (Gender == Gender.Female) {
Titles.AddRange(new[] {"Ms", "Mrs"});
else
Titles.AddRange(new[] {"Mr", "Sir"});
}
}
public enum Gender
{
Male,
Female
};
// this should be a simple class (POCO), persistence agnostic
public class PersonDTO
{
public string Name { get; set; }
public int Age { get; set; }
public Gender Gender { get; set; }
}
// repository interface
public interface IRepository<T>
{
IQueryable<T> GetAll();
T GetById(int id);
}
// this is responsible for delivering person related information without exposing fetching details
public class PersonService
{
private IRepository<PersonDTO> _Repository;
public PersonService(IRepository<PersonDTO> repository)
{
_Repository = repository;
}
// normally, service should return service models that are view agnostic, but this requires extra mapping
// so, for convenience service returns the view model
public PersonEditorModel GetPerson(int id)
{
var ret = AutoMapper.Mapper.Map<PersonEditorModel>(_Repository.GetById(id));
return ret;
}
}
// base editor model (or view model)
public class BaseEditorModel : INotifyPropertyChanged
{
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises the PropertyChanged event
/// </summary>
/// <param name="propertyName">Name of the property</param>
protected void OnPropertyChanged(string propertyName)
{
var ev = PropertyChanged;
if (ev != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
// base editor model (or view model) that allow statically-typed property changed notifications
public abstract class BaseEditorModel<TVm> : BaseEditorModel
where TVm : BaseEditorModel<TVm>
{
/// <summary>
/// Raises the PropertyChanged event
/// </summary>
/// <param name="expr">Lambda expression that identifies the updated property</param>
protected void OnPropertyChanged<TProp>(Expression<Func<TVm, TProp>> expr)
{
var prop = (MemberExpression)expr.Body;
OnPropertyChanged(prop.Member.Name);
}
}
// the actual editor
// notice that property changed is done directly on setters and without magic strings
public class PersonEditorModel : BaseEditorModel<PersonEditorModel>
{
public BindingList<string> Titles { get; private set; }
public PersonEditorModel()
{
Titles = new BindingList<string>();
UpdateTitles();
}
private Gender _Gender;
public Gender Gender
{
get { return _Gender; }
set
{
_Gender = value;
UpdateTitles();
OnPropertyChanged(m => m.Gender);
}
}
private void UpdateTitles()
{
Titles = Gender == Gender.Female ?
new BindingList<string>(new[] { "Ms", "Mrs" }) :
new BindingList<string>(new[] { "Mr", "Sir" });
OnPropertyChanged(m => m.Titles);
}
}
// just an example
class Program
{
static void Main(string[] args)
{
// this should be performed one per application run
// obsolete in a newer version
AutoMapper.Mapper.CreateMap<PersonDTO, PersonEditorModel>();
var service = new PersonService(null); // get service using DI
// work on a dummy/mock person
var somePerson = service.GetPerson(30);
// bind and do stuff with person view model
}
}