2

I created some test code so I could try and figure out how to use multiple windows in UWP properly. I wanted to see if I could fire an event and have multiple windows update their UI in the event handler. I finally got something working but I'm not entirely sure why it works.

Here's the class that's being created in my Page

public class NumberCruncher
{
    private static Dictionary<int, Tuple<CoreDispatcher, NumberCruncher>> StaticDispatchers { get; set; }

    static NumberCruncher()
    {
        StaticDispatchers = new Dictionary<int, Tuple<CoreDispatcher, NumberCruncher>>();
    }

    public NumberCruncher()
    {
    }

    public event EventHandler<NumberEventArgs> NumberEvent;

    public static void Register(int id, CoreDispatcher dispatcher, NumberCruncher numberCruncher)
    {
        StaticDispatchers.Add(id, new Tuple<CoreDispatcher, NumberCruncher>(dispatcher, numberCruncher));
    }

    public async Task SendInNumber(int id, int value)
    {
        foreach (var dispatcher in StaticDispatchers)
        {
            await dispatcher.Value.Item1.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                Debug.WriteLine($"invoking {dispatcher.Key}");
                dispatcher.Value.Item2.NumberEvent?.Invoke(null, new NumberEventArgs(id, value));
            });
        }
    }
}

And here's the relevant part of my MainPage code

    NumberCruncher numberCruncher;

    public MainPage()
    {
        this.InitializeComponent();
        numberCruncher = new NumberCruncher();
        numberCruncher.NumberEvent += NumberCruncher_NumberEvent;
    }

    private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
    {
        listView.Items.Add($"{e.Id} sent {e.Number}");
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        NumberCruncher.Register(ApplicationView.GetForCurrentView().Id, Window.Current.Dispatcher, numberCruncher);
    }

I have a button that creates new views of the MainPage. Then I have another button that calls the SendInNumber() method.

When I navigate to the MainPage I register the Dispatcher for the window and the instance of NumberCruncher. Then when firing the event I use the NumberCruncher EventHandler for that specific Dispatcher.

This works without throwing marshaling exceptions. If I try to use the current class's EventHandler

await dispatcher.Value.Item1.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            Debug.WriteLine($"invoking {dispatcher.Key}");
            NumberEvent?.Invoke(null, new NumberEventArgs(id, value));
        });

I get a marshaling exception when trying to add the item to the listView. However if I maintain the SynchronizationContext in my MainPage and then use SynchronizationContext.Post to update the listView. It works fine

    SynchronizationContext synchronizationContext;

    public MainPage()
    {
        this.InitializeComponent();
        numberCruncher = new NumberCruncher();
        numberCruncher.NumberEvent += NumberCruncher_NumberEvent;
        synchronizationContext = SynchronizationContext.Current;
    }

    private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
    {
        synchronizationContext.Post(_ =>
        {
            listView.Items.Add($"{e.Id} sent {e.Number}");
        }, null);
    }

However this does not work and throws a marshaling exception when trying to update listView.

private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
    {
        await CoreApplication.GetCurrentView().CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            listView.Items.Add($"{e.Id} sent {e.Number}");
        });
    }

What is going on here?

golf1052
  • 23
  • 1
  • 5
  • 1
    GetCurrentView returns a pointer to the active window, not necessarily the window that contains the listView instance you are trying to update. So I think you end up on the wrong UI thread (as each window has its own UI thread). Have you tried using "listView.Dispatcher" instead? That should marshal the call onto the right UI thread for this instance of listView. – Stefan Wick MSFT Jan 07 '18 at 03:10
  • The docs say that GetCurrentView returns the active view but I set up a thread that prints the status of ApplicationView.GetForCurrentView().Id and CoreApplication.GetCurrentView().IsMain to the debug console. When I have 2 windows I get an output that is different per window. This leads me to believe GetCurrentView() returns the view for that UI thread, not the active window. – golf1052 Jan 08 '18 at 03:53
  • Yes, but does your event fire on the UI thread? If it does, then you don't need the dispatcher in the first place. The fact that you seem to need it indicates that the event doesn't fire on the UI thread that corresponds to the ListView that you are trying to update. What happens if you just do "listView.Dispatcher.RunAsync"? (instead of CoreApp.GetCurrentView().CoreWin.Dispatcher.RunAsync) – Stefan Wick MSFT Jan 08 '18 at 15:58
  • if I use the listView Dispatcher I get a message in my listView for each window I have open so i would see `-23473 sent 123` on window -23473 twice for two windows open. – golf1052 Jan 09 '18 at 16:31

1 Answers1

1

Important thing to remember is that when an event is fired, the subscribed methods are called on the same thread as the Invoke method.

If I try to use the current class's EventHandler, I get a marshaling exception when trying to add the item to the listView.

This first error happens because you are trying to fire the event of current class on another Window's dispatcher (dispatcher.Value.Item1). Let's say the event is on Window 1 and the dispatcher.Value.Item1 belongs to Window 2. Once inside the Dispatcher block, you are or the UI thread of Window 2 and firing the NumberEvent of Window 1's NumberCruncher will run the Window 1's handler on the Window 2's UI thread and that causes the exception.

However this does not work and throws a marshaling exception when trying to update listView.

The GetCurrentView() method returns the currently active view. So whichever application view is active at that moment will be the one returned. In your case it will be the one on which you clicked the button. In case you call the Invoke method on the target Window's UI thread, you don't need any additional code inside the NumberEvent handler.

Martin Zikmund
  • 38,440
  • 7
  • 70
  • 91
  • This is what I thought was happening. I'm also guessing that storing the SynchronizationContext and then using that to update the UI works because I am using the UI thread to update the UI. – golf1052 Jan 08 '18 at 03:56
  • The reason the `SynchronizationContext` works is that you store it in the page constructor which has to run on the UI thread, so the context is associated with the correct UI thread :-) – Martin Zikmund Jan 08 '18 at 08:33