Multiple views sharing same data with two-way data binding between multiple threads

834 Views Asked by At

UWP app ( mvvm architecture ) I have a MainView which has a collection in its ViewModel, used to bind to the GridView on MainView and each item has a TextBox with 2 way databinding with Description property of class Note.

Xaml of the TextBox of each gridviewitem.

<TextBlock Text="{x:Bind Description,Mode=TwoWay}"

Collection property used to bind to ItemSource of gridview.

public ObservableCollection<Note> Notes { get; }

and this is the class Note

public class Note : Observable
{
    private string _description;
    public string Description
    {
        get => _description;
        set => Set(ref _description, value, nameof(Description));
    }        
}

the Observable class is for two way data binding help.

public class Observable : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
    {
        if (Equals(storage, value))
        {
            return;
        }

        storage = value;
        OnPropertyChanged(propertyName);
    }

    protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Now everything uptil this point works perfectly, when I change the text in textbox, it changes the value of Description as well.

Second View

Now I have a feature where each GridViewItem has a button in it which opens the Note in new window. and this new window has nothing but only 1 TextBox, so now the secondary view and the GridViewItem which opened that view are using the same object of Note.

This TextBox in secondary view also has 2 way data binding with the Description of the Note.

The Problem

What I want is that whether the textbox in gridview or the textbox on the secondary view is edited, the value of description must remain synced between these 2 textboxes, that is why I tried to bind them 2 way with same object of Note hence the same Description object is bound to both of them.

Error here was expected to me which was Marshalling threading error, so whenever I try to change value of any textbox, it tried to update UI on other view ( which is another thread ) which is ofcourse not allowed.

I know about CoreDisptcher

I already know about the Dispatcher feature of UWP for safe cross thread communication, I already have it all setup and if I use it from a normal method I can easily use it for cross thread UI update and it totally works. But my issue is the following line :

protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\

Exception occurs when it tried to invoke the PropertyChanged I tried to wrap following line in my Dispatcher :

OnPropertyChanged(propertyName);

but INotify interface does not allow me to have a Set<> method which returns a Task instead it needs to return just an object, this is the point where I am stuck and I dont know how to make use of Dispatcher in this scenario, please let me know if there is some better way to do this, it seems this way might not be so efficient. Thanks.

2

There are 2 best solutions below

0
On BEST ANSWER

So I finally had to take a totally different approach centralizing TextChanged events of MainView textbox and the one on the secondaryview.

I essentially passed the textbox on the mainpage through to the secondary page ( secondary view ) and then subscribed to its TextChanged event. I also subscribed to the TextChanged event of textbox on the secondary view, and then with help of reverse dispatchers I was able to sync the text between 2 windows without any problems.

Note : always make sure to unsubscribe to events when the secondary window closes to prevent memory leaks.

private async void PipBox_TextChanged(object sender, TextChangedEventArgs e)
{
    string text = PipBox.Text;
    await CoreApplication.MainView.Dispatcher.AwaitableRunAsync(() =>
    {
        if (parentBox.Text != text)
            parentBox.Text = text;
    });
}
private async void ParentBox_TextChanged(object sender, TextChangedEventArgs e)
{
    string text = parentBox.Text;
    // the awaitablerunasync extension method comes from "Windows Community Toolkit".
    await _viewLifetimeControl.Dispatcher.AwaitableRunAsync(() =>
    {
        if (ViewModel.MyNote.Description != text)
            ViewModel.MyNote.Description = text;
    });
}

Notice that I still have 2 way data binding on both textboxes and it does not cause any exceptions because I am using 2 different instances of Note for both views.

<TextBox Text="{x:Bind ViewModel.MyNote.Description, Mode=TwoWay}"
                 x:Name="PipBox"/>

but because I have twoway data binding on both textboxes, that is how I can easily keep both instances of Note in sync as well on separate threads.

I will keep the github repo in case it can help anyone else : https://github.com/touseefbsb/MultiWindowBindingSync

P.S : A special thanks to Martin Zikmund who helped me a lot in figuring out this solution.

7
On

The best solution in this case would be to have a separate set of INotifyPropertyChanged instances for each window and using some kind of messaging solution like EventHub in MvvmLight, which publishes message that the underlying model changed and all interested parties should update their instances.

Another option would be to create a base model class, which maintains a dictionary of INotifyPropertyChanged instances for each UI thread (so it would be a Dictionary<Dispatcher, YourModelClass>. Now the parent would subscribe to PropertyChanged event of each child instance and once it executes would propagate the event to other childs using the appropriate Dispatcher.

Also there is a very interesting utility class ViewSpecificBindableClass by Marian Dolinský on his GitHub which could potentially be a solution that would allow you to have "single" class in multiple views, aware of multiple dispatchers. I haven't tried it yet, but it seems promising.