Introduction
Many a times, developers starting with WPF struggle with the concept of WPF and background worker. It would be difficult for them to understand the bindings, MVVM and keeping the page responsive. Many of the developers would have observed that if a window is idle for more than a minute, it will be marked as a ghost process and the window would hang. This creates a problem when the process takes a lot of time for loading. Even if the background worker is started, how do you stop the user from navigating to another window or do some other action.
The following implementation will show how to use background workers in WPF and then refresh the collections/bindings after the background worker is completed using MVVM pattern. We will also use an animation to make sure the user doesn’t navigate away from the screen.
Background
It is assumed that the reader has an understanding and experience of the WPF concepts and has hands on experience with WPF. We will cover the topics briefly as and when needed, but we will not cover them in detail as they are separate topics altogether.
Using the Code
Before we start, we would need PRISM. The library can be downloaded from:
The following are the classes and their respective functionalities.
- MainWindow.xaml – It is the WPF window.
- ItemsViewModel.cs – is the View Model where we have written all the Logic.
- ItemsDAL.cs – is the Data Access class but is not doing any DB access. We are using hard coded data.
- OpenWindowBehavior.cs -is the class where all the behaviour of the window like
IsModal
, Open
, Close
, etc. and which file to load is defined. - CircularProgressBar.xaml – is the circular animation which you can download from any site. We won’t be looking at the code of the animation part as the code and explanation can be found in lot of sites
- ItemsModel.cs – is the class which represents the Model.
To start with, we have a window in which we will load list of items on button click event. We have 3 buttons “Load Data With Animation”, “Load Data Without Animation” and “Clear Data”. Just to mimic that, there is a complex processing happening in the ItemsDAL
, i.e., the business class I am putting the thread to sleep for a time more than a minute.
On click of “Load Data With Animation” button, the load data is called asynchronously. During this time, an animation is shown, i.e., is a circular progress bar in this case. So the user knows that some processing is under progress. As the circular progress bar dialog is a modal dialog, the user cannot perform an activity till the processing is complete. Once the processing is complete, the circular progress bar dialog is closed and the data is loaded.
Clear Data will clear the data on the screen.
On click of “Load Data With Out Animation” button, the load is called asynchronously, but the animation is not shown. As there is no animation, you might feel the loading is taking ages. We know that they both are taking the same amount of time. The user will be able to perform other activities like click on other buttons, close the window, etc. This is not the default behavior expected as you might want to restrict the users from doing an activity till the loading is complete. Once the processing is complete, all the data will be loaded.
This is how the UI looks:
Let’s look at the code.
The code for the MainWindow.xaml is as given below:
<Window x:Class="WPFBackgroundWorkersAnimationDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:System.Windows.Interactivity;
assembly=System.Windows.Interactivity"
xmlns:windowBehavior="clr-namespace:WPFBackgroundWorkerAnimationDemo.Animation;
assembly=WPFBackgroundWorkerAnimationDemo.Animation"
Title="Demo" Height="430" Width="525"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
<i:Interaction.Behaviors>
<windowBehavior:OpenWindowBehavior IsModal="True"
Owner="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType={x:Type Window}}}"
IsOpen="{Binding StartAnimation}"
CloseCommand="{Binding StopAnimationCommand}" />
</i:Interaction.Behaviors>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="726*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="350"></RowDefinition>
<RowDefinition Height="25"></RowDefinition>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Grid.ColumnSpan="1"
Margin="5 5 5 5">
<ListView Name="dtgrid" Grid.Row="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.CanContentScroll="False" Style="{x:Null}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" ItemsSource="
{Binding ItemsData}" Background="Transparent"
BorderBrush="Transparent">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Height" Value="53"></Setter>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Style.Triggers>
<Trigger Property="IsSelected"
Value="True">
<Setter Property="Background"
Value="#ECF8F9"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Item" Width="200">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="TxtBlockItem"
Text="{Binding Path=Item}"
VerticalAlignment="Top" Margin="10,0,0,0"
TextTrimming="WordEllipsis"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Description" Width="320">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
MaxHeight="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=ListViewItem}, Path=ActualHeight}"
VerticalScrollBarVisibility="Auto">
<TextBlock Name="txtDescription"
Margin="10 0 0 0"
Text="{Binding Path=Description,Mode=OneWay}"
TextWrapping="Wrap"/>
</ScrollViewer>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
<Border BorderBrush="Black"
Grid.RowSpan="2" BorderThickness="1" />
</Grid>
<StackPanel Grid.Column="1" Grid.Row="2" Height="25"
Width="410" Orientation="Horizontal">
<Button Margin="5 0 0 0"
Command="{Binding LoadDataWithAnimationCommand}"
Content="Load Data With Animation"></Button>
<Button Margin="5 0 0 0"
Command="{Binding LoadDataWithoutAnimationCommand}"
Content="Load Data With Out Animation"></Button>
<Button Margin="5 0 0 0"
Command="{Binding ClearDataCommand}"
Content="Clear All Data"></Button>
</StackPanel>
</Grid>
</Window>
The code for ItemsViewModel.cs is as follows:
public class ItemsViewModel : INotifyPropertyChanged
{
#region View Model Logic
private ObservableCollection<ItemsModel> itemsData;
public ObservableCollection<ItemsModel> ItemsData
{
get { return itemsData; }
set {
itemsData = value;
Refresh("ItemsData");
}
}
private PrismCommands.DelegateCommand loadDataWithAnimationCommand;
public ICommand LoadDataWithAnimationCommand
{
get
{
if (loadDataWithAnimationCommand == null)
{
loadDataWithAnimationCommand = new PrismCommands.DelegateCommand
(LoadDataWithAnimation);
}
return loadDataWithAnimationCommand;
}
}
private PrismCommands.DelegateCommand loadDataWithoutAnimationCommand;
public ICommand LoadDataWithoutAnimationCommand
{
get
{
if (loadDataWithoutAnimationCommand == null)
{
loadDataWithoutAnimationCommand = new PrismCommands.DelegateCommand
(LoadDataWithOutAnimation);
}
return loadDataWithoutAnimationCommand;
}
}
private PrismCommands.DelegateCommand clearDataCommand;
public ICommand ClearDataCommand
{
get
{
if (clearDataCommand == null)
{
clearDataCommand = new PrismCommands.DelegateCommand(ClearData);
}
return clearDataCommand;
}
}
public void LoadDataWithOutAnimation()
{
BackgroundWorker bgWorker = new BackgroundWorker();
bgWorker.DoWork += (sender, e) => LoadItemsWithOutAnimation(sender, e);
bgWorker.RunWorkerAsync();
}
public void LoadItemsWithOutAnimation(object sender, DoWorkEventArgs e)
{
ItemsDAL dataAccessLayer = new ItemsDAL();
List<ItemsModel> lstItemsModel = dataAccessLayer.LoadItems();
PopulateItems(lstItemsModel);
}
public void LoadDataWithAnimation()
{
BackgroundWorker bgWorker = new BackgroundWorker();
bgWorker.DoWork += (sender, e) => LoadItemsWithAnimation(sender, e);
bgWorker.RunWorkerCompleted += LoadItemsCompleted;
bgWorker.RunWorkerAsync();
}
public void LoadItemsWithAnimation(object sender, DoWorkEventArgs e)
{
StartAnimation = true;
ItemsDAL dataAccessLayer = new ItemsDAL();
List<ItemsModel> lstItemsModel = dataAccessLayer.LoadItems();
PopulateItems(lstItemsModel);
}
ObservableCollection<ItemsModel> obColl = new ObservableCollection<ItemsModel>();
private void PopulateItems(List<ItemsModel> lstItemsModel)
{
foreach(ItemsModel item in lstItemsModel)
{
obColl.Add(item);
}
Application.Current.Dispatcher.Invoke(new Action(delegate
{
ItemsData = obColl;
}));
}
public void LoadItemsCompleted(object sender, RunWorkerCompletedEventArgs e)
{
StartAnimation = false;
}
public void ClearData()
{
ItemsData = new ObservableCollection<ItemsModel>();
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void Refresh(string propertyName)
{
if (this.PropertyChanged != null)
{
var e = new PropertyChangedEventArgs(propertyName);
this.PropertyChanged(this, e);
}
}
#endregion INotifyPropertyChanged Members
#region Animation
private bool startAnimation;
public bool StartAnimation
{
get { return startAnimation; }
set
{
startAnimation = value;
Refresh("StartAnimation");
}
}
private Microsoft.Practices.Prism.Commands.DelegateCommand<bool?> stopAnimationCommand;
public ICommand StopAnimationCommand
{
get
{
if (stopAnimationCommand == null)
{
stopAnimationCommand = new Microsoft.Practices.Prism.Commands.DelegateCommand<bool?>
(StopAnimation, CanStopAnimation);
}
return stopAnimationCommand;
}
}
public bool CanStopAnimation(bool? param)
{
return true;
}
public void StopAnimation(bool? param)
{
StartAnimation = false;
}
#endregion
}
We got to know the difference in user experience it makes and how to bind the code call it asynchronously. Hope this helps lots of developers in a real time scenario.
Points of Interest
As mentioned, this article tells about providing a good user experience and making development easy.
To run the ProgressBar
, we have used System.Windows.Interactivity
to start and close the animation from a viewmodel
.
To know more about System.Windows.Interactivity
, check out:
I have deliberately not concentrated on the architecture of the application. I have kept it simple with only one focus point for the users to understand.
History
- 03 March 2015: First version 1.0
- 05 March 2015: Images link updated
- 15 March 2015: Source code added