10
\$\begingroup\$

I have an application where I use Task to run an operation. During the operation, there can be a problem which will need some user interaction (continue or not). I need to bubble up a messagebox and get the user result back to the operation thread.

This is how I got it working:

public interface IPlugin
{
    void ConnectAndProduce(Func<string, bool> retryRequest);
}

public partial class MainWindow : MetroWindow
{
    [ImportMany(typeof(IPlugin))]
    private IEnumerable<IPlugin> Plugins;

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        Func<string, bool> func = message =>
        {
            var result = Application.Current.Dispatcher.Invoke(new Func<Task<MessageDialogResult>>(async () =>
                {
                    var mySettings = new MetroDialogSettings()
                    {
                        AffirmativeButtonText = "YES",
                        NegativeButtonText = "NO",
                        ColorScheme = MetroDialogColorScheme.Accented
                    };

                    return await this.ShowMessageAsync("Hello!", message, MessageDialogStyle.AffirmativeAndNegative, mySettings);
                }));

            if ((result as Task<MessageDialogResult>).Result != MessageDialogResult.Negative)
                return true;

            return false;
        };

        ImportPlugins();
        var plugin = Plugins.FirstOrDefault();

        await Task.Run(() =>
        {
            plugin.ConnectAndProduce(func);
        });
    }

    public void ImportPlugins()
    {
        var catalog = new AggregateCatalog();
        catalog.Catalogs.Add(new DirectoryCatalog(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)));
        CompositionContainer container = new CompositionContainer(catalog);
        container.ComposeParts(this);
    }
}

And here is the Plugin what is going to do all the work:

[Export(typeof(IPlugin))]
public class Machine : IPlugin
{
    public void ConnectAndProduce(Func<string, bool> retryRequest)
    {
        var connection = MachineConnection();

        var retry = true;
        var retryCount = 0;
        while (retry)
        {
            if (connection != true)
                retry = retryRequest("Do you want to retry?");

            if (retryCount < 3)
                retryCount++;
            else
                retry = false;
        }

        connection.Produce();
    }
}

I use MEF to load the plugin and do all the work in non-UI thread by calling the plugin method in a new Task. Once the plugin has called MachineConnect(), the connection fails and I can't just stop the plugins from working and call it again. I need to continue, and that's why the plugin has to ask if I want to retry.

Is there any better way doing the retry request from plugin to UI thread? This looks very 'hacky' to me.

\$\endgroup\$
7
\$\begingroup\$

You're scheduling a new non-UI operation that immediately goes back and schedules an operation to run in the UI thread, which then does some work.

Just avoid all of that, and do the work that you want to do right from the start, since you're in the UI thread:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    var mySettings = new MetroDialogSettings()
    {
        AffirmativeButtonText = "YES",
        NegativeButtonText = "NO",
        ColorScheme = MetroDialogColorScheme.Accented
    };

    var response = await this.ShowMessageAsync("Hello!", "message", MessageDialogStyle.AffirmativeAndNegative, mySettings);

    if(response == MessageDialogResult.Negative)
        return;
}
|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ This code is simplified example, in reality the non-UI operation is longer and wont call func() straight away. \$\endgroup\$ – hs2d Feb 9 '15 at 18:59
  • 2
    \$\begingroup\$ @hs2d If you have non-UI work to do, then wrap that non-UI work in a call to Task.Run, let it compute its result, and await it. If you have some non-UI work to do after this UI code, then do the same. The whole design of the await model is to avoid the need to explicitly marshal to the UI thread, and to let it happen implicitly when calling await. This happens naturally so long as you separate your UI from your non-UI operations. \$\endgroup\$ – Servy Feb 9 '15 at 19:00
  • \$\begingroup\$ The non-UI work is a plugin what connects to external hardware, the func Func is called when it fails at some operation. I cant just close the connection and return to the UI thread and start the whole non-UI work again. func Func is ment to be a retry request to try something again by the plugin. \$\endgroup\$ – hs2d Feb 9 '15 at 19:11
  • 1
    \$\begingroup\$ @hs2d If you really need to you can wrap this code in a method and then call that method with Task.Run passing in the UI synchronization context as the body of that callback, but it's a pretty strong sign that something's wrong with the design of the program. \$\endgroup\$ – Servy Feb 9 '15 at 19:17
  • 1
    \$\begingroup\$ Moderator note: - this answer was migrated from Stack Overflow, and addresses a version of the code that is a simplification of the now-expanded code in the question. The concepts suggested are still relevant even if the code is not an exact 1-to-1 match. \$\endgroup\$ – rolfl Feb 11 '15 at 12:45
6
\$\begingroup\$

There is a lot of play here on booleans and if statements. my suggestion is that you simply return the boolean statements, because everything is returning a true or false.

try something like this

private async void Button_Click(object sender, RoutedEventArgs e)
{
    Func<string, bool> func = message =>
    {
        var result = Application.Current.Dispatcher.Invoke(new Func<Task<MessageDialogResult>>(async () => 
        {
            var mySettings = new MetroDialogSettings()
            {
                AffirmativeButtonText = "YES",
                NegativeButtonText = "NO",
                ColorScheme = MetroDialogColorScheme.Accented
            };

            return await this.ShowMessageAsync("Hello!", message, MessageDialogStyle.AffirmativeAndNegative, mySettings);
        }));

        return (result as Task<MessageDialogResult>).Result != MessageDialogResult.Negative
    };

    await Task.Factory.StartNew(() => 
    {
        return !func("message");
    });
}

Just return the product of the boolean statement.


With this piece of code I was wondering why you didn't use MessageDialogResult.Affirmative

return (result as Task<MessageDialogResult>).Result != MessageDialogResult.Negative

it should be something like this

return (result as Task<MessageDialogResult>).Result = MessageDialogResult.Affirmative

With this piece of code

public void ConnectAndProduce(Func<string, bool> retryRequest)
{
    var connection = MachineConnection();

    var retry = true;
    var retryCount = 0;
    while (retry)
    {
        if (connection != true)
            retry = retryRequest("Do you want to retry?");

        if (retryCount < 3)
            retryCount++;
        else
            retry = false;
    }

    connection.Produce();
}

I would change the while loop conditional statement to eliminate some of your if statements, we can move the retryRequest("Do you want to retry?") into the conditional of the if statement and break out of the loop.

it also looks to me like MachineConnection() returns a boolean so let's skip creating a variable that we don't have to.

Here is what I came up with.

public void ConnectAndProduce(Func<string, bool> retryRequest)
{
    var retryCount = 0;
    while (retryCount < 3)
    {
        if (MachineConnection() != true && retryRequest("Do you want to retry?"))
        {
            break;
        }
        retryCount++;
    }
    connection.Produce();
}
|improve this answer|||||
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.