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.
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.