Snake Eyes
Snake Eyes

Reputation: 16764

Using async command MVVM WPF get stuck and UI is frozen

I have the following view model used in MainWindow.xaml, the view model is called MainViewModel:

public abstract class AbstractPropNotifier : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

public sealed class MainViewModel : AbstractPropNotifier
{
    private bool _editEnabled;
    private bool _deleteEnabled;
    private ICommand _editCommand;
    private ICommand _deleteCommand;
    private IRssViewModel _selectedIrssi;
    private IAsyncCommand _addCommand;

    private readonly Dispatcher _dispatcher;

    public MainViewModel(Dispatcher dispatcher)
    {
      _dispatcher = dispatcher;

      IrssItems = new ObservableCollection<IRssViewModel>();
      Log = new ObservableCollection<string>();
      EditEnabled = false;
      DeleteEnabled = false;

      EditCommand = new RelayCommand(c => EditItem(), p => EditEnabled);
      DeleteCommand = new RelayCommand(DeleteItems, p => DeleteEnabled);
      AddCommand = new AsyncCommand(AddItem, () => true);
    }

    public ObservableCollection<IRssViewModel> IrssItems { get; set; }

    public IRssViewModel SelectedIrssi
    {
      get
      {
        return _selectedIrssi;
      }
      set
      {
        _selectedIrssi = value;
        OnPropertyChanged(nameof(SelectedIrssi));
        EditEnabled = DeleteEnabled = true;
      }
    }

    public ObservableCollection<string> Log { get; set; }

    public bool EditEnabled
    {
      get
      {
        return _editEnabled;
      }
      set
      {
        _editEnabled = value || SelectedIrssi != null;
        OnPropertyChanged(nameof(EditEnabled));
      }
    }

    public bool DeleteEnabled
    {
      get
      {
        return _deleteEnabled;
      }
      set
      {
        _deleteEnabled = value || SelectedIrssi != null;
        OnPropertyChanged(nameof(DeleteEnabled));
      }
    }

    public ICommand EditCommand
    {
      get
      {
        return _editCommand;
      }
      set
      {
        _editCommand = value;
      }
    }

    public ICommand DeleteCommand
    {
      get
      {
        return _deleteCommand;
      }
      set
      {
        _deleteCommand = value;
      }
    }

    public IAsyncCommand AddCommand
    {
      get
      {
        return _addCommand;
      }
      set
      {
        _addCommand = value;
      }
    }

    private void EditItem()
    {

    }

    private void DeleteItems(object selectedItems)
    {
      var list = selectedItems as IList;
      var newList = new List<IRssViewModel>(list.Cast<IRssViewModel>());

      if (MessageBox.Show($"Are you sure that want to delete {newList.Count} item{(newList.Count > 1 ? "s" : "")} ?", "Deletion", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
      {

        foreach (var item in newList)
        {
          IrssItems.Remove(item as IRssViewModel);
        }

        EditEnabled = DeleteEnabled = false;
      }
    }

    private async Task AddItem()
    {
      var win = new ManageIrssi("Add item");
      var result = win.ShowDialog();

      if (result.HasValue && result.Value)
      {
        foreach (var data in win.Model.Items)
        {
          //check stuff

          IrssItems.Add(data);
          await CreateConnection(data);
        }
      }
    }

    private async Task CreateConnection(IRssViewModel data)
    {
      await Task.Run(() =>
      {
        IrcManager manager = new IrcManager(new CustomLogger(), data);
        manager.Build(s => _dispatcher.Invoke(() => Log.Add(s)));

        data.IsConnected = true;
      });
    }
}

and AsynCommand is got from https://johnthiriet.com/mvvm-going-async-with-async-command/

public class AsyncCommand : IAsyncCommand
{
    public event EventHandler CanExecuteChanged;

    private bool _isExecuting;
    private readonly Func<Task> _execute;
    private readonly Func<bool> _canExecute;
    private readonly IErrorHandler _errorHandler;

    public AsyncCommand(
        Func<Task> execute,
        Func<bool> canExecute = null,
        IErrorHandler errorHandler = null)
    {
      _execute = execute;
      _canExecute = canExecute;
      _errorHandler = errorHandler;
    }

    public bool CanExecute()
    {
      return !_isExecuting && (_canExecute?.Invoke() ?? true);
    }

    public async Task ExecuteAsync()
    {
      if (CanExecute())
      {
        try
        {
          _isExecuting = true;
          await _execute();
        }
        finally
        {
          _isExecuting = false;
        }
      }

      RaiseCanExecuteChanged();
    }

    public void RaiseCanExecuteChanged()
    {
      CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

    #region Explicit implementations
    bool ICommand.CanExecute(object parameter)
    {
      return CanExecute();
    }

    void ICommand.Execute(object parameter)
    {
      ExecuteAsync().GetAwaiter().GetResult();
    }
    #endregion
}

The problem I met is that After press a button Add, the last line data.IsConnected = true; is executed and then nothing happens means UI is frozen and no item is added in UI datagrid.

I removed also part _dispatcher.Invoke(() => Log.Add(s), same issue, UI frozen.

Why ? Where is my mistake ? Seems the problem is in await CreateConnection(data)

Upvotes: 1

Views: 2198

Answers (2)

mm8
mm8

Reputation: 169400

Your sample code is neither compilable or minimal, but I can spot a flaw in the Execute method of your command:

void ICommand.Execute(object parameter)
{
  ExecuteAsync().GetAwaiter().GetResult();
}

Calling Result on a Task may deadlock and is a big no-no, especially in GUI applications. Try to fire away the Task and then return from the method:

async void ICommand.Execute(object parameter)
{
    await ExecuteAsync().ConfigureAwait(false);
}

Upvotes: 2

Muds
Muds

Reputation: 4116

Problem is AddItem is on UI thread and since it is Awaits on UI Thread, your UI stalls. Take AddItem on new thread and release UI thread, dispatch it to main thread once it is complete and update the UI

Upvotes: 0

Related Questions