现在的位置: 首页 > 综合 > 正文

玩转INotifyPropertyChanged和ObservableCollection

2012年08月03日 ⁄ 综合 ⁄ 共 6338字 ⁄ 字号 评论关闭

      本文的代码都是基于WPF的,对于Silverlight,这些技术也同样适用。

      (一)INotifyPropertyChanged的使用场合

      先写一个最简单的数据绑定,每次点击Button后,TextBlock的值都会自增1。

      效果图如下所示:

      clip_image002

      这里使用了MVVM模式,并把Click事件抽象为了Command。

      代码下载:WpfApplication4_1.zip

      观察上面的代码,注意到几个细节:

      1. UserName和Age属性作为ViewModel的两个属性,因为Age递增是基于绑定实现的,所以ViewModel要实现INotifyPropertyChanged接口。

      2. 我们只在Age上添加了OnPropertyChanged方法,它会根据Age属性的变化而自动更新XAML中绑定的值。

            而对于UserName属性,由于它是始终不变的,所以没有添加OnPropertyChanged方法。

            由此可见,OnPropertyChanged方法决定了后台数据的变化是否能影响到前台绑定的XAML。

            此外,对于一次性绑定(以后不会再改变)的属性,不要添加OnPropertyChanged方法,因为该方法会增加额外的性能开销。

      但是,在MVVM模式中,我们常常将UserName和Age属性抽象出来,作为一个实体类: 

    public class UserInfo
    
{
        
public string UserName getset; }
        
public int Age getset; }
    }

      与此同时,在ViewModel中添加一个UserInfo类型的属性: 

    public class RegisterUserViewModel : INotifyPropertyChanged
    {
        
public UserInfo User { getset; }
    

      注意:此时XAML中的绑定相应要修改为: 

<TextBlock Name="tbUserName" Text="{Binding User.UserName}" …. />
<TextBlock Name="tbAge" Text="{Binding User. Age}" …. />

      这时候问题就来了。到底是UserInfo实现INotifyPropertyChanged接口呢,还是ViewModel实现INotifyPropertyChanged接口呢?

      1.如果UserInfo实现INotifyPropertyChanged接口,,那么UserInfo实体类相应地修改为:

    public class UserInfo : INotifyPropertyChanged
    
{
        
public string UserName getset; }

        
private int age;
        
public int Age
        
{
            
get
            
{
                
return this.age;
            }

            
set
            
{
                
if (this.age != value)
                
{
                    
this.age = value;
                    OnPropertyChanged(
"Age");
                }

            }

        }


        
INotifyPropertyChanged Members
    }

      而ViewModel相应地就简化了: 

    public class RegisterUserViewModel
    
{
        
public RegisterUserViewModel()
        
{
            
this.User = new UserInfo { UserName = "Baobao", Age = 27 };
            ClickCommand 
= new DelegateCommand<object>(OnClick, arg => true);
        }


        
void OnClick(object obj)
        
{
            
this.User.Age += 1;
        }


        
public UserInfo User getset; }

        
public RegisterUserView View getset; }
        
        
public ICommand ClickCommand getset; }
    }

      注意到,此时ViewModel不需要实现INotifyPropertyChanged接口。因为INotifyPropertyChanged监视的是UserInfo实体里面的Age属性,所以在OnClick方法中,只要修改User实体的Age属性就能通知XAML做出改变。

      示例下载:WpfApplication4_2.zip

      2.如果UserInfo不实现INotifyPropertyChanged接口,那么这个实体类就非常简单了: 

    public class UserInfo
    
{
        
public string UserName getset; }
        
public int Age getset; }
    }

      这时,ViewModel就必须要实现INotifyPropertyChanged接口,而且要为User属性添加OnPropertyChanged方法,以下是部分代码(省略了INotifyPropertyChanged接口的实现): 

    public class RegisterUserViewModel : INotifyPropertyChanged
    
{
        
public RegisterUserViewModel()
        
{
            
this.User = new UserInfo { UserName = "Baobao", Age = 27 };
            ClickCommand 
= new DelegateCommand<object>(OnClick, arg => true);
        }


        
void OnClick(object obj)
        
{
            
this.User.Age += 1;
        }


        
private UserInfo user;
        
public UserInfo User
        
{
            
get
            
{
                
return this.user;
            }

            
set
            
{
                
if (this.user != value)
                
{
                    
this.user = value;
                    OnPropertyChanged(
"User");
                }

            }

        }


        
public RegisterUserView View getset; }

        
public ICommand ClickCommand getset; }
    }

      代码下载:WpfApplication4_3.zip

      如果运行上述代码,你会发现XAML中的Age并不会自增1。问题应该出在OnClick方法上,我在下面一行添加了断点: 

this.User.Age += 1;

      我发现User的Age属性确实由27变成了28,但是并没有把改变结果通知XAML。

      这是因为,INotifyPropertyChanged接口只监视UserInfo这个实体的地址是否发生了改变,而目前这个地址并没有变化,只是存储在UserInfo实体中的成员Age发生了改变,而Age的地址并不在INotifyPropertyChanged接口的监视之下,所以XAML中没有任何改变。

      看来只有修改UserInfo这个实体的地址了,最好的方法就是重新实例化一个UserInfo实体: 

Code

      示例下载:WpfApplication4_4.zip

      比较两种实现方式,我个人倾向于第一种,因为它更自然,而第二种,由于要new一个新的UserInfo对象,所以要多牺牲一些性能。但是不管哪种实现方式,都不要ViewModel和UserInfo同时实现INotifyPropertyChanged接口,这是非常消耗性能的。

      (二)ObservableCollection和List的使用场合

      始终对ObservableCollection怀有恐惧,因为它太笼统了。

      首先它是一个集合,但它是一个既实现了INotifyPropertyChanged接口又实现了INotifyCollectionChanged接口的集合,这是它与List<T>的不同之处。

      使用ObservableCollection<T>注定要比List<T>消耗性能,所以我一直在探寻它们各自不同的使用场合。而且,当泛型参数T也实现了INotifyPropertyChanged接口时,就更加复杂了。

      于是分为以下四种情况:

      1.最简单是List<T>,其中T没有实现INotifyPropertyChanged接口,就是一个简单的实体。

      做一个简单的示例,统计一个班级的计算机成绩。

      clip_image004

      由于List<T>中的T没有实现INotifyPropertyChanged接口,所以增加、删除、修改这3个Button都不会起作用。

      着眼于“及格”这个CheckBox,勾选后就只显示分数不小于60的学生列表,取消勾选择显示全部学生。为此,在XAML中要把CheckBox的绑定方式设置为TwoWay(虽然CheckBox默认就是TwoWay,但还是建议显式指定): 

<CheckBox Name="chkPass" IsChecked="{Binding Path=IsPassed, Mode=TwoWay}">及格</CheckBox>

      在ViewModel中,关键是ModelPropertyChanged方法,在IsPassed属性变化的时候,会执行这个方法: 

        void ModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        
{
            
switch (e.PropertyName)
            
{
                
case "IsPassed":
                    
this.StudentList = this.IsPassed ? StudentService.RetrievePassedStudentList() : StudentService.RetrieveStudentList();
                    
break;
                
default:
                    
break;
            }

        }

      看得出,根据IsPassed属性的不同,分别为StudentList分配不同的地址,所以在XAML中的DataGrid会跟着发生改变。

      随着这里的ViewModel也实现了INotifyPropertyChanged接口,但这是为了IsPassed属性和StudentList属性而实现的,与Student实体类无关。

      clip_image006

      代码下载:WpfApplication6_1.zip

      如果大家读过我之前介绍MVP模式的一系列文章,这个小例子应该很简单。

      结论:List<T>(T未实现INotifyPropertyChanged接口),适用于一次性绑定,适用于重新实例化整个List集合的绑定(例如上文的StudentList)。

      2.稍微复杂点,List<T>,其中T实现了INotifyPropertyChanged接口。

      也就是说,让Student实体类实现INotifyPropertyChanged接口。 

    public class Student : INotifyPropertyChanged
    
{
        
public int UserId getset; }
        
public 

抱歉!评论已关闭.