In my WPF application I have an issue where I am writing numerous times to a text file via a StreamWriter object, primarily using the WriteLineAsync() method. I am confident that I am correctly awaiting all of the tasks. However, the UI thread is being blocked, instead of being allowed to process UI changes. I have written a small application that demonstrates the issue.
public class MainWindowViewModel : INotifyPropertyChanged
{
private string _theText;
private string _theText2;
public MainWindowViewModel()
{
TestCommand = new DelegateCommand(TestCommand_Execute, TestCommand_CanExecute);
TestCommand2 = new DelegateCommand(TestCommand2_Execute, TestCommand2_CanExecute);
}
public ICommand TestCommand { get; }
private bool TestCommand_CanExecute(object parameter)
{
return true;
}
private async void TestCommand_Execute(object parameter)
{
using (StreamWriter writer = new StreamWriter(new MemoryStream()))
{
TheText = "Started";
await DoWork(writer).ConfigureAwait(true);
TheText = "Complete";
}
}
public ICommand TestCommand2 { get; }
private bool TestCommand2_CanExecute(object parameter)
{
return true;
}
private async void TestCommand2_Execute(object parameter)
{
using (StreamWriter writer = new StreamWriter(new MemoryStream()))
{
TheText2 = "Started";
await Task.Delay(1).ConfigureAwait(false);
await DoWork(writer).ConfigureAwait(true);
TheText2 = "Complete";
}
}
public string TheText
{
get => _theText;
set => SetValue(ref _theText, value);
}
public string TheText2
{
get => _theText2;
set => SetValue(ref _theText2, value);
}
private bool SetValue<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private async Task DoWork(StreamWriter writer)
{
for (int i = 0; i < 100; i++)
{
await writer.WriteLineAsync("test" + i).ConfigureAwait(false);
Thread.Sleep(100);
}
}
}
And the XAML
<Window x:Class="AsyncWpfToy.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:AsyncWpfToy"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="20" />
<RowDefinition Height="20" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Button Grid.Row="0" Command="{Binding TestCommand}" Content="Button" />
<TextBlock Grid.Row="1" Text="{Binding TheText}" />
<Button Grid.Row="2" Command="{Binding TestCommand2}" Content="Button2" />
<TextBlock Grid.Row="3" Text="{Binding TheText2}" />
</Grid>
And, for the sake of being complete, a basic implementation of DelegateCommand
public class DelegateCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public DelegateCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute?.Invoke(parameter) ?? true;
}
public void Execute(object parameter)
{
_execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
When I click the button simply labeled "Button", my UI freezes even though the body of DoWork() has all Async methods awaited with ConfigureAwait(false) set.
When I click Button2, however, I am performing an await Task.Delay(1).ConfigureAwait(false) before awaiting the DoWork() method. This appears to correctly shift processing to another context, allowing the UI to continue. Indeed, if I move the await Task.Delay(1).ConfigureAwait(false) to the DoWork() method and set it up before the for loop, everything runs as one would expect - the UI remains responsive.
It appears that, for whatever reason, StreamWriter.WriteLineAsync() is either not truly async, or the processing is happening so fast that the framework determines that there is no need for a context switch and allows continuation on the captured context, regardless. I have found that if I remove the Thread.Sleep(100) and, instead, iterate with a much higher number (i<10000 or so, though I have not tried to find the threshold), it will lock for a few seconds but eventually switch contexts until it completes. So I'm guessing that the latter explanation is more likely.
Ultimately, the question I have is, "How do I ensure that my StreamWriter.WriteLineAsync() calls continue on another context so that my UI thread can remain responsive?"