Заполните ObservableCollection данными из ожидаемого Task.Run - PullRequest
1 голос
/ 04 мая 2020

Я делаю приложение Xamarin и у меня проблема с отображением данных в моем списке. Итак, в основном у меня есть веб-сервис, из которого я получаю данные (эта конечная точка не асинхронная c), поэтому я не хочу блокировать поток пользовательского интерфейса моего приложения, поэтому я заключаю вызов веб-сервиса в 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(() => CarEndpoint.GetAll(client.Context)).Result или

CarEndpoint.GetAll(client.Context)

все работает как положено, но это неприемлемо, потому что это поток пользовательского интерфейса блока до завершения задачи. Я знаю, что не рекомендуется использовать Task.Run для выполнения ложных асинхронных вызовов, но я не вижу другого способа обеспечить отзывчивость приложения, поскольку не могу изменить конечную точку веб-службы.

Спасибо за ответ. Ура:)

Ответы [ 2 ]

3 голосов
/ 05 мая 2020

Я знаю, что не рекомендуется использовать Task.Run для выполнения ложных асинхронных вызовов

Использование Task.Run для разблокировки потока пользовательского интерфейса - даже в искусственном асинхронном режиме - хорошо.

Я не могу изменить конечную точку веб-службы.

Это предложение не имеет особого смысла. Все веб-сервисы по своей природе асинхронны. Однако возможно, что ваша клиентская библиотека строго синхронна, и в этом случае Task.Run - это прекрасный способ разблокировать пользовательский интерфейс при вызове. Но вы будете работать с ограничением в библиотеке на стороне клиента, а не с самим веб-сервисом.

У меня проблема с отображением данных в виде списка.

Ваша проблема, вероятно, связана с IEnumerable<T>, а не Task.Run. Вот некоторый код, который я ожидаю, будет работать; ключ в том, чтобы переместить ToList внутри делегата 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
      });
  }
}

Примечания:

  • BeginInvokeOnMainThread не требуется, если вы удалите ConfigureAwait(false); await возобновляется в потоке пользовательского интерфейса для нас. На самом деле, BeginInvokeOnMainThread - это запах кода.
  • Возможно, вам здесь не нужна асинхронная команда; вы просто хотите асинхронно загрузить данные .
1 голос
/ 04 мая 2020

Когда вы используете команды Asyn c в MVVM, основной проблемой является 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());
}

И этот AsyncRelayCommand класс с помощью StackOverflow ( Русское сообщество ). Он ничего не замораживает.

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);
}

Использование как с обычным 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 is также поддерживается.

При этом вам не нужно помещать вызовы изменения коллекции в поток пользовательского интерфейса, используйте его, как в обычном коде syn c. И Thread.Sleep() или тяжелая работа в команде не замораживает пользовательский интерфейс, потому что он будет работать в отдельном потоке.

Использование с вашим кодом

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
        });
}));
...