Asynchronous feed with Spinner indicator in WPF with MVVM pattern

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.

Cheers!

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();
        }
    }
}

Cheers!

Petr