【问题标题】:Populate ObservableCollection with data from awaited Task.Run使用等待的 Task.Run 中的数据填充 ObservableCollection
【发布时间】:2020-05-04 18:53:36
【问题描述】:

我正在制作 Xamarin 应用程序,但在列表视图中显示数据时遇到问题。所以,基本上我有网络服务,我从那里获取数据(那个端点是非异步的),所以我不想阻止我的应用程序的 UI 线程,所以我将对网络服务的调用包装在 Task.Run 中并等待任务。

public class HomeDetailPageViewModel : ViewModelNavigatable
{
    public ObservableCollection<CarViewModel> cars;

    public ObservableCollection<CarViewModel> Cars
    {
        get { return this.cars; }
    }

    public HomeDetailPageViewModel(INavigationService navigationService)
        :base(navigationService)
    {
        this.cars = new ObservableCollection<CarViewModel>();
        AppearingCommand = new AsyncCommand(this.OnAppearingCommandExecuted);
    }

    public IAsyncCommand AppearingCommand { get; set; }


    public async Task OnAppearingCommandExecuted()
    {
        using (UserDialogs.Instance.Loading("Loading..."))
        {
            this.Cars.Clear();
            IEnumerable<Car> carsFromEndpoint = await Task.Run(() => CarEndpoint.GetAll(client.Context)).ConfigureAwait(false);

            Device.BeginInvokeOnMainThread(() =>
            {
                foreach (var carFromEndpoint in carsFromEndpoint.ToList())
                    this.Cars.Add(new CarViewModel
                    {
                        Manufacturer = carFromEndpoint.Code,
                        Model = carFromEndpoint.Name,
                        Price = carFromEndpoint.Price,
                        Year = carFromEndpoint.Year
                    });
            });
        }
    }
}

正如我所说 CarEndpoint.GetAll(client.Context) 是同步端点。如果我使用:

Task.Run(() =&gt; CarEndpoint.GetAll(client.Context)).Result

CarEndpoint.GetAll(client.Context)

一切都按预期工作,但这是不可接受的,因为它会阻塞 UI 线程,直到 Task 完成。我知道使用Task.Run 进行虚假异步调用不是一个好主意,但我没有看到其他保持应用响应的方法,因为我无法更改 Web 服务的端点。

感谢您的回答。 干杯:)

【问题讨论】:

  • 你的问题是什么?

标签: c# xamarin async-await


【解决方案1】:

我知道使用 Task.Run 进行虚假异步调用不是一个好主意

使用Task.Run 解除对 UI 线程的阻塞——即使是伪异步方式——也可以。

我无法更改 Web 服务的端点。

这句话没有多大意义。所有 Web 服务本质上都是异步的。但是,您的 客户端库 可能是严格同步的,在这种情况下,Task.Run 是在调用 UI 时解除阻塞的好方法。但是您将解决客户端库中的限制,而不是 Web 服务本身。

我在列表视图中显示数据时遇到问题。

您的问题可能是由于IEnumerable&lt;T&gt; 而不是Task.Run。这是我期望的一些代码;关键是将ToList inside 移动到Task.Run 代表:

public async Task OnAppearingCommandExecuted()
{
  using (UserDialogs.Instance.Loading("Loading..."))
  {
    this.Cars.Clear();
    List<Car> carsFromEndpoint = await Task.Run(() => CarEndpoint.GetAll(client.Context).ToList());

    foreach (var carFromEndpoint in carsFromEndpoint)
      this.Cars.Add(new CarViewModel
      {
        Manufacturer = carFromEndpoint.Code,
        Model = carFromEndpoint.Name,
        Price = carFromEndpoint.Price,
        Year = carFromEndpoint.Year
      });
  }
}

注意事项:

  • 如果您删除了ConfigureAwait(false),则不需要BeginInvokeOnMainThreadawait 为我们在 UI 线程上恢复。事实上,BeginInvokeOnMainThread 是一种代码味道。
  • 您可能不希望在这里使用异步命令;你只想asynchronously load data

【讨论】:

  • BeginInvokeOnMainThread 是不必要的;等待在我们的 UI 线程上恢复。事实上,BeginInvokeOnMainThread 是一种代码味道。 使用 ConfigureAwait(false) 它不会在 UI 线程上恢复。
【解决方案2】:

当您在 MVVM 中使用 Async 命令时,主要问题是 NotSupportedException 在修改 ObservableCollection 时。如果您注意并发性,其余所有内容都不会引起任何问题。

这是一个代表ObservableCollection 的类,供多个线程使用,它将所有操作转发到构造它的线程的SynchronizationsContext

用它代替ObservableCollection(不是我的,从GitHub抓来的)

public class AsyncObservableCollection<T> : ObservableCollection<T>
{
    private readonly SynchronizationContext _synchronizationContext = SynchronizationContext.Current;

    public AsyncObservableCollection() : base() { }
    public AsyncObservableCollection(IEnumerable<T> collection) : base(collection) { }
    public AsyncObservableCollection(List<T> list) : base(list) { }

    private void ExecuteOnSyncContext(Action action)
    {
        if (SynchronizationContext.Current == _synchronizationContext)
            action();
        else
            _synchronizationContext.Send(_ => action(), null);
    }

    protected override void InsertItem(int index, T item) => ExecuteOnSyncContext(() => base.InsertItem(index, item));
    protected override void RemoveItem(int index) => ExecuteOnSyncContext(() => base.RemoveItem(index));
    protected override void SetItem(int index, T item) => ExecuteOnSyncContext(() => base.SetItem(index, item));
    protected override void MoveItem(int oldIndex, int newIndex) => ExecuteOnSyncContext(() => base.MoveItem(oldIndex, newIndex));
    protected override void ClearItems() => ExecuteOnSyncContext(() => base.ClearItems());
}

我在 StackOverflow (Russian Community) 的帮助下创建了这个 AsyncRelayCommand 类。它不会冻结任何东西。

public interface IAsyncCommand : ICommand
{
    Task ExecuteAsync(object param);
}

public class AsyncRelayCommand : IAsyncCommand
{
    private bool _isExecuting;
    private readonly SynchronizationContext _context;
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public AsyncRelayCommand(Action<object> execute, Predicate<object> canExecute = null) 
        => (_execute, _canExecute, _context) = (execute, canExecute, SynchronizationContext.Current);

    private void InvalidateRequerySuggested()
    {
        if (_context.Equals(SynchronizationContext.Current))
            CommandManager.InvalidateRequerySuggested();
        else
            _context.Send(_ => CommandManager.InvalidateRequerySuggested(), null);
    }

    public bool CanExecute(object parameter) => !_isExecuting && (_canExecute == null || _canExecute(parameter));

    public async Task ExecuteAsync(object parameter)
    {
        if (CanExecute(parameter))
        {
            try
            {
                _isExecuting = true;
                InvalidateRequerySuggested();
                await Task.Run(() => _execute(parameter));
            }
            finally
            {
                _isExecuting = false;
                InvalidateRequerySuggested();
            }
        }
    }

    public void Execute(object parameter) => _ = ExecuteAsync(parameter);
}

this article 中的常规RelayCommand 类一样使用。

private IAsyncCommand _myAsyncCommand;

// "lazy" instantiation with single instance
public IAsyncCommand MyAsyncCommand => _myAsyncCommand ?? (_myAsyncCommand = new AsyncRelayCommand(parameter =>
{

}));
<Button Content="Click me!" Command="{Binding MyAsyncCommand}" />

还支持CommandParameter

有了这个,您不需要将收集更改调用推送到 UI 线程中,就像在通常的同步代码中一样使用它。并且Thread.Sleep() 或命令中的繁重工作不会冻结 UI,因为它将在单独的线程上运行。

与您的代码一起使用

private IAsyncCommand _appearingCommand;
public AsyncObservableCollection<CarViewModel> cars; // are you sure that it must be public?

public HomeDetailPageViewModel(INavigationService navigationService)
        :base(navigationService)
{
    this.cars = new AsyncObservableCollection<CarViewModel>();
}

public AsyncObservableCollection<CarViewModel> Cars
{
    get => this.cars;
}

public IAsyncCommand AppearingCommand => _appearingCommand ?? (_appearingCommand = new AsyncRelayCommand(parameter =>
{
    // edit: fixed regarding to the accepted answer
    List<Car> carsFromEndpoint = CarEndpoint.GetAll(client.Context).ToList();
    foreach (var carFromEndpoint in carsFromEndpoint)
        this.Cars.Add(new CarViewModel
        {
            Manufacturer = carFromEndpoint.Code,
            Model = carFromEndpoint.Name,
            Price = carFromEndpoint.Price,
            Year = carFromEndpoint.Year
        });
}));

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-16
    • 2021-04-10
    • 1970-01-01
    • 2013-06-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多