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:

  1. 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.
  2. 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

  1. Create the WorkerEventArgs class that extends EventArgs.
    public class WorkerEventArgs : EventArgs
    {
        public WorkerEventArgs(string message)
        {
            Message = message;
        }
    
        public string Message { get; private set; }
    }
    
  2. Add delegates and events that will allow the UI to listen for when work is being done and the work is completed.
    public delegate void WorkDoneEventHandler(object sender, WorkerEventArgs e);
    public delegate void WorkCompletedEventHandler(object sender, WorkerEventArgs e);
    
    public static event WorkDoneEventHandler WorkDone;
    public static event WorkCompletedEventHandler WorkCompleted;
    
    public static void OnWorkDone(WorkerEventArgs e)
    {
        if (WorkDone != null)
            WorkDone(null, e);
    }
    
    public static void OnWorkCompleted(WorkerEventArgs e)
    {
        if (WorkCompleted != null)
            WorkCompleted(null, e);
    }
    
  3. 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.
    public static void 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

<Window x: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>
        <ListBox x:Name="lbLogger" 
                 Margin="10,10,10,35" 
                 ItemsSource="{Binding Log, ElementName=winLogWindow}" 
                 VirtualizingPanel.IsVirtualizing="True" 
                 SelectionMode="Multiple">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button x:Name="btnStart" 
                Content="Start" 
                HorizontalAlignment="Left" 
                Margin="10,0,0,5" 
                VerticalAlignment="Bottom" 
                Width="75" 
                Click="btnStart_Click"/>
        <Button x: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:

  1. The ListBox ItemSource binds to an OberservableCollection property in our backend code called Log.
  2. 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.
  3. 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

  1. To listen for the Worker events, we need to assign handlers in the MainWindow Constructor.
    Worker.WorkDone += Worker_WorkDone;
    Worker.WorkCompleted += Worker_WorkCompleted;
  2. 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;
        }
    }
  3. 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;
    
    private void 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;
    
  4. 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);
    }
  5. Lastly, we implement the handler for our Start button to call DoWork on its own thread.
    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
        // Quick and dirty thread start
        // A BackgroundWorker is a more robust and feature-rich solution
        new 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/