Wednesday, April 3, 2024

I found a bug in the Visual Basic editor of Visual Studio

I remember 50 years ago my math teacher spent a lesson on arithmetic precision, or the importance of knowing how accurate your numbers are.

For example...

1.4  <= I am confident this number lies between 1.35 and 1.45

1.40 <= I am confident this number lies between 1.395 and 1.405

There is a difference, even if it's not always important.

In .Net decimal variables know their precision. For example, in C# the code

            float f = 1.40F;
            Console.WriteLine(f);
            decimal d = 1.40M;
            Console.WriteLine(d);

outputs

1.4 1.40

As you can see, floats (and doubles) do not know their precision but Decimals do.

Similarly in Visual Basic, decimals understand their precision but you cannot enter a decimal literal with trailing zeros because the editor won't let you.

Dim d As Decimal = 1.40D    <= The editor removes the trailing zero as soon as you leave the line

However, you can initialize the decimal with a Decimal.Parse and the trailing zero is honored.

        Dim d As Decimal = Decimal.Parse("1.40")
        Console.WriteLine(d)

outputs

1.40

So it's clear both C# and Visual Basic treat decimals the same internally (as I would expect) but the Visual Basic editor has a bug that makes it think it should remove trailing zeroes even when it should not.

Looking through the editor options and Googling does not reveal a way to suppress this behavior. It's just a bug.

Friday, March 1, 2024

A converter to filter lists before binding to them

A member of my team has a user control with two dropdown lists on it. The dropdown lists were to display the same items except that one of them was to exclude one of the items. He could have created two different collections and bound each dropdown list to its own collection, but he decided to write a converter for one of them that would exclude the unwanted item. In my opinion, this is the superior solution.

But he wrote a converter that was specific to this one requirement and it occurred to me that it should be possible to write a more generic converter using reflection. So I took a crack at it and this is my solution.

The idea is that you bind the dropdown list's ItemSource to a collection and pass a filter in as the converter parameter. Something like 'ID <> 0'. The converter will only return items whose ID is not zero, thus removing the item whose ID is zero.

Start a new Visual Studio C# project called FilteredEnumerableConverterDemo. 

Add a class called Converters and add the FilteredEnumerableConverter like this. It only supports Lists and ObservableCollections. The parameter has very strict syntax and only supports basic comparisons. Feel free to use it as a starting point to add ranges, lists, startswith, etc.

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

namespace FilteredEnumerableConverterDemo
{
    public class FilteredEnumerableConverter : IValueConverter
    {
        /// <summary>
        ///     Returns only the enumerable members with properties that match the filter specified in the parameter
        /// </summary>
        /// <param name="value">A List or ObservableCollection</param>
        /// <param name="targetType"></param>
        /// <param name="parameter">filter in the form <propertyname> <op> <value> </param>
        /// <param name="culture"></param>
        /// <returns>An Enumerable containing only the members that match the filter</returns>
        /// <exception cref="NotImplementedException"></exception>
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (string.IsNullOrWhiteSpace(parameter?.ToString())) return value;
            if (value == null) return null;

            try
            {
                if (new[] { "List`1", "ObservableCollection`1" }.Contains(value.GetType().Name))
                    return FilterList((System.Collections.IEnumerable)value, parameter.ToString());
                else
                    return value;
            }
            catch (Exception ex)
            {
                throw new Exception("FilteredEnumerableConverter:" + ex.Message);
            }
        }

        private System.Collections.IEnumerable FilterList(System.Collections.IEnumerable list, string filter)
        {
            List<object> newList = new List<object>();
            String[] filterParts = filter.Split(' ');

            if (filterParts.Length != 3) return list;

            String propertyName = filterParts[0];
            String op = filterParts[1];
            String targetValue = filterParts[2];
            String sourceValue;
            bool useElement;

            Type T = list.GetType().GetGenericArguments()[0];
            PropertyInfo PI = T.GetProperty(propertyName);
            if (PI == null) return list;

            foreach (object element in list)
            {
                useElement = false;
                sourceValue = PI.GetValue(element).ToString();
                switch (op)
                {
                    case "=": useElement = (sourceValue == targetValue); break;
                    case ">": useElement = (sourceValue.CompareTo(targetValue) > 0); break;
                    case "<": useElement = (sourceValue.CompareTo(targetValue) < 0); break;
                    case ">=": useElement = (sourceValue.CompareTo(targetValue) >= 0); break;
                    case "<=": useElement = (sourceValue.CompareTo(targetValue) <= 0); break;
                    case "!=":
                    case "<>": useElement = (sourceValue != targetValue); break;
                }
                if (useElement)
                    newList.Add(element);
            }

            return newList;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value;
        }
    }
}

