在 UWP 開發中,我們在進行數據綁定時,除了可以使用傳統的綁定 Binding,也可以使用全新的 x:Bind,由於後者是在程式編譯時進行初始化操作(不同於 Binding,它是在運行時創建、初始化),所以我們可以稱 x:Bind 為編譯型綁定,正像本文標題一樣。之所以引入 x:Bind,是因為它 ...
在 UWP 開發中,我們在進行數據綁定時,除了可以使用傳統的綁定 Binding,也可以使用全新的 x:Bind,由於後者是在程式編譯時進行初始化操作(不同於 Binding,它是在運行時創建、初始化),所以我們可以稱 x:Bind 為編譯型綁定,正像本文標題一樣。
之所以引入 x:Bind,是因為它相比傳統的 Binding 有很多優點,比如:
- 性能更好;
- 編譯時錯誤;
- 便於調試:
- 使用方便(綁定到函數、事件等)
鑒於 x:Bind 有以上這些優點,所以這裡推薦大家在自己的項目中儘可能地使用它;當然,相比 Binding,它也少了一些功能,所以在必要的時候,你任然需要使用傳統的綁定。換句話說,在項目中,你可以混合使用這兩種綁定方式。再次聲明:建議儘可能地使用 x:Bind,除非 x:Bind 不能完成你要的操作時,才考慮使用 Binding。
以下我會把 x:Bind 的使用方法以及上面提及的優點,進行較為詳細的說明。本文假設你已經掌握了(或者至少理解) WPF/UWP 中的數據綁定的基本知識;在繼續學習下文之前,如果你還不瞭解數據綁定,建議你最好瞭解相關知識(相信大多數的 XAML 開發人員都沒問題)。
x:Bind 的數據源
與傳統綁定較大的區別,是 x:Bind 的數據源為當前 View(即頁面 Page 或用戶控制項UserControl)自身,也就是說,它使用 Page 或 User Control 的實例為作數據源;因此如果你設置了 Path 屬性, x:Bind 會到當前 Code-Bebind 類中找對應名稱的成員(屬性、欄位、方法)。在下例中,x:Bind 會在當前用戶控制項實例中找到其 InfoA 屬性併進行綁定。
<UserControl x:Class="xBindTest.Controls.BindingModeControl"... <TextBlock Text="{x:Bind InfoA}" /> ... </UserControl>
public sealed partial class BindingModeControl : UserControl, INotifyPropertyChanged { public string InfoA { get; set; } }
順便提一下,如果找不到 InfoA 屬性,編譯就會失敗,這就是 x:Bind 的優點之一,提供編譯時錯誤,不像 Binding 一樣,僅在 VS 的 輸出(Output) 視窗輸入錯誤提示而已。
在傳統的綁定中,Binding 的數據源可以通過四種形式指定,它們分別是 DataContext(預設)、RelativeSource、Source、ElementName。而 x:Bind 既然將當前 View 的實例作為唯一數據源,那麼我們就完全不需要像傳統 Binding 一樣設置 DataContext;而對於後面三種設置數據源的方式, x:Bind 也僅支持以下兩種情況:
- ElementName
x:Bind -> {x:Bind slider1.Value}
Binding -> {Binding Value, ElementName=slider1} - RelativeSource: Self
x:Bind -> <Rectangle x:Name="rect1" Width="200" Height="{x:Bind rect1.Width}" ... />
Binding -> <Rectangle Width="200" Height="{Binding Width, RelativeSource={RelativeSource Self}}" ... />
說明:上例中,slider1 和 rect1 都是當前 View 中的控制項,本質上它們都是當前 View 的欄位,所以可以直接在 x:Bind 中使用;除了上述兩種情況外,x:Bind 對於 Source 和其它形式的 RelativeSource 均不支持。
綁定模式(Binding Mode)
接下來,我們來看 x:Bind 的綁定模式。
x:Bind 的 Binding Mode 值有以下三項:OneWay、OneTime、TwoWay;它的預設值是 OneTime。記住這一點非常重要,因為在開發過程中,很多時候綁定並不像我們想象的正常工作,就是因為 Mode 沒有被設置為合適的值。OneTime 的意思是僅在界面初始化時去初始化界面中的綁定;這一點也是 x:Bind 性能更優化的原因。順便提一下,傳統 Binding 的 Mode 屬性預設值是 Default(這個值的意義是對於只讀控制項它是 OneWay,對於可編輯的控制項,它是 TwoWay)。
更具體來說,x:Bind 的綁定是在 Page 或 User Control 的 Loading 事件中初始化的;也就是說,在 Mode=OneTime(預設值)時,僅當一個屬性值的設置在 View 的構造函數中時(在 Loading 事件之前)才會在 x:Bind 初始化中被更新到 UI 中;在其它位置(如 Loaded 事件或某一操作的響應事件中等等)修改此屬性的值,都不會再被更新(即使調用了 INotifyPropertyChanged 中的 PropertyChanged 事件)。參考以下代碼:
<TextBlock Margin="{StaticResource ContentMargin}" Text="{x:Bind InfoA}" />
public BindingModeControl() { InfoA = "InfoA: Value for x:Bind (Mode=One Time)"; }
而如果設置了 Mode=OneWay,綁定初始化時,會創建關聯,當綁定源的值更改後,綁定目標(UI)也及時更新。參考以下代碼:
<TextBlock VerticalAlignment="Center" Text="{x:Bind InfoB, Mode=OneWay}" />
private void btnUpdateValueForOneWay_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e) { InfoB = "InfoB: Value Updated"; }
現在重新考慮第一種情況,如果我們沒有為綁定設置 Mode,它就使用預設值 OneTime。在這種情況下,如果我們確實想要在構造函數之外的其它地方通過更新該屬性值以更新 UI,該怎麼辦呢?這裡就需要使用當前 View 的 Bindings 對象。
如果當前 View 中使用了 x:Bind,那麼它就會有一個欄位 Bindings,這個欄位是在 obj 文件夾中生成的 <viewname>.g.cs 文件中動態生成的。它有三個方法如下:
- Update() 調用此方法將更新當前 View 中所有 x:Bind 綁定的值
- Initialize() 調用此方法時,將會判斷綁定是否初始化;如果沒有,就直接調用 Update 方法,如果已經初始化,則什麼都不作;
- StopTracking() 移除初始化 OneWay 和 TwoWay 綁定時創建的所以 Listeners,也即 View 不再監聽屬性值的更新;
當我們修改了某個屬性值時,即使它是 OneTime 綁定模式,通過 Bindings 的 Update 方法也可以更新 UI。參考以下代碼:
<TextBlock VerticalAlignment="Center" Text="{x:Bind InfoC}" />
private void btnUpdateValueForOneTime_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e) { InfoC = "InfoC: Value updated by this.Bindings.Update() method"; this.Bindings.Update(); }
轉換(Converting)
在數據源屬性類型和綁定目標屬性類型不一致時,如果我們使用傳統的 Binding,可以將一個實現了 IValueConverter 的對象設置到 Binding 的 Converter 屬性來實現值的轉換。而使用 x:Bind,除了這種方式之外,還有更方便的——綁定屬性到函數。也就是說,你可以將一個函數放到 x:Bind 中。當然,x:Bind 仍然是在當前 View 的 Code-Behind 代碼中來找所指定的函數。參考如下代碼:
<Border x:Name="border" Background="{x:Bind GetBrush(IsPass), Mode=OneWay}"> <Image Margin="20" Source="{x:Bind GetImage(IsPass), Mode=OneWay}" /> </Border>
public Brush GetBrush(bool isPass) { return isPass ? new SolidColorBrush(Colors.LimeGreen) : new SolidColorBrush(Colors.Crimson); } // both public and private work well private ImageSource GetImage(bool isPass) { return isPass ? new BitmapImage(new Uri("ms-appx:///Assets/Happy.png")) : new BitmapImage(new Uri("ms-appx:///Assets/Sad.png")); }
在上面的例子中,兩處被綁定的函數均接受一個參數,事實上,這裡支持多個參數。所以這一點也要比 IValueConverter 方便;此外,綁定屬性到函數也支持類似於 IValueConverter 中的雙向轉換,除了能從源類型轉換到目標類型,也支持從目標類型轉換到源類型,方法是使用 BindBack 屬性指定另外一個方法。
另外,還一個非常便捷的轉換是 Visibility 和 bool 之間的轉換:控制項的 Visibility 可以直接綁定到一個布爾屬性或欄位;當布爾值為 true 時,Visibility 的值是 Visible,反之,是 Collapsed。參考如下代碼:
<Button Content="Logout" Visibility="{x:Bind IsLogin}" />
最後,需要說明的是,上述兩項轉換功能僅在周年更新(14393/1607)版本及更高版本中才支持。
在 DataTemplate 中使用
為列表控制項(如 ListView 等)設置 ItemTemplate 屬性時,要用到 DataTemplate;如果在 DataTemplate 中使用 x:Bind,要怎麼做呢?
首先要為 DataTemplate 指定 x:DataType,告訴它要展示的數據 Model 類,一般情況下,這需要引入 xmlns 命令空間;然後,在 DataTemplate 內部,使用 x:Bind 直接綁定到該 Model 的相關屬性。參考以下代碼:
<UserControl ... xmlns:models="using:xBindTest.Models"> <UserControl.Resources> <DataTemplate x:Key="FriendItemTemplate" x:DataType="models:Friend"> <StackPanel Margin="0,4"> <TextBlock FontSize="20" FontWeight="SemiBold" Text="{x:Bind Name}" /> <TextBlock Margin="{StaticResource ContentMargin}" FontSize="14" Text="{x:Bind Email}" /> </StackPanel> </DataTemplate> </UserControl.Resources> <Grid> <ListView ItemTemplate="{StaticResource FriendItemTemplate}" ItemsSource="{x:Bind AllFriends}" /> </Grid> </UserControl>
編譯即可正常運行。如果沒有為 DataTemplate 設置 x:DataType,或在 DataTemplate 中綁定了 Model 中不存在的屬性都會編譯失敗。
上面是在當前 View 中引用 DataTemplate 資源。在實際項目開發中,你可能會將資源統一放到一個或若幹個 ResourceDictionary 文件中,目的是為了更方便地組織資源。那麼上面這個使用了 x:Bind 的 DataTemplate 應該如何被移動到 ResourceDictionary 文件中呢?
首先,直接移動一定會編譯出錯,原因是使用 x:Bind 的 XAML 文件必須得有 Code-Behind 文件。怎麼解決呢?
可以新建一個 Page 或 UserControl,然後,將其基類由 Page 或 UserControl 改為 ResourceDictionary,刪去不需要的、預設添加出來的 UI 元素,然後將 DataTemplate 資源項複製進來,即可。參考如下代碼:
<ResourceDictionary x:Class="xBindTest.Styles.DataTemplates" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:models="using:xBindTest.Models" mc:Ignorable="d"> <DataTemplate x:Key="AnotherFriendItemTemplate" x:DataType="models:Friend"> <StackPanel Margin="0,4"> <TextBlock FontSize="20" FontWeight="SemiBold" Text="{x:Bind Name}" /> <TextBlock Margin="{StaticResource ContentMargin}" FontSize="14" Text="{x:Bind Email}" /> </StackPanel> </DataTemplate> </ResourceDictionary>
using Windows.UI.Xaml; namespace xBindTest.Styles { public sealed partial class DataTemplates : ResourceDictionary { public DataTemplates() { this.InitializeComponent(); } } }
綁定到事件
在使用傳統綁定時,對於控制項操作的響應,我們一般會用到命令或行為(當控制項不支持 Command 或對控制項的某一特定事件進行響應時,如 ListView 控制項的 SelectionChanged 事件);然而 x:Bind 可以輕鬆地實現同樣的操作,因為它支持綁定到事件,來看代碼:
<Button Click="{x:Bind ShowInfoTest1}" Content="Show Info" />
public void ShowInfoTest1() { Info = "Update Info in method: ShowInfoTest1()"; }
像綁定屬性一樣簡單,不同的是,被綁定的不再是屬性,而是事件名,而 Path 也不是屬性名,而是方法名。相比 ICommand 或行為對此操作的實現,要簡單的多。
這裡需要補充的是,關於方法的簽名:
- 參數可以為空,如:
void ShowInfoTest1()
- 也可以與被綁定事件的簽名一致,如:
void ShowInfoTest1(object sender, RoutedEventArgs e)
- 還可以是個數與事件簽名的個數一致,事件簽名中每個參數類型都可以轉換為方法中所定義的參數類型,如:
void ShowInfoTest1(object sender, object e)
如果不一致,在項目編譯時就不會通過。
另外,在綁定到事件中,x:Bind 除了支持上述靈活的方法簽名,對於方法的返回值並沒有要求,不僅可以是 void,也可以是其它任何返回類型;並且也支持 async 方法的綁定。
MVVM
基本上,x:Bind 的主要特性到這裡就基本上都提到了。但是,有一個問題,在 UWP 應用開發過程中,我們一般使用 MVVM 模式,而 x:Bind 將當前 View 作為數據源,怎麼才能使其綁定到 ViewModel 中的成員呢?很簡單,只要在 Page 或 UserControl 中添加 ViewModel 屬性,其類型為對應 View 的 ViewModel,而在 x:Bind 中使用多級 Path 即可。參考如下代碼:
public sealed partial class BindToEventControl : UserControl { public BindToEventControl() { this.InitializeComponent(); ViewModel = new BindToEventViewModel(); } public BindToEventViewModel ViewModel { get; set; } }
<Button Click="{x:Bind ViewModel.ShowInfoTest1}" /> <TextBlock Text="{x:Bind ViewModel.Info, Mode=OneWay, TargetNullValue='(no value)'}" />
另外,為了實現 View 與 ViewModel 的解耦,你可能會使用類似 ViewModelLocator 的類來實現對 ViewModel 的定位。在這種情況下,怎麼結合 x:Bind 呢?
首先,你仍然可以保留 Page 的 DataContext 對 Locator 的引用;需要進一步處理的是,像上例一樣,在 Page 中添加 ViewModel。參考如下代碼:
<Page ... DataContext="{Binding HomeViewModel, Source={StaticResource Locator}}">
public sealed partial class HomePage : Page { public HomePage() { this.InitializeComponent(); } public HomeViewModel ViewModel => DataContext as HomeViewModel; }
其它
在使用 x:Bind 時,有以下幾點,也值得註意:
- x:Bind 不支持 UpdateSourceTrigger,所以對於 TextBox 可以在不失去焦點前提下更新綁定源的值(通過設置 UpdateSourceTrigger=ProppertyChanged)這一情況,在 x:Bind 中是不能實現的;也就是說,對於這種需求,你仍然需要使用傳統的 Binding;
- 本文一開始曾提到 x:Bind 的優點之一是便於調試,當你在 View 中使用了 x:Bind,那麼在 obj\(x64/x86/ARM)\<viewname>.g.cs 文件中生成相應的關於綁定的代碼,你在這裡可以查看動態生成的代碼,並設置斷點以調試。由於我對此並未作深入的調研,所以在此不再詳述;
總結
本文主要講到 UWP 中的 x:Bind,包括它的優點以及用法。它有性能更好、使用更方便、編譯時檢查錯誤、便於調試等優點,所以給大家的建議就是在你的項目中儘可能地使用它。當然,與傳統 Binding 相比,它也有不及的地方,所以你仍然可以結合傳統 Binding 完成你所要的功能。最後附上 xBindTest 的截圖和源碼,它是我針對 x:Bind 寫的一個 Demo,本文中引用的代碼幾乎都在此項目中能找得到。
參考資料: