Заполните ObservableCollection результатами нескольких асинхронных вызовов без ожидания - PullRequest
0 голосов
/ 08 ноября 2018

В моем модуле Prism в классе ViewModel в методе OnNavigatedTo

Я хотел бы заполнить ObservableCollection результатами нескольких асинхронных вызовов, не дожидаясь завершения всех вызовов .

Я использую ответ на этот вопрос: Как гидрировать словарь с результатами асинхронных вызовов?

Следующий код является очищенной версией моего реального кода:

Мой статус класс:

public class Status
{
    public string ipAddress;
    public string status;
}

Моя модель просмотра:

using Prism.Mvvm;
using Prism.Regions;

public class StatusViewModel : BindableBase, INavigationAware
{
    ObservableCollection<Status> statusCollection = new ObservableCollection<Status>();
    HttpClient httpClient = new HttpClient();
    Ping ping = new Ping();
    List<string> ipAddressList = new List<string>();

    public void OnNavigatedTo(NavigationContext navigationContext)
    {
        GetEveryStatus();
    }

    public void GetEveryIp() // this is not important, works ok
    {
        var addressBytes = Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork).GetAddressBytes();
        for (byte i = 1; i < 255; ++i)
        {
            addressBytes[3] = i;
            string ipAddress = new IPAddress(addressBytes).ToString();
            if (ping.Send(ipAddress, 10).Status == IPStatus.Success)
            {
                ipAddressList.Add(ipAddress);
            }
        }
    }

    public void GetEveryStatus() // this is important, here is the problem
    {
        GetEveryIp();
        var task = GetStatusArray(ipAddressList);
        statusCollection.AddRange(task.Result);
    }

    // solution from stackoverflow, but it throws exception
    public async Task<Status[]> GetStatusArray(List<string> ipAddressList)
    {
        Status[] statusArray = await Task.WhenAll(
            ipAddressList.Select(
                async ipAddress => new Status(
                    ipAddress,
                    await httpClient.GetStringAsync("http://" + ipAddress + ":8080" + "/status")
                    )
                )
            );

        return statusArray;
    }
}

но это не сработало, потому что GetStringAsync может выдать исключение, поэтому я изменил его на следующее:

    public void GetEveryStatus() // this is important, here is the problem
    {
        GetEveryIp();
        foreach (string ipAddress in ipAddressList)
        {
            try
            {
                var task = httpClient.GetStringAsync("http://" + ipAddress + ":8080" + "/status");
                statusCollection.Add(new Status(ipAddress, task.Result));
            }
            catch (Exception)
            {
            }
        }
    }

но все равно не работает.

Как правильно это сделать? Спасибо!

1 Ответ

0 голосов
/ 09 ноября 2018

Спасибо @AccessDenied за объяснение роли async в реализации интерфейса.

Спасибо @Selvin за объяснение Task.Result и Task.Wait.

Если кто-то заинтересован в окончательном решении, вот оно:

PositioningModule - аппаратное устройство, этот класс не имеет ничего общего с Prism.Modularity.IModule

public class PositioningModule
{
    public string IpAddress { get; set; }
    public PositioningModuleStatus PositioningModuleStatus { get; set; }

    public PositioningModule(string ipAddress, PositioningModuleStatus positioningModuleStatus)
    {
        IpAddress = ipAddress;
        PositioningModuleStatus = positioningModuleStatus;
    }
}

ViewModel:

Мне пришлось использовать BindingOperations.EnableCollectionSynchronization и lock на ObservableCollection. Это основная причина, по которой он не работал async раньше!

Изменение OnNavigatedTo на async заблокировало пользовательский интерфейс, поэтому я использовал Task.Run().

using Prism.Mvvm;
using Prism.Regions;

public class DomePositioningViewModel : BindableBase, INavigationAware
{
    ObservableCollection<PositioningModule> _positioningModuleCollection = new ObservableCollection<PositioningModule>();
    readonly object _lock = new object();

    DomePositioningModel _domePositioningModel = new DomePositioningModel();

    public DomePositioningViewModel()
    {
        BindingOperations.EnableCollectionSynchronization(_positioningModuleCollection, _lock);
    }

    public /* async */ void OnNavigatedTo(NavigationContext navigationContext)
    {
        //await _domePositioningModel.ScanForModulesAsync(AddModule); - this blocks the UI

        Task.Run(() => _domePositioningModel.ScanForModulesAsync(AddModule));
    }

    private void AddModule(PositioningModule module)
    {
        lock (_lock)
        {
            _positioningModuleCollection.Add(module);
        }
    }
}

Модель:

Я изменил Send на SendPingAsync, и мне пришлось использовать new Ping() вместо ping.

Использование Select вместо foreach для параллельных вызовов сделало все намного быстрее!

#define PARALLEL

public class DomePositioningModel
{
    private readonly HttpClient _httpClient = new HttpClient();

    public DomePositioningModel()
    {
        _httpClient.Timeout = TimeSpan.FromMilliseconds(50);
    }

    public async Task ScanForModulesAsync(Action<PositioningModule> AddModule)
    {
        List<string> ipAddressList = new List<string>();

        var addressBytes = Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork).GetAddressBytes();

        for (addressBytes[3] = 1; addressBytes[3] < 255; ++addressBytes[3])
        {
            ipAddressList.Add(new IPAddress(addressBytes).ToString());
        }

        //Ping ping = new Ping(); - this behaves strangely, use "new Ping()" instead of "ping"

#if PARALLEL
        var tasks = ipAddressList.Select(async ipAddress => // much faster
#else
        foreach (string ipAddress in ipAddressList) // much slower
#endif
        {
            PingReply pingReply = await new Ping().SendPingAsync(ipAddress, 10); // use "new Ping()" instead of "ping"

            if (pingReply.Status == IPStatus.Success)
            {
                try
                {
                    string status = await _httpClient.GetStringAsync("http://" + ipAddress + ":8080" + "/status");

                    if (Enum.TryParse(status, true, out PositioningModuleStatus positioningModuleStatus))
                    {
                        AddModule?.Invoke(new PositioningModule(ipAddress, positioningModuleStatus));
                    }
                }
                catch (TaskCanceledException) // timeout
                {
                }
                catch (HttpRequestException) // could not reach IP
                {
                }
                catch (Exception ex)
                {
                    System.Windows.MessageBox.Show(ex.Message);
                }
            }
        }
#if PARALLEL
        );
        await Task.WhenAll(tasks);
#endif
    }
}

Он не тестировался, потому что разница настолько очевидна - около 0,5 с вместо 14 с!

...