Now let's consume this converter.

Change MainWindow to look like this...

<Window x:Class="FilteredEnumerableConverterDemo.MainWindow"
        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:local="clr-namespace:FilteredEnumerableConverterDemo"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:FilteredEnumerableConverter x:Key="FilteredEnumerableConverter"/>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" Grid.Column="0" Text="All animals"/>
        <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Animals}" DisplayMemberPath="Name" HorizontalAlignment="Stretch"/>

        <TextBlock Grid.Row="1" Grid.Column="0" Text="Dangerous animals"/>
        <ComboBox Grid.Row="1" Grid.Column="1" DisplayMemberPath="Name" HorizontalAlignment="Stretch"
                  ItemsSource="{Binding Animals, Converter={StaticResource FilteredEnumerableConverter}, ConverterParameter='IsDangerous = True'}"/>

        <TextBlock Grid.Row="2" Grid.Column="0" Text="Edible animals"/>
        <ComboBox Grid.Row="2" Grid.Column="1" DisplayMemberPath="Name" HorizontalAlignment="Stretch"
                  ItemsSource="{Binding Animals, Converter={StaticResource FilteredEnumerableConverter}, ConverterParameter='IsEdible = True'}"/>
    </Grid>
</Window>

----------------------------------------------------------
using System.Windows;

namespace FilteredEnumerableConverterDemo
{
    public partial class MainWindow : Window
    {
        public class cAnimal
        {
            public String Name { get; set; }
            public bool IsDangerous { get; set; }
            public bool IsEdible {  get; set; }
        }

        public List<cAnimal> Animals { get; set; } = new List<cAnimal>()
        {
            new cAnimal() {Name="Lion", IsDangerous=true, IsEdible=false},
            new cAnimal() {Name="Cockroach", IsDangerous=false, IsEdible=false},
            new cAnimal() {Name="Cow", IsDangerous=false, IsEdible=true},
            new cAnimal() {Name="Rattlesnake", IsDangerous=true, IsEdible=true}
        };

        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

If you run the application you can see three dropdown lists, all populated from the same collection, but with different lists.




Note: < and > operators have to be XML encoded. eg.

        <TextBlock Grid.Row="3" Grid.Column="0" Text="A-M only"/>
        <ComboBox Grid.Row="3" Grid.Column="1" DisplayMemberPath="Name" HorizontalAlignment="Stretch"
                  ItemsSource="{Binding Animals, Converter={StaticResource FilteredEnumerableConverter}, ConverterParameter='Name &lt;= M'}"/>



Monday, February 26, 2024

Creating a meaningful and unique file name

We have applications that create a report when the user hits a button. The application creates a temporary file which we pass to Process.Start to launch the appropriate viewer (normally, but not always, Acrobat Reader). The temporary file is named using the GetTempFileName function and dropped in the temporary folder. One advantage to using this technique is it guarantees a unique file name.

Imagine that the user is looking at document ABC and presses the Print button. We generate a file called "Document ABC.pdf" and launch Acrobat Reader. Then the user presses the Print button again. We cannot create a new Document ABD.pdf file because the original is still open in Acrobat Reader. That's the advantage of GetTempFileName.

In addition we have applications that can generate reports for arbitrary lists of documents. If the file name lists all the document numbers, we could exceed the maximum file name length.

I wrote a function to do all this over the weekend. It is written as a VB console app. It has nothing to do with WPF, but it was interesting.

Imports System.IO
Imports System.Reflection.Metadata.Ecma335

Module Program
    Sub Main(args As String())
        Dim DocumentType As String = "PO"
        Dim DocumentNumbers As New List(Of Integer) From {123, 240001, 240010, 240017, 240002, 240003, 240016}
        Dim FileTypeSuffix As String = "pdf"
        Dim FolderName As String = IO.Path.GetTempPath()
        Dim FileName As String = ""

        Try
            FileName = GetUniqueFileName(FolderName, DocumentType, DocumentNumbers, FileTypeSuffix)

            ' Make sure we got a valid, creatable file name
            File.Create(Path.Combine(FolderName, FileName), 256, FileOptions.DeleteOnClose).Close()
            Console.WriteLine(Path.Combine(FolderName, FileName))
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try
        Console.ReadLine()

    End Sub

    ''' <summary>
    ''' Return a unique and meaningful file name
    ''' </summary>
    ''' <param name="DocumentType">I description of the types of document in the list. Included in the file name</param>
    ''' <param name="DocumentNumbers">A list of document numbers</param>
    ''' <param name="FileTypeSuffix">The suffix for the file name</param>
    ''' <returns>A unique and meaningful filaname. Could delete an existing file</returns>
    Function GetUniqueFileName(FolderName As String, DocumentType As String, DocumentNumbers As List(Of Integer), FileTypeSuffix As String) As String

        Dim DocumentNumberRanges As New Dictionary(Of Integer, Integer)()
        Dim DocumentNumberRangesAsString As New List(Of String)()
        Dim DocumentFormatString As String = "000000"
        Dim DateFormatString As String = " yyyy-MM-dd"  ' If you want to add time, remember you cannot have colons in file names
        Dim MaxDocumentNumbers As Integer = 5
        Dim StrictlyEnforceMax As Boolean = False       ' When false, this uses the full range if the list ends with a range
        Dim DocumentNumberCount As Integer = 0
        Dim IsMoreDocuments As Boolean = False
        Dim FileName As String = ""
        Dim DeleteExistingFile As Boolean = True        ' Can we delete an existing file to enforce uniqueness?

        Try
            DocumentNumberRanges = ConvertListToRanges(DocumentNumbers)
            For Each Range As KeyValuePair(Of Integer, Integer) In DocumentNumberRanges
                If DocumentNumberCount < MaxDocumentNumbers Then
                    If Range.Key = Range.Value OrElse (DocumentNumberCount + 2 > MaxDocumentNumbers And StrictlyEnforceMax) Then
                        DocumentNumberRangesAsString.Add(Range.Key.ToString(DocumentFormatString))
                        DocumentNumberCount += 1
                    Else
                        DocumentNumberRangesAsString.Add($"{Range.Key.ToString(DocumentFormatString)}-{Range.Value.ToString(DocumentFormatString)}")
                        DocumentNumberCount += 2
                    End If
                Else
                    IsMoreDocuments = True
                End If
            Next
            FileName = $"{DocumentType} {String.Join(",", DocumentNumberRangesAsString)}{If(IsMoreDocuments, "...", "")}{Date.Now.ToString(DateFormatString)}"
            FileName = UniquifyFileName(FolderName, FileName, FileTypeSuffix, DeleteExisting:=DeleteExistingFile)
            Return FileName
        Catch ex As Exception
            Throw New Exception("GetUniqueFileName:" & ex.Message)
        End Try

    End Function

    ''' <summary>
    ''' Convert a random list of values into a sorted list of ranges
    ''' </summary>
    ''' <param name="Values">A list of numbers</param>
    ''' <returns>The list sorted into a list of ranges</returns>
    Function ConvertListToRanges(Values As List(Of Integer)) As Dictionary(Of Integer, Integer)
        Dim Ranges As New Dictionary(Of Integer, Integer)

        Try
            Values.Sort()
            For Each Value As Integer In Values
                If Ranges.Count = 0 OrElse Value <> Ranges.Last().Value + 1 Then
                    Ranges.Add(Value, Value)
                Else
                    Ranges(Ranges.Last().Key) = Value
                End If
            Next
            Return Ranges
        Catch ex As Exception
            Throw New Exception("ConvertListToRanges:" & ex.Message)
        End Try
    End Function

