windowsxamldata-bindingwinui-3winui

WinUI3 Async Binding Converter


I have an API which returns an array of models, each with a thumbnail ID which needs to be fetched via an authenticated API call and shown in an Image element in data template for each item in a grid view.

I found a number of suggestions online to use a TaskCompletionNotifier so attempted to use this pattern but I get an error in the output of the program suggesting that the WinUI binding code won't take a TaskCompletionNotifier and use the value when ready, or perhaps I'm just using it wrong.

Error: Converter failed to convert value of type 'Converters.TaskCompletionNotifier`1[Microsoft.UI.Xaml.Media.Imaging.BitmapImage]' to type 'ImageSource'; BindingExpression: Path='ThumbnailId' DataItem='Models.CallRecording'; target element is 'Microsoft.UI.Xaml.Controls.Image' (Name='null'); target property is 'Source' (type 'ImageSource').

The code I am using is

Converter:


using Interfaces;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Media.Imaging;

namespace Converters;

public class StorageIdToImageSourceConverter : IValueConverter
{
    private readonly IImageService imageService;

    public StorageIdToImageSourceConverter()
    {
       imageService = Ioc.Default.GetRequiredService<IImageService>();
    }

    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (value is string storageId)
        {
            if (string.IsNullOrWhiteSpace(storageId))
            {
                return null;
            }

            var task = Task.Run(async () => {
                var getBlobAsBitmapImageResult = await imageService.GetBlobAsBitmapImageAsync(storageId);
                if (getBlobAsBitmapImageResult.IsFailed)
                {
                    return null;
                }

                return getBlobAsBitmapImageResult.Value;
            });

            return new TaskCompletionNotifier<BitmapImage?>(task);
        }

        return null;
    }

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

TaskCompletionNotifier: (Found via github search at https://github.com/Tlaster/Cosimg/blob/679d23010bbb9b839e840b2f07e68621999f742b/TBase/TaskCompletionNotifier.cs#L11)

using System.ComponentModel;

namespace Augment.Converters;

public sealed class TaskCompletionNotifier<T> : INotifyPropertyChanged
{
    public TaskCompletionNotifier(Task<T> task)
    {
        Task = task;
        if (!task.IsCompleted)
        {
            var scheduler = (SynchronizationContext.Current == null) ? TaskScheduler.Current : TaskScheduler.FromCurrentSynchronizationContext();
            task.ContinueWith(t =>
            {
                var propertyChanged = PropertyChanged;
                if (propertyChanged != null)
                {
                    propertyChanged(this, new PropertyChangedEventArgs("IsCompleted"));
                    if (t.IsCanceled)
                    {
                        propertyChanged(this, new PropertyChangedEventArgs("IsCanceled"));
                    }
                    else if (t.IsFaulted)
                    {
                        propertyChanged(this, new PropertyChangedEventArgs("IsFaulted"));
                        propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage"));
                    }
                    else
                    {
                        propertyChanged(this, new PropertyChangedEventArgs("IsSuccessfullyCompleted"));
                        propertyChanged(this, new PropertyChangedEventArgs("Result"));
                    }
                }
            },
            CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            scheduler);
        }
    }

    // Gets the task being watched. This property never changes and is never <c>null</c>.
    public Task<T> Task { get; private set; }


    // Gets the result of the task. Returns the default value of TResult if the task has not completed successfully.
    public T Result { get { return (Task.Status == TaskStatus.RanToCompletion) ? Task.Result : default(T); } }

    // Gets whether the task has completed.
    public bool IsCompleted { get { return Task.IsCompleted; } }

    // Gets whether the task has completed successfully.
    public bool IsSuccessfullyCompleted { get { return Task.Status == TaskStatus.RanToCompletion; } }

    // Gets whether the task has been canceled.
    public bool IsCanceled { get { return Task.IsCanceled; } }

    // Gets whether the task has faulted.
    public bool IsFaulted { get { return Task.IsFaulted; } }

    // Gets the error message for the original faulting exception for the task. Returns <c>null</c> if the task is not faulted.
    //public string ErrorMessage { get { return (InnerException == null) ? null : InnerException.Message; } }

    public event PropertyChangedEventHandler PropertyChanged;
}

Page.xaml

            <GridView ItemsSource="{x:Bind ViewModel.CallRecordings, Mode=OneWay}">
                <GridView.ItemTemplate>
                    <DataTemplate x:DataType="models:CallRecording">
...
                                <Image
                                    Grid.Row="0" Grid.RowSpan="2"
                                    Grid.Column="0" Grid.ColumnSpan="2" 
                                    Source="{Binding ThumbnailId, Mode=OneWay, Converter={StaticResource StorageIdToImageSourceConverter}, FallbackValue='ms-appx:///Assets/NoImageSquare100x100.jpg', TargetNullValue='ms-appx:///Assets/NoImageSquare100x100.jpg'}"/>
...

What is the best approach to doing this in a WinUI3 project?

I'd like to avoid the boilerplate of having to create a ViewModel for each item in the list view if possible.


Solution

  • The piece I was missing was using using the Async Converter as a Binding for the Image DataContext, then binding the Image Source to the Result.

    Updating the Page.xaml to look like

    <Image
      Grid.Row="0" Grid.RowSpan="2"
      Grid.Column="0" Grid.ColumnSpan="2"
      DataContext="{Binding ThumbnailId, Mode=OneWay, Converter={StaticResource StorageIdToImageSourceConverter}}"
      Source="{Binding Path=Result, Mode=OneWay}"/>
    

    The DataContext of the Image element then becomes the converted TaskCompletionNotifier, the Result property of which can then be bound to, setting the Image source as the result when it's ready.