Asynchronous Indication of a progress in WPF with MVVM pattern

17 May 2020

Category: Mini

Hi everyone,
I was playing some more with tasks and async-await because I was curious how to make looping task to be looping just as long as the other long task is running. Functionality like loaders on websites and games and such. I felt BackgrounbdWorker is not a good tool for this so I chose Tasks for this. After some digging and posting questions on Stack Overflow, I found the solution. THANKS to Stack Overflow community - a place where I always find help or answers. Important notes:

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="AsynIndicationStartStop.View.AsyncIndicationStartStopWindow"
        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:commands="clr-namespace:AsynIndicationStartStop.ViewModel.Commands"
        xmlns:converters="clr-namespace:AsynIndicationStartStop.ViewModel.Converters"
        xmlns:local="clr-namespace:AsynIndicationStartStop.View"
        mc:Ignorable="d"
        Title="Async Indication Start Stop" Height="273" Width="440">

    <Window.Resources>
        <converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
    </Window.Resources>
    
    <Grid x:Name="container"  VerticalAlignment="Center" HorizontalAlignment="Center">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <Border BorderBrush="Coral" BorderThickness="1" CornerRadius="10" 
                Grid.Row="0" Grid.ColumnSpan="2"
                >
            <Label VerticalAlignment="Stretch" HorizontalAlignment="Center"
               Content="{Binding Title}"
               FontSize="25"               
               />
        </Border>
        <Button Grid.Column="0" Grid.Row="1" Margin="10, 20"
                Content="Start Process"
                HorizontalAlignment="Center"
                FontSize="20"
                Command="{Binding UpdateProgressCommand}"/>
        <TextBlock Grid.Column="1" Grid.Row="1"
                   FontSize="20"
                   VerticalAlignment="Center"
                   Text="{Binding ProgressText}"/>
        <Border BorderBrush="Coral" BorderThickness="1" CornerRadius="10" Margin="0,0,0,20"
                Grid.Row="2" Grid.ColumnSpan="2"
                Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisibilityConverter}}"
                >
            <Label VerticalAlignment="Stretch" HorizontalAlignment="Center"
               Content="Long working task is running."
               FontSize="25"               
               />
        </Border>
    </Grid>
</Window>

MainWindow cs:

using AsynIndicationStartStop.ViewModel;
using System.Windows;

namespace AsynIndicationStartStop.View
{    
    /// <summary>
    /// Interaction logic for AsyncIndicationStartStopWindow.xaml
    /// </summary>
    public partial class AsyncIndicationStartStopWindow : Window
    {
        public MainVM MainVM;

        public AsyncIndicationStartStopWindow()
        {
            InitializeComponent();
            MainVM = new MainVM();
            container.DataContext = MainVM;
        }
    }
}

MainVM:

using AsynIndicationStartStop.ViewModel.Commands;
using JetBrains.Annotations;
using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace AsynIndicationStartStop.ViewModel
{
    public class MainVM : INotifyPropertyChanged
    {
        private const string PROGRESS = "Progress";
        private const int PROGRESS_DELAY = 200;

        private string progressText;

        public string ProgressText
        {
            get { return progressText; }
            set { progressText = value; OnPropertyChanged(); }
        }

        private bool isRunning;

        public bool IsRunning
        {
            get { return isRunning; }
            set { isRunning = value; OnPropertyChanged(); }
        }

        private string title;

        public string Title
        {
            get { return title; }
            set { title = value; OnPropertyChanged(nameof(Title)); }
        }

        public UpdateProgressCommand UpdateProgressCommand { get; set; }

        public MainVM()
        {
            ProgressText = PROGRESS;
            Title = "Async process looping till long working task finishes.";

            UpdateProgressCommand = new UpdateProgressCommand(this);
        }

        public async void RunProgressTextUpdate()
        {
            var cts = new CancellationTokenSource();

            if (!IsRunning)
            {
                IsRunning = true;
                UpdateProgressTextTask(cts.Token);
                string longTaskText = await Task.Run(() => LongTask(cts));
                await Task.Delay(PROGRESS_DELAY); // Additional delay to prevent alternating finished text by looping task
                ProgressText = longTaskText;
                IsRunning = false;
            }                  
        }

        private void UpdateProgressTextTask(CancellationToken token)
        {
            Task.Run(async () =>
            {
                ProgressText = PROGRESS;
                while (!token.IsCancellationRequested)
                {
                    await Task.Delay(PROGRESS_DELAY);
                    var dotsCount = ProgressText.Count<char>(ch => ch == '.');

                    ProgressText = dotsCount < 6 ? ProgressText + "." : ProgressText.Replace(".", "");
                }
            });
           
        }

        private string LongTask(CancellationTokenSource cts)
        {
            var result = Task.Run(async () =>
            {
                await Task.Delay(5000);
                cts.Cancel();
                return "Long task finished.";
            });

            return result.Result;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

BoolToVisibilityConverter

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace AsynIndicationStartStop.ViewModel.Converters
{
    class BoolToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (bool)value ? Visibility.Visible : Visibility.Hidden;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Cheers!

Petr