Search code examples
c#windowsmultithreadinguwpmultiple-views

Raising and handling events in a multi window UWP app


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?


Solution

  • 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.