Status tracking of computationally intensive tasks keeps users informed of progress and possible errors as your background thread(s) execute. Calling a worker on its own thread is simple enough, but we want to be able to report status without the Worker taking dependency on the UI (i.e. referencing the Log collection explicitly). A great pattern is to listen to events in the worker class and collect results which bind directly to the UI. Adding event logic to a worker task and binding to a Virtualizing ListBox allows you to efficiently track progress of worker tasks.
This example consists of two parts:
- A static Worker helper class. This is where your task code lives and is set to trigger events: allowing the UI to listen for status events.
- A WPF application with a Virtualizing ListBox that binds to an ObservableCollection. The observable collection will be updated when the worker thread triggers its WorkDone event which, in turn, displays on the UI because observable collections implement INotifyPropertyChanged.
Worker Class
The static class, Worker, holds a DoWork() method. This is the method that holds the actual computation you're looking to perform which, for this example, we will call in its own thread. Interspersed within this code will be triggers to announce status/progress of the work being done for anyone subscribed to the events the Worker class exposes (WorkDone and WorkCompleted). In the sample, we have a simple loop with thread sleeps to simulate actual work being done. There is also an example to show how multi-line status updates can be sent out and displayed. Lastly, we also have a WorkerEventArgs class that extends EventArgs -- this allows us to pass a custom message to display status and can be extended to send any relevant information you wish to have.
Event Logic and Worker Class
- Create the WorkerEventArgs class that extends EventArgs.
publicclass WorkerEventArgs : EventArgs {public WorkerEventArgs(string message) { Message = message; }publicstring Message { get; privateset; } }
- Add delegates and events that will allow the UI to listen for when work is being done and the work is completed.
publicdelegatevoid WorkDoneEventHandler(object sender, WorkerEventArgs e);publicdelegatevoid WorkCompletedEventHandler(object sender, WorkerEventArgs e);publicstaticevent WorkDoneEventHandler WorkDone;publicstaticevent WorkCompletedEventHandler WorkCompleted;publicstaticvoid OnWorkDone(WorkerEventArgs e) {if (WorkDone != null) WorkDone(null, e); }publicstaticvoid OnWorkCompleted(WorkerEventArgs e) {if (WorkCompleted != null) WorkCompleted(null, e); }
- Implement the DoWork method in Worker, which will trigger WorkDone events at your choosing and the WorkCompleted event when it ends. We'll end up with 100 status updates when this completes, each separated by 200 milliseconds. The only exception being a multi-line item that will occur on the 21st iteration. A multi-line status update is great for showing a stack trace or exception information. A point of extension for this model could be adding a enum that describes the type of status update (error, warning, info, etc.), which can be used to color code results in the log window.
publicstaticvoid DoWork() {for (int i = 0; i < 100; i++) {if(i == 20) { OnWorkDone(new WorkerEventArgs(string.Format("MultiLine log item.{1}Iteration {0}{1}Extra Information.", (i + 1), Environment.NewLine))); Thread.Sleep(8000); }else { OnWorkDone(new WorkerEventArgs(string.Format("Iteration #{0} Complete.", (i + 1)))); Thread.Sleep(200); } } OnWorkCompleted(new WorkerEventArgs("All done.")); }
UI, Binding, and Event Listening
Now that we have our worker implemented, we can build a UI and listen for the events exposed in the Worker class.
XAML
<Windowx:Class="LoggerApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="MainWindow"Height="335.793"Width="525"x:Name="winLogWindow"><Grid><ListBoxx:Name="lbLogger"Margin="10,10,10,35"ItemsSource="{Binding Log, ElementName=winLogWindow}"VirtualizingPanel.IsVirtualizing="True"SelectionMode="Multiple"><ListBox.ItemTemplate><DataTemplate><TextBlockText="{Binding}"/></DataTemplate></ListBox.ItemTemplate></ListBox><Buttonx:Name="btnStart"Content="Start"HorizontalAlignment="Left"Margin="10,0,0,5"VerticalAlignment="Bottom"Width="75"Click="btnStart_Click"/><Buttonx:Name="btnCopy"Content="Copy Selected Line(s)"HorizontalAlignment="Left"Margin="378,0,0,5"Width="129"Height="20"VerticalAlignment="Bottom"Click="btnCopy_Click"/></Grid></Window>
Key Takeaways:
- The ListBox ItemSource binds to an OberservableCollection property in our backend code called Log.
- Note that the ListBox has the IsVirtualizing attribute set to true. This is imperative for good performance, since our ListBox will now only render log items that are in view.
- Our DataTemplate is pretty simple, at the moment, but you could extend this example to bind more than just a string. Perhaps an entire status/log object with multiple properties.
This is the entirety of the project XAML. I also include functionality for copying selected lines of the log window, but will not go into detail in this summary. The full source code is available at the bottom of the page.
C# Backend
- To listen for the Worker events, we need to assign handlers in the MainWindow Constructor.
Worker.WorkDone += Worker_WorkDone; Worker.WorkCompleted += Worker_WorkCompleted;
- Implement the ObservableCollection that will bind to our XAML. Note that we can only databind to properties, so we will use a property wrapper pattern over our private ObservableCollection.
private ObservableCollection<string> _Log = new ObservableCollection<string>();public ObservableCollection<string> Log {get {return _Log; } }
- Create a method that encapsulates adding an item to our ObservableCollection Log and assign it to an Action in the MainWindow constructor.
private Action<string> addtoLogAction;privatevoid AddToLog(string message) { Log.Add(message);// Maintain position at the bottom of the listbox lbLogger.ScrollIntoView(lbLogger.Items[lbLogger.Items.Count - 1]); }// This goes in MainWindow() constructor addtoLogAction = AddToLog;
- Implement the event handlers: Worker_WorkDone and Worker_WorkCompleted. Note that when we want to add an item to the Log, we must use the Dispatcher, since the Log is an ObservableCollection.
void Worker_WorkCompleted(object sender, WorkerEventArgs e) { MessageBox.Show("Work Completed."); }void Worker_WorkDone(object sender, WorkerEventArgs e) { Dispatcher.BeginInvoke(addtoLogAction, e.Message); }
- Lastly, we implement the handler for our Start button to call DoWork on its own thread.
privatevoid btnStart_Click(object sender, RoutedEventArgs e) {// Quick and dirty thread start// A ThreadPool is a more robust solution for multiple worker threadsnew Thread(() => { Worker.DoWork(); }).Start(); }
Summary
That's it! Load up the project and click "Start" to see logging status fill the listbox. Note the code in the AddToLog method that keeps the listbox scrolled to the bottom so that we can always view the last item. Try kicking off multiple DoWork() threads and see how the results come in. This is where color coding and more information in the WorkerEventArgs class would prove to be useful.
If you want to have the most recent entries at the top, you can remove that line and change Log.Add(message) to Log.Insert(0, message). As mentioned above, I've also included code in the sample for copying log lines. Check out my GitHub project below for more details.
Full Source Code on GitHub - (petersbattaglia)
Code formatted by http://manoli.net/csharpformat/