使用编译绑定(x:bind)时,为什么需要调用Bindings.Update()?

20

我正在尝试使用新的编译绑定,并且已经达到(再次)一个瓶颈:为什么我要调用 Bindings.Update?到目前为止,我认为实现INotifyPropertyChanged就足够了?

在我的示例中,只有当我调用这个神秘的方法(由编译绑定自动生成)时,GUI才会正确显示值。

我正在使用以下(简化过的)XAML语法的用户控件:

<UserControl>
  <TextBlock Text="x:Bind TextValue"/>
</UserControl>

这里的TextValue是此用户控件的简单依赖属性。在页面中,我使用该控件如下:

<Page>
  <SampleControl TextValue="{x:Bind ViewModel.Instance.Name}"/>
</Page>

where:

  • ViewModel是一个标准属性,它在运行InitializeComponent()之前被设置
  • Instance是一个简单的实现了INotifyPropertyChanged的对象

加载Instance后,我会为Instance触发一次属性更改事件。我甚至可以调试到依赖属性TextValue获取正确值的代码行,但什么也没显示出来。只有当我调用Bindings.Update()时,值才会被显示。我错过了什么重要的东西吗?

更新

即使使用{x:Bind ... Mode=OneWay}也不起作用。

更多代码

Person.cs:

using System.ComponentModel;
using System.Threading.Tasks;

namespace App1 {
    public class Person : INotifyPropertyChanged {
        public event PropertyChangedEventHandler PropertyChanged;

        private string name;
        public string Name { get {
                return this.name;
            }
            set {
                name = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Name"));
            }
        }
    }

    public class ViewModel : INotifyPropertyChanged {

        public event PropertyChangedEventHandler PropertyChanged;

        private Person instance;
        public Person Instance {
            get {
                return instance;
            }
            set {
                instance = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Instance"));
            }
        }

        public Task Load() {
            return Task.Delay(1000).ContinueWith((t) => {
                var person = new Person() { Name = "Sample Person" };                
                this.Instance = person;
            });
        }


    }
}

SampleControl.cs:

<UserControl
    x:Class="App1.SampleControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="100"
    d:DesignWidth="100">

    <TextBlock Text="{x:Bind TextValue, Mode=OneWay}"/>

</UserControl>

SampleControl.xaml.cs:

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App1 {
    public sealed partial class SampleControl : UserControl {

        public SampleControl() {
            this.InitializeComponent();
        }

        public string TextValue {
            get { return (string)GetValue(TextValueProperty); }
            set { SetValue(TextValueProperty, value); }
        }

        public static readonly DependencyProperty TextValueProperty =
            DependencyProperty.Register("TextValue", typeof(string), typeof(SampleControl), new PropertyMetadata(string.Empty));

    }
}

MainPage.xaml:

<Page
    x:Class="App1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <local:SampleControl TextValue="{x:Bind ViewModel.Instance.Name, Mode=OneWay}"/>
    </StackPanel>
</Page>

MainPage.xaml.cs:

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App1 {

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.DataContext = new ViewModel();
            this.Loaded += MainPage_Loaded;
            this.InitializeComponent();
        }

        public ViewModel ViewModel {
            get {
                return DataContext as ViewModel;
            }
        }

        private void MainPage_Loaded(object sender, RoutedEventArgs e) {
            ViewModel.Load();
            Bindings.Update(); /* <<<<< Why ????? */
        }
    }
}

又一个更新

我更新了Load方法,使用了任务(请参见上面的代码)!


请发布您视图背后的代码和视图模型代码,然后或许我们可以提供帮助。 - kkyr
我添加了所有相关的代码。如果我包括 Bindings.Update();,主页面才会显示“示例人物”。 - ventiseis
1
感谢您提出这个好问题!顺便说一句,Mode=OneWay 对我的 UWP 应用程序有效。您的问题和优秀的答案已经让我坚定了继续使用 Binding 的决心。虽然它需要很多改进,但并不是 x:Bind 所提供的。如果微软想要改进 Binding,只需看看 Android 中新的数据绑定中使用的强大表达式语言即可。 - Hong
4个回答

50
有时候你想展示的数据直到页面加载和渲染之后几秒钟才可用(比如从服务器或数据库返回)。特别是当你在后台/异步进程中调用数据时,可以使你的用户界面不会卡顿。你明白我的意思吗?
现在创建一个绑定,就像这样:
<TextBlock Text="{x:Bind ViewModel.User.FirstName}" />

您在代码背后的ViewModel属性将具有实际值并且可以正常绑定。另一方面,您的User将没有值,因为尚未从服务器返回。因此,无法显示该User或User的FirstName属性,对吧?

然后您的数据被更新。

当您将User对象的值设置为真实对象时,您会认为绑定会自动更新。特别是如果您花时间使其成为INotifyPropertyChanged属性,那么这将是正确的吗?对于传统的{Binding}来说是正确的,因为默认的绑定模式是OneWay。

