Client/server desktop application using C#, WCF, WPF. Since pretty much every action is going to require a trip to the server (list/create/save/delete/etc), every action has the potential to freeze the entire UI. Here's an example of a naive implementation with a call to service.GetAll()
which could take a "long" time (more than a few hundred milliseconds):
private void btnRefresh_Click(object sender, RoutedEventArgs e)
{
vm.Users.Clear();
foreach (var user in service.GetAllUsers())
vm.Users.Add(user);
}
(Aside: I'd love to know why List has AddRange
and ObservableCollection doesn't.)
BackgroundWorker
to the rescue:
private void btnRefresh_Click(object sender, RoutedEventArgs e)
{
var worker = new BackgroundWorker();
worker.DoWork += (s, e) =>
{
Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = false; });
e.Result = service.GetAllUsers();
};
worker.RunWorkerCompleted += (s, e) =>
{
vm.Users.Clear();
foreach (var user in (List<UserDto>)e.Result)
vm.Users.Add(user);
Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = true; });
};
worker.RunWorkerAsync();
}
(Aside: code above has been simplified, but that's the gist of it.)
The code using BackgroundWorker
works exactly how I want it to. The app remains responsive at all times, and the button is disabled for the duration of the call. However, this means adding 15 lines to every possible action the user might make.
Say it ain't so.
BackgroundWorker(); Step 2: In order to run the BackgroundWorker process we use the RunWorkerAsync method of the BackgroundWorker class. This mehod is used to start the execution of the background process by the raising of the DoWork event. The DoWork EventHandler is run in a separate thread.
All WPF applications start out with two important threads, one for rendering and one for managing the user interface.
WPF supports a single-threaded apartment model that has the following rules: One thread runs in the entire application and owns all the WPF objects. WPF elements have thread affinity, in other words other threads can't interact with each other.
BackgroundWorker makes the implementation of threads in Windows Forms. Intensive tasks need to be done on another thread so the UI does not freeze. It is necessary to post messages and update the user interface when the task is done.
No, BackgroundWorker
is not the only way, but it's one way. Any other way will allso include some form of asynchronous construct with the need to use Dispatch.BeginInvoke
to update the UI. You could for instance use the ThreadPool
:
ThreadPool.QueueUserWorkItem(state => {
Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = false; });
foreach (var user in service.GetAllUsers())
vm.Users.Add(user);
Dispatcher.BeginInvoke((Action)delegate() { btnRefresh.IsEnabled = true; });
});
If this is a recurring pattern (a button will trigger some action that should be performed asynchronously, with the button being disabled during the process) you can wrap this into a method:
private void PerformAsync(Action action, Control triggeringControl)
{
ThreadPool.QueueUserWorkItem(state => {
Dispatcher.BeginInvoke((Action)delegate() { triggeringControl.IsEnabled = false; });
action();
Dispatcher.BeginInvoke((Action)delegate() { triggeringControl.IsEnabled = true; });
});
}
...and call it:
PerformAsync(() =>
{
foreach (var user in service.GetAllUsers())
vm.Users.Add(user);
}, btnRefresh);
As an option to using the ThreadPool
, you should also perhaps look into the Task Parallel Library.
When doing this you should pay attention to how you handle UI state. For instance of you have more than one control which triggers the same action, make sure that all of them are disabled during the action.
Note: these are just quick ideas. The code has not been tested so it may contain errors. It's more to be regarded as discussion material than finished solutions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With