With compiled bindings (x:bind), why do I have to call Bindings.Update()?
Asked Answered
P

4

20

I'm currently experimenting with the new compiled bindings and have reached (again) a point where I'm missing a pice in the puzzle: why do I have to call Bindings.Update? Until now, I thought implementing INotifyPropertyChanged is enough?

In my example, the GUI is only displaying correct values, if I call this mysterious method (which is autogenerated by the compiled bindings).

I am using a user control with the following (here simplified) xaml syntax:

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

where TextValue is a simple dependency property of this user control. In a page, I'm using this control as:

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

where:

  • ViewModel is a standard propery which is set before InitializeComponent() is run
  • Instance is a simple object implementing INotifyPropertyChanged

After loading Instance, i raise a property changed event for Instance. I can even debug to the line, where the depency property TextValue of user control gets the correct value -- but nothing is displayed. Only if I call Bindings.Update(), the value is displayed. What am I missing here?

Update

I doesn`t work with {x:Bind ... Mode=OneWay} either.

More code

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 ????? */
        }
    }
}

One more update

I updated the Load method to use task (see the code above)!

Pueblo answered 11/10, 2015 at 22:16 Comment(3)
Please post the code behind your view and the viewmodel code then perhaps we can helpCarrefour
I added all relevant code. The main page displays "Sample Person" only if I include Bindings.Update();.Pueblo
Thank you for the great question! BTW, Mode=OneWay works for my UWP apps. Your question and the excellent answer have convinced me to continue using Binding. It could use a lot of improvement, but not what x:Bind offers. If MSFT wants to improve Binding, just take a look at the powerful expression language used by the new comer in data binding - Android.Cuthbertson
H
50

Sometimes the data you want to show is not available (like returned from the server or database) until several seconds after your page has loaded and rendered. This is especially true if you call your data in a background/async process that frees up your UI to render without a hang.

Make sense so far?

Now create a binding; let's say something like this:

<TextBlock Text="{x:Bind ViewModel.User.FirstName}" />

The value of your ViewModel property in your code-behind will have a real value and will bind just fine. Your User, on the other hand, will not have a value because it is not returned from the server yet. As a result neither that nor the FirstName property of the User can be displayed, right?

Then your data is updated.

You would think that your binding would automatically update when you set the value of the User object to a real object. Especially if you took the time to make it a INotifyPropertyChanged property, right? That would be true with traditional {Binding} because the default binding mode is OneWay.

What is the OneWay binding mode?

The OneWay binding mode means that you can update your backend model properties that implement INotifyPropertyChanged and the UI element bound to that property will reflect the data/value change. It's wonderful.

Why does it not work?

It is NOT because {x:Bind} does not support Mode=OneWay, it is because it defaults to Mode=OneTime. To recap, traditional {Binding} defaults to Mode=OneWay and compiled {x:Bind} defaults to Mode=OneTime.

What is the OneTime binding mode?

The OneTime binding mode means that you bind to the underlying model only once, at the time of load/render of the UI element with the binding. This means that if your underlying data is not yet available, it cannot display that data and once the data is available it will not display that data. Why? Because OneTime does not monitor INotifyPropertyChanged. It only reads when it loads.

Modes (from MSDN): For OneWay and TwoWay bindings, dynamic changes to the source don't automatically propagate to the target without providing some support from the source. You must implement the INotifyPropertyChanged interface on the source object so that the source can report changes through events that the binding engine listens for. For C# or Microsoft Visual Basic, implement System.ComponentModel.INotifyPropertyChanged. For Visual C++ component extensions (C++/CX), implement Windows::UI::Xaml::Data::INotifyPropertyChanged.

How to solve this problem?

There are a few ways. The first and easiest is to change your binding from ="{x:Bind ViewModel.User.FirstName} to ="{x:Bind ViewModel.User.FirstName, Mode=OneWay}. Doing this will monitor for INotifyPropertyChanged events.

This is the right time to warn you that using OneTime by default is one of the many ways {x:Bind} tries to improve performance of binding. That's because OneTime is the fastest possible with the least memory reqs. Changing your binding to OneWay undermines this, but it might be necessary for your app.

The other way to fix this problem and still maintain the performance benefits that come out of the box with {x:Bind} is to call Bindings.Update(); after your view model has completely prepared your data for presenting. This is easy if your work is async - but, like your sample above, if you can't be sure a timer might be your only viable option.

That sucks of course because a timer implies clock time, and on slow devices like a phone, that clock time might not properly apply. This is something every developer will have to work out specific to their app - that is to say, when is your data fully loaded and ready?

I hope this explains what is happening.

Best of luck!

Highspirited answered 13/10, 2015 at 0:49 Comment(9)
If you are new to binding, you might find this article interesting: blogs.msdn.com/b/jerrynixon/archive/2012/10/12/…Highspirited
Hey Jerry, I was wondering why the default mode of x:Bind is OneTime before. Though you have answered that this will improve performance, but I think that we use OneWay more often than OneTime and there are so many times I forgot to change it to OneTime which results in my finding the "not displaying data" bug everywhere :-DPentamerous
@JerryNixon-MSFT Thank you for this very detailed answer. I already forgot a few times to add the binding mode, but this time this was not the cause of the answer. Instead, I think the bug was me changing a dependency property not in the ui thread. I marked your reply as the correct answer, becaue it explains the interactions between binding and the view model very good.Pueblo
I still think I will continue to use OneWay or TwoWay because then I don't have to write a repeating line of code with Bindings.Update(); in my code-behind files.Pueblo
Most exceptional explanation. Can't said it better. I just noticed you are the Jerry Nixon of Template 10.Signpost
Nicely written answer. x++.Corregidor
Another source of confusion: At least as of Visual Studio 2015 Update 3, if you add INotifyPropertyChanged support to your code the build system may not update the generated binding code to use it unless you force a full rebuild. I've been left wondering what was going on because of this several times...Nammu
You, Sir, are a genius!Whop
Great answer. I had no idea that x:Bind defaulted to a OneTime Binding. I was wondering why I couldn't get x:Bind to work correctly. Thank you very much.Saber
V
3

While "traditional" bindings default to "one-way" (or two-ways in some case), compiled bindings default to "one-time". Just change the mode when setting the binding:

<TextBlock Text="{x:Bind TextValue, Mode=OneWay}" />
Volar answered 11/10, 2015 at 22:20 Comment(0)
P
2

Finally I found the bug myself: I was using an task-based operation to load my view model, which resulted in setting the dependency property by the incorrect thread (I think). It works if I set the Instance property via the dispatcher.

    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;
            });                
        });
    }

But there was no exception, just the gui displaying no value!

Pueblo answered 12/10, 2015 at 20:12 Comment(0)
P
1

First of all,the default binding mode of x:Bind is OneTime, you need to changed it to OneWay as the answer said above to make it work if you call RaisePropertyChanged method.

It seems like something has wrong with your code of data binding. PLEASE paste all the involved code to let us see the source of this issue.

Pentamerous answered 12/10, 2015 at 7:59 Comment(5)
Code added. Sorry, I'm always afraid that I'm posting too much code!Pueblo
That's kind of a stupid bug ;-) Try change this code this.instance = person; to this.Instance = person;. You updated the field instance instead of the property Instance,which results in the not working of PropertyChanged.Pentamerous
Oh yeah! That was a bug I introduced in the MCVE. Now I corrected it and it works.Pueblo
If it helps, think about marking this as the answer :-)Pentamerous
It helps but the the original problem was different. I marked Jerry's answer as correct, because it explains so very much details. But you made me look another hour on the code, trying to solve this bug ;-)Pueblo

© 2022 - 2024 — McMap. All rights reserved.