Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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)!

like image 697
ventiseis Avatar asked Oct 11 '15 22:10

ventiseis


3 Answers

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!

like image 184
Jerry Nixon Avatar answered Oct 12 '22 08:10

Jerry Nixon


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}" />
like image 37
Kevin Gosse Avatar answered Oct 12 '22 06:10

Kevin Gosse


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!

like image 25
ventiseis Avatar answered Oct 12 '22 08:10

ventiseis