How to Show Progress and to Cancel Asynchronous Operation Using WPF ObjectDataProvider and XAML

by Zoran Horvat

Using ObjectDataProvider in XAML allows us to declaratively set data source on the control even in situations when that requires us to create and maintain an object that provides actual data. This is especially useful in asynchronous mode where UI remains responsive during the long data fetching operation.

When running data load asynchronously, i.e. in background thread, object which provides the operation should also provide feedback which allows the application to present progress and optionally to cancel the long operation. In this article we are demonstrating solution which is completely XAML-based, once having progress and cancellation features provided by the object which performs long operation.

Goal of this demonstration is to produce application which declaratively initiates long data loading operation and also provides progress bar and cancel button so that user can observe progressing and cancel it at will. Below is the window snapshot depicting the desired effects.

Data window with progress

Solution Proposal

In order to be able to provide progress and cancellation features in UI, class that performs long operation must support them. Therefore, we have the following prerequisites:

  • Class which provides data loading facility via a single method - This class will be instantiated and its method called using ObjectDataProvider XAML element,
  • INotifyPropertyChanged implemented - Class must implement this interface so that UI can be notified when progress and other feedback properties change,
  • Progress indicating property - Integer property which will be bound by UI to update progress bar's value,
  • Is cancellation allowed property - Boolean property which will be bound by UI to enable or disable Cancel button; normally, cancellation is disallowed after operation completes,
  • Cancel feature - Method which will be called from the application when Cancel button is pressed.

Cancel method is not synchronous, i.e. it does not guarantee that operation will be cancelled immediately. Cancellation feature highly depends on nature of data being loaded. Operation might not be stopped at any point, but only in some points in time. For example, when number of records are being fetched from the database, operation can be cancelled only between two records are read. But on the bright side, caller may forget about the asynchronous operation as soon as it has signaled the carrying object to cancel the operation. Only one condition is required, and that is to have data loader guarantee that it will not interact with the caller once cancel signal has been passed to it (e.g. it will never post messages, send objects to other entities, etc.). As long as this rule is obeyed, data loading object may live within the process for some time until it becomes able to die out in a clean manner, i.e. to effectively cancel further operations - UI will not notice that object is still active on the background thread which is, from point of view of the application user, exactly the same as if the operation was cancelled immediately.

XAML Design

Suppose that we have a class which obeys design proposed above. Important elements that are needed to construct XAML are these:

  • Class name (e.g. DataLoader) - Will be used to instantiate the class,
  • Loading method name (e.g. Load) - Will be called on background thread to load data,
  • Progress indicating property (e.g. int Progress) - Must return value between 0 and 100, inclusive, and will be bound to progress bar's Value property,
  • Cancel method (e.g. void Cancel() - Will be called from the Cancel button's Click event handler to cancel current long operation.

Wish list is quite short as we can see. XAML that produces window shown above can then be constructed by following these simple steps:

  • Instantiate DataLoader class in the window resources. This instance will perform the long operation and provide progress feedback and cancellation.
  • Decare ObjectDataProvider which references instance created above and calls its Load method; make sure that data provider is declared as asynchronous (IsAsynchronous="True". Do not let ObjectDataProvider instantiate the class but rather use ObjectInstance to enforce using previously created instance (this is important because that is the instance that will provide us feedback during the long operation).
  • Declare ItemsControl which will receive data (e.g. ListBox; bind this control to ObjectDataProvider created before.
  • Declare progress bar; set its range to 0 thru 100 and bind its Value property to the Progress property of the instance created in the first step.
  • Declare cancel button. Implement its Click event handler to call data loader's Cancel method. Bind button's IsEnabled property to data loader's IsCancelable Boolean property.

Example Data Provider Class

In this section we will provide full code of the data loading class which simulates long operation of loading integer values from external source. Class will provide Load method which loads data. It also provides feedback properties (Progress and IsCancelable) and Cancel method wich can be used to initiate loading cancellation. Below is the complete source code of this class.

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace ProgressDemo
{
    /// <summary>
    /// Simulates external data loading as a long operation.
    /// </summary>
    public class DataLoader: INotifyPropertyChanged
    {
        /// <summary>
        /// Simulates data loading which takes long time to complete.
        /// This function can be interrupted by calling the Cancel method.
        /// When cancelled, method will exit at first convenient moment,
        /// which may occur after unknown period of time.
        /// </summary>
        /// <returns>Collection of integers, which represents loaded data.</returns>
        public ObservableCollection<int> Load()
        {

            Random rnd = new Random();
            ObservableCollection<int> data = new ObservableCollection<int>();
            int count = 1000;       // Number of items to fetch;
                                    // this method supposes that this number is known in advance;
                                    // if not, then it can be estimated using knowledge about particular data source,
                                    // e.g. if loading from file then file size is the count

            int pauseAfter = 7;     // Helper value, used to perform delays on coarser grain basis
            int avgPause = 30;      // Average pause length (in msec.)

            for (int i = 0; i < count && !_isCancelling; i++)
            {   // Loop iterates until complete or until cancel has been requested from the outside

                int value = 100000 + rnd.Next(900000);  // New value fetched from external source
                data.Add(value);

                if ((i + 1) % pauseAfter == 0)
                {   // Simulate delay that occurs in fetching external data
                    int pause = rnd.Next(avgPause * 2);
                    System.Threading.Thread.Sleep(pause);
                }

                SetProgress(i + 1, count);

            }

            if (_isCancelling)
            {   // Operation has been cancelled
                System.Threading.Thread.Sleep(1000 + rnd.Next(2000));   // Simulate cancellation delay
                SetCancelled();
                data.Clear();       // Method should return empty collection on cancellation
                                    // because data would anyway be incomplete
            }
            else
            {   // Operation was completed successfully
                SetComplete();
            }

            return data;

        }

        /// <summary>
        /// Sets amount of work completed up to this point.
        /// Method may change value returned by the Progress property,
        /// which in turn may raise PropertyChanged event.
        /// </summary>
        /// <param name="loaded">Number of items loaded.</param>
        /// <param name="total">Total number of items that should be loaded.</param>
        private void SetProgress(int loaded, int total)
        {
            int progress = 100 * loaded  total;
            if (_progress != progress)
            {
                _progress = progress;
                OnPropertyChanged("Progress");
            }
        }

        /// <summary>
        /// Sets IsCancelled property to true. If property value was false,
        /// then raises PropertyChanged event.
        /// </summary>
        private void SetCancelled()
        {
            if (!_isCancelled)
            {
                _isCancelled = true;
                OnPropertyChanged("IsCancelled");
            }
        }

        /// <summary>
        /// Sets IsComplete property to true. May cause IsCancelable property
        /// value to change, which leads to PropertyChanged event referring to
        /// IsCancelable property.
        /// </summary>
        private void SetComplete()
        {
            if (!_isComplete)
            {

                bool isCancelable = IsCancelable;
                _isComplete = true;

                OnPropertyChanged("IsComplete");

                if (isCancelable != IsCancelable)
                    OnPropertyChanged("IsCancelable");

            }
        }

        /// <summary>
        /// Raises PropertyChanged event.
        /// </summary>
        /// <param name="name">Name of the property which was changed.</param>
        protected virtual void OnPropertyChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

        /// <summary>
        /// Cancels data loading, i.e. signals that loading should be cancelled.
        /// Effective cancellation will occur at first moment convenient to stop
        /// the operation, which might not be immediate and is certainly not
        /// synchronized with call to this method. Calling this method may
        /// raise PropertyChanged event, referring to change of IsCancelable property.
        /// </summary>
        public void Cancel()
        {

            if (!_isCancelling)
            {

                bool isCancelable = IsCancelable;
                _isCancelling = true;

                if (isCancelable != IsCancelable)
                    OnPropertyChanged("IsCancelable");

            }

        }

        /// <summary>
        /// Gets percentage of work completed (in range 0..100 inclusive).
        /// </summary>
        public int Progress
        {
            get { return _progress; }
        }

        /// <summary>
        /// Gets value indicating whether data loading operation
        /// has ben cancelled.
        /// </summary>
        public bool IsCancelled
        {
            get { return _isCancelled; }
        }

        /// <summary>
        /// Gets value indicating whether data loading operation
        /// can be cancelled. Cancellation is allowed before loading
        /// completes and before cancellation has already been requested.
        /// </summary>
        public bool IsCancelable
        {
            get { return !_isCancelling && !_isCancelled && !_isComplete; }
        }

        /// <summary>
        /// Gets value indicating whether loading operation has completed without being cancelled.
        /// </summary>
        public bool IsComplete
        {
            get { return _isComplete; }
        }

        /// <summary> Event raised when public property value changes on this instance.</summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>Current percentual progress of the loading operation.</summary>
        private int _progress;

        /// <summary>True if cancel request has been posted and loading should stop.</summary>
        private bool _isCancelling;

        /// <summary>
        /// True if load operation has been cancelled before completion.
        /// </summary>
        private bool _isCancelled;

        /// <summary>True if load operation has completed (i.e. not cancelled).</summary>
        private bool _isComplete;

    }
}

Apart from the Load method which provides core functionality of the class, all other members are there only to allow feedback. This may look like a burden, but it is certainly in data loader's description to provide operation feedback.

Example XAML Code

DataLoader example class provided above can be tied up within XAML to provide data loading and operation feedback. Below is the XAML code which defines window with list box populated from DataLoader. Apart from list box, progress bar and Cancel button are also defined to provide visual feedback and cancellation feature to the user.

<Window x:Class="ProgressDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ProgressDemo"
        Title="Progress Demo" Height="250" Width="300" MinHeight="150" MinWidth="250">
    <Window.Resources>
        <local:DataLoader x:Key="Loader" />
        <ObjectDataProvider x:Key="LoadedData" IsAsynchronous="True"
                ObjectInstance="{StaticResource ResourceKey=Loader}" MethodName="Load" />
    </Window.Resources>
    <DockPanel>
        <Grid DockPanel.Dock="Bottom">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <ProgressBar Minimum="0" Maximum="100" Grid.Row="0" Grid.Column="0" Margin="3"
                         Value="{Binding Source={StaticResource ResourceKey=Loader}, Path=Progress, Mode=OneWay}"
                         IsIndeterminate="{Binding Source={StaticResource ResourceKey=Loader}, Path=IsCancelled, Mode=OneWay}"
                         HorizontalAlignment="Stretch" Height="12" />
            <Button Grid.Row="0" Grid.Column="1" Margin="3" Padding="10,2" Click="CancelClick"
                    IsEnabled="{Binding Source={StaticResource ResourceKey=Loader}, Path=IsCancelable}">Cancel</Button>
        </Grid>
        <ListBox Margin="3,3,3,0" ItemsSource="{Binding Source={StaticResource ResourceKey=LoadedData}}" />
    </DockPanel>
</Window>

This XAML code creates instance of the DataLoader class (instance is named Loader, and it will be referenced later by that name). Furhter on, ObjectDataProvider is defined and linked with Loader. Note again that ObjectDataProvider does not create own instance of the DataLoader class, but it rather references previously created instance. This is because that instance is bound to other controls in UI to provide feedback through them.

Once ObjectDataProvider and loader object are in place, they can be used in bindings on list box, progress bar and Cancel button in the rest of the XAML. Only CancelClick method remains to be defined to call Cancel operation on the loader object:

private void CancelClick(object sender, RoutedEventArgs e)
{
    ((DataLoader)FindResource("Loader")).Cancel();
}

This code produces window as shown in the following picture. First frame depicts window while long operation is in progress. List box is empty as it is still awaiting data; progress bar is displaying operation progress and Cancel button is enabled so that user can cancel the operation. Second frame shows state of the window after cancellation - progress bar is in indeterminate state, Cancel button is disabled because operation has already been cancelled, and list is empty because data loading has been stopped before completed. Last frame shows window after loading has been completed without being cancelled: list is populated with numbers produced by the DataLoader instance, progress bar is full, while Cancel button is dimmed, as it has no function any more.

Data window with progress

Conclusion

In this article we have presented one simple technique that can be employed to provide feedback in long operations, yet to produce all functionality strictly declaratively, i.e. in XAML. Feedback-related code is located where it naturally belongs, and that is in the class which performs long operation. These features are simply bound to UI using XAML elements which is the preferred method in UI programming.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub