0

In my Prism module in ViewModel class in OnNavigatedTo method

I would like to fill an ObservableCollection with results of multiple async calls without waiting for all calls to complete.

I am using answer from this question: How to hydrate a Dictionary with the results of async calls?

The following code is a cleaned-up version of my real code:

My status class:

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

My view model:

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

but that didn't work because GetStringAsync can throw an exception, so I changed it to this:

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

but it still doesn't work.

What is the right way to do this? Thank you!

Jinjinov
  • 2,554
  • 4
  • 26
  • 45

1 Answers1

1

Thanks to @AccessDenied for explaining the role of async in interface implementation.

Thanks to @Selvin for explaining Task.Result and Task.Wait.

If anyone is interesed in the final solution, here it is:

PositioningModule is a hardware device, this class has nothing to do with 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;
    }
}

The ViewModel:

I had to use BindingOperations.EnableCollectionSynchronization and lock on the ObservableCollection. This is the main reason why it didn't work async before!

Changing OnNavigatedTo to async blocked the UI, so I used 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);
        }
    }
}

The Model:

I changed Send to SendPingAsync and I had to use new Ping() instead of ping.

Using Select instead of foreach to make the calls parallel made everything much faster!

#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
    }
}

It didn't benchmark it because the difference is so obvious - about 0.5 sec instead of 14 sec!

Jinjinov
  • 2,554
  • 4
  • 26
  • 45