C# – Async / Await (Part 2)
When someone does a small task beautifully, their whole environment is affected by it.
Jerry Seinfeld
In this post I’m going to continue my look at the async / await pattern within C# – this time looking at asynchronous properties. These are items that can be used as the source for a read-only data binding within WPF, where the value is fetched once asynchronously, and then cached for future usage. This might be at either View load, as an alternative to the event to command pattern I discussed previously, or on demand following a user action. An example of the latter might be a product catalog where the product images are only held within the database as URLs to an external content provider. If you have the entire catalog displayed in a datagrid control, you don’t want to fetch the full set of images for every product, only the ones that are currently displayed.
The base class for these asynchronous properties is perAsyncProperty
/// <summary> /// A read only property where the value is fetched once when required, in an async manner, and then cached for future use. /// </summary> /// <remarks> /// in Xaml, bind to perAsyncPropertyInstance.Value /// </remarks> /// <typeparam name="T"></typeparam> public class perAsyncProperty<T> : perViewModelBase where T : class { private readonly Func<Task<T>> _fetchValue; public perAsyncProperty(Func<Task<T>> fetchValue) { _fetchValue = fetchValue; } private bool _fetchingValue; private T _value; /// <summary> /// The property's value - use this as the source of data binding. /// </summary> public T Value { get { if (_value != null || _fetchingValue) { return _value; } _fetchingValue = true; // can't use await inside a property getter, so use a continuation instead _fetchValue() .EvaluateFunctionWithTimeoutAsync(FetchValueTimeOut) .ContinueWith(FetchValueContinuation); // Local function to refresh Value once the data fetch task has completed async void FetchValueContinuation(Task<perAsyncFunctionResponse<T>> task) { var taskResult = await task.ConfigureAwait(false); if (taskResult.IsCompletedOk) { Value = taskResult.Data; } else if (taskResult.IsTimedOut) { OnTimeOutAction?.Invoke(taskResult); } else if (taskResult.IsError) { OnErrorAction?.Invoke(taskResult); } _fetchingValue = false; } return _value; } private set => Set(nameof(Value), ref _value, value); } /// <summary> /// Timeout value for FetchValue invocation /// </summary> public TimeSpan FetchValueTimeOut { get; set; } = perTimeSpanHelper.Forever; /// <summary> /// Optional action to perform if FetchValue generates an error. /// </summary> public Action<perAsyncFunctionResponse<T>> OnErrorAction { get; set; } /// <summary> /// Optional action to perform if FetchValue times out. /// </summary> public Action<perAsyncFunctionResponse<T>> OnTimeOutAction { get; set; } /// <summary> /// Clear Value and force it to be re-fetched then next time it is read. /// </summary> public void ResetValue() { _fetchingValue = false; Value = null; } }
Note how the constructor parameter is a Func<Task<T>> rather than just Task<T> – we don’t want the task to be triggered until the property value is read for the first time.
In the Xaml, bind to perAsyncPropertyInstance.Value rather than just perAsyncPropertyInstance. Reading the property value for the first time will trigger _fetchValue to be evaluated. Once this task is completed, the Value property is updated, firing the PropertyChanged event to refresh any bindings.
You can’t use await inside a c# property getter, so instead a continuation function is used to trigger that the fetched value is now available.
There are also optional actions to perform if FetchValue() times out or generates an exception.
ResetValue() is used if you ever need to force the propety value to be re-evaluated.
The library also contains extensions of this base class that fetch a byte array from a given file path (using methods from the perIOAsync section of the library) …
public class perBytesFromFileAsyncProperty : perAsyncProperty<byte[]> { public perBytesFromFileAsyncProperty(string filePath) : base(() => FetchData(filePath)) { } private static Task<byte[]> FetchData(string filePath) { Debug.WriteLine("loading file - " + filePath); return perIOAsync.ReadAllBytesFromFileRawAsync(filePath); } }
or a Url …
public class perBytesFromUrlAsyncProperty : perAsyncProperty<byte[]> { public perBytesFromUrlAsyncProperty(string url): base(() => FetchData(url)) { } private static Task<byte[]> FetchData(string url) { Debug.WriteLine("downloading Url - " + url); return perIOAsync.ReadAllBytesFromUrlRawAsync(url); } }
The demo project for this post shows two examples of asyncronous properties. The collection of data item view models will be fetched as soon as the view is loaded and data binding is established.
public class MainViewModel { private readonly DataItemRepository _repository; public MainViewModel() { _repository = new DataItemRepository(); DataItemsCollection = new perAsyncProperty<IReadOnlyCollection<DataItemViewModel>>(() => _repository.LoadData()); ResetImagesCommand = new perRelayCommand(OnResetImages); } public perAsyncProperty<IReadOnlyCollection<DataItemViewModel>> DataItemsCollection { get; } public ICommand ResetImagesCommand { get; } private void OnResetImages() { if (DataItemsCollection.Value == null) { return; } foreach (var item in DataItemsCollection.Value) { item.ImageBytes.ResetValue(); } } }
<vctrl:perViewBase x:Class="AsyncPropertyDemo.MainView" ... > <Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Grid Margin="8"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="8" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <ListBox ItemsSource="{Binding DataItemsCollection.Value}"> ...
Each data item contains a text description and a file path for the image – ImageBytes is a perBytesFromFileAsyncProperty instance used as the binding source for the image display inside the listbox item template. The call to Debug.WriteLine() shows when each image is requested.
public class DataItemViewModel { public DataItemViewModel(string imageFilePath, string caption) { ImageBytes = new perBytesFromFileAsyncProperty(imageFilePath); Caption = caption; } public perBytesFromFileAsyncProperty ImageBytes { get; } public string Caption { get; } }
Note the binding for the image control.
<Image ... Source="{Binding ImageBytes.Value, TargetNullValue={x:Static vhelp:perImageHelper.TransparentPixel}}" />
ImageBytes.Value is just a byte array, yet it can be bound directly to Image.Source. This is another example of automatic type conversion within WPF data binding.
As usual the code samples for this blog are available on Github, along with my own personal C# / WPF library.
If you find this article useful, or you have any other feedback, please leave a comment below.