    ''' <summary>
    ''' Uniquifies a file name by inserting (Copy n) to avoid conflicting with an existing file
    ''' </summary>
    ''' <param name="FileName">The file name with no suffix</param>
    ''' <param name="FileTypeSuffix">The file suffix</param>
    ''' <param name="DeleteExisting">Uniquify the file name by deleting the existing file if possible</param>
    ''' <returns>A unique file name in the form {filename}[ (Copy n)].{suffix}</returns>
    Function UniquifyFileName(FolderName As String, FileName As String, FileTypeSuffix As String, DeleteExisting As Boolean) As String

        Dim NewFileName As String = $"{FileName}.{FileTypeSuffix}"
        Dim CopyNumber As Integer = 0
        Dim FileExists As Boolean

        Try
            Do
                FileExists = File.Exists(Path.Combine(FolderName, NewFileName))
                If FileExists And DeleteExisting Then
                    Try
                        File.Delete(Path.Combine(FolderName, NewFileName))
                        FileExists = False
                    Catch ex As Exception
                        ' We could not delete it
                    End Try
                End If
                If FileExists Then
                    CopyNumber += 1
                    NewFileName = $"{FileName} (Copy {CopyNumber}).{FileTypeSuffix}"
                End If
            Loop Until Not FileExists
        Catch ex As Exception
            Throw New Exception("ApplyCopyNumber:" & ex.Message)
        End Try
        Return NewFileName

    End Function
End Module

If you run the program you get this output.




Tuesday, January 23, 2024

Event Aggregator

I found a good article about basic event aggregation on Stack Overflow and decided to create a demonstration that runs on WPF using MVVM. The original article was written by Mike Hankey.

https://www.codeproject.com/Articles/5376132/EventAggregator-My-Take

I recommend you read his article first. I think it needs an unsubscribe method otherwise references to the handlers will prevent objects being removed from memory. Here's my fully working WPF version.

Use Visual Studio to create a C# WPF Core project called EventAggregator. We will start by creating some base classes.

Add a class called NotifyWindow. It inherits Window and adds NotifyPropertyChanged functionality.

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace EventAggregator
{
    public class NotifyWindow : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

Add a class called EventAggregator. This is taken directly from Mike's code. I added my own Unsubscribe method.

namespace EventAggregator
{
    public class EventAggregator<T>
    {
        private readonly Dictionary<Type, List<Delegate>> subscribers = new Dictionary<Type, List<Delegate>>();

        public void Subscribe<TEvent>(Action<TEvent> handler)
        {
            Type eventType = typeof(TEvent);
            if (!subscribers.TryGetValue(eventType, out var handlers))
            {
                handlers = new List<Delegate>();
                subscribers[eventType] = handlers;
            }

            handlers.Add(handler);
        }

        public bool Unsubscribe<TEvent>(Action<TEvent> handler)
        {
            if (subscribers.TryGetValue(typeof(TEvent), out var handlers))
            {
                return handlers.Remove(handler);
            }
            return false;
        }

        public void Publish<TEvent>(TEvent message)
        {
            Type eventType = typeof(TEvent);
            if (subscribers.TryGetValue(eventType, out var handlers))
            {
                foreach (Action<TEvent> handler in handlers)
                {
                    handler.Invoke(message);
                }
            }
        }
    }
}

The EventAggregator class is very generic and can handle messages of any type as long as the type implements IEvent. In this example we define a message of type TestMessage. I have simplified Mike's TestMessage class (and made it less useful - you should probably stay with his class). Add a class called EventArgs.

namespace EventAggregator
{
    public interface IEventArgs<T>
    {
        T GetData();
    }

    public class Test
    {
        public class TestMessage : IEventArgs<String>
        {
            String _Message = "";

            public TestMessage(string s)
            {
                _Message = s;
            }

            public String GetData()
            {
                return _Message;
            }
        }
    }
}

The next step is to create an instance of the aggregator. In this example I have declared it in the App class. In most scenarios it will be accessible through dependency injection. Alter App.xaml.cs to look like this.

using System.Windows;

namespace EventAggregator
{
    public partial class App : Application
    {
        public static EventAggregator<IEventArgs<Object>> g_Aggregator = new EventAggregator<IEventArgs<Object>>();
    }
}

The application will display a main window and a dialog window. The two windows will communicate through a publish/subscribe model. Add a Window called DialogWindow. The xaml and C# look like this. Changing the value of DialogMessage causes all subscribers to be notified.

<local:NotifyWindow x:Class="EventAggregator.DialogWindow"
        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:local="clr-namespace:EventAggregator"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="DialogWindow" Height="450" Width="800">
    <StackPanel Orientation="Vertical" HorizontalAlignment="Left">
        <TextBlock Text="Please enter a message for the main window"/>
        <TextBox Text="{Binding DialogMessage, UpdateSourceTrigger=PropertyChanged}" Width="200" BorderBrush="Gray" BorderThickness="1"/>
    </StackPanel>
</local:NotifyWindow>


namespace EventAggregator
{
    public partial class DialogWindow : NotifyWindow
    {
        private string _DialogMessage = "";
        public string DialogMessage
        {
            get => _DialogMessage;
            set
            {
                SetProperty(ref _DialogMessage, value);
                App.g_Aggregator.Publish(new Test.TestMessage(DialogMessage));
            }
        }

        public DialogWindow()
        {
            InitializeComponent();
        }
    }
}

Finally, here is the MainWindow which subscribes to the TestMessage publications.

<local:NotifyWindow x:Class="EventAggregator.MainWindow"
        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:local="clr-namespace:EventAggregator"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Vertical">
        <TextBlock Text="A message from the dialog window"/>
        <TextBlock Text="{Binding DialogMessage}"/>
    </StackPanel>
</local:NotifyWindow>


namespace EventAggregator
{
    public partial class MainWindow : NotifyWindow
    {
        private string _DialogMessage = "";
        public string DialogMessage
        {
            get => _DialogMessage;
            set => SetProperty(ref _DialogMessage, value);
        }

        public MainWindow()
        {
            this.Unloaded += MainWindow_Unloaded;
            InitializeComponent();

            DialogWindow dw = new DialogWindow();
            App.g_Aggregator.Subscribe<Test.TestMessage>(HandleTestMessage);

            dw.Show();
        }

        private void MainWindow_Unloaded(object sender, System.Windows.RoutedEventArgs e)
        {
            App.g_Aggregator.Unsubscribe<Test.TestMessage>(HandleTestMessage);
        }

        private void HandleTestMessage(Test.TestMessage message)
        {
            DialogMessage = message.GetData();
        }
    }
}

When you run this application you see two windows. Anything typed into the dialog window is updated in the main window through the event aggregator.





Friday, January 19, 2024

Blocking the UI thread might not be so bad

I can't believe it's been so long since I last posted. We've been very busy here at work.

We have a standard client/server architecture and when the server is doing things, the client is not responsive because the call to the server is made on the UI thread. This means the user cannot move or resize the client while it's waiting for the server to do its thing. Many people think this is bad.

I have been following the UK postal service scandal where hundreds of post office masters were prosecuted for theft and fraud because of errors in the new accounting software they were required to use.

https://www.theguardian.com/uk-news/2024/jan/09/how-the-post-offices-horizon-system-failed-a-technical-breakdown

One thing in the article that caught my attention was that clicking the [Enter] button multiple times while the software appeared to be frozen caused the transaction to be sent multiple times.

One, named the “Dalmellington Bug”, after the village in Scotland where a post office operator first fell prey to it, would see the screen freeze as the user was attempting to confirm receipt of cash. Each time the user pressed “enter” on the frozen screen, it would silently update the record. In Dalmellington, that bug created a £24,000 discrepancy, which the Post Office tried to hold the post office operator responsible for.

Our software does not have that problem, because we are communicating with the server on the UI thread. Is there an alternative?

I saw this project that explains how to implement a task to execute code off the UI thread. I converted it to be MVVM compliant and noticed you can still click the button even while the code from the first click was executing. This is similar to the Dalmellington bug.

Here's the WPF project with the code run on the UI thread. The project is written in C# using .Net 8 and is called Tasks.

<Window x:Class="Tasks.MainWindow"
        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:local="clr-namespace:Tasks"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="RunCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource RunCommand}" Executed="Run_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <Button Content="The Button" Command="{StaticResource RunCommand}"/>
    </Grid>
</Window>

using System.Windows;
namespace Tasks
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Run_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            for (int i = 0; i != 100; ++i)
            {
                Thread.Sleep(100);
            }
        }
    }
}



Clicking the button runs a task on the UI thread. The user cannot move, resize, or close the application until the task has completed. Button clicks while the task is still running are ignored.

When I implemented a Task to run the code off the UI thread I added a progress monitor and the project looks like this. Note progress bars don't like null values so I wrote a converter.

<Window x:Class="Tasks.MainWindow"
        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:local="clr-namespace:Tasks"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:NullConverter x:Key="NullConverter"/>
        <RoutedCommand x:Key="RunCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource RunCommand}" Executed="Run_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <Button Content="The Button" Command="{StaticResource RunCommand}"/>
        <ProgressBar Value="{Binding ProgressValue, Converter={StaticResource NullConverter}}"
VerticalAlignment="Top" HorizontalAlignment="Stretch" Height="10">
            <ProgressBar.Style>
                <Style TargetType="ProgressBar">
                    <Setter Property="Visibility" Value="Visible"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding ProgressValue}" Value="{x:Null}">
                            <Setter Property="Visibility" Value="Hidden"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ProgressBar.Style>
        </ProgressBar>
    </Grid>
</Window>


using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace Tasks
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private int? _ProgressValue = null;
        public int? ProgressValue
        {
            get => _ProgressValue;
            set { SetProperty(ref _ProgressValue, value); }
        }

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Run_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            IProgress<int> progress = new Progress<int>(value => ProgressValue = value);
            await Task.Run(() =>
            {
                for (int i = 0; i != 100; ++i)
                {
                    progress.Report(i);
                    Thread.Sleep(100);
                }
            });
            ProgressValue = null;
        }

        public event PropertyChangedEventHandler? PropertyChanged;
        public void SetProperty<T>(ref T storage, T value, [CallerMemberName] string name = "")
        {
            if (!Object.Equals(storage, value))
            {
                storage = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}

The NullConverter goes in a class library and looks like this.

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

namespace Tasks
{
    class NullConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
           return (value == null) ? 0 : System.Convert.ToInt32(value);
        }

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


Because we are executing the code in a task, the UI still responds to the user. They can move, resize, and close the application. But they can also still click the button. Clicking the button while the task is executing causes the task to execute again. If the task was posting a new transaction, it would be posted multiple times.

A solution is to bind the IsEnabled property of the window or root element to the ProgressValue property via a new converter. That's why I made ProgressValue nullable.

Here's the new converter and the modified XAML.

    class IsNullConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (value == null);
        }

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

<Window x:Class="Tasks.MainWindow"
        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:local="clr-namespace:Tasks"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:NullConverter x:Key="NullConverter"/>
        <local:IsNullConverter x:Key="IsNullConverter"/>
        <RoutedCommand x:Key="RunCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource RunCommand}" Executed="Run_Executed"/>
    </Window.CommandBindings>
    <Grid IsEnabled="{Binding ProgressValue, Converter={StaticResource IsNullConverter}}">
        <Button Content="The Button" Command="{StaticResource RunCommand}"/>
        <ProgressBar Value="{Binding ProgressValue, Converter={StaticResource NullConverter}}"
VerticalAlignment="Top" HorizontalAlignment="Stretch" Height="10">
            <ProgressBar.Style>
                <Style TargetType="ProgressBar">
                    <Setter Property="Visibility" Value="Visible"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding ProgressValue}" Value="{x:Null}">
                            <Setter Property="Visibility" Value="Hidden"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ProgressBar.Style>
        </ProgressBar>
    </Grid>
</Window>

This causes all the controls on the page to be disabled while the task is executing (while ProgressValue is not null). If you run the application now, you can still move and resize the client while the task is executing but button clicks are ignored.

I could have bound the Window's IsEnabled property, but the XAML to do that is a little more complex and binding the root element is just as good.

So blocking the UI thread while performing background tasks has its benefits, but there is a fairly easy way to safely perform tasks on non-UI threads, especially if it is implemented in a base class.