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

不管是在流氓騰的問問社區,仍是在黑度貼吧,或是「廁所等你」論壇上,曾經看到過很多朋友討論INotifyPropertyChanged接口。很多朋友認爲該接口是爲雙向綁定而使用的,那麼,真實的狀況是這樣的嗎?dom

INotifyPropertyChanged接口位於System.ComponentModel命名空間,在該命名空間下還有另外一個接口:INotifyPropertyChanging。INotifyPropertyChanging接口定義了PropertyChanging事件,應該在在屬性值正在改變時引起;INotifyPropertyChanged接口定義了PropertyChanged事件,應當在屬性的值已經改變後引起。工具

因爲INotifyPropertyChanging接口僅在完整的.net庫纔有,在可移植的庫裏面並無定義,所以,INotifyPropertyChanged接口的使用頻率更高。並且,多數狀況下,咱們只關心屬性值是否已經改變,而對屬性值的修改過程並不關注。開發工具

上面廢話了一大堆,本文的主旨問題就來了——INotifyPropertyChanged接口是否只是跟雙向綁定有關?測試

 

下面咱們考慮第一種狀況。this

在單向綁定中,使用INotifyPropertyChanged接口和不使用INotifyPropertyChanged接口會有什麼不一樣。spa

我們定義一個類,這個類有一個公共的Value屬性,當實例化類時,會經過Timer類,每隔3秒鐘更新一下Value屬性,屬性值使用隨機整數。代碼以下:.net

    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上。3d

<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有新的認識。

相關文章
相關標籤/搜索