bobah75
bobah75

Reputation: 3570

Catel async await command in ShowDialog - Deadlock

Using a library Сatel latest version (3.8.1 beta).

How can I use TAP method from dialog window?

Example. In main ViewModel calling a method

private bool ShowDialogWindow()
{
    var typeFactory = TypeFactory.Default ;
    var vm = typeFactory.CreateInstanceWithParametersAndAutoCompletion<LoginWindowViewModel>();
    return _uiVisualizerService.ShowDialog(vm) ?? false;
}

In LoginWindowViewModel I have Command (also try AsynchronousCommand) which is called method

public async Task<int> Test(string login, string password)
{
     var a = await Task<int>.Factory.StartNew(() =>
     {
         using (var uow = new UnitOfWork<TSDbContext>())
         {
             var userRep = uow.GetRepository<IUserRepository>();
             userRep.GetAll();
             return 5;
         }
     });
     a++;
     return a;
}

I got the results from awaited method only when close the dialog window. Lock appears on line

var uow = new UnitOfWork()

ConfigureAwait(false) - does not help solve the problem

When I delete UnitOfWork - method works

When I Change Method Code to This var d = TypeFactory.Default.CreateInstanceWithParameters(); return 5;

Blocking is also reproduced on the line TypeFactory...

Depending on the services Catel are not allowed in the dialog box

Upvotes: 2

Views: 1292

Answers (2)

Geert van Horrik
Geert van Horrik

Reputation: 5724

Note: I edited this answer so it contains the answer to this question. The previous answer contained some hints for the topic starter to investigate the issue.

You invoke the command in the constructor of the MainViewModel. Note that we never recommend that you invoke anything in the constructor. We have the Initialize method for that.

The reason is that you construct the MainViewModel with the TypeFactory (Catel does that for you). Then in the same (async) command being execution in that thread, you want to instantiate a UnitOfWork which also wants to instantiate a type via the TypeFactory. This is on a different thread. The TypeFactory is still locked because you are still constructoring the MainViewModel.

Again, Catel provides the Initialize method on the ViewModelBase which is called outside of the creation so it is safe to do anything in there. Please use that instead.

Upvotes: 3

noseratio
noseratio

Reputation: 61744

I think I know what might be the problem here. If my understanding of the problem is correct, the following code reproduces it:

public partial class MainWindow : Window
{
    class Model
    {
        Model() { }

        public Task<int> AsyncTask { get; private set; }

        public static Model Create()
        {
            var model = new Model();
            Func<Task<int>> doTaskAsync = async () =>
            {
                await Task.Delay(1);
                return 42;
            };
            model.AsyncTask = doTaskAsync();
            return model;
        }
    }

    public MainWindow()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        var textBlock = new TextBlock();
        var window = new Window { Content = textBlock };

        window.Loaded += (sIgnore, eIgnore) =>
        {
            // int result = ((Model)window.DataContext).AsyncTask.Result;
            // textBlock.Text = result.ToString();
        };

        window.DataContext = Model.Create();
        window.ShowDialog();

        MessageBox.Show("Result: " + 
            ((Model)window.DataContext).AsyncTask.Result);
    }
}

Uncomment the commented lines, and there will be a deadlock inside window.Loaded event handler, at ((Model)window.DataContext).AsyncTask.Result.

That happens because the window.Loaded is fired synchronously on the same iteration of the Dispatcher message loop which called ShowDialog. The AsyncTask did not have a chance to complete, because the continuation after await Task.Delay(1) is scheduled on the DispatcherSynchronizationContext of the UI thread.

The same AsyncTask.Result code works immediately after ShowDialog. That's because quite a few more iterations of the message loop have been executed (on the dialog's new Dispatcher frame), before the dialog has been closed.

The fix is simple:

window.Loaded += async (sIgnore, eIgnore) =>
{
    int result = await ((Model)window.DataContext).AsyncTask;
    textBlock.Text = result.ToString();
};

This will asynchronously complete the task on the dialog's Dispatcher frame.

I'm not sure how close this is to the OP's scenario, because placing await Task.Delay(1).ConfigureAwait(false) would solve the problem in the above case, as well. Nevertheless, this is as far as I can make guesses based upon the OP's code.

Upvotes: 2

Related Questions