什么是OneWay绑定模式?

OneWay绑定模式意味着您可以更新实现了INotifyPropertyChanged的后端模型属性,并且与该属性绑定的UI元素将反映数据/值更改。非常好。

为什么它不起作用?

这不是因为{x:Bind}不支持Mode=OneWay,而是因为它默认为Mode=OneTime。简而言之,传统的{Binding}默认为Mode=OneWay,而编译的{x:Bind}默认为Mode=OneTime。

什么是OneTime绑定模式?

OneTime绑定模式意味着您只能在加载/渲染具有绑定的UI元素时一次绑定到底层模型。这意味着如果您的底层数据尚不可用,则无法显示该数据,而一旦数据可用,则不会显示该数据。为什么?因为OneTime不监视INotifyPropertyChanged,它只在加载时读取。

  

模式(来自MSDN):对于OneWay和TwoWay绑定,源的动态更改不会自动传播到目标,而无需提供某些支持源。您必须在源对象上实现INotifyPropertyChanged接口,以便源可以通过绑定引擎监听的事件报告更改。对于C#或Microsoft Visual Basic,请实现System.ComponentModel.INotifyPropertyChanged。对于Visual C++组件扩展(C++ / CX),请实现Windows :: UI :: Xaml :: Data :: INotifyPropertyChanged。

如何解决这个问题?

有几种方法。第一个也是最简单的方法是将绑定从 =“{x:Bind ViewModel.User.FirstName} ”更改为 =" {x:Bind ViewModel.User.FirstName,Mode=OneWay} ”。这样做将监视INotifyPropertyChanged事件。

  

这是提醒您的正确时间,通过默认使用OneTime是{x:Bind}试图提高绑定性能的众多方式之一。这是因为OneTime是具有最少内存需求的最快速的。将您的绑定更改为OneWay会破坏这一点,但是这可能对您的应用程序是必要的。

修复此问题并仍保持{x:Bind}带来的性能优势的另一种方法是在视图模型完全准备好呈现数据后调用 Bindings.Update(); 。如果您的工作是异步进行,则很容易-但是,就像上面的示例一样,如果无法确保计时器可能是唯一可行的选择。

  

当然,这很讨厌,因为计时器意味着时钟


2
如果你是对数据绑定比较陌生的话,你可能会觉得这篇文章很有趣:http://blogs.msdn.com/b/jerrynixon/archive/2012/10/12/xaml-binding-basics-101.aspx - Jerry Nixon
3
嘿,Jerry,我在想为什么 x:Bind 的默认模式以前是 "OneTime"。虽然你已经回答了这将提高性能,但我认为我们更经常使用的是 "OneWay" 而不是 "OneTime",有很多次我忘记把它改成 "OneTime",导致我到处找“不显示数据”的错误 :-D - JuniperPhoton
1
最好的解释。无法说得更好。我刚刚注意到你是Template 10的Jerry Nixon。 - Syaiful Nizam Yahya
1
您,先生,真是个天才! - FloppyNotFound
1
很棒的答案。我不知道x:Bind默认为OneTime绑定。我一直在想为什么x:Bind无法正常工作。非常感谢。 - Allen Rufolo
显示剩余4条评论

3

传统绑定默认为“单向”(在某些情况下为“双向”),而编译绑定默认为“一次性”。只需在设置绑定时更改模式即可:

<TextBlock Text="{x:Bind TextValue, Mode=OneWay}" />

2

最终我自己找到了错误:我使用了基于任务的操作来加载我的视图模型,这导致依赖属性被错误的线程设置(我想)。如果我通过调度程序设置Instance属性,则可以解决该问题。

    public Task Load() {
        return Task.Delay(1000).ContinueWith((t) => {
            var person = new Person() { Name = "Sample Person" };
            Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
            () => {
                this.Instance = person;
            });                
        });
    }

但是没有任何异常,只是GUI没有显示任何值!


1
首先,x:Bind 的默认绑定模式是 OneTime,如上面的答案所说,如果你调用了 RaisePropertyChanged 方法,则需要将其更改为OneWay,以使其正常工作。
看起来你的数据绑定代码出现了问题。请粘贴涉及的所有代码让我们看到这个问题的源头。

代码已添加。抱歉,我总是担心我发布了太多的代码! - ventiseis
这是一个有点傻的 bug ;-) 尝试将这段代码 this.instance = person; 更改为 this.Instance = person;。你更新了字段 instance 而不是属性 Instance,导致 PropertyChanged 不起作用。 - JuniperPhoton
哦,是啊!那是我在 MCVE 中引入的一个 bug。现在我已经纠正了它,它可以工作了。 - ventiseis
如果有帮助的话,请考虑将其标记为答案 :-) - JuniperPhoton
1
它确实有帮助,但原始问题是不同的。我将杰瑞的答案标记为正确,因为它解释了非常详细的内容。但你让我再花一个小时在代码上,试图解决这个错误;-) - ventiseis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接