Skip to content

Uno.UI.Dispatching.CoreDispatcherSynchronizationContext unexpectedly null when using databinding in Microsoft.Xaml.UI.Controls.TabView #12265

@metacoresystems

Description

@metacoresystems

Current behavior

UnoIssueRepro.zip

I attach a simplified representative solution that consistently reproduces this unexpected behaviour. Note this is using Uno 4.7.37 running in a WebAssembly environment.

  • I have a simple single main page that comprises a Microsoft.Xaml.Controls.UI.TabView control bound to a list of tab view model items. Each tab's appearance is based upon the TabView's TabItemTemplate.
  • Each TabViewModelItem has a child list of ItemViewModel items, which are displayed in the TabItemTemplate via use of an ItemsControl.
  • Each of these ItemViewModel items has an ObservableCollection of numbers which are returned by a FakeNumbers ObservableCollection property on the ItemViewModel.
  • Each ItemViewModel is templated with a data template, that lists the FakeNumbers list via the use of another ItemsControl which is bound to this property via its ItemsSource.
  • When I first open the page, my TabView control opens as expected on the first tab. The tab item template for the first tab is rendered, which contains the ItemsControl bound to the list of ItemViewModel items. The templates for each of these ItemViewModel items then causes the FakeNumbers property to be fetched on ItemViewModel, by the data binding engine.
  • For that first tab that is open, when I log the thread ID and presence of a dispatcher sync context using basic console logging in the FakeNumbers ItemViewModel property:
        public ObservableCollection<int> FakeNumbers
        {
            get
            {
                if (_fakeNumbers == null)
                {
                    _fakeNumbers = new ObservableCollection<int>();

                    Console.WriteLine($"#1 Thread ID {Thread.CurrentThread.ManagedThreadId} - {(SynchronizationContext.Current == null ? "null" : SynchronizationContext.Current.ToString())}");

                    _fakeAsyncService.FakeMethodAsync().ContinueWith(task =>
                    {
                        Console.WriteLine($"#3 Thread ID {Thread.CurrentThread.ManagedThreadId} - {(SynchronizationContext.Current == null ? "null" : SynchronizationContext.Current.ToString())}");

                        foreach (var number in task.Result)
                        {
                            _fakeNumbers.Add(number);
                        }
                    }, TaskScheduler.FromCurrentSynchronizationContext());
                }

                return _fakeNumbers;
            }
        }
  • I see as I would expect that the data binding engine when fetching the property has been called in a thread where the SynchronizationContext is set to the Uno.UI.Dispatching.CoreDispatcherSynchronizationContext:

#1 Thread ID 1 - Uno.UI.Dispatching.CoreDispatcherSynchronizationContext
#2 Thread ID 1 - Uno.UI.Dispatching.CoreDispatcherSynchronizationContext
#1 Thread ID 1 - Uno.UI.Dispatching.CoreDispatcherSynchronizationContext
#2 Thread ID 1 - Uno.UI.Dispatching.CoreDispatcherSynchronizationContext
#3 Thread ID 1 - Uno.UI.Dispatching.CoreDispatcherSynchronizationContext
#3 Thread ID 1 - Uno.UI.Dispatching.CoreDispatcherSynchronizationContext

  • This I would expect, as the data binding engine has gone to fetch the FakeNumbers property on the WASM UI thead.
  • However when I then switch to the second tab (which hasn't been rendered before), I am using the same view model structure and data. But, the same logging shows that when data binding goes to fetch the same FakeNumbers property on the second tab's TabViewModelItem the current SynchronisationContext is not set - it is null. This is shown by the same logging for the second tab:

#1 Thread ID 1 - null
#2 Thread ID 1 - null
fail: Uno.UI.DataBinding.BindingPath+BindingItem[0]
Failed to get the source value for [FakeNumbers]
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
---> System.InvalidOperationException: The current SynchronizationContext may not be used as a TaskScheduler.
at System.Threading.Tasks.SynchronizationContextTaskScheduler..ctor() in D:\a\Uno.DotnetRuntime.WebAssembly\Uno.DotnetRuntime.WebAssembly\runtime\src\libraries\System.Private.CoreLib\src\System\Threading\Tasks\TaskScheduler.cs:line 577
at System.Threading.Tasks.TaskScheduler.FromCurrentSynchronizationContext() in D:\a\Uno.DotnetRuntime.WebAssembly\Uno.DotnetRuntime.WebAssembly\runtime\src\libraries\System.Private.CoreLib\src\System\Threading\Tasks\TaskScheduler.cs:line 346
at UnoIssueRepro.ItemViewModel.get_FakeNumbers() in D:\Git\EntityForge\entity-forge\EntityForge.Framework.net7\UnoIssueRepro\UnoIssueRepro\ItemViewModel.cs:line 34
at System.Reflection.MethodInvoker.InterpretedInvoke(Object obj, Span`1 args, BindingFlags invokeAttr) in D:\a\Uno.DotnetRuntime.WebAssembly\Uno.DotnetRuntime.WebAssembly\runtime\src\mono\System.Private.CoreLib\src\System\Reflection\MethodInvoker.Mono.cs:line 33
--- End of inner exception stack trace ---
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) in D:\a\Uno.DotnetRuntime.WebAssembly\Uno.DotnetRuntime.WebAssembly\runtime\src\libraries\System.Private.CoreLib\src\System\Reflection\RuntimeMethodInfo.cs:line 131
at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters) in D:\a\Uno.DotnetRuntime.WebAssembly\Uno.DotnetRuntime.WebAssembly\runtime\src\libraries\System.Private.CoreLib\src\System\Reflection\MethodBase.cs:line 54
at Uno.UI.DataBinding.BindingPropertyHelper.<>c__DisplayClass12_0.b__0(Object instance, Object[] args) in /home/vsts/work/1/s/src/Uno.UI/DataBinding/BindingPropertyHelper.cs:line 81
at Uno.UI.DataBinding.BindingPropertyHelper.<>c__DisplayClass41_12.b__19(Object instance) in /home/vsts/work/1/s/src/Uno.UI/DataBinding/BindingPropertyHelper.cs:line 665
at Uno.UI.DataBinding.BindingPath.BindingItem.GetSourceValue(ValueGetterHandler getter) in /home/vsts/work/1/s/src/Uno.UI/DataBinding/BindingPath.cs:line 778
fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
The [Name] property getter does not exist on type [System.Int32]
fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
The [FakeNumbers] property getter does not exist on type [System.Int32]
#1 Thread ID 1 -
#2 Thread ID 1 -

  • As you can see, on switching to the second tab, the Uno.UI.Dispatching.CoreDispatcherSynchronizationContext is no longer set to SynchronizationContext.Current as I would expect - it is null. Even though, curiously, the logging of the thread ID shows it is still the same thread that fetched the property successfully for the first open tab. This has caused the attempt to execute the continuation on the sync context to fail.

Expected behavior

When I change to the second tab in UI, and the FakeNumbers collection goes to be fetched by data binding, I expect the Uno.UI.Dispatching.CoreDispatcherSynchronizationContext to be set to SynchronizationContext.Current in the same way as it was when data binding fetched the FakeNumbers collection in the first open tab.

This issue reproduces consistently using the attached solution.

How to reproduce it (as minimally and precisely as possible)

Run the attached solution WASM project. Observe from the Output Window console logging that when the first tab opens the Uno.UI.Dispatching.CoreDispatcherSynchronizationContext is set to SynchronizationContext.Current when data binding goes to fetch the FakeNumbers ObservableCollection.

But now click on the second tab to display it. Observe that the same logging now shows that whilst the same thread is being used to fetch the FakeNumbers collection, SynchronizationContext.Current is unexpectedly null.

Workaround

Yes, I have found one via a (more generally useful) AsyncProperty utility class I've created - attached. This utility class attempts to asynchronously fetch the value you want if not fetched already, and can be bound to in the UI. For example:

public class MyViewModel : ObservableObject
{
   public MyViewModel()
   {
      _fakeNumbers = new AsyncProperty<List<int>, ObservableCollection<int>>(_fakeAsyncService.FakeMethodAsync(),
         (numbers) =>
         {
            var observable = new ObservableCollection<int>();

            foreach (var number in numbers)
            {
               observable.Add(number);
            }

            return observable;
         });
   }

   public AsyncProperty<ObservableCollection<int>> FakeNumbers => _fakeNumbers;
}

You can then bind directly to the asynchronously returned value via data binding, e.g.:

<ItemsControl ItemsSource="{Binding FakeNumbers.Value}"> </ItemsControl>

When .Value is first called by the data binding thread, the utility class runs the asynchonous work necessary to fetch the value and on return, updates the value and raises a property changed on .Value. This approach works, because even though Synchronization.Current is null when the value returns as fetched, the observable collection is created and populated at least on the same thread. This at least prevents exceptions and means the property binds on the value property change being raised, even though really the SynchronizationContext should always be set in the data binding thread.
Utility class is here: AsyncProperty.zip

Environment

Uno.WinUI / Uno.WinUI.WebAssembly / Uno.WinUI.Skia

NuGet package version(s)

Uno.Winui.WebAssembly 4.7.37

Affected platforms

WebAssembly

IDE

Visual Studio 2022

IDE version

17.5.4

Relevant plugins

No response

Anything else we need to know?

I don't feel familiar enough with Uno and WebAssembly at the moment to amend the code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    difficulty/medium 🤔Categorizes an issue for which the difficulty level is reachable with a good understanding of WinUIhas-repro-branchkind/bugSomething isn't workingproject/navigation-lifecycle 🧬Categorizes an issue or PR as relevant to the navigation and lifecycle (NavigationView, AppBar, ...)triage/potentially-fixedCategorizes an issue as potentially fixed by some unlinked PR, fix needs to be verified

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions