Windows 10, UWP, Multiple Windows, ViewModels and Sharing State

I wrote this post about multiple windows in a UWP app a few months ago;

Windows 10 and UWP Apps–Experiments with Multiple Windows

which really was a follow up to this post;

Windows 8.1 Preview- Multiple Windows

which I wrote in the context of Windows 8.1.

As part of that post, I made a statement around how creating multiple Windows means creating multiple threads (one per Window) and how you have to be ‘careful’ if you share data items between these Windows because;

  1. You always have to be careful (or maybe completely paranoid) when you share data across threads – generally, I’d steer well clear of it whenever I could.
  2. Those dispatcher threads for those windows are ‘Application Single Threaded Apartment’ (ASTA) threads which have automatic protection built-in to avoid re-entrancy and you can easily tie yourself up in knots with this (as I have).

Someone came back to me and asked for more detail on this and so I thought I’d experiment and write up some sketchy notes here based on a simple example.

To do that, I knocked together some code. Starting with a blank app I made a MainPage.xaml with a Button in it;

  <Grid
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Button
      Content="Create"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
      Click="OnCreate" />
  </Grid>

and I wrote a little code behind this which creates a new window;

    async void OnCreate(object sender, RoutedEventArgs e)
    {
      CoreApplicationView newCoreView = CoreApplication.CreateNewView();

      ApplicationView newAppView = null;
      int mainViewId = ApplicationView.GetApplicationViewIdForWindow(
        CoreApplication.MainView.CoreWindow);

      await newCoreView.Dispatcher.RunAsync(
        CoreDispatcherPriority.Normal,
        () =>
        {
          newAppView = ApplicationView.GetForCurrentView();
          Window.Current.Content = new SubWindowUserControl();
          Window.Current.Activate();
        });

      await ApplicationViewSwitcher.TryShowAsStandaloneAsync(
        newAppView.Id,
        ViewSizePreference.UseHalf,
        mainViewId,
        ViewSizePreference.UseHalf);
    }

and that SubWindowUserControl is just a blank user control displaying a grid with a red background.

With that in place, I can run this up and create windows;

image

but what if I wanted to share some data across these windows? For instance, perhaps I can make a ViewModel with a simple integer in it and display it in the windows. I wrote this class (deriving it from a base class which implements INotifyPropertyChanged) and using a SimpleCommand class which implements ICommand in the most obvious way possible;

namespace App158
{
  using System;
  using System.Windows.Input;

  class SimpleViewModel : ViewModelBase
  {
    public SimpleViewModel()
    {
      this.incrementCommand = new SimpleCommand(this.OnIncrementCommand);
    }
    public int Count
    {
      get
      {
        return (this.count);
      }
      set
      {
        base.SetProperty(ref this.count, value);
      }
    }
    public int ThreadId
    {
      get
      {
        return (Environment.CurrentManagedThreadId);
      }
    }
    public ICommand IncrementCommand
    {
      get
      {
        return (this.incrementCommand);
      }
    }
    void OnIncrementCommand()
    {
      this.Count++;
    }
    ICommand incrementCommand;
    int count;
  }
}

and I declared an instance of it in my App.xaml as a resource;

<Application
    x:Class="App158.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App158"
    RequestedTheme="Light">
  <Application.Resources>
    <local:SimpleViewModel x:Key="viewModel"/>
  </Application.Resources>

</Application>

and I made it the ViewModel for my secondary user control via;

  <Grid
    Background="Red"
    DataContext="{StaticResource viewModel}">

    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
      <TextBlock Text="{Binding Count}"/>
      <TextBlock Text="{Binding ThreadId}"/>
      <Button Content="Increment" Command="{Binding IncrementCommand}"/>
    </StackPanel>
  </Grid>

and I ran up my code, created a couple of windows and clicked a few buttons and saw;

image

and, clearly, the count displayed in one window is different from the count displayed in the second window and if I use the debugger I can see three instances of my SimpleViewModel being created here – once at application startup and once when each window is created.

Being honest, I didn’t know that this would happen and so I definitely learned something here – my app.xaml resources are being instantiated on a per window basis and that was news to me.

That means, I’m going to have to be a little more devious if I actually want to share that object instance across multiple windows but I can probably be more devious. I changed my SimpleViewModel;

namespace App158
{
  using System;
  using System.Windows.Input;

  class SimpleViewModel : ViewModelBase
  {
    static SimpleViewModel()
    {
      instance = new Lazy<SimpleViewModel>(
        () => new SimpleViewModel(),
        true);
    }
    public SimpleViewModel()
    {
      this.incrementCommand = new SimpleCommand(this.OnIncrementCommand);
    }
    public SimpleViewModel Instance
    {
      get
      {
        return (instance.Value);
      }
    }
    public int Count
    {
      get
      {
        return (this.count);
      }
      set
      {
        base.SetProperty(ref this.count, value);
      }
    }
    public int ThreadId
    {
      get
      {
        return (Environment.CurrentManagedThreadId);
      }
    }
    public ICommand IncrementCommand
    {
      get
      {
        return (this.incrementCommand);
      }
    }
    void OnIncrementCommand()
    {
      this.Count++;
    }
    ICommand incrementCommand;
    int count;
    static Lazy<SimpleViewModel> instance;
  }
}

and I changed the DataContext in my user control;

  <Grid
    Background="Red"
    DataContext="{Binding Source={StaticResource viewModel}, Path=Instance}">

and now, while I still see multiple SimpleViewModel instances being created as I run my app and create 2 secondary windows, it’s clear (I think) that the secondary windows are sharing the same object instance as their DataContext.

With more than one secondary window present on the screen, if I press the ‘Increment’ button;

image

then the code blows up. Specifically, I see;

image

and that’s coming from this call stack;

image

and on this thread;

image

which makes sense because I clicked on the ‘increment’ button in the window that was showing a managed thread id of 6.

What’s going on here? Somehow an event handler has been added to an event and now that event handler can’t be called? I suspect it’s more that the event handler that’s been added has been marshalled from a particular context and that context is making a conscious choice to avoid being invoked here.

How about an experiment, a tweak to my SimpleViewModel;

    void OnIncrementCommand()
    {
      Task.Run(() => { this.Count++; });      
    }

Now when I click the button in the UI, the code blows up in a different way which is kind of what I expected. The familiar;

image

and that was no big surprise. The question then really becomes one of which thread should be firing these property changed notifications and the answer is going to presumably be ‘the right thread for each window.

To achieve that means somehow keeping a mapping table between event handlers and Dispatchers and then trying to use that table to do some kind of look up to figure out which dispatcher should be used to invoke which event handler.

That’s not really where I want to end up.

What to do? One idea might be to not share the view model between these 2 windows. What if I add a simple class that manages the data for me;

 using System;
  using System.Threading;

  static class SharedDataValue
  {
    public static event EventHandler ValueChanged;
    public static int Value
    {
      get
      {
        return (_value);
      }           
    }
    public static void IncrementValue()
    {
      Interlocked.Increment(ref _value);
      var handlers = ValueChanged;
      if (handlers != null)
      {
        handlers(null, EventArgs.Empty);
      }
    }
    static int _value;
  }

and then maybe I can change my simple view model;

 using System;
  using System.Threading;
  using System.Windows.Input;

  class SimpleViewModel : ViewModelBase
  {
    public SimpleViewModel()
    {
      this.incrementCommand = new SimpleCommand(this.OnIncrementCommand);
      SharedDataValue.ValueChanged += this.OnValueChanged;

      // Capture the sync context that we are created with.
      this.syncContext = SynchronizationContext.Current;
    }
    void OnValueChanged(object sender, EventArgs e)
    {
      this.syncContext.Post(
        _ =>
        {
          base.OnPropertyChanged("Count");
        },
        null);
    }
    public int Count
    {
      get
      {
        return (SharedDataValue.Value);
      }
    }
    public int ThreadId
    {
      get
      {
        return (Environment.CurrentManagedThreadId);
      }
    }
    public ICommand IncrementCommand
    {
      get
      {
        return (this.incrementCommand);
      }
    }
    void OnIncrementCommand()
    {
      SharedDataValue.IncrementValue();
    }
    ICommand incrementCommand;
    SynchronizationContext syncContext;
  }

