user677607
user677607

Reputation:

How to make the whole application responsive rather than the individual UI Controls using async and await?

I am using async and await keywords to make an app responsive

I have the following code in the View when the property is raised in the ViewModel.

VIEW: Button Click Event

    private async void button1_Click(object sender, RoutedEventArgs e)
    {
        button1.IsEnabled = false;

        // Property Change
        _viewModel.Words = await Task.Factory.StartNew(() => File.ReadAllLines("Words.txt").ToList()); // takes time to read about 3 - 4 seconds

        switch (_viewModel.RadioButtonWordOrderSelection)
        {
            case MainWindowViewModel.RadioButtonWordOrderSelections.NormalOrder:
                break;

            case MainWindowViewModel.RadioButtonWordOrderSelections.ReverseOrder:
                await Task.Factory.StartNew(() =>
                                                {
                                                    var words = _viewModel.Words.ToList();
                                                    words.Reverse();
                                                    // Property Change
                                                    _viewModel.Words = words;
                                                });
                break;

            case MainWindowViewModel.RadioButtonWordOrderSelections.Shuffle:
                await Task.Factory.StartNew(() =>
                                                {
                                                    // Property Change
                                                    _viewModel.Words = _viewModel.Words.Shuffle().ToList();
                                                });
                break;
        }

        await Task.Factory.StartNew(() => DownloadSomething(_viewModel.Words)); // takes time to read about 30 - 40 seconds

        button1.IsEnabled = true;
    }

_viewModel.Progress gets updated in the View

VIEW: Private Method Takes 30 - 40 seconds To Complete

    private void DownloadSomething(IEnumerable<string> words)
    {
        // Property Change
        _viewModel.Progress = 0;

        foreach (var word in words)
        {
            // Property Change
            _viewModel.Word = word;
            try
            {
                // Some code WebClient download code here
            }
            catch (Exception e)
            {
                //Trace.WriteLine(e.Message);
            }

            // Property Change
            _viewModel.Progress++;
        }
    }

VIEW: Property Change Event Handled

    void _viewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        try
        {
            switch(e.PropertyName)
            {
                case "Progress":
                    // Since its a different Thread
                    // http://msdn.microsoft.com/en-us/magazine/cc163328.aspx
                    // Sets the Value on a ProgressBar Control.
                    // This will work as its using the dispatcher

                    // The following works
                    //Dispatcher.Invoke(
                    //    DispatcherPriority.Normal, 
                    //    new Action<double>(SetProgressValue), 
                    //    _viewModel.Progress);

                    // from http://stackoverflow.com/questions/2982498/wpf-dispatcher-the-calling-thread-cannot-access-this-object-because-a-differen
                    progress1.Dispatcher.Invoke(
                        DispatcherPriority.Normal, 
                        new Action(() =>
                        {
                            progress1.Value = _viewModel.Progress;
                        })
                    );

                    // This will throw an exception 
                    // (it's on the wrong thread)
                    //progress1.Value = _viewModel.Progress;
                    break;

                case "Words":
                    // Since its a different Thread
                    // http://msdn.microsoft.com/en-us/magazine/cc163328.aspx
                    // Sets the Max Value on a ProgressBar Control.
                    // This will work as its using the dispatcher

                    // The following Works
                    //Dispatcher.Invoke(
                    //    DispatcherPriority.Normal,
                    //    new Action<double>(SetProgressMaxValue),
                    //    _viewModel.Words.Count);

                    // from http://stackoverflow.com/questions/2982498/wpf-dispatcher-the-calling-thread-cannot-access-this-object-because-a-differen
                    progress1.Dispatcher.Invoke(
                        DispatcherPriority.Normal, 
                        new Action(() =>
                        {
                            progress1.Maximum = _viewModel.Words.Count;
                        })
                    );

                    // This will throw an exception 
                    // (it's on the wrong thread)
                    //progress1.Maximum = _viewModel.Words.Count;
                    break;

                case "Word":
                    // Since its a different Thread
                    // http://msdn.microsoft.com/en-us/magazine/cc163328.aspx
                    // Sets the Contant on a Label Control.
                    // This will work as its using the dispatcher

                    // The following Works
                    //Dispatcher.Invoke(
                    //    DispatcherPriority.Normal,
                    //    new Action<string>(SetLastWordValue),
                    //    _viewModel.Word);

                    // from http://stackoverflow.com/questions/2982498/wpf-dispatcher-the-calling-thread-cannot-access-this-object-because-a-differen
                    labelLastWord.Dispatcher.Invoke(
                        DispatcherPriority.Normal, 
                        new Action(() =>
                        {
                            labelLastWord.Content = _viewModel.Word;
                        })
                    );

                    // This will throw an exception 
                    // (it's on the wrong thread)
                    //labelLastWord.Content = _viewModel.Word;
                    break;

                case "RadioButtonWordOrderSelection":
                    break;

                default:
                    throw new NotImplementedException("[Not implemented for 'Property Name': " + e.PropertyName + "]");
            }
        }
        catch(Exception ex)
        {
            MessageBox.Show(ex.Message + "\n" + ex.StackTrace);
        }
    }

The UI updates perfectly for the progressBar1 and labelLastWord! I am facing a problem though, when progressBar1 and labelLastWord is updating the rest of the UI is frozen.

Is there a work around to fix this?

I greatly appreciate any help!

Upvotes: 0

Views: 441

Answers (1)

Stephen Cleary
Stephen Cleary

Reputation: 456447

I strongly recommend you follow the guidelines in the Task-based Async Programming document. This is far better than just shunting work off to the thread pool via StartNew. If you do have CPU-bound operations, you could use Task.Run, but for everything else, use the existing async-ready endpoints.

Also, I find it useful to treat the entire VM as being in the UI context. So PropertyChanged is always raised in a UI context. Data binding in particular depends on this.

private async Task<List<string>> ReadAllLinesAsync(string file)
{
  var ret = new List<string>();
  using (var reader = new StreamReader(file))
  {
    string str = await reader.ReadLineAsync();
    while (str != null)
    {
      ret.Add(str);
      str = await reader.ReadLineAsync();
    }
  }
  return ret;
}

private async void button1_Click(object sender, RoutedEventArgs e)
{
  button1.IsEnabled = false;

  _viewModel.Words = await ReadAllLinesAsync("Words.txt");

  List<string> words;
  switch (_viewModel.RadioButtonWordOrderSelection)
  {
    case MainWindowViewModel.RadioButtonWordOrderSelections.NormalOrder:
      break;

    case MainWindowViewModel.RadioButtonWordOrderSelections.ReverseOrder:
      await Task.Run(() =>
      {
        words = _viewModel.Words.ToList();
        words.Reverse();
      });
      _viewModel.Words = words;
      break;

    case MainWindowViewModel.RadioButtonWordOrderSelections.Shuffle:
      await Task.Run(() =>
      {
        words = _viewModel.Words.Shuffle().ToList();
      });
      _viewModel.Words = words;
      break;
  }

  await DownloadSomething(_viewModel.Words);

  button1.IsEnabled = true;
}

private async Task DownloadSomething(IEnumerable<string> words)
{
  _viewModel.Progress = 0;

  foreach (var word in words)
  {
    _viewModel.Word = word;
    try
    {
      await ...; // async WebClient/HttpClient code here
    }
    catch (Exception e)
    {
      //Trace.WriteLine(e.Message);
    }

    _viewModel.Progress++;
  }
}

void _viewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
  try
  {
    switch(e.PropertyName)
    {
      case "Progress":
        progress1.Value = _viewModel.Progress;
        break;

      case "Words":
        progress1.Maximum = _viewModel.Words.Count;
        break;

      case "Word":
        labelLastWord.Content = _viewModel.Word;
        break;

      case "RadioButtonWordOrderSelection":
        break;

      default:
        throw new NotImplementedException("[Not implemented for 'Property Name': " + e.PropertyName + "]");
    }
  }
  catch(Exception ex)
  {
    MessageBox.Show(ex.Message + "\n" + ex.StackTrace);
  }
}

As a closing note, I recommend you purchase and read thoroughly Josh Smith's MVVM book. You're using terms like "View" and "ViewModel", but the way you're using these components, you're completely avoiding all the advantages of the MVVM pattern.

Upvotes: 1

Related Questions