首页 > 解决方案 > 使用异步命令 MVVM WPF 卡住并且 UI 被冻结

问题描述

我使用了以下视图模型MainWindow.xaml,该视图模型称为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;
      });
    }
}

并且AsynCommand来自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
}

我遇到的问题是,按下按钮后Add,执行最后一行data.IsConnected = true;,然后什么也没有发生,这意味着 UI 被冻结并且 UI 数据网格中没有添加任何项目。

我也删除了部分_dispatcher.Invoke(() => Log.Add(s),同样的问题,UI 冻结。

为什么 ?我的错误在哪里?似乎问题出在await CreateConnection(data)

标签: c#wpf

解决方案


您的示例代码既不是可编译的也不是最小的Execute,但我可以发现您的命令方法中的一个缺陷:

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

调用Result一个Task可能的死锁是一个很大的禁忌,尤其是在 GUI 应用程序中。尝试触发Task然后从该方法返回:

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

推荐阅读