用多个异步调用的结果填充 ObservableCollection,无需等待

Fill ObservableCollection with results of multiple async calls without waiting

在我的 ViewModel 中的 Prism 模块中 class 在 OnNavigatedTo 方法中

我想用多个异步调用的结果填充一个 ObservableCollection,而不用等待所有调用完成

我正在使用这个问题的答案: How to hydrate a Dictionary with the results of async calls?

以下代码是我的真实代码的清理版本:

我的状态class:

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 Whosebug, 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)
            {
            }
        }
    }

但是还是不行。

正确的做法是什么?谢谢!

感谢@AccessDenied 解释async在接口实现中的作用

感谢@Selvin 解释 Task.ResultTask.Wait

如果有人对最终解决方案感兴趣,请看这里:

PositioningModule 是硬件设备,这个class与无关 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;
    }
}

视图模型:

我不得不在 ObservableCollection 上使用 BindingOperations.EnableCollectionSynchronizationlock这就是之前async不行的主要原因!

OnNavigatedTo 更改为 async 阻止了 UI,所以我使用了 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 秒!