1

I am new the using Task.Run() along with async and await to make UI more responsive, so likely I have not implemented something correctly.

I have reviewed the great articles from Stephen Cleary about using AsyncCommands and have used his code from Patterns for Asynchronous MVVM Applications: Commands as a basis for having a responsive UI but when I run the code it still seems to freeze up (I am not able to move the window or interact with other buttons until the function has fully finished.

I am trying to perform a search which usually takes 5-10 seconds to return. Below is the code that creates the AsyncCommand along with the what the function does.

Code:

    public ICommand SearchCommand
    {
        get
        {
            if (_SearchCommand == null)
            {
                _SearchCommand = AsyncCommand.Create(() => Search());
            }
            return _SearchCommand;
        }
    }
    private async Task Search()
    {
        IEnumerable<PIPoint> points = await SearchAsync(_CurrentPIServer, NameSearch, PointSourceSearch).ConfigureAwait(false);
        SearchResults.Clear();
        foreach (PIPoint point in points)
        {
            SearchResults.Add(point.Name);
        }
    }
    private async Task<IEnumerable<PIPoint>> SearchAsync(string Server, string NameSearch, string PointSourceSearch)
    {
        {
            PIServers KnownServers = new PIServers();
            PIServer server = KnownServers[Server];
            server.Connect();
            return await Task.Run<IEnumerable<PIPoint>>(()=>PIPoint.FindPIPoints(server, NameSearch, PointSourceSearch)).ConfigureAwait(false);
        }
    }

I am thinking that the issue is somewhere in how I am pushing the long running function onto a thread and its not getting off of the UI thread or my understanding of how Tasks and async/await are completely off.

EDIT 1: Following Stephen's answer I updated the functions, but I am not seeing any change in the UI responsiveness. I created a second command that performs the same actions and I get the same response from UI in either case. The code now looks like the following

CODE:

    public ICommand SearchCommand
    {
        get
        {
            if (_SearchCommand == null)
            {
                _SearchCommand = AsyncCommand.Create(async () =>
                {
                    var results = await Task.Run(()=>Search(_CurrentPIServer, NameSearch, PointSourceSearch));
                    SearchResults = new ObservableCollection<string>(results.Select(x => x.Name));
                });
            }
            return _SearchCommand;
        }
    }
    public ICommand SearchCommand2
    {
        get
        {
            if (_SearchCommand2 == null)
            {
                _SearchCommand2 = new RelayCommand(() =>
                {
                    var results = Search(_CurrentPIServer, NameSearch, PointSourceSearch);
                    SearchResults = new ObservableCollection<string>(results.Select(x => x.Name));
                }
                ,()=> true);
            }
            return _SearchCommand2;
        }
    }
    private IEnumerable<PIPoint> Search(string Server, string NameSearch, string PointSourceSearch)
    {
        PIServers KnownServers = new PIServers();
        PIServer server = KnownServers[Server];
        server.Connect();
        return PIPoint.FindPIPoints(server, NameSearch, PointSourceSearch);
    }

I must be missing something but I am not sure what at this point.

EDIT 2: After more investigation on what was taking so long it turns out the iterating of the list after the results are found is what was hanging the process. By simply changing what the Search function was returning and having it already iterated over the list of objects allows for the UI to remain responsive. I marked Stephen's answer as correct as it handled my main problem of properly moving work off of the UI thread I just didnt move the actual time consuming work off.

4
  • What is your server.Connect() method doing? Commented Aug 28, 2014 at 19:09
  • @yuval-itzchakov that object and method are supplied by a third party, but in debugging it is a fast operation. The the .FindPIPoints method is the slow operation that is blocking the program from being responsive. Commented Aug 28, 2014 at 19:45
  • Is there anything else running other than this method? Any long running UI operation? Commented Aug 28, 2014 at 20:08
  • @YuvalItzchakov This is the only method running. The entire program before clicking to invoke the command is just waiting on the user. Once the user clicks the button I just want the UI to be responsive while it waits for a response. Commented Aug 28, 2014 at 20:10

2 Answers 2

3

My first guess is that the work queued to Task.Run is quite fast, and the delay is caused by other code (e.g., PIServer.Connect).

Another thing of note is that you are using ConfigureAwait(false) in Search which updates SearchResults - which I suspect is wrong. If SearchResults is bound to the UI, then you should be in the UI context when updating it, so ConfigureAwait(false) should not be used.

That said, there's a Task.Run principle that's good to keep in mind: push Task.Run as far up your call stack as possible. I explain this in more detail on my blog. The general idea is that Task.Run should be used to invoke synchronous methods; it shouldn't be used in the implementation of an asynchronous method (at least, not one that is intended to be reused).

As a final note, async is functional in nature. So it's more natural to return results than update collections as a side effect.

Combining these recommendations, the resulting code would look like:

private IEnumerable<PIPoint> Search(string Server, string NameSearch, string PointSourceSearch)
{
  PIServers KnownServers = new PIServers();
  PIServer server = KnownServers[Server];
  // TODO: If "Connect" or "FindPIPoints" are naturally asynchronous,
  //  then this method should be converted back to an asynchronous method.
  server.Connect();
  return PIPoint.FindPIPoints(server, NameSearch, PointSourceSearch);
}

public ICommand SearchCommand
{
  get
  {
    if (_SearchCommand == null)
    {
      _SearchCommand = AsyncCommand.Create(async () =>
      {
        var results = await Task.Run(() =>
            Search(_CurrentPIServer, NameSearch, PointSourceSearch));
        SearchResults = new ObservableCollection<string>(
            results.Select(x => x.Name));
      });
    }
    return _SearchCommand;
  }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for responding so quickly. I tried using the code above with a slight change of ()=>Search(. In stepping through debugging PIServer.Connect() is fast, it is the PIPoint.FindPIPoints that is slow which is why I was trying to move it off to another thread. In any case the window still freezes during execution. Are there are things I should be looking at to get it to be responsive?
Are you sure it isn't anything else? With this code, everything is offloaded via Task.Run except copying the collection, which should be very fast.
0

Old post, but now it hits me.
Taking notes from Stephen answers (https://stackoverflow.com/a/25556369/1394031, https://stackoverflow.com/a/74650902/1394031, https://stackoverflow.com/a/18015586/1394031) and tried to execute a non-blocking UI command. The solution is to use Task.Run to free the UI immediately. It may not be the right approach, but it's the only one that really works.

Test 1

        [RelayCommand]
        private async Task CommandAsync()
        {
            //IsRunning never gets set to true and the UI is always blocked
            var imgResource = Application.GetResourceStream(new Uri("pack://application:,,,/Assets/logo.png"));
            var imgStream = new MemoryStream();
            await imgResource.Stream.CopyToAsync(imgStream);
            var imgBytes = imgStream.ToArray();
        }

Test 2

        [RelayCommand]
        private async Task CommandAsync()
        {
            //IsRunning sets to true but UI remains blocked
            await Task.Yield();
            var imgResource = Application.GetResourceStream(new Uri("pack://application:,,,/Assets/logo.png"));
            var imgStream = new MemoryStream();
            await imgResource.Stream.CopyToAsync(imgStream);
            var imgBytes = imgStream.ToArray();
        }

Test 3

        [RelayCommand]
        private async Task CommandAsync()
        {
            //UI unlocked, IsRunning sets to true, loading spinner is shown
            await Task.Delay(TimeSpan.FromSeconds(5));

            //UI blocks again so loading spinner is now frozen
            var imgResource = Application.GetResourceStream(new Uri("pack://application:,,,/Assets/logo.png"));
            var imgStream = new MemoryStream();
            await imgResource.Stream.CopyToAsync(imgStream);
            var imgBytes = imgStream.ToArray();
        }

Test 4

        [RelayCommand]
        private async Task CommandAsync()
        {
            //UI unlocked, IsRunning sets to true, loading spinner is shown
            await Task.Delay(TimeSpan.FromSeconds(5));

            //UI blocks again so loading spinner is now frozen
            await CommandTask().ConfigureAwait(false);
        }

        private async Task CommandTask()
        {
            var imgResource = Application.GetResourceStream(new Uri("pack://application:,,,/Assets/logo.png"));
            var imgStream = new MemoryStream();
            await imgResource.Stream.CopyToAsync(imgStream);
            var imgBytes = imgStream.ToArray();
        }

Test 5

        [RelayCommand]
        private async Task CommandAsync()
        {
            //UI is released and loading spinner is shown immediately. IT WORKS!!!
            await Task.Run(CommandTask);
        }

        private async Task CommandTask()
        {
            var imgResource = Application.GetResourceStream(new Uri("pack://application:,,,/Assets/logo.png"));
            var imgStream = new MemoryStream();
            await imgResource.Stream.CopyToAsync(imgStream);
            var imgBytes = imgStream.ToArray();
        }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.