Binding and x:Bind problems with TwoWay mode - c#

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.

Related

x:bind UI not update when PropertyChanged in UWP

I'm using x:Bind and INotifyPropertyChanged to update UI in UWP application. But it behaves like OneTime binding even though I set it to OneWay.
Bindings.Update() works, but I want to know why INotifyPropertyChanged fails.
XAML
<TextBlock Text="{x:Bind staffVM.Name, Mode=OneWay}"/>
Code-behind:
private StaffViewModel staffVM;
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// I want to change staffVM according to ListView's selection.
staffVM = staffListView.SelectedItem as StaffViewModel;
staffVM.Update(); // If change this to Bindings.Update(), It works.
}
ViewModel:
public class StaffViewModel: INotifyPropertyChanged
{
private Character character;
public string Name => character.name == string.Empty ? null : character.name;
public void Update()
{
RaisePropertyChanged(string.Empty);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName]string propName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
Firstly, you need to specify the name of the variable that you want to update:
public void Update()
{
RaisePropertyChanged(nameof(Name));
}
Documentation and sample: https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.data.inotifypropertychanged.propertychanged
Secondly, by default x:Bind is OneTime
To fix it, add Mode="OneWay"
Mode Specifies the binding mode, as one of these strings: "OneTime", "OneWay", or "TwoWay". The default is "OneTime". Note that this differs from the default for {Binding}, which is "OneWay" in most cases.
Please read documentation
https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/x-bind-markup-extension
The problem here is not on the level of the StaffViewModel class, but on the level of the page. When you do:
staffVM = staffListView.SelectedItem as StaffViewModel;
The UI has no notification about the fact that the staffVM field has changed. So the binding is still pointing to the old instance of StaffViewModel. Hence when you do staffVM.Update(), it does notify about changes, but the UI is not listening to that instance - it is still listening to notifications on the first selected item. Bindings.Update() fixes this because it completely re-evaluates all bindings so it will "get" the new value of staffVM field.
Solution would be to implement INotifyPropertyChanged on the Page and encapsulate the staffVM in a property which raises PropertyChanged event.
Ideally I would however suggest creating a "root" view model, which you will set only once and will not change and which will contain the selected item as its property. This way you don't have to implement INotifyPropertyChanged in the Page and its code-behind will be simpler. As a result you will have something like the following in the code-behind:
public RootViewModel VM {get;} = new RootViewModel();
And in XAML:
<TextBlock Text="{x:Bind VM.SelectedStaff.Name, Mode=OneWay}"/>

Label Content binding only updating once

My label only seems to get the data from the property it is bound to once. I have the Property raising the Property Changed event in the setter, but when the value of the property gets changed, it raises the event properly (I know this because of the break point I set), but the text in the Label on the window doesn't change. I should maybe also note that the window with the label isn't the main window, but a new one that pops up.
ViewModel:
public class PurchaseVerificationViewModel : ViewModelBase
{
private WindowService.WindowService windowService = new WindowService.WindowService();
private string _verificationQuestion = "Question"; //default so i can check if it changed in the window
public string VerificationQuestion
{
get { return _verificationQuestion; }
set
{
if (_verificationQuestion != value)
{
_verificationQuestion = value;
OnPropertyChanged(nameof(VerificationQuestion));
}
}
}
}
Window:
<Window>
<Window.DataContext>
<viewmodels:PurchaseVerificationViewModel/>
</Window.DataContext>
<Grid>
<Label Content="{Binding VerificationQuestion, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Window>
ViewModelBase:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
There's no problem with your implementation of the INotifyPropertyChanged, since you are correctly invoking the notification that your property has been modified.
So if the problem is not with the one who's raising the notification, might it rather be with what is actively listening to it?
And the problem is that you're defining the DataContext of your Window to the class itself, rather than to the instance which you are utilizing and modifying in the code-behind of your application.
What is actually happening under the hoods, due to the way you defined your DataContext in xaml, is that a new PurchaseVerificationViewModel class is being constructed (is the not the one who are modifying on your logic) and therefore your VerificationQuestion will return it's default value (or rather the private backing field default value, "Question").
In reality the problem is that you have induced your listener to listen to the wrong thing.
Since you want the content of the Label (target) to be update based on a source change, what you have to do, is to set as the DataContextof the Window the specific instance which you are modifying on the logic of your application, and make sure you define it as a property!
public PurchaseVerificationViewModel myViewModel {get;set;}
For instance after InitializeComponent(), on your page constructor, you could initialize the property and set it as the DataContext, like this:
myViewModel = new PurchaseVerificationViewModel();
this.DataContext = myViewModel;

WPF, set DataContext to property of same class

I have a class Device that uses the INotifyPropertyChanged, it is tested and it works.
Now I have a deviceMonitor that is the UI representation of this device. In the code I have a reference to Device and I want to link changes in the device to changes in the UI (two way is not needed, but clicking the deviceMonitor should call a certain function of the device)
I'm using expression Blend with VS2015 so guidance based on where to click to get it to work would be extremely welcome.
this is a mockup of the device
public class Device : INotifyPropertyChanged
{
public string Name { ... } //uses NotifyPropertyChanged in the set
// other properties and their relative private vars.
}
Then the xaml.cs for the GUI, here I have a reference to the dll containing the Device:
public partial class DeviceControl : UserControl
{
public Device myDevice = new Device();
public DeviceControl()
{
InitializeComponent();
// here I tried setting the datacontest to the myDevice
// also tried to set the dataContext in Blend and here grab a
// reference to it and store it in myDevice. But nothing workerd
}
public void ChangeDevName()
{
this.myDevice.DeviceName = "Test";
//UI Representation of deviceName never changed
}
}
Then the XAML
<UserControl>
<UserControl.DataContext>
<recoveriX:RecoverixDevice DeviceName="thisIsAName"/>
</UserControl.DataContext>
<Grid>
<TextBlock x:Name="title" Text="{Binding DeviceName}"/>
</Grid>
</UserControl>
This might work:
In your DeviceControl UserControl, wire up events for OnLoaded and OnUnloaded of the control.
In the code-behind for the event handlers, subscribe/unsubscribe to the PropertyChanged event of UserControl's DataContext (this.DataContext) ; like so:
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (this.DataContext is INotifyPropertyChanged)
{
((INotifyPropertyChanged)this.DataContext).PropertyChanged += OnDataContextPropertyChanged;
}
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
if (this.DataContext is INotifyPropertyChanged)
{
((INotifyPropertyChanged)this.DataContext).PropertyChanged -= OnDataContextPropertyChanged;
}
}
private void OnDataContextPropertyChanged(object sender, PropertyChangedEventArgs e)
{
// You could also just update every time something is changed.
// As an example you could check for the "Name" property being changed.
if (e.PropertyName == nameof(Device.Name))
{
title.Text = this.DataContext.Name;
}
}
An important note to make is the if (this.myDevice is INotifyPropertyChanged) check.
It ensures the Device class inherits from INotifyPropertyChanged.
Providing it does, it casts the Device being your DataContext (this.DataContext) to (INotifyPropertyChanged) so you can subscribe to the PropertyChanged event from the INotifyPropertyChanged interface.
Then, when a property on the DataContext is changed, your handler will be fired. Obviously you can put what you want to do in the code of OnMyDevicePropertyChanged, I've just used "Name" as an example.
Hope this helps!
EDIT
Furthermore; you could also store a private field of type Device in the UserControl's code-behind. A bit like so:
private Device _viewModel; // You could also use the interface (like 'IDevice'), too.
Then in your `OnLoaded' event, store it in the field:
if (this.DataContext is INotifyPropertyChanged)
{
this.viewModel = this.DataContext;
// Wire up your PropertyChanged handler as before.
}
And on your OnUnloaded event, just unsubscribe from the viewModel if it is not null:
if (this.viewModel != null)
{
this.viewModel.PropertyChanged -= OnDataContextPropertyChanged;
}
This also gives you a bit more flexibility when you've got the DataContext stored as a field, as you can use it within other methods (if you use any more in your code behind - you shouldn't...; but it saves CPU time casting it to INotifyPropertyChanged all the time.
For future reference I would look at Implementing MVVM Practices into your projects.
Good luck!
Problem was overwriting the private device, setting the datacontext fixed the thing.
This is the final class:
public partial class DeviceControl : UserControl
private Device _device = new Device();
public DeviceControl()
{
InitializeComponnents();
this.DataContext = _device;
}
public void SetDevice(Device d)
{
//This fails:
//_device = d;
//This works
this.DataContext = d;
}

Dependency property changed callback - multiple firing

I want to listen to changes of the DependencyProperty. This code works, but after every reload page with the CustomControl is callback method called multiple times...
public partial class CustomControl : UserControl
{
public CustomControl()
{
InitializeComponent();
}
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(CustomControl), new PropertyMetadata(IsOpenPropertyChangedCallback));
private static void IsOpenPropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Debug.WriteLine("Fire!");
}
}
Update
ViewModel
private bool _isOpen;
public bool IsOpen
{
get { return this._isOpen; }
set { this.Set(() => this.IsOpen, ref this._isOpen, value); } // MVVM Light Toolkit
}
View
<local:CustomControl IsOpen="{Binding Path=IsOpen}" />
Sample
project
tap "second page"
tap "true" (look at the output window)
go back
tap "second page"
tap "false" (look at the output window)
This solved my problem.
this.Unloaded += CustomControlUnloaded;
private void CustomControlUnloaded(object sender, RoutedEventArgs e)
{
this.ClearValue(CustomControl.IsOpenProperty);
}
It sounds as though the number of times the event is triggered relates to the number of times that you open the page with the control on it. This would suggest that you have multiple instances of the page.
The issue then is really that your pages are doing something that stop them from being destroyed correctly.
Unfortunately, without being able to see the code it's impossible to say what is causing this. It's probably that you've subscribed to an event in code and not unsubscribed to it though. (I see that a lot in Phone apps.)
What's happening is that the SecondPageView is being loaded multiple times. Each time a new instance is created, it binds to the data context and retrieves the value of IsOpen from the view model. Then the dependency property is set.
This is actually the desired behavior. If the properties were not set again, the view model's state would not be reflected in the page. There is no way to forward-navigate to an old page instance using the phone's native navigation API.

TextBox.Text binding to a ViewModel's property

I have a problem with two way binding a TextBox content to a property in another class. Searching stackoverflow gave a lot of tips/solutions but none seem to work.
In my XAML code I have:
< TextBox ... Width="336" IsReadOnly="True"
Text="{Binding Path=AssignedClearProgram, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
(... I removed all non important items)
In the accompanies cs code I have:
public CombiWindow(Combi combi)
{
ViewModel = new CombiViewModel(combi);
DataContext = ViewModel;
}
In the CombiViewModel:
[UsedImplicitly]
public string AssignedClearProgram { get; set; }
It seems that the first time I assign AssignedClearProgram, the textbox is filled with the text that I set, however after the window is displayed and AssignedClearProgram gets updated from the code (i.e. the set method is called), the data is not updated in the screen.
Does anybody have a solution to update the textbox when this variable is changed?
Kind regards,
Michel
Your viewmodel class needs to implement INotifyPropertyChanged and you need to raise that interface's event whenever you change the property. Then the binding will spot changes and update the textbox.
Your view model class should implement the INotifyPropertyChanged interface.
Your property would then look like the following:
private string assignedClearProgram;
public string AssignedClearProgram
{
get { return assignedClearProgram; }
set
{
if (assignedClearProgram != value)
{
assignedClearProgram = value;
// Notify property has changed here using PropertyChanged event from INotifyPropertyChanged.
}
}
}
Read this article for an example of how to implement the INotifyPropertyChanged interface and utilize its PropertyChanged event.

Categories