Monday, September 11, 2023

Async and Await

I have never taken the time to understand Async and Await modifiers. There are a few places in the application I'm currently working on where the application becomes non-responsive for a few seconds and I'd like to see if I can fix that. Async and Await are obvious candidates.

I wrote a test program that has four features.

  1. Calling a method synchronously
  2. Calling a method asynchronously.
  3. Calling a synchronous method from an asynchronous method
  4. Calling an asynchronous method from a synchronous method.

Start a new C#, .Net Core project called AsyncAwait. Change the MainWindow xaml and code to look like this.

<Window x:Class="AsyncAwait.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:AsyncAwait"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="SyncCommand"/>
        <RoutedCommand x:Key="AsyncCommand"/>
        <RoutedCommand x:Key="SyncToAsyncCommand"/>
        <RoutedCommand x:Key="AsyncToSyncCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource SyncCommand}" Executed="SyncExecuted"/>
        <CommandBinding Command="{StaticResource AsyncCommand}" Executed="AsyncExecuted"/>
        <CommandBinding Command="{StaticResource SyncToAsyncCommand}" Executed="SyncToAsyncExecuted"/>
        <CommandBinding Command="{StaticResource AsyncToSyncCommand}" Executed="AsyncToSyncExecuted"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
 
        <Button Grid.Row="0" Content="Sync" Command="{StaticResource SyncCommand}"/>
        <Button Grid.Row="1" Content="ASync" Command="{StaticResource AsyncCommand}"/>
        <Button Grid.Row="2" Content="Sync to Async" Command="{StaticResource SyncToAsyncCommand}"/>
        <Button Grid.Row="3" Content="Async to Sync" Command="{StaticResource AsyncToSyncCommand}"/>
        <ListView Grid.Row="4" ItemsSource="{Binding Items}"/>
    </Grid>
</Window>

 ---------------------------------------------------------------------

using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
 
namespace AsyncAwait
{
    [ObservableObject]
    public partial class MainWindow : Window
    {
        [ObservableProperty]
        private ObservableCollection<string> items = new ObservableCollection<string>();
 
        public MainWindow()
        {
            InitializeComponent();
        }
 
        private void InitItems()
        {
            Items.Clear();
            AppendItem("InitItems");
        }
 
        private async Task InitItemsAsync()
        {
            await Application.Current.Dispatcher.BeginInvoke(() => Items.Clear());
            await AppendItemAsync("InitItemsAsync");
        }
 
        private void AppendItem(string Item)
        {
            string s = $"{System.DateTime.Now.ToString("HH:mm:ss.fff")} {Item}";
            Items.Add(s);
            Debug.WriteLine(s);
        }
 
        private async Task AppendItemAsync(string Item)
        {
            string s = $"{System.DateTime.Now.ToString("HH:mm:ss.fff")} {Item}";
            await Application.Current.Dispatcher.BeginInvoke(() => Items.Add(s));
            Debug.WriteLine(s);
        }
 
        private void SyncExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            InitItems();
            AppendItem("SyncExecuted");
            SyncExecuted2();
            AppendItem("SyncExecuted Exit");
        }
 
        private void SyncExecuted2()
        {
            AppendItem("SyncExecuted2");
            AppendItem("Pause 2 seconds");
            Thread.Sleep(2000);
            AppendItem("SyncExecuted2 Exit");
        }
 
        private async void AsyncExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            await InitItemsAsync();
            await AppendItemAsync("AsyncExecuted");
            await AsyncExecuted2();
            await AppendItemAsync("AsyncExecuted Exit");
        }
 
        private async Task AsyncExecuted2()
        {
            await AppendItemAsync("AsyncExecuted2");
            await AppendItemAsync("Pause 2 seconds");
            await Task.Delay(2000);
            await AppendItemAsync("AsyncExecuted2 Exit");
        }
 
        private void SyncToAsyncExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            InitItems();
            AppendItem("SyncToAsyncExecuted");
            SyncToAsyncExecuted2();
            AppendItem("SyncToAsyncExecuted Exit");
        }
 
        private async void SyncToAsyncExecuted2()
        {
            await AppendItemAsync("SyncToAsyncExecuted2");
            await AppendItemAsync("Pause 2 seconds");
            await Task.Delay(2000);
            await AppendItemAsync("SyncToAsyncExecuted2 Exit");
        }
 
        private async void AsyncToSyncExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
        {
            await InitItemsAsync();
            await AppendItemAsync("AsyncExecuted");
            AsyncToSyncExecuted2();
            await AppendItemAsync("AsyncExecuted Exit");
        }
 
        private void AsyncToSyncExecuted2()
        {
            AppendItem("AsyncToSyncExecuted2");
            AppendItem("Pause 2 seconds");
            Thread.Sleep(2000);
            AppendItem("AsyncToSyncExecuted2 Exit");
        }
    }
}

Run the program and click the [Sync] button. Try moving the window while the code is running.


You will notice you cannot move the window while the method is executing and the listbox is only populated once the SyncExecuted method has finished. This is the behavior we are trying to avoid.

Now click the [Async] button and try to move the window.


You can move the window while the AsyncExecuted method is executing and the results are shown as the code executes. The is the behavior we want.

The first thing to notice is the AsyncExecuted event handler is marked async void which is how you call an async method from a non-async method. AsyncExecuted calls async versions of everything. This means they are running on non-UI threads. The Items collection is owned by the UI thread which means we have to use Dispatcher.BeginInvoke to update the collection. If we don't use BeginInvoke we will get a run-time exception.

So it looks like it's fairly easy to add some Async and Await operators and solve our responsiveness problem. But there are some wrinkles...

One of the problems with Async and Await is that you can only call Await from an Async method and you can only call an Async method with the Await modifier. This forces us to modify our entire call stack if we want one method in it to run asynchronously. We need to figure out how to call an async method from a non-async method and vice versa.

The third button calls a non-async event handler called SyncToAsyncExecuted and this method calls the async method SyncToAsyncExecuted2. If you click the third button, take a close look at the output.


Notice that the SyncToAsyncExecuted method exits immediately. It did not wait for the SyncToAsyncExecuted2 method to complete. That's because the Executed2 method is marked Async but we didn't mark the call with Await. We are not allowed to mark the call with Await because SyncToAsyncExecuted is not an Async method itself.

So although we are allowed to call an Async method without the Await operator, we must be aware that the calling code with continue immediately. Sometimes this is what we want, sometimes it is not.

The fourth button calls synchronous code from async code. 


Because most of the code executes synchronously we see we cannot move the window while it is running and the listbox is only updated once all the code has run. This is what we would expect. As you can see, there is no special syntax for calling synchronous methods from async methods.

Because AsyncToSyncExecuted2 is not marked as Async we don't have the option or need to call it with the await operator.

I hope this cleared a few things up for you.



Thursday, September 7, 2023

How can a ViewModel get access to its View?

Even though the example targets MAUI, the same technique will work for WPF too.

Short Answer is it doesn't. Normally you do not want your ViewModel to contain references to the View. It should be able to execute without a View even existing. However, I was working through one of Microsoft's MAUI walkthroughs and they were referencing color resources defined in the View.

https://learn.microsoft.com/en-us/training/modules/use-shared-resources/5-exercise-use-dynamic-resources-to-update-element

I had tweaked the walkthrough a bit and was using a ViewModel. Normally the Color resources would be defined as properties in the ViewModel and I would bind to them. But I was curious to see how difficult it would be for the ViewModel to access resources defined in the XAML.

    <ContentPage.Resources>
        <Color x:Key="bgColor">#C0C0C0</Color>
        <Color x:Key="fgColor">#0000AD</Color>
    </ContentPage.Resources>

It turns out there's a fairly easy way to do it, even though you should never do it.

You define a public property in the ViewModel and have the View populate it at the appropriate times. The appropriate times are OnLoaded and OnBindingContextChanged.

Here's the property definition in the ViewModel.

    partial class MainViewModel: ObservableObject
    {
        public MainPage mainPage { get; set; }

Here is the View

namespace Notes;
 
public partial class MainPage : ContentPage
{
      public MainPage()
      {
            InitializeComponent();
            this.Loaded += LinkThisToBindingContext;
            this.BindingContextChanged += LinkThisToBindingContext;
      }
 
      private void LinkThisToBindingContext(object sender, EventArgs e)
      {
            (this.BindingContext as MainViewModel).mainPage = this;
      }
}

The View's BindingContext is already populated in the View's constructor so BindingContextChanged is not normally raised after the constructor. I populate the ViewModel in the loaded event too so I have all bases covered.

Here's the XAML

 <?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:Notes"
             x:Class="Notes.MainPage">
    <ContentPage.BindingContext>
        <local:MainViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Resources>
        <Color x:Key="bgColor">#C0C0C0</Color>
        <Color x:Key="fgColor">#0000AD</Color>
    </ContentPage.Resources>
    <Grid RowDefinitions="Auto"
          Background="{DynamicResource bgColor}"
         ColumnDefinitions="*,*"
         Padding="40">
 
        <Button  Margin="5" Text="Dark" Grid.Row="0" Grid.Column="0"
                 Command="{Binding DarkCommand}"
                 TextColor="{DynamicResource fgColor}"
                 BorderColor="{DynamicResource fgColor}"
                 BorderWidth="1"
                 BackgroundColor="{DynamicResource bgColor}"/>
        <Button  Margin="5" Text="Light" Grid.Row="0" Grid.Column="1"
                 Command="{Binding LightCommand}"
                 TextColor="{DynamicResource fgColor}"
                 BorderColor="{DynamicResource fgColor}"
                 BorderWidth="1"
                 BackgroundColor="{DynamicResource bgColor}"/>
    </Grid>
</ContentPage>

Here is the code behind...

namespace Notes;
 
public partial class MainPage : ContentPage
{
      public MainPage()
      {
            InitializeComponent();
            this.Loaded += LinkThisToBindingContext;
            this.BindingContextChanged += LinkThisToBindingContext;
      }
 
      private void LinkThisToBindingContext(object sender, EventArgs e)
      {
            (this.BindingContext as MainViewModel).mainPage = this;
      }
}

and here is the ViewModel.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
 
namespace Notes
{
    partial class MainViewModel: ObservableObject
    {
        public MainPage mainPage { get; set; }
 
        [RelayCommand]
        public void Dark()
        {
            mainPage.Resources["fgColor"] = Colors.Navy;
            mainPage.Resources["bgColor"] = Colors.Silver;
        }
 
        [RelayCommand]
        public void Light()
        {
            mainPage.Resources["fgColor"] = Colors.Silver;
            mainPage.Resources["bgColor"] = Colors.Navy;
        }
 
    }
}

If you run the application you can see the relay commands have access to the resources via the mainPage property. It's really bad architecture, but it can be done.