1

I've added a Dispatcher and still getting UI freezes after my command executes on a button press.

My current attempted fix

        new Thread(() =>
        {
            Parallel.ForEach(BootstrapNodes, 
            new ParallelOptions { MaxDegreeOfParallelism = 2 }, 
            (node) =>
                {
                    Console.WriteLine(String.Format("Currently bootstrapping {0} on {1}",
                    node.NodeName,
                    node.IPAddress));
                    ChefServer.BootstrapNode(node);
                });
        }).Start();

Version that freezes ui

        Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new Action(() => {
            Parallel.ForEach(BootstrapNodes, 
            new ParallelOptions { MaxDegreeOfParallelism = 2 }, 
            (node) =>
                {
                    Console.WriteLine(String.Format("Currently bootstrapping {0} on {1}",
                    node.NodeName,
                    node.IPAddress));
                    ChefServer.BootstrapNode(node);
                });
            }));

Do I need to dive deeper into my function calls to avoid UI freezes? I'm trying to avoid spawning threads all over the place.

EDIT: I want to note that my background task is heavily expensive.

Ryan Shocker
  • 693
  • 1
  • 7
  • 22
  • Isn't `Invoke` about doing a threadsafe write from a background thread to a UI control on a UI thread, and not the other way around? – Robert Harvey Jul 15 '17 at 22:09
  • Have a look at this example: https://stackoverflow.com/a/1644254 – Robert Harvey Jul 15 '17 at 22:13
  • @RobertHarvey Thanks Robert. So, in my case, my operation is extremely costly and doesn't belong in a dispatcher thread like I had thought. Wouldn't a parallel foreach avoid UI freezes? Not sure why I'm still being blocked on UI – Ryan Shocker Jul 15 '17 at 22:16
  • `Parallel.ForEach` still blocks the current thread (as it has to wait for the result), but employs extra threads to process your data concurrently. – Enigmativity Jul 15 '17 at 22:18
  • @Enigmativity What do you suggest? Throw the parallel foreach in it's own thread... ? – Ryan Shocker Jul 15 '17 at 22:19

3 Answers3

2

You are moving your whole lambda to the UI thread and in there, you go async (parallel) to the UI. You should only put the code in the UI thread that really updates the UI using the information you calculated in the background.

// runs in a background thread
public void backgroundFoo()
{
    // do heavy stuff here
    var result = Work();

    Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new Action(() => 
    {
        // update UI here after the work as been done ...
        Console.WriteLine(... result.Anything ...);
    }));
}
Waescher
  • 5,361
  • 3
  • 34
  • 51
  • The thing is, I'm data binding a value to the UI way deep down, so the UI updates will happen asynchronously anyways. I just want to run my background computation on button click without freezing the UI. – Ryan Shocker Jul 15 '17 at 22:21
  • I think you can remove that `Invoke()` (the one in your question) then and just put it to the places where the UI is updated "way down" as you said. Make everything run without `Invoke()` and make the `Invokes()` then each time you reach out for the UI/main thread. – Waescher Jul 15 '17 at 22:23
  • Interesting.. But why would I need to Invoke on something that is hooked up with INotifyPropChange and bound to the UI element on the form? All those UI changes will occur async by the nature of the implementation. – Ryan Shocker Jul 15 '17 at 22:26
  • Ah, I did not know about that. If the processing (calculation & property writes) is done async in the background, the notifications are thrown in the background as well. I'm not sure if your components can handle that but it might be the issue. Ideally you shoud divide data logic and UI logic, maybe you could use a ViewModel which is updated after the calculation is done and not on each change on your data structure (might be too often). – Waescher Jul 15 '17 at 22:30
  • I have a command hooked up to button inside a viewmodel that will run a background thread through a static class to initiate some serverside computation. The tableview is bound to the properties of a singleton instance. It sounds a bit complicated, but I've been refactoring the hell out of this for 3 days. I think I fixed my issue by running for parallel foreach in a background thread.. – Ryan Shocker Jul 15 '17 at 22:32
  • Hm, it might be too deep to chat that in here. But anyway, you should strive for defined points to update the UI like when the complete calculation is done or when certain steps are done (to update a progressbar for example). Then, you can use the `Invoke()` coming from the background. If this does not help you, I'm sorry, probably it's too complicated without seeing that code. – Waescher Jul 15 '17 at 22:37
1

Note the dispatcher in WPF is used for ensuring thread safety, not unfreezing UI. You can use BackgroundWorker to do heavy work instead.

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;

namespace ThreadingSample.WPF
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private BackgroundWorker _worker;
        public MainWindow()
        {
            InitializeComponent();
            _worker = new BackgroundWorker();
            _worker.DoWork += WorkHeavy;
            _worker.ProgressChanged += ReportWork;
            _worker.RunWorkerCompleted += UpdateUI;
            _worker.WorkerReportsProgress = true;
        }

        private void ReportWork(object sender, ProgressChangedEventArgs e)
        {
            //get node object from e.UserState
            //update ui...
            //Console.WriteLine(String.Format("Currently bootstrapping {0} on {1}",
            //node.NodeName,
            //node.IPAddress));
        }

        private void UpdateUI(object sender, RunWorkerCompletedEventArgs e)
        {
           //update  ui...
        }

        private void WorkHeavy(object sender, DoWorkEventArgs e)
        {
            //heavy work....
            Parallel.ForEach(BootstrapNodes,
            new ParallelOptions { MaxDegreeOfParallelism = 2 },
            (node) =>
            {
                _worker.ReportProgress(node);
                ChefServer.BootstrapNode(node);
            });
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if (_worker.IsBusy == false)
            {
                _worker.RunWorkerAsync();
            }
        }
    }
}
John Zhu
  • 1,009
  • 11
  • 11
0

I'd use Microsoft's Reactive Framework (Rx) for this.

This code should work for you:

IDisposable subscription =
    BootstrapNodes
        .ToObservable()
        .Select(node =>
            Observable
                .Start(() =>
                {
                    Console.WriteLine(String.Format("Currently bootstrapping {0} on {1}",
                    node.NodeName,
                    node.IPAddress));
                    ChefServer.BootstrapNode(node);
                }))
        .Merge(maxConcurrent : 2)
        .ObserveOnDispatcher()
        .Subscribe(u => { }, () =>
        {
            // Back on UI thread - Code completed
        });

If you want the computation to finish early just call subscription.Dispose().

To get the bits for WPF just NuGet "System.Reactive.Windows.Threading".

Enigmativity
  • 113,464
  • 11
  • 89
  • 172