and if I remove the instance of SimpleViewModel from my App.xaml then I can add an instance per window by putting an instance into my user control that appears in each secondary window;

<Grid
    Background="Red">
    <Grid.DataContext>
      <local:SimpleViewModel/>
    </Grid.DataContext>

    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
      <TextBlock Text="{Binding Count}"/>
      <TextBlock Text="{Binding ThreadId}"/>
      <Button Content="Increment" Command="{Binding IncrementCommand}"/>
    </StackPanel>
  </Grid>

and that ‘kind of works’ – i.e. I can click the button and increment a count and see it happening in multiple windows;

image

and I think it works in the sense that when I click the button, the dispatcher thread for the window that picks up the event and executes the command code is ultimately the thread that increments the integer and fires the change notification handlers in the method SharedDataValue.OnValueChanged.

But, the handler for that event in SimpleViewModel.OnValueChanged uses the synchronization context (i.e. dispatcher thread) that it captured at construction time from the owning Window to actually fire the INotifyPropertyChanged.PropertyChanged event back into the XAML binding code and so that ends up happening on the ‘right’ thread.

Without the use of the synchronization context, this code blows up in the same way as the first example and, in many ways, I’ve ended up with an implicit lookup table in the sense that each SimpleViewModel instance has a reference to the ‘right’ synchronization context in which to do work on behalf of the consuming View.

This also works ok for a two-way binding I think. If I change my SharedDataValue class such that it can actually set the value rather than just increment it;

using System;

  static class SharedDataValue
  {
    public static event EventHandler ValueChanged;
    public static int Value
    {
      get
      {
        return (_value);
      }
      set
      {
        _value = value;
        var handlers = ValueChanged;
        if (handlers != null)
        {
          handlers(null, EventArgs.Empty);
        }
      }
    }
    static int _value;
  }

and then change the SimpleViewModel that uses it;

 using System;
  using System.Threading;

  class SimpleViewModel : ViewModelBase
  {
    public SimpleViewModel()
    {
      SharedDataValue.ValueChanged += this.OnValueChanged;

      // Capture the sync context that we are created with.
      this.syncContext = SynchronizationContext.Current;
    }
    void OnValueChanged(object sender, EventArgs e)
    {
      this.syncContext.Post(
        _ =>
        {
          base.OnPropertyChanged("Count");
        },
        null);
    }
    public int Count
    {
      get
      {
        return (SharedDataValue.Value);
      }
      set
      {
        SharedDataValue.Value = value;
      }
    }
    public int ThreadId
    {
      get
      {
        return (Environment.CurrentManagedThreadId);
      }
    }
    SynchronizationContext syncContext;
  }

and then change the binding that’s in use;

  <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
      <TextBox Text="{Binding Count,Mode=TwoWay}"/>
      <TextBlock Text="{Binding ThreadId}"/>
    </StackPanel>

then that seems to work as expected.

image

In summary – trying to share a view model across windows here is going to cause me a problem because it means that the thread associated with one Window starts trying to talk directly to UI constructs owned by another Window. Ultimately, I made each window have a separate ViewModel but I also made that ViewModel aware of the synchronization context that it was created with such that it can use it to dispatch work at a later stage. With that in place, both of those view models can pull data from some central, shared “service” (represented by a simple integer value here) which provides data change notifications.

All of that said, I bet it wouldn’t take much additional code here to get me back to a situation where I can get a ‘deadlock avoidance’ type of exception from the runtime Smile