Search code examples
c#mvvmasync-awaitcaliburn.microeventaggregator

Caliburn.Micro EventAggregator.PublishOnUIThreadAsync "The tasks argument included a null value. Parameter name: tasks"


I'm trying to make use of the EventAggregators for Cross-VM communication.

In my ChildViewModel I do something like that:

public async void ThisMethodIsCalledByUI()
{
    // ShowMessageEvent is a simple class with with only 1 string property and a MessageDialogResult enum
    ShowMessageEvent msg = new ShowMessageEvent("This is the message from ChildVM");

    // doing this works, but MessageDialogResult will be false below since no await happens
    //_eventAggregator.PublishOnUIThreadAsync(msg);

    // doing this triggers exception
    // however msg.DialogResult is already Affirmative by the time 
    // the exception is thrown, so it's almost as intended here
    await _eventAggregator.PublishOnUIThreadAsync(msg);

    // expected: Affirmative
    Debug.WriteLine(msg.DialogResult);

}

However I need to await since I want to make use of the MessageDialogResult. In the MainViewModel:

public class MainViewModel : Conductor<IScreen>.Collection.OneActive, IHandle<object>
{
    private readonly IEventAggregator _eventAggregator;

    public MainViewModel()
    {
        _eventAggregator = new EventAggregator();
        _events.SubscribeOnPublishedThread(this);
    }

    public async Task HandleAsync(object message, CancellationToken cancellationToken)
    {
        if (message is ShowMessageEvent msg)
        {
            // Simulating work, show dialog, etc.
            await Task.Delay(500);
            // Implementation irrelevant, assume the dialog returns Affirmative
            msg.DialogResult = await _dialogs.ShowMessageAsync(msg.Message);

        }
    }
}

So the goal is to inform the MainViewModel to show a dialog, then await and retrieve the dialog result in the ChildViewModel before continuing. Yes, I could call the dialog from ChildViewModel directly as a workaround, but that's not the point, I want to avoid that.

My implementation basically already does everything as expected, the only thing I need to resolve is the thrown exception.

Anyone has an idea what I'm doing wrong?

EDIT

I can actually make it work like this:

public async void ThisMethodIsCalledByUI()
{
    // ShowMessageEvent is a simple class with with only 1 string property and a MessageDialogResult enum
    ShowMessageEvent msg = new ShowMessageEvent("This is the message from ChildVM");

    try
    {
        await _eventAggregator.PublishOnUIThreadAsync(msg);
    }
    // suppress the problematic exception
    catch (ArgumentException e)
    {
        Console.WriteLine(e);
    }

    // works as expected: Affirmative
    Debug.WriteLine(msg.DialogResult);

}

Though this is working I'd prefer a design without the need to suppress exceptions...


Solution

  • For now I ended up with my own extension implementation:

    /// <summary>
    /// EventAggregatorExtensions
    /// </summary>
    public static class EventAggregatorExtensions
    {
        /// <summary>
        /// Publishes a message to the UI thread 
        /// </summary>
        /// <param name="eventAggregator"></param>
        /// <param name="message"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public static async Task PublishOnUIThreadAsyncCustom(this IEventAggregator eventAggregator, object message,
                                                  CancellationToken cancellationToken)
        {
            try
            {
                await eventAggregator.PublishOnUIThreadAsync(message, cancellationToken);
            }
            // Suppress a specific exception that is always caused when above line is awaited
            catch (ArgumentException e)
            {
                if (!e.Message.Contains("The tasks argument included a null value.") | !e.ParamName.Equals("tasks"))
                {
                    throw;
                }
            }
        }
    
        /// <summary>
        /// Publishes a message to the UI thread 
        /// </summary>
        /// <param name="eventAggregator"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public static async Task PublishOnUIThreadAsyncCustom(this IEventAggregator eventAggregator, object message)
        {
            await PublishOnUIThreadAsyncCustom(eventAggregator, message, CancellationToken.None);
        }
    }