question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Proposal: implicit DispatcherQueue support for x:Bind

See original GitHub issue

Proposal: implicit DispatcherQueue support for x:Bind

Summary

Add automatic (possibly optional) dispatching to the DispatcherQueue instance for the current UI thread in the _BindingsTracking type and to the necessary internal WinUI paths (eg. to deal with collection changed UI updates), specifically in the PropertyChanged event handler and the CollectionChanged. This would completely reverse the chain of responsability for proper thread dispatching from the viewmodel side to the UI side, and make the whole thing much more resilient and flexible. A similar concept would apply to other events handled by code within WinUI.

The core idea is this: it should be responsability of an event handler to dispatch to whatever synchronization context it needs, not to the object raising the event. Specifically because if you have multiple listeners on different threads, the latter simply will never work anyway. And in general, objects raising events shouldn’t need to account for who’s listening to them.

Details

Note: what follows is just the details for the PropertyChanged event specifically, not the others. I’ll describe this one since it’s more “easily” addressed by looking at the x:Bind codegen, whereas the others would likely need some tweaks in the code within WinUI dealing with those events. The idea in general is still the same though: WinUI as a framework should automatically deal with dispatching when handling notification events tied to the UI.

Handling the synchronization context for the PropertyChanged event in viewmodels has always been a major pain point when working with MVVM. There have been a variety of different solutions proposed, but each with their shortcomings, namely:

  • Lots of overhead in the viewmodels to deal with dispatching to the “UI thread”. This also breaks modularity as a viewmodel should conceptually just be a .NET Standard and portable component, where the term “UI thread” has no actual meaning. This should be purely a UI-related aspect that the viewmodels should not really be concerned with.
  • Single-context solutions involve capturing a synchronization context from the viewmodel and dispatching there when raising the PropertyChanged event. Again this has two issues: for one it’s not really flexible as it wouldn’t work in case the same observable object is being displayed in different ways across more than a single window (where two different UI threads would be used), and the second issue is that it would inject purely platform dependent code back into a more abstract layer, i.e. the viewmodel.
  • Even when using dependency injection to keep the viewmodels technically platform agnostic, this still adds a lot of potentially unnecessary overhead and complexity to what should be a pretty simple implementation of the INotifyPropertyChanged interface, plus again I’d argue that conceptually the UI thread dispatching should only be relegated to the actual platform dependent code. From the point of view of a .NET Standard component, individual threads should (generally) not matter much.

The proposed solution is to completely flip this over and add a simple extension to the codegen for x:Bind (pinging @MikeHillberg about this) to allow for a more general solution to this issue, built right into the framework itself. The advantages here would be multiple, as mentioned above as well:

  • Less overhead and complexity in the viewmodels, and increased code modularity 🚀
  • Less error prone code ✨
  • More flexible results, with supports for an arbitrary number of receiving UI threads 🧰

Implementation

Consider this simple viewmodel (not implemented, it doesn’t matter here):

public class MainPageViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public string Text { get; set; }
}

And this XAML snippet:

<Page
    x:Class="XBindSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:XBindSample">
    <Page.DataContext>
        <local:MainPageViewModel x:Name="ViewModel"/>
    </Page.DataContext>

    <Grid>
        <TextBlock Text="{x:Bind ViewModel.Text, Mode=OneWay}"/>
    </Grid>
</Page>

If we build this and go to inspect the generated MainPage.g.cs file, we’ll find a number of classes with various responsabilities - from updating individual UI controls to tracking the bindings, etc. In particular, we’re interested in this one:

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 10.0.19041.1")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
private class MainPage_obj1_BindingsTracking
{
    private global::System.WeakReference<MainPage_obj1_Bindings> weakRefToBindingObj; 

    public MainPage_obj1_BindingsTracking(MainPage_obj1_Bindings obj)
    {
        weakRefToBindingObj = new global::System.WeakReference<MainPage_obj1_Bindings>(obj);
    }

    public MainPage_obj1_Bindings TryGetBindingObject()
    {
        return null; // Removed for brevity
    }

