深入探究 {x:Bind}
今天无意间谈到了UWP
应用中为数据绑定新引入的一个XAML
标记:{x:Bind}
,对于这个新的标记的作用和原理,网上的一般说法都是:
相比较传统的
{Binding}
方式,{x:Bind}
的效率更高,因为{x:Bind}
的数据绑定发生编译期间,绑定工作在编译时期就已经完成了,所以我们在使用{x:Bind}
的时候,需要一个明确的数据类型(按:这也符合强类型语言的特点)。
这些也都基本说的过去,相对于传统的{Binding}
方式,这种类型严格的绑定行为,最起码也可以减少对象装箱和拆箱行为的性能损耗,而至于其它的由于强类型带来的可以优化的地方,就要看编译器的行为了。
但是基本上也就到此为止了,大部分关于{x:Bind}
的说明文章,接下来就是将示例,陈述简单的用法,不说讲解一下{x:Bind}
的实现方式,就连深入一点的用法,比如双向绑定等都没有提到多少。
这里别说那些示例中写了个
Mode=TwoWay
就是双向绑定了,如果双向绑定这么简单,那在传统的{Binding}
方式中又为什么要弄个INotifyPropertyChanged
接口,还要弄个叫依赖属性的东西出来?
缘起
所以今天突然提起了这个新的绑定方式,一时间就突然发现认识的还是太少了,仅仅只是从网上看到了一点关于它的一点概述而已,一旦深入讨论下去,才发现很多东西都没有关注过,也许是我搜索的少了吧,没看到有谁深入的说明这个新加入的标记,即使是MSDN上也是说明了一下用法以及与{Binding}
的对比而已。
多的也不多说了,既然找不到,那还不如自己去测试探索一翻。
先把网上总结的一些{x:Bind}
特点罗列一下:
- 强类型,因此某些时候你需要使用XAML中的方式强制类型转换。
- 上下文是
Page
或者UserControl
。 - 默认的绑定模式是OneTime。
- 在数据模版中使用时,需要指定
{x:DataType}
。 - 支持绑定事件到方法。
而关于{x:Bind}
的更多详细说明,可以参考MSDN文档:{x:Bind} 标记扩展。关于{x:Bind}
和{Binding}
标记使用方法和功能的对比,也可以参考MSDN的说明:{x:Bind} 和 {Binding} 功能比较。
测试
新建一个UWP项目,然后开始测试了。因为只是看看{x:Bind}
的工作方式,因此程序有些设计细节就不用过多关注了……
OneTime绑定
这是{x:Bind}
的默认绑定模式,也基本是比较常用的模式了,这种情况没什么好测试的了,正常的写法都能跑起来,而且也可以想像得到编译器会怎么处理:直接在页面构造的某个时期赋值一次罢了。这里需要注意的所有事项也就是{x:Bind}
的基本用法注意事项了。
// ** .cs **
public string Title { get; set; } = "Title";
// ** .xaml **
<TextBlock Text="{x:Bind Title}" />
OneWay & TwoWay绑定
这两个之所以放到一起,是因为我没有测试过OneWay模式,而是直接观察TwoWay模式下的绑定情况的。
下面是测试用的XAML主要代码:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<TextBlock
Text="{x:Bind Time, Mode=TwoWay}"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Height="100"/>
<TextBox
Text="{x:Bind Time, Mode=TwoWay}"
Margin="0,100,0,0"
VerticalAlignment="Top"/>
<Button
VerticalAlignment="Bottom"
Content="Update Time"
Click="ButtonBase_OnClick"/>
</Grid>
其中中间那个TextBox
是测试过程中加入的,是因为TwoWay模式的TextBlock
不好测试,没有文本框方便。
C#主要代码如下:
public sealed partial class MainPage : Page
{
public string Time { get; set; }
public MainPage()
{
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
this.Time = DateTime.Now.ToString("D");
}
}
这个相对于默认用法来说,其实也就是简单的给{x:Bind}
加了一句说明:Mode=TwoWay
,这也是网上大部分示例的做法。
在第一次测试中,忽略那个TextBox
控件,我们使用按钮更改Time
的值,看看界面的变化。而测试结果也在意料之中,不管你怎么点击那个按钮,界面都不会有任何的变化,Time
的值根本不能传到控件属性上,这很好解释,这个Time
就是一个很简单的自动属性而已,深入一点也就是两个普通的对象方法而已,根本没有地方把属性值传出去,甚至外界也根本不知道这个值已经变化了。
TwoWay绑定中从目标到源
继续上面那个测试,不同的是这次使用TextBox
控件。
给Time
属性的set访问器添加断点,然后开始在TextBox
中输入一些内容,然后让文本框失去焦点,这时候可以看到断点命中了,当运行完毕之后,Time
的值也被设置成为了文本框中输入的内容。当然这个时候TextBlock
的内容仍然没有任何变化,原因和上面的一样,这时候的Time
只是个普通属性。
但是无论如何,这里可以说明{x:Bind}
是具有一般绑定的行为的,那么目前我们就剩下一个问题了,为什么从绑定源到绑定目标的路径不通?也就是绑定源的变化无法反馈给绑定目标?
INotifyPropertyChanged
既然{x:Bind}
和{Binding}
如此类似,那么可不可以借鉴在{Binding}
方式中使用的一些方式呢?比如INotifyPropertyChanged
。上面提到无法报告绑定源的变化,而在传统的绑定中,出了使用依赖属性之外,我们还可以使用INotifyPropertyChanged
接口,而在{x:Bind}
方式中呢,是不是也可以试试这种方式,将属性的变化通知出去。
那么对C#代码稍微改变一下:
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _time;
public string Time
{
get { return _time; }
set
{
if (value != _time)
{
_time = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Time"));
}
}
}
// ...
}
然后在运行测试一下,结果很出人意外。
无论是更改文本框的内容,还是点击按钮主动更新Time
的值,都没有任何反应,也就是说,绑定仍然没有起作用,准确来说,是绑定仍然无法完成从绑定源到绑定目标的更新。
这也就是说,传统的方式无法适用于{x:Bind}
了。因此需要看看编译器对这个标记扩展做了哪些工作了,而在VS的智能提示中,输入this.
之后也可以看到一个新的Page
类的字段:Bindings
,也许和这种新的编译时绑定有关。
深入
既然要看看编译器究竟对XAML文档中的{x:Bind}
做了什么,第一个想法当然是看看编译后生成的代码了,于是找到obj目录下临时文件,找到所有的以MainPage开头的文件,然后打开看看其中的内容。而很幸运,在这里找到了答案,具体来说,实在下面三个文件中:
- MainPage.g.i.cs
- MainPage.g.cs
- MainPage.xaml
MainPage.g.i.cs
下面是这个文件中的主要内容:
partial class MainPage : global::Windows.UI.Xaml.Controls.Page
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 14.0.0.0")]
private bool _contentLoaded;
/// <summary>
/// InitializeComponent()
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 14.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponent()
{
// ...
}
private interface IMainPage_Bindings
{
void Initialize();
void Update();
void StopTracking();
}
#pragma warning disable 0169
private IMainPage_Bindings Bindings;
#pragma warning restore 0169
}
可以看到这里声明了一个私有接口和对应的字段。在这个接口中定义了三个操作方法,而在看完后面两个文件内容之后,就可以知道每个方法的作用了。而这个接口,也是整个绑定环节中的一个重要部分。
MainPage.g.cs
这个文件内容有点多,也是绑定实现的主要部分。
partial class MainPage :
global::Windows.UI.Xaml.Controls.Page,
global::Windows.UI.Xaml.Markup.IComponentConnector,
global::Windows.UI.Xaml.Markup.IComponentConnector2
{
internal class XamlBindingSetters
{
public static void Set_Windows_UI_Xaml_Controls_TextBlock_Text(global::Windows.UI.Xaml.Controls.TextBlock obj, global::System.String value, string targetNullValue)
{
if (value == null && targetNullValue != null)
{
value = targetNullValue;
}
obj.Text = value ?? global::System.String.Empty;
}
public static void Set_Windows_UI_Xaml_Controls_TextBox_Text(global::Windows.UI.Xaml.Controls.TextBox obj, global::System.String value, string targetNullValue)
{
if (value == null && targetNullValue != null)
{
value = targetNullValue;
}
obj.Text = value ?? global::System.String.Empty;
}
};
private class MainPage_obj1_Bindings :
global::Windows.UI.Xaml.Markup.IComponentConnector,
IMainPage_Bindings
{
private global::TestApp1.MainPage dataRoot;
private bool initialized = false;
private const int NOT_PHASED = (1 << 31);
private const int DATA_CHANGED = (1 << 30);
// Fields for each control that has bindings.
private global::Windows.UI.Xaml.Controls.TextBlock obj2;
private global::Windows.UI.Xaml.Controls.TextBox obj3;
private MainPage_obj1_BindingsTracking bindingsTracking;
public MainPage_obj1_Bindings()
{
this.bindingsTracking = new MainPage_obj1_BindingsTracking(this);
}
// IComponentConnector
public void Connect(int connectionId, global::System.Object target)
{
switch(connectionId)
{
case 2:
this.obj2 = (global::Windows.UI.Xaml.Controls.TextBlock)target;
(this.obj2).RegisterPropertyChangedCallback(global::Windows.UI.Xaml.Controls.TextBlock.TextProperty,
(global::Windows.UI.Xaml.DependencyObject sender, global::Windows.UI.Xaml.DependencyProperty prop) =>
{
if (this.initialized)
{
// Update Two Way binding
this.dataRoot.Time = (this.obj2).Text;
}
});
break;
case 3:
this.obj3 = (global::Windows.UI.Xaml.Controls.TextBox)target;
(this.obj3).LostFocus += (global::System.Object sender, global::Windows.UI.Xaml.RoutedEventArgs e) =>
{
if (this.initialized)
{
// Update Two Way binding
this.dataRoot.Time = (this.obj3).Text;
}
};
break;
default:
break;
}
}
// IMainPage_Bindings
public void Initialize()
{
if (!this.initialized)
{
this.Update();
}
}
public void Update()
{
this.Update_(this.dataRoot, NOT_PHASED);
this.initialized = true;
}
public void StopTracking()
{
this.bindingsTracking.ReleaseAllListeners();
this.initialized = false;
}
// MainPage_obj1_Bindings
public void SetDataRoot(global::TestApp1.MainPage newDataRoot)
{
this.bindingsTracking.ReleaseAllListeners();
this.dataRoot = newDataRoot;
}
public void Loading(global::Windows.UI.Xaml.FrameworkElement src, object data)
{
this.Initialize();
}
// Update methods for each path node used in binding steps.
private void Update_(global::TestApp1.MainPage obj, int phase)
{
if (obj != null)
{
if ((phase & (NOT_PHASED | DATA_CHANGED | (1 << 0))) != 0)
{
this.Update_Time(obj.Time, phase);
}
}
}
private void Update_Time(global::System.String obj, int phase)
{
if((phase & ((1 << 0) | NOT_PHASED | DATA_CHANGED)) != 0)
{
XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBlock_Text(this.obj2, obj, null);
XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBox_Text(this.obj3, obj, null);
}
}
private class MainPage_obj1_BindingsTracking
{
global::System.WeakReference<MainPage_obj1_Bindings> WeakRefToBindingObj;
public MainPage_obj1_BindingsTracking(MainPage_obj1_Bindings obj)
{
WeakRefToBindingObj = new global::System.WeakReference<MainPage_obj1_Bindings>(obj);
}
public void ReleaseAllListeners()
{
}
}
}
/// <summary>
/// Connect()
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 14.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void Connect(int connectionId, object target)
{
switch(connectionId)
{
case 4:
{
global::Windows.UI.Xaml.Controls.Button element4 = (global::Windows.UI.Xaml.Controls.Button)(target);
#line 23 "..\..\..\MainPage.xaml"
((global::Windows.UI.Xaml.Controls.Button)element4).Click += this.ButtonBase_OnClick;
#line default
}
break;
default:
break;
}
this._contentLoaded = true;
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 14.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public global::Windows.UI.Xaml.Markup.IComponentConnector GetBindingConnector(int connectionId, object target)
{
global::Windows.UI.Xaml.Markup.IComponentConnector returnValue = null;
switch(connectionId)
{
case 1:
{
global::Windows.UI.Xaml.Controls.Page element1 = (global::Windows.UI.Xaml.Controls.Page)target;
MainPage_obj1_Bindings bindings = new MainPage_obj1_Bindings();
returnValue = bindings;
bindings.SetDataRoot(this);
this.Bindings = bindings;
element1.Loading += bindings.Loading;
}
break;
}
return returnValue;
}
}
不过具体内容也很好理解,抛开几个属于MainPage
类的方法先不管,在这里定义了几个内部类:XamlBindingSetters
、MainPage_obj1_Bindings
,其中MainPage_obj1_Bindings
类实现了前面定义的那个接口,同时还定义了一个内部类MainPage_obj1_BindingsTracking
作为所谓跟踪的一个实现,具体在这里我们不管它的作用。
首先看第一个类XamlBindingSetters
,这个类中定义的方法很明显是给控件属性赋值使用的,而且是专门真对声明了{x:Bind}
标记的控件和属性。
然后是主要的类MainPage_obj1_Bindings
了,这个实现了所谓绑定接口的类中,我们重点关注接口声明的三个方法。而对于另外有个相对比较长的方法Connect
,可以看出来就是实现从绑定目标到绑定源的更新了,这里就不分析了,很简单的代码。
对于接口中三个方法的实现中,可以抛去StopTracking()
方法不管,这个方法和上面所说的MainPage_obj1_BindingsTracking
类有关,也许是我的例子过于简单,在这个类中看不出来有什么特殊的工作要做,除了保持了一个弱引用。
而剩下的两个方法,最后的调用都指向了一个名叫Update_
的私有方法,在这个方法里面有调用了一个针对具体属性的Update_Time
方法,而从这里,我们就可以看到对XamlBindingSetters
类的调用了,而这个类是为了设置控件的绑定属性的值的。那么很显然,在{x:Bind}
方式的绑定中,从绑定源到绑定目标的更新是由上述内部接口中定义的Update
方法来完成的。
具体为什么跳跃这么远,又扯到了上一个文件中定义的接口,我们可以上面最后一个方法的内容:
MainPage_obj1_Bindings bindings = new MainPage_obj1_Bindings();
this.Bindings = bindings;
因此,具体来说,是使用私有字段Bindings
来完成更新的。
到这里已经基本结束了,也许关于那些switch
分支中的数字感到有些莫名其妙,那么可以再看看最后一个文件。
MainPage.xaml
内容很少,可以很容易看出那些奇怪的数字来源:
<Page x:ConnectionId='1'
x:Class="TestApp1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TestApp1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<TextBlock x:ConnectionId='2'
HorizontalAlignment="Right"
VerticalAlignment="Top"
Height="100"/>
<TextBox x:ConnectionId='3'
Margin="0,100,0,0"
VerticalAlignment="Top"/>
<Button x:ConnectionId='4'
VerticalAlignment="Bottom"
Content="Update Time"
/>
</Grid>
</Page>
所谓留空的地方也不是我故意弄得,使本来就如此,对比一下,可以发现留空的位置就是我们使用了绑定或者声明事件的位置。
额外的
其实关于上面三个文件,特别是第二个里面,有些地方我们不用深究,最起码对于这个简单的例子不用,比如那些奇怪的数字和常量定义,还有那条件判断等。除非你打算在深入研究这个框架,不过那时候也不应该看这些代码了。
而对于上面这些具体代码,具体的方法,我们也不用过分深究究竟是什么时候被调用的,我们只需要知道当你在XAML中使用了{x:Bind}
的时候,Bindings
字段就处于可用的情况了,至于那些Initialize
、Initialize
、GetBindingConnector
方法是什么时候调用的,可以不用管了。而如果不放心,你可以像我下面调用的那样使用。
最后
最后就留下测试之后的正确工作的代码吧,改动也不大,就是换了一个通知方式而已。
public sealed partial class MainPage : Page
{
private string _time;
public string Time
{
get { return _time; }
set
{
if (value != _time)
{
_time = value;
this.Bindings?.Update();
}
}
}
public MainPage()
{
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
this.Time = DateTime.Now.ToString("D");
}
}
重点就是属性的set访问器中的对Bindings.Update()
方法的调用了。
结语
关于{x:Bind}
目前就告一段落吧,其实还有很多细节可以去深究,比如针对依赖属性的行为。如果你多去试试,未尝不能和{Binding}
一样玩出各种花样来。