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.
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:
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.
Suppose that we have a class which obeys design proposed above. Important elements that are needed to construct XAML are these:
Wish list is quite short as we can see. XAML that produces window shown above can then be constructed by following these simple steps:
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.
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.
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
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
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.