简体   繁体   中英

Binding and x:Bind problems with TwoWay mode

I've encountered weird issue, which I cannot understand. In main page I've only one button which navigates to second page and holds my model:

public class Model : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void RaiseProperty(string property) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));

    private int index = 0;
    public int Index
    {
        get { Debug.WriteLine($"Getting value {index}"); return index; }
        set { Debug.WriteLine($"Setting value {value}"); index = value; RaiseProperty(nameof(Index)); }
    }
}

public sealed partial class MainPage : Page
{
    public static Model MyModel = new Model();

    public MainPage()
    {
        this.InitializeComponent();
        SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible;
        SystemNavigationManager.GetForCurrentView().BackRequested += (s, e) => { if (Frame.CanGoBack) { e.Handled = true; Frame.GoBack(); } };
    }

    private void Button_Click(object sender, RoutedEventArgs e) => Frame.Navigate(typeof(BlankPage));
}

On second page there is only ComboBox which has two way binding in SelectedIndex :

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <ComboBox SelectedIndex="{x:Bind MyModel.Index, Mode=TwoWay}">
        <x:String>First</x:String>
        <x:String>Second</x:String>
        <x:String>Third</x:String>
    </ComboBox>
</Grid>
public sealed partial class BlankPage : Page
{
    public Model MyModel => MainPage.MyModel;

    public BlankPage()
    {
        this.InitializeComponent();
        this.Unloaded += (s, e) => Debug.WriteLine("--- page unloaded ---");
        DataContext = this;
    }
}

Nothing extraordinary. The problem is that I get two different outputs when I use Binding and x:Bind , but the worst is that after every new navigation to same page the property's getter (and setter in x:Bind ) is called more and more times:

在此处输入图片说明

The old page still resides in memory and is still subscribed to property, that is understandable. If we run GC.Collect() after returning from page, we will begin from start.

But if we use old Binding with one-way and selection changed event:

<ComboBox SelectedIndex="{Binding MyModel.Index, Mode=OneWay}" SelectionChanged="ComboBox_SelectionChanged">

along with the eventhandler:

private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.RemovedItems.Count > 0 && e.AddedItems.FirstOrDefault() != null)
        MyModel.Index = (sender as ComboBox).Items.IndexOf(e.AddedItems.FirstOrDefault());
}

then it will work 'properly' - only one getter and setter, no matter how many times we navigate to page before.

So my main questions are:

  • where this difference in one-way - two-way binding comes from?
  • taking into account that one-way Binding fires getter only once - is the described behavior of two-way desired/intended?
  • how you deal with this two-way binding in case of multiple getters/setters getting called?

A working sample you can download from here .

Actually when you use OneWay binding with the SectionChanged event, only the setter of the Index property is called after changing the selection. The getter is never reached hence you don't see multiple "Getting value ..." .

But why is the getter not called??

Put a breakpoint on this line -

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));

You will see that the value of PropertyChanged is null . So the Invoke method is never fired. I suspect this might be a bug in ComboBox with traditional binding set to OneWay . Whenever you change the selection, the binding is broken hence the PropertyChanged is null . If you change to use x:Bind , this problem goes away.

As you are already aware, the GC will only collect abandoned page instances when needed. So there are times when you see Index is referenced in multiple places, no matter which binding mechanism you chose.

One way to guarantee the getter and setter only get called once is to change the the NavigationCacheMode of your second Page to Enabled/Required . Doing so will ensure a single instance of the page.

Even after you navigated from and to a new BlankPage the other pages are still in the memory and still binded in your static model as @KiranPaul commented.

Now if you, as you commented, change in to no-static and still behaves the same, is because you make the same mistake. Even if it isn't static you still use the same variable from MainPage.(I think its not possible though, cause its not static)

So all the pages that are in the memory that havent GC.Collect() -ed will get the PropertyChanged event raised. Because the MyModel is always the same one.

Try this should work. Each time that you navigate to the BlankPage you instantiate a new Model and pass your index. Then when you unload the page you update the value in the MainPage.Model. That way when you leave the BlankPage you will only see a Set and a Get in Output.

 public sealed partial class BlankPage : Page
    {
        public Model MyModel = new Model() { Index = MainPage.MyModel.Index };

        public BlankPage()
        {
            this.InitializeComponent();
            this.Unloaded += (s, e) => { MainPage.MyModel.Index = MyModel.Index; Debug.WriteLine("--- page unloaded ---"); };
            DataContext = this;
        }
    }

在此处输入图片说明

Or when you leave BlankPage you can either:

  • call GC.Collect()
  • unbind the MyModel when you unload the page?

Edit:

With Binding it also does the same if you do it really fast. My guess is that the GC.Collect() is called

So I searched a bit and I found this:

Binding vs. x:Bind, using StaticResource as a default and their differences in DataContext

An answer says:

The {x:Bind} markup extension—new for Windows 10—is an alternative to {Binding}. {x:Bind} lacks some of the features of {Binding}, but it runs in less time and less memory than {Binding} and supports better debugging.

So Binding surely works different, it might calls GC.Collect() or Unbind it self??. Maybe have a look in x:Bind markup

在此处输入图片说明

You could try to add tracing to this binding to shed some light.

Also I would advise to swap these lines so that they look like this:

DataContext = this;
this.InitializeComponent();

It could be messing with your binding. As when you call initializeComponent it builds xaml tree but for binding I guess it uses old DataContext and then you immediately change DataContext, forcing rebinding of every property.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM