13 May 2020
Category: Mini
Hi everyone,
This is slowly becomming a series… :P Next task I wanted to get right was async process with BackgroundWorker.
I’m not an expert in asynchronous programming, but it seems to me that BW is better solution over async-await
model. Or at least when I want good control over the process.
This mini also includes FontAwesome Spinner
(you can find installation process in the link). I love this one and it’s easy to use.
So, what does this mini do? It simulates a continuous feed from some data source. In this very simplified example
it’s just a delayed crawl through the list of data. But imagine loading post by post from an http request. This mini demonstrates a way to achieve it without a frozen app.
Important notes:
And last but not least - this is definitely not exhaustive reference project for BackgroundWorkers, just a short
demonstration of how this works.
I hope this could help somebody on their path to conquer WPF. And as usual, there is a link to the repo with the project.
Petr
Main Window:
<Window x:Class="AsyncFeedWithSpinner.View.AsyncFeedWithSpinner"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:local="clr-namespace:AsyncFeedWithSpinner.View"
mc:Ignorable="d"
Title="Async Feed With Spinner"
Height="350"
Width="350"
SizeToContent="Height"
>
<StackPanel x:Name="container" VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0,20">
<Border BorderBrush="Coral" BorderThickness="1" CornerRadius="10" Margin="0,0,0,20">
<Label VerticalAlignment="Top" HorizontalAlignment="Center"
Content="{Binding Title}"
FontWeight="Bold"
FontSize="16"
/>
</Border>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Read Data"
Margin="5"
Grid.Column="0"
Grid.Row="0"
Command="{Binding SpinSpinnerCommand}"
/>
<fa:ImageAwesome Icon="Cog"
Grid.Column="1" Grid.Row="0"
Margin="10, 5"
Height="30"
Spin="{Binding SpinnerShouldSpin}"
VerticalAlignment="Center" HorizontalAlignment="Center"
/>
<ListBox Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
MinHeight="100"
x:Name="ReceivingListBox"
ItemsSource="{Binding ReceivedStrings}"/>
</Grid>
</StackPanel>
</Window>
MainWindow cs:
using AsyncFeedWithSpinner.ViewModel;
using System.Windows;
namespace AsyncFeedWithSpinner.View
{
/// <summary>
/// Interaction logic for AsyncFeedWithSpinner.xaml
/// </summary>
public partial class AsyncFeedWithSpinner : Window
{
MainVM MainVM;
public AsyncFeedWithSpinner()
{
InitializeComponent();
MainVM = new MainVM();
container.DataContext = MainVM;
}
}
}
MainVM:
using AsyncFeedWithSpinner.ViewModel.Commands;
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
namespace AsyncFeedWithSpinner.ViewModel
{
public class MainVM : INotifyPropertyChanged
{
private bool spinnerShouldSpin;
public bool SpinnerShouldSpin
{
get { return spinnerShouldSpin; }
set { spinnerShouldSpin = value; OnPropertyChanged(nameof(SpinnerShouldSpin)); }
}
private string title;
public string Title
{
get { return title; }
set { title = value; OnPropertyChanged(nameof(Title)); }
}
public ObservableCollection<string> ReceivedStrings { get; set; }
private List<string> dummyStringData = new List<string>();
public FetchDataCommand SpinSpinnerCommand { get; set; }
public MainVM()
{
spinnerShouldSpin = false;
Title = "Asynchronous feed with Spinner\nClick Read Data to start the feed.";
ReceivedStrings = new ObservableCollection<string>();
SpinSpinnerCommand = new FetchDataCommand(this);
generateDummyData();
}
private void generateDummyData()
{
for (int i = 0; i < 20; i++)
{
dummyStringData.Add($"Dummy string data, line: {i}");
}
}
public void FetchData()
{
ReceivedStrings.Clear();
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += BackgroundWorker_DoWork;
worker.WorkerReportsProgress = true;
worker.ProgressChanged += BackgroundWorker_ProgressChanged;
worker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
worker.RunWorkerAsync(new BackgrounWorkerState(dummyStringData));
}
/// <summary>
/// Wrapper class for BackgroundWorker RunWorkerAsync Argument
/// </summary>
sealed private class BackgrounWorkerState
{
public List<string> stringList { get; private set; }
public BackgrounWorkerState(List<string> stringList)
{
this.stringList = stringList ?? throw new ArgumentNullException(nameof(stringList));
}
}
/// <summary>
/// Cant update UI thread from DoWork so instead sending data into ProgressChanged
/// </summary>
/// <param name="sender">BackgroundWorker</param>
/// <param name="e"></param>
private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
SpinSpinner(true);
var workingData = (e.Argument as BackgrounWorkerState).stringList;
BackgroundWorker worker = sender as BackgroundWorker;
double progressStep = (100 / workingData.Count);
double currentProgress = 0;
workingData.ForEach(s =>
{
currentProgress += progressStep;
worker.ReportProgress((int)currentProgress, s);
Thread.Sleep(200);
});
}
/// <summary>
/// e.UserState holds state data sent from DoWork
/// </summary>
/// <param name="sender">BackgroundWorker</param>
/// <param name="e"></param>
private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if(e.UserState != null)
{
ReceivedStrings.Add((string)e.UserState);
}
Console.WriteLine($"BW-ProgressChanged: ProgressPercentage = {e.ProgressPercentage}");
}
private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
var resultLabel = "";
if (e.Cancelled == true)
{
resultLabel = "Canceled!";
}
else if (e.Error != null)
{
resultLabel = "Error: " + e.Error.Message;
}
else
{
resultLabel = "Done!";
}
Console.WriteLine($"BW-Completed status: {resultLabel}");
SpinSpinner(false);
}
public void SpinSpinner(bool shouldSpin)
{
SpinnerShouldSpin = shouldSpin;
Console.WriteLine($"MainVM/SpinSpinner: SpinnerShouldSpin is: {SpinnerShouldSpin}");
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
fetchDataCommand:
using System;
using System.Windows.Input;
namespace AsyncFeedWithSpinner.ViewModel.Commands
{
public class FetchDataCommand : ICommand
{
private MainVM VM = null;
public event EventHandler CanExecuteChanged;
public FetchDataCommand(MainVM vM)
{
VM = vM ?? throw new ArgumentNullException(nameof(vM));
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
VM.FetchData();
}
}
}
Petr