    public void ReleaseAllListeners()
    {
        // Removed for brevity
    }

    public void PropertyChanged_ViewModel(object sender, global::System.ComponentModel.PropertyChangedEventArgs e)
    {
        MainPage_obj1_Bindings bindings = TryGetBindingObject();
        if (bindings != null)
        {
            string propName = e.PropertyName;
            global::XBindSynchronizationContextSample.MainPageViewModel obj = sender as global::XBindSynchronizationContextSample.MainPageViewModel;
            if (global::System.String.IsNullOrEmpty(propName))
            {
                if (obj != null)
                {
                    bindings.Update_ViewModel_Text(obj.Text, DATA_CHANGED);
                }
            }
            else
            {
                switch (propName)
                {
                    case "Text":
                    {
                        if (obj != null)
                        {
                            bindings.Update_ViewModel_Text(obj.Text, DATA_CHANGED);
                        }
                        break;
                    }
                    default:
                        break;
                }
            }
        }
    }
    private global::XBindSynchronizationContextSample.MainPageViewModel cache_ViewModel = null;
    public void UpdateChildListeners_ViewModel(global::XBindSynchronizationContextSample.MainPageViewModel obj)
    {
        if (obj != cache_ViewModel)
        {
            if (cache_ViewModel != null)
            {
                ((global::System.ComponentModel.INotifyPropertyChanged)cache_ViewModel).PropertyChanged -= PropertyChanged_ViewModel;
                cache_ViewModel = null;
            }
            if (obj != null)
            {
                cache_ViewModel = obj;
                ((global::System.ComponentModel.INotifyPropertyChanged)obj).PropertyChanged += PropertyChanged_ViewModel;
            }
        }
    }
}

This is the type that actually subscribes to the PropertyChanged event in our viewmodel and goes about updating the UI components. This is the only part that should need to care about the “UI thread”, and this is the only part that should include the code to automatically deal with this for the user, both because it’d make all the rest of the code much simpler, and also because injecting the change here would be pretty efficient and with a small code change required.

In particular, the issue is when the PropertyChanged_ViewModel handler is invoked, since that could be done from an other thread if the PropertyChanged event was raised on another thread. To fix this, I propose the following:

private global::System.WeakReference<MainPage_obj1_Bindings> weakRefToBindingObj;
private readonly DispatcherQueue dispatcherQueue;

public MainPage_obj1_BindingsTracking(MainPage_obj1_Bindings obj)
{
    weakRefToBindingObj = new global::System.WeakReference<MainPage_obj1_Bindings>(obj);
    dispatcherQueue = DispatcherQueue.GetForCurrentThread();
}

