文章目录
  1. 1. 缘起
  2. 2. 测试
    1. 2.1. OneTime绑定
    2. 2.2. OneWay & TwoWay绑定
    3. 2.3. TwoWay绑定中从目标到源
    4. 2.4. INotifyPropertyChanged
  3. 3. 深入
    1. 3.1. MainPage.g.i.cs
    2. 3.2. MainPage.g.cs
    3. 3.3. MainPage.xaml
    4. 3.4. 额外的
  4. 4. 最后
  5. 5. 结语

今天无意间谈到了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类的方法先不管,在这里定义了几个内部类:XamlBindingSettersMainPage_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字段就处于可用的情况了,至于那些InitializeInitializeGetBindingConnector方法是什么时候调用的,可以不用管了。而如果不放心,你可以像我下面调用的那样使用。

最后

最后就留下测试之后的正确工作的代码吧,改动也不大,就是换了一个通知方式而已。

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}一样玩出各种花样来。

文章目录
  1. 1. 缘起
  2. 2. 测试
    1. 2.1. OneTime绑定
    2. 2.2. OneWay & TwoWay绑定
    3. 2.3. TwoWay绑定中从目标到源
    4. 2.4. INotifyPropertyChanged
  3. 3. 深入
    1. 3.1. MainPage.g.i.cs
    2. 3.2. MainPage.g.cs
    3. 3.3. MainPage.xaml
    4. 3.4. 额外的
  4. 4. 最后
  5. 5. 结语