WPF快速入門系列(4)——深入解析WPF綁定


一、引言

  WPF綁定使得原本需要多行代碼實現的功能,現在只需要簡單的XAML代碼就可以完成之前多行后台代碼實現的功能。WPF綁定可以理解為一種關系,該關系告訴WPF從一個源對象提取一些信息,並將這些信息來設置目標對象的屬性。目標屬性總是依賴屬性。然而,源對象可以是任何內容,可以是一個WPF元素、或ADO.NET數據對象或自定義的數據對象等。下面詳細介紹了WPF綁定中的相關知識點。

二、綁定元素對象

2.1 如何實現綁定元素對象

  這里首先介紹綁定最簡單的情況——綁定元素對象,即數據源是一個WPF元素對象並且源屬性是依賴屬性。由於依賴屬性具有內置的更改通知支持,因此,當在源對象中改變依賴屬性的值時,會立即更新目標對象中的綁定屬性。下面通過一個簡單的例子來演示下如何綁定元素對象。具體的XAML代碼(這里不需要后台代碼)如下所示:

 <StackPanel>
        <Slider Name="sliderFontSize" Margin="3"
            Minimum="1" Maximum="40" Value="10" TickFrequency="1" TickPlacement="TopLeft"/>
        <TextBlock Margin="10" Text="LearningHard" Name="lbtext"
                   FontSize="{Binding ElementName=sliderFontSize, Path=Value}"></TextBlock>
    </StackPanel>

  在上面XAML代碼中,TextBlock控件的FontSize屬性綁定了Slider控件的Value屬性,感覺說綁定有點拗口,你可以直接理解為TextBlock的FontSize屬性的值來自與Slider控件的Value值,由於源屬性Value是依賴屬性,具體內置的更改通知功能,所以Slider控件Value值的改變,直接影響TextBlock控件FontSize的值。正如我們分析的那樣,實際運行結果也是如此,運行結果如下圖所示:

  當移動上圖中Slider控件上的游標時,下面的文本字體大小也會跟着一起改變。具體效果這里就不貼圖了,大家可以自行嘗試。從中可以看到WPF綁定的強大了吧,如果放到以前WinForm開發中,你需要監聽Slider的ValueChanged事件,然后在事件處理程序中去動態改變文本的字體大小。

  這里Path除了可以直接綁定屬性之外,還可以綁定屬性的屬性,如FontFamily.Source,也可以指向屬性使用的索引器,如Content.Children[0]。當然你也可以執行多層次的路徑,如指向屬性的屬性的屬性等。

  另外,如果綁定失敗時,WPF不會引發異常來告知綁定失敗的原因。例如,指定的元素或屬性不存在,此時不會收到任何提示,只是在目標屬性不能顯示數據罷了。然而在調試模式下,你可以在輸出窗口來查看綁定失敗的信息,例如,在上面XAML代碼,我們綁定Slider控件一個不存在的屬性,如Text屬性,此時在Output窗口中就會看到如下信息:

2.2 綁定模式

  綁定的一個最大的特點就是源屬性改變時,目標屬性會自動地更新。然而上面的示例有一個問題,即目標對象的改變不會自動更新源對象的屬性。通過下面的例子可以看出這個問題所在。此時XAML代碼修改為:

<Window x:Class="WPFBindingDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="400">
    <StackPanel>
        <Slider Name="sliderFontSize" Margin="3"
            Minimum="1" Maximum="40" Value="10" TickFrequency="1" TickPlacement="TopLeft"/>
        <TextBlock Margin="10" Text="LearningHard" Name="lbtext"
                   FontSize="{Binding ElementName=sliderFontSize, Path=Value}"></TextBlock>
        
        <!--在按鈕的Click事件處理程序中去改變目標對象的FontSize的值-->
        <StackPanel Orientation="Horizontal">
        <Button Margin="10" Padding="5" Click="cmd_SetSmall">Set to Small</Button>
        <Button Margin="10" Padding="5" Click="cmd_SetNormal">Set to Normal</Button>
        <Button Margin="10" Padding="5" Click="cmd_SetLarge">Set to Large</Button>
        </StackPanel>
    </StackPanel>
</Window>

  此時后台C#代碼如下所示:

        private void cmd_SetSmall(object sender, RoutedEventArgs e)
        {
            // 僅僅在雙向模式下工作
            lbtext.FontSize = 5;
        }

        private void cmd_SetNormal(object sender, RoutedEventArgs e)
        {
            sliderFontSize.Value = 20;
        }

        private void cmd_SetLarge(object sender, RoutedEventArgs e)
        {
            // 僅僅在雙向模式下工作
            lbtext.FontSize = 40;
        }

  具體的運行效果如下圖所示:

  從上圖可以看到,當在后台更改TextBlock的FontSize屬性值,而Slider的Value值卻沒有進行更新。此時,你肯定會想問,能不能實現目標屬性的更變也會自動改變綁定中源屬性的機制呢?因為這樣就不會顯得那樣呆板了,然而,你想到的了WPF團隊肯定也想到了,WPF支持雙向綁定,即從源到目標以及目標到源,要支持雙向綁定,只需要設置Binding對象的Mode屬性為TwoWay即可,修改后的XAML代碼為:

 <StackPanel>
        <Slider Name="sliderFontSize" Margin="3"
            Minimum="1" Maximum="40" Value="10" TickFrequency="1" TickPlacement="TopLeft"/>
        <TextBlock Margin="10" Text="LearningHard" Name="lbtext"
                   FontSize="{Binding ElementName=sliderFontSize, Path=Value, Mode=TwoWay}"></TextBlock>
        
        <!--在按鈕的Click事件處理程序中去改變目標對象的FontSize的值-->
        <StackPanel Orientation="Horizontal">
        <Button Margin="10" Padding="5" Click="cmd_SetSmall">Set to Small</Button>
        <Button Margin="10" Padding="5" Click="cmd_SetNormal">Set to Normal</Button>
        <Button Margin="10" Padding="5" Click="cmd_SetLarge">Set to Large</Button>
        </StackPanel>
    </StackPanel>

  Mode屬性除了可以設置OneWay,TwoWay值外,還可以設置Default、OneTime和OneWayToSource,關於這些值更詳細的介紹請自行參考MSDN:http://msdn.microsoft.com/zh-cn/library/system.windows.data.bindingmode(v=vs.110).aspx

  另外,除了可以在XAML中通過Binding標記地方式聲明綁定外,還可以使用代碼方式動態創建綁定。如上面的例子中代碼創建綁定的實現代碼如下所示:

1  Binding binding = new Binding();
2  binding.Source = sliderFontSize;
3  binding.Path = new PropertyPath("Value");
4  binding.Mode = BindingMode.TwoWay;
5  lbtext.SetBinding(TextBlock.FontSizeProperty, binding);

  還可以通過使用BindingOperations類的ClearBinding方法來移除數據綁定。還可以使用ClearAllBindings移除一個元素的所有數據綁定。

2.3 綁定更新

  在上面的例子中,還存在另一個問題,當通過在文本框中輸入內容來改變顯示的字體尺寸時,此時什么事情都不會發生,知道使用tab鍵將焦點轉移到另外一個控件時,才會應用對應的改變。此時XAML代碼如下所示:

<Window x:Class="WPFBindingDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="400">
    <StackPanel>
        <Slider Name="sliderFontSize" Margin="3"
            Minimum="1" Maximum="40" Value="10" TickFrequency="1" TickPlacement="TopLeft"/>
        <TextBlock Margin="10" Text="LearningHard" Name="lbtext"
                   FontSize="{Binding ElementName=sliderFontSize, Path=Value, Mode=TwoWay}"></TextBlock>
        
        <!--在按鈕的Click事件處理程序中去改變目標對象的FontSize的值-->
        <StackPanel Orientation="Horizontal">
        <Button Margin="10" Padding="5" Click="cmd_SetSmall">Set to Small</Button>
        <Button Margin="10" Padding="5" Click="cmd_SetNormal">Set to Normal</Button>
        <Button Margin="10" Padding="5" Click="cmd_SetLarge">Set to Large</Button>
        </StackPanel>
        
        <!--添加一個輸入文本框來設置文本字體大小進行測試問題-->
        <StackPanel Orientation="Horizontal" Margin="5">
            <TextBlock VerticalAlignment="Center">Set FontSize:</TextBlock>
            <TextBox Text="{Binding ElementName=lbtext, Path=FontSize, Mode=TwoWay}" Width="100"/>
        </StackPanel>
    </StackPanel>
</Window>

  后台代碼實現與前面的一樣,此時運行的效果如下圖所示:

  為了明白導致這個問題的原因,這里需要深入分析下綁定表達式。當使用OneWay或TwoWay綁定時,改變后的值會立即從源傳播到目標。對於滑動條,然而,從目標到源傳播未必會立即發生。因為,它們的行為是由Binding.UpdateSourceTrigger屬性控制,該屬性可以使用下圖列出的某個值。注意,UpdateSourceTrigger屬性值並不影響目標的更新方式,它僅僅控制TwoWay模式或OneWayToSource模式的綁定更新源的方式。而文本框正是使用LostFocus方式從目標向源進行更新的。

  既然,找出了導致原因,此時可以對XAML代碼進行修改,使得當用於在文本框中輸入內容時將變化應用於字體尺寸,具體改變部分的XAML代碼為:

 <!--添加一個輸入文本框來設置文本字體大小進行測試問題-->
        <StackPanel Orientation="Horizontal" Margin="5">
            <TextBlock VerticalAlignment="Center">Set FontSize:</TextBlock>
            <TextBox Text="{Binding ElementName=lbtext, Path=FontSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
        </StackPanel>

  另外,需要注意的是,TextBox的Text屬性的默認行為是LostFocus,這是因為當用於輸入內容時,文本框中文本會不斷變化,從而引起多次更新。所以PropertyChanged模式可能會使應用程序運行更緩慢,所以LostFocus默認行為可以說是合理的。

  要完全控制源對象的更新時機,也可以選擇UpdateSourceTrigger.Explicit模式。此時就需要額外編寫代碼手動觸發更新,此時可以添加一個Apply按鈕,並在按鈕的Click事件處理程序中調用BindingExpression.UpdateSource方法觸發立即刷新並更新字體大小的操作。具體的實現代碼如下所示:

// 獲得應用於文本框上的綁定
BindingExpression be = txtFontSize.GetBindingExpression(TextBox.TextProperty);
// 調用UpdateSource更新源對象
be.UpdateSource();

三、綁定非元素對象

   上面都是介紹如何鏈接兩個元素的綁定,但是在數據驅動的應用程序中,更常見的情況是創建從一個對象中提起數據的綁定表達式。不過希望綁定的信息必須存儲在一個公有屬性中。因為WPF綁定不能獲取私有信息或公有字段。

  當綁定一個非元素對象時,不能使用Binding.ElementName屬性,但可以使用以下屬性中的一個:

  • Source——該屬性是指向源對象的引用,即提供數據的對象。
  • RelativeSource——該屬性使用RelativeSource對象指定綁定源的相對位置,默認值為null。
  • DataContext屬性——如果沒有使用Source或RelativeSource屬性指定一個數據源,WPF會從當前元素開始在元素樹中向上查找。檢查每個元素的DataContext屬性,並使用第一個非空的DataContext屬性。當然你也可以自己設置DataContext屬性。

  下面通過一個例子來演示下如何綁定到非元素對象。下面的演示如何使用DataContext屬性來綁定一個自定義對象的屬性。首先自定義一個實現了INotifyPropertyChanged接口的類。這個接口是為了發出屬性更改的通知,即實現了這個接口將會實現當源對象的公共屬性發生改變時,該屬性的值會立即響應到界面上顯式。當然不實現這個接口的對象也可以綁定控件中,只要被綁定是公有屬性就可以。具體的實現代碼如下所示:

 1 using System.ComponentModel;
 2 
 3 namespace WPFBindingDemo
 4 {
 5     public class Student:INotifyPropertyChanged
 6     {
 7         private int m_ID;
 8         private string m_StudentName;
 9         private double m_Score;
10        
11         public int ID
12         {
13             get { return m_ID; }
14             set 
15             {
16                 if (value != m_ID)
17                 {
18                     m_ID = value;
19                     Notify("ID");
20                 }
21             }
22         }
23 
24         public string StudentName
25         {
26             get { return m_StudentName; }
27             set
28             {
29                 if (value != m_StudentName)
30                 {
31                     m_StudentName = value;
32                     Notify("StudentName");
33                 }
34             }
35         }
36 
37         public double Score
38         {
39             get { return m_Score; }
40             set 
41             {
42                 if (value != m_Score)
43                 {
44                     m_Score = value;
45                     Notify("Score");
46                 }
47             }
48         }
49 
50         public event PropertyChangedEventHandler PropertyChanged;
51         private void Notify(string propertyName)
52         {
53             if (PropertyChanged != null)
54             {
55                 this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
56             }
57         }
58     }
59 }

  既然源數據對象以准備好了,自然接下來就是去設計WPF界面來讓控件來綁定這個源對象了,具體的XAML代碼如下所示:

<Window x:Class="WPFBindingDemo.BindingToCollection"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="BindingToCollection" Height="300" Width="300">
    <StackPanel Margin="50">
        <StackPanel Orientation="Horizontal" Margin="10">
            <TextBlock Text="學號:"  />
            <TextBlock Text="{Binding Path=ID}" Width="100"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal" Margin="10">
            <TextBlock Text="姓名:"  />
            <TextBlock Text="{Binding Path=StudentName}" Width="100"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal" Margin="10">
            <TextBlock Text="分數:"  />
            <TextBlock Text="{Binding Path=Score}" Width="100"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal" Margin="10">
            <Button Content="改變姓名" Name="changeName" Click="changeName_Click_1"/>
            <Button Content="改變分數" Name="changeScore" Margin="20,0,0,0" Click="ChangeScore_Click"/>
        </StackPanel>
    </StackPanel>
</Window>

  對應的后台代碼邏輯如下所示:

public partial class BindingToCollection : Window
    {
        private Student m_student;
        public BindingToCollection()
        {
            InitializeComponent();
            m_student = new Student() { ID = 1, StudentName = "LearningHard", Score = 60 };
            // 設置Window對象的DataContext屬性
            this.DataContext = m_student ;
        }

        private void ChangeScore_Click(object sender, RoutedEventArgs e)
        {
            m_student.Score = 90;
        }

        private void changeName_Click_1(object sender, RoutedEventArgs e)
        {
            m_student.StudentName = "Learning";
        }
    }

  完成了示例所有代碼的編寫之后,下面具體看看示例的運行效果,看看是否可以成功完成綁定並源對象的屬性的更改會立即反應到界面中,具體的效果圖如下所示:

  從上圖示例的演示動畫效果可以看出,上面的代碼確實實現我們預期的功能。從上面代碼可以看出,我們並沒有對每個控件單獨設置它的Source屬性,而是直接設置了Window對象的DataContext屬性。這樣綁定的控件發現沒有設置source屬性或RelativeSource屬性,就會從元素樹中查找DataContex屬性不為null的值來作為自己的DataContext。通過這樣的方式可以省去重復在多個控件中設置相同的DataContext屬性。

  這里只是演示了綁定單個數據對象的情況,就如之前所說的,數據源還可以是XAML文件,ADO.NET數據對象、集合等,這里就不一一實現了,只要了解具體思路,具體問題具體搜索解決就好了。這里給出兩個非常的好例子。

Simple Demo of Binding to a Database in WPF using LINQ-SQL

How to Perform WPF Data Binding Using LINQ to XML

四、提高大列表的性能

   如果綁定的數據源具有大量記錄時,此時就需要考慮性能的問題了。然而,幸運的是,WPF很多列表控件都已經幫我們做好了相應的支持,這里只是提出來讓大家知道這點。

  對於大列表顯示性能問題,WPF做了以下幾種支持:

  • UI虛擬化——UI虛擬化是列表僅為當前顯示項創建容器對象的一種技術,例如,如果有一個具有5萬條記錄的列表,但是可見區域只能包含30條記錄,ListBox控件只創建30個ListBoxItem對象。如果ListBox控件不支持UI虛擬化的話,它將需要生成全部5萬個ListBoxItem對象,這顯然需要占用更多的內存。並且分配這些對象的時間用戶明顯可以感覺到,這就帶來了非常不好的用戶體驗。WPF中UI虛擬化是通過VirtualizingStackPanel容器實現的。像ListBox、ListView和DataGrid都自動使用VirtualizingStackPanel面板布局它們的子元素,所以,這些控件都默認支持虛擬化功能。然而,ComboBox需要支持虛擬化支持,必須明確提供新的ItemPanelTemplate添加虛擬化支持,具體實現如下所示:
<ComboBox>
   <ComboBox.ItemsPanel>
       <ItemsPanelTemplate>
           <VirtualizingStackPanel></VirtualizingStackPanel>
       </ItemsPanelTemplate>
   </ComboBox.ItemsPanel>
</ComboBox>

  TreeView控件也支持虛擬化,但它在默認情況下,關閉了該支持,你需要顯式啟用該特性,具體使用的啟用代碼如下所示:

 <TreeView VirtualizingPanel.IsVirtualizing="True" />
  • 項目容器再循環——WPF 3.5 SP1使用項目容器再循環改進了虛擬化。通常支持虛擬化的列表在滾動時,控件不斷地創建新的項目容器對象來保存新的可見項。例如,當具有5萬個項的ListBox控件,在滾動時,ListBox需要重新生成新的ListBoxItem對象。但是如果啟用了項目容器再循環,ListBox控件會保存少量ListBoxItem對象存活,當滾動時,將新數據加到這些之前的ListBoxItem對象,從而重復使用它們。具體支持代碼如下所示
<ListBox VirtualizingPanel.VirtualizationMode="Recycling"/>

  項目容器再循環提供了滾動性能,並降低了內存消耗量,因為垃圾回收器不需要查找舊對象進行回收。為了確保向后兼容,除了DataGrid之后的所有列表控件默認都禁用該特性,如需支持,需要顯式啟用。

  • 延遲滾動——為了進一步提供滾動性能,可以開啟延遲滾動功能。使用延遲滾動,當用戶在滾動條上拖動滾動滑塊時不更新列表顯示,只有用戶釋放了滾動滑塊時才刷新。當使用常規滾動時,在拖動的同時會刷新列表,使列表顯示正在改變的位置。這個特性也需要顯式啟用,啟用代碼如下:
 <ListBox ScrollViewer.IsDeferredScrollingEnabled="True"/>

  顯然,需要在響應性和易用性之間平衡。如果有一個復雜的模板和大量數據,對於提高速度可能會選擇使用延遲滾動特性,但當用戶需要在滾動時查看目前滾動位置,則就可以不啟用該特性。

  上面介紹了這么多,其實提供列表控件的性能主要在兩方面:UI虛擬化提高了列表項初始化的時間,因為UI虛擬化支持一次性不初始化所有項,而在滾動是自動創建新的項。項目容器再循環和延遲滾動提高了滾動性能。

  另外WPF綁定還有兩個知識點:數據驗證和數據轉換,對於數據驗證與Asp.net中驗證類似,都是為了保證輸入數據的合法性,而數據轉換指的是在源數據綁定到目標依賴屬性之前要做對應的轉換,例如WPF顯示人民幣都需要顯示一個¥符號,但是如果數據源的內容只是“120”這樣的字符串怎么辦呢?這時候就可以通過數據轉換在綁定之前,把數據源的值轉換成顯示所需要的格式。對於這兩個知識點,我覺得在遇到問題時再去學就好了,因為我們已經明白了解決問題的思路了。所以,在快速入門系列中不想太深入的介紹這兩個知識點,以使大家可以快速掌握WPF要領。這里給出幾個學習鏈接:

      數據綁定概述
      WPF Data Binding - Part 1

      WPF Simple Data Converter Example

  如何:實現綁定驗證

五、小結

    到這里,這篇博文的內容就介紹結束了,時間不知不覺的已經2點多了。下面一篇博文將分享WPF命令的內容。

  本文所有源碼下載:WPFBindingDemo.zip

 


免責聲明!

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



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