public void PropertyChanged_ViewModel(object obj, global::System.ComponentModel.PropertyChangedEventArgs e)
{
    if (dispatcherQueue.HasThreadAccess)
    {
        PropertyChanged_ViewModel_OnDispatcherQueue(obj, e);
    }
    else
    {
        PropertyChanged_ViewModel_Dispatch(obj, e);
    }
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void PropertyChanged_ViewModel_Dispatch(Object obj, global::System.ComponentModel.PropertyChangedEventArgs e)
{
    _ = dispatcherQueue.TryEnqueue(() => PropertyChanged_ViewModel_OnDispatcherQueue(obj, e));
}

private void PropertyChanged_ViewModel_OnDispatcherQueue(object sender, global::System.ComponentModel.PropertyChangedEventArgs e)
{
    MainPage_obj1_Bindings bindings = TryGetBindingObject();
    if (bindings != null)
    {
        string propName = e.PropertyName;
        global::XBindSynchronizationContextSample.MainPageViewModel obj = sender as global::XBindSynchronizationContextSample.MainPageViewModel;
        if (global::System.String.IsNullOrEmpty(propName))
        {
            if (obj != null)
            {
                bindings.Update_ViewModel_Text(obj.Text, DATA_CHANGED);
            }
        }
        else
        {
            switch (propName)
            {
                case "Text":
                    {
                        if (obj != null)
                        {
                            bindings.Update_ViewModel_Text(obj.Text, DATA_CHANGED);
                        }
                        break;
                    }
                default:
                    break;
            }
        }
    }
}

Here’s the git diff:

+using Windows.System;
+
 namespace XBindSynchronizationContextSample
 {
     partial class MainPage :
@@ -170,11 +172,13 @@ namespace XBindSynchronizationContextSample
             [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
             private class MainPage_obj1_BindingsTracking
             {
+                private readonly DispatcherQueue dispatcherQueue;

                 public MainPage_obj1_BindingsTracking(MainPage_obj1_Bindings obj)
                 {
                     weakRefToBindingObj = new global::System.WeakReference<MainPage_obj1_Bindings>(obj);
+                    dispatcherQueue = DispatcherQueue.GetForCurrentThread();
                 }

                 public MainPage_obj1_Bindings TryGetBindingObject()
@@ -198,6 +202,17 @@ namespace XBindSynchronizationContextSample
                 }

                 public void PropertyChanged_ViewModel(object sender, global::System.ComponentModel.PropertyChangedEventArgs e)
+                {
+                    if (dispatcherQueue.HasThreadAccess)
+                    {
+                        PropertyChanged_ViewModel_OnDispatcherQueue(sender, e);
+                    }
+                    else
+                    {
+                        PropertyChanged_ViewModel_Dispatch(obj, e);
+                    }
+                }
+
+                [MethodImpl(MethodImplOptions.NoInlining)]
+                private void PropertyChanged_ViewModel_Dispatch(Object obj, global::System.ComponentModel.PropertyChangedEventArgs e)
+                {
+                    _ = dispatcherQueue.TryEnqueue(() => PropertyChanged_ViewModel_OnDispatcherQueue(obj, e));
+                }
+
+                public void PropertyChanged_ViewModel_OnDispatcherQueue(object sender, global::System.ComponentModel.PropertyChangedEventArgs e)

Now, this is just a proof of concept, but hopefully it illustrates the concept well enough 😄

Rationale

  • Offer a new, modern and flexible handling of UI thread dispatching on WinUI
  • Make working with multiple windows much easier and less error prone
  • Greatly reduce the complexity on the backend side and increase the code modularity
  • Address a major pain point in MVVM/similar that has been there for years, with a built-in solution

Scope

Capability Priority
Update the x:Bind codegen to handle the UI thread dispatch Must
Update internal handlers to handle UI thread dispatch (eg. CollectionChanged) Must
Introduce changes that would break existing codebases Won’t
Introduce overhead in current codebases or when the dispatch is not needed Won’t

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:32
  • Comments:9 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
kmgallahancommented, Jan 6, 2022

As this would make every developer’s life easier and it seems straightforward to implement, can we create a list a list of reasons why it can’t or shouldn’t be done ASAP?

We have entered that “post WinUI 3.0” phase after all…

@Sergio0694 @andrewleader @ryandemopoulos

2reactions
JeanRocacommented, Nov 26, 2020

Thank you for this @Sergio0694. We would be able to look into this more post WinUI 3.0. I will leave this open for now.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Feature tracking
Proposal : implicit DispatcherQueue support for x:Bind #2795 opened by Sergio0694 area-Binding feature proposal. New feature proposal
Read more >
XAML – Windows Developer Blog - RSSing.com
SVG support; Normal and Virtual Surfaces; Hover Interactions; Implicit Show and Hide; Expression Helpers; Offset stomping fix.
Read more >
DispatcherQueueExtensions - Windows Community Toolkit
Helpers for executing code on a specific UI thread through a DispatcherQueue instance.
Read more >
DispatcherQueue Class (Windows.System) - UWP
Manages a prioritized queue on which tasks execute in a serial fashion on a thread.
Read more >
UWP/WinUI 부터 컨트롤에 바인딩으로 연결된 속성을 ...
Proposal : implicit DispatcherQueue support for x:Bind ... feature proposal needs-winui-3 area-Binding team-Markup wct.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found