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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM