• 【.NET深呼吸】INotifyPropertyChanged接口的真故事


    无论是在流氓腾的问问社区,还是在黑度贴吧,或是“厕所等你”论坛上,曾经看到过不少朋友讨论INotifyPropertyChanged接口。不少朋友认为该接口是为双向绑定而使用的,那么,真实的情况是这样的吗?

    INotifyPropertyChanged接口位于System.ComponentModel命名空间,在该命名空间下还有另一个接口:INotifyPropertyChanging。INotifyPropertyChanging接口定义了PropertyChanging事件,应该在在属性值正在改变时引发;INotifyPropertyChanged接口定义了PropertyChanged事件,应当在属性的值已经改变后引发。

    由于INotifyPropertyChanging接口仅在完整的.net库才有,在可移植的库里面并没有定义,因此,INotifyPropertyChanged接口的使用频率更高。而且,多数情况下,我们只关心属性值是否已经改变,而对属性值的修改过程并不关注。

    上面废话了一大堆,本文的主旨问题就来了——INotifyPropertyChanged接口是否只是跟双向绑定有关?

    下面我们考虑第一种情况。

    在单向绑定中,使用INotifyPropertyChanged接口和不使用INotifyPropertyChanged接口会有什么不同。

    咱们定义一个类,这个类有一个公共的Value属性,当实例化类时,会通过Timer类,每隔3秒钟更新一下Value属性,属性值使用随机整数。代码如下:

        public class TestDemo
        {
            Timer _timer = null;
            Random _rand = null;
    
            public TestDemo ()
            {
                _rand = new Random();
                TimerCallback cb = ( s ) =>
                    {
                        Value = _rand.Next();
                    };
                _timer = new Timer(cb, null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(3));
            }
    
            int _val;
            public int Value
            {
                get { return _val; }
                set { _val = value; }
            }
        }

    代码我不解释了,相信大家能看懂,因为不复杂,注意的是,Timer对象一但实例化就会马上计时的。
    现在把这个示范类用在单向绑定上,让Value属性的值显示在TextBlock上。

    <Window x:Class="SampleApp1.MainWindow"
            ……
            xmlns:local="clr-namespace:SampleApp1"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Grid.Resources>
                <local:TestDemo x:Key="td"/>
            </Grid.Resources>
            <TextBlock FontSize="24" Text="{Binding Source={StaticResource td},Path=Value,Mode=OneWay}"/>
        </Grid>
    </Window>

    OneWay就是单向绑定,现在运行应用程序,这时会发现,TextBlock上的文本一值没有改变。那是不是计时器没有成功计时呢?

    通过断点调试发现,计时器是成功计时了,而Value属性也顺利地被修改,如下图:

    按理说,单向绑定会让数据从数据源流向绑定目标的,那为什么TextBlock控件没有即时更新呢? 原因是Binding没有接收到属性更改通知,故没有去取最新的值。

    下面我们让示范类实现INotifyPropertyChanged接口。

        public class TestDemo:INotifyPropertyChanged
        {
            Timer _timer = null;
            Random _rand = null;
    
            public TestDemo ()
            {
                _rand = new Random();
                TimerCallback cb = ( s ) =>
                {
                    Value = _rand.Next();
                };
                _timer = new Timer(cb, null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(3));
            }
    
            int _val;
            public int Value
            {
                get { return _val; }
                set
                {
                    if (_val != value)
                    {
                        _val = value;
                        // 引发属性更改通知事件
                        if (this.PropertyChanged != null)
                        {
                            PropertyChanged(this, new PropertyChangedEventArgs("Value"));
                        }
                    }
                }
            }
    
            // 实现INotifyPropertyChanged接口的事件
            public event PropertyChangedEventHandler PropertyChanged;
        }

    这时候,再次运行应用程序,就发现TextBlock中的值能够自动更新了。
         

    上面的例子说明了什么? 它表明,属性更改通知并不是只有在双向绑定中才使用,在单向绑定中同样需要。

    下面再看看双向绑定的情况。

    我们先来验证一个问题:作为数据源的类型是不是一定要实现INotifyPropertyChanged接口才能被UI更新呢?

    先定义一个用来测试的类。

        public class Employee
        {
            private string _name;
            private string _city;
    
            public string Name
            {
                get
                {
                    return _name; 
                }
                set
                {
                    _name = value;
                }
            }
    
            public string City
            {
                get 
                {
                    return _city; 
                }
                set
                {
                    _city = value;
                }
            }
        }
    <Window x:Class="SampleApp2.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:SampleApp2"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Grid.Resources>
                <local:Employee x:Key="emp" Name="小明" City="重庆"/>
            </Grid.Resources>
            
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
            </Grid.RowDefinitions>
            <GroupBox Grid.Row="0">
                <GroupBox.Header>
                    <TextBlock Text="修改信息" FontSize="24" Foreground="Blue"/>
                </GroupBox.Header>
                <StackPanel DataContext="{Binding Source={StaticResource emp}}">
                    <TextBlock Text="贡工姓名:"/>
                    <TextBox Width="200" HorizontalAlignment="Left" Text="{Binding Path=Name,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                    <TextBlock Margin="0,15,0,0" Text="所在城市:"/>
                    <TextBox Width="200" HorizontalAlignment="Left" Text="{Binding Path=City,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                </StackPanel>
            </GroupBox>
            
            <GroupBox Grid.Row="1" Margin="0,20,0,0">
                <GroupBox.Header>
                    <TextBlock Text="显示信息" Foreground="Blue" FontSize="24"/>
                </GroupBox.Header>
                <TextBlock DataContext="{Binding Source={StaticResource emp}}">
                    员工姓名;
                    <Run Text="{Binding Name}"/>
                    <LineBreak/>
                    所在城市:
                    <Run Text="{Binding City}"/>
                </TextBlock>
            </GroupBox>
        </Grid>
    </Window>

    Employee类并没有实现INotifyPropertyChanged接口,但是运行上面程序后会发现,在TextBox中修改数据后,下面的TextBlock是可以自动更新的。下面我们把上面例子改一下,不通过Binding来更新数据,而是用代码来手动改。

                <StackPanel DataContext="{Binding Source={StaticResource emp}}">
                    <TextBlock Text="贡工姓名:"/>
                    <!--<TextBox Width="200" HorizontalAlignment="Left" Text="{Binding Path=Name,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>-->
                    <TextBox Width="200" HorizontalAlignment="Left" x:Name="txtName"/>
                    <TextBlock Margin="0,15,0,0" Text="所在城市:"/>
                    <!--<TextBox Width="200" HorizontalAlignment="Left" Text="{Binding Path=City,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>-->
                    <TextBox Width="200" HorizontalAlignment="Left" x:Name="txtCity"/>
                    <Button Content="更  新" Click="OnClick" Width="200" HorizontalAlignment="Left" Margin="0,10,0,0"/>
                </StackPanel>
            private void OnClick ( object sender, RoutedEventArgs e )
            {
                Employee emp = layoutRoot.Resources["emp"] as Employee;
                if (emp != null)
                {
                    emp.Name = txtName.Text;
                    emp.City = txtCity.Text;
                }
            }

    这种情况下,是通过代码来修改示例对象的属性。运行示例程序后,会发现,修改内容后,下面的TextBlock控件不会自动更新。而通过断点调试,发现Employee实例的属性值确实已经被更新,可是TextBlock没有显示新的值。

    然后,我们让Employee类实现

        public class Employee : INotifyPropertyChanged
        {
            private string _name;
            private string _city;
    
            public string Name
            {
                get
                {
                    return _name; 
                }
                set
                {
                    if (_name != value)
                    {
                        _name = value;
                        OnPropertyChanged();
                    }
                }
            }
    
            public string City
            {
                get 
                {
                    return _city; 
                }
                set
                {
                    if (_city != value)
                    {
                        _city = value;
                        OnPropertyChanged();
                    }
                }
            }
    
            private void OnPropertyChanged([CallerMemberName] string propName=""){
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propName));
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
        }

    在每个属性值发生更改后都要引发PropertyChanged事件,这里用一个OnPropertyChanged方法封装起来,参数是发生更改的属性的名字。该处用到一个技巧,就是在参数上附加CallerMemberNameAttribute特性,并给参数一个默认值:空字符串。
    在属性的set访问器中调用OnPropertyChanged方法时就不需要写上属性的名字了,CallerMemberNameAttribute会自动把调用方的成员名字赋给方法参数,由于OnPropertyChanged方法是在被更改的属性内调用的,所以CallerMemberNameAttribute得到的正是这个属性的名字,如此一来我们就省事很多了。

    现在运行应用程序。修改对象属性,TextBlock就能够自动更新了。

    通过以上各例,可以发现,INotifyPropertyChanged接口并不是绝对地与双向绑定有关,在完全使用Binding进行双向处理的时候,即使不实现INotifyPropertyChanged接口也可以实现获取更新,当然,Binding的源一定是同一个实例。但如果修改数据不是通过Binding来完成的,使用数据源的各个客户方就不会获得属性更改通知,因此这时候需要实现INotifyPropertyChanged接口。

    经过上面几个演示,我们可以发现,INotifyPropertyChanged接口并不一定要在双向绑定的时候使用,但是为了让使用数据的代码能够及时获得属性更改通知,数据源对象都应该实现INotifyPropertyChanged接口,大家可以看看Linq to SQL或者实体模型中,开发工具生成的实体类型都是实现INotifyPropertyChanged接口的,这正是考虑到要让所有数据使用都能及时获得更新通知的做法。

    希望,通过我这篇烂文的讲述,大家能够对INotifyPropertyChanged有新的认识。

  • 相关阅读:
    网站统计中的数据收集原理及实现
    启动hadoop报ERROR org.apache.hadoop.hdfs.server.namenode.FSImage: Failed to load image from FSImageFile
    淘宝(大数据库应用)--转载
    MapReduce作业的map task和reduce task调度参数
    Spark和Hadoop作业之间的区别
    分析MapReduce执行过程
    MapReduce框架Partitioner分区方法
    LVS+keepalived实现负载均衡
    Tomcat 详解
    linux Tomcat restart脚本简单版
  • 原文地址:https://www.cnblogs.com/tcjiaan/p/4149203.html
Copyright © 2020-2023  润新知