Ryan Shocker
Ryan Shocker

Reputation: 703

Button click freezing UI even with Dispatcher in WPF

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.

Upvotes: 1

Views: 2173

Answers (3)

John Zhu
John Zhu

Reputation: 1099

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

Upvotes: 1

Enigmativity
Enigmativity

Reputation: 117019

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".

Upvotes: 0

Waescher
Waescher

Reputation: 5737

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

Upvotes: 2

Related Questions