29

I have an asynchronous method which I want to trigger inside an IValueConverter.

Is there a better way than forcing it to be synchronous by calling the Result property?

public async Task<object> Convert(object value, Type targetType, object parameter, string language)
{
    StorageFile file = value as StorageFile;

    if (file != null)
    {
        var image = ImageEx.ImageFromFile(file).Result;
        return image;
    }
    else
    {
        throw new InvalidOperationException("invalid parameter");
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Boas Enkler
  • 12,264
  • 16
  • 69
  • 143

2 Answers2

56

You probably don't want to call Task.Result, for a couple of reasons.

Firstly, as I explain in detail on my blog, you can deadlock unless your async code is has been written using ConfigureAwait everywhere. Secondly, you probably don't want to (synchronously) block your UI; it would be better to temporarily show a "loading..." or blank image while reading from the disk, and update when the read completes.

So, personally, I would make this part of my ViewModel, not a value converter. I have a blog post describing some databinding-friendly ways to do asynchronous initialization. That would be my first choice. It just doesn't feel right to have a value converter kicking off asynchronous background operations.

However, if you've considered your design and really think an asynchronous value converter is what you need, then you have to get a bit inventive. The problem with value converters is that they have to be synchronous: the data binding starts at the data context, evaluates the path, and then invokes a value conversion. Only the data context and path support change notifications.

So, you have to use a (synchronous) value converter in your data context to convert your original value into a databinding-friendly Task-like object and then your property binding just uses one of the properties on the Task-like object to get the result.

Here's an example of what I mean:

<TextBox Text="" Name="Input"/>
<TextBlock DataContext="{Binding ElementName=Input, Path=Text, Converter={local:MyAsyncValueConverter}}"
           Text="{Binding Path=Result}"/>

The TextBox is just an input box. The TextBlock first sets its own DataContext to the TextBox's input text running it through an "asynchronous" converter. TextBlock.Text is set to the Result of that converter.

The converter is pretty simple:

public class MyAsyncValueConverter : MarkupExtension, IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var val = (string)value;
        var task = Task.Run(async () =>
        {
            await Task.Delay(5000);
            return val + " done!";
        });
        return new TaskCompletionNotifier<string>(task);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return null;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

The converter first starts an asynchronous operation to wait 5 seconds and then add " done!" to the end of the input string. The result of the converter can't be just a plain Task because Task doesn't implement IPropertyNotifyChanged, so I'm using a type that will be in the next release of my AsyncEx library. It looks something like this (simplified for this example; full source is available):

// Watches a task and raises property-changed notifications when the task completes.
public sealed class TaskCompletionNotifier<TResult> : INotifyPropertyChanged
{
    public TaskCompletionNotifier(Task<TResult> 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<TResult> Task { get; private set; }

    Task ITaskCompletionNotifier.Task
    {
        get { return Task; }
    }

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

    // 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;
}

By putting these pieces together, we've created an asynchronous data context that is the result of a value converter. The databinding-friendly Task wrapper will just use the default result (usually null or 0) until the Task completes. So the wrapper's Result is quite different than Task.Result: it won't synchronously block and there's no danger of deadlock.

But to reiterate: I'd choose to put asynchronous logic into the ViewModel rather than a value converter.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Hi Thanks for yoru reply. Making the async operation in viewmodel is indeed the solution i currently have as a workaround. but this feel very nice. There are some concerns for which I feel they were right in a converter. I hoped that I overlooked something like a IAsyncValueConverter. But it seems like there is nothing like that :-( . Will mark your post although as an answer because I think it will help some other guys with same problems :-) – Boas Enkler Feb 21 '13 at 16:57
  • Very nice, but I want to ask you a question: why the converter should extend `MarkupExtension` and why `ProvideValue` returns itself? – Alberto Oct 15 '13 at 11:09
  • 3
    @Alberto: It's just a XAML convenience so you don't have to declare a global instance in a resource dictionary and reference it from your markup. – Stephen Cleary Oct 15 '13 at 12:22
  • @StephenCleary, what is the purpose of the explicit implementation of ```ITaskCompletionNotifier```? I don't see the definition in your answer anywhere. – Paul Knopf May 13 '14 at 16:49
  • 1
    The code in my answer is a simplification of the code linked to in my answer. The explicit implementation is because I have a generic `ITaskCompletionNotifier` that derives from `ITaskCompletionNotifier`. See my [MSDN article on async data binding](http://msdn.microsoft.com/en-us/magazine/dn605875.aspx) for a more complete example. – Stephen Cleary May 13 '14 at 17:45
  • Is this applicable in Xamarin Forms? – mr5 May 01 '19 at 02:37
  • @mr5: I don't know. [This kind of data binding](http://msdn.microsoft.com/en-us/magazine/dn605875.aspx) is applicable to any MVVM framework, including Xamarin; but the `MarkupConverter` / `IValueConverter` wrapper is pretty WPF-specific and would only work on Xamarin if they have similar types. – Stephen Cleary May 01 '19 at 12:32
  • 1
    Codeplex shuts down. The source code linked in the answer can now be found under https://github.com/StephenCleary/AsyncEx – LionAM Jun 01 '21 at 12:41
1

The alternative approach is to make your own control which supports async sources or data.

Here is the example with the image

    public class AsyncSourceCachedImage : CachedImage
{
    public static BindableProperty AsyncSourceProperty = BindableProperty.Create(nameof(AsyncSource), typeof(Task<Xamarin.Forms.ImageSource>), typeof(AsyncSourceSvgCachedImage), null, propertyChanged: SourceAsyncPropertyChanged);

    public Task<Xamarin.Forms.ImageSource> AsyncSource
    {
        get { return (Task<Xamarin.Forms.ImageSource>)GetValue(AsyncSourceProperty); }
        set { SetValue(AsyncSourceProperty, value); }
    }

    private static async void SourceAsyncPropertyChanged(BindableObject bindable, object oldColor, object newColor)
    {
        var view = bindable as AsyncSourceCachedImage;
        var taskForImageSource = newColor as Task<Xamarin.Forms.ImageSource>;

        if (taskForImageSource != null)
        {
            var awaitedImageSource = await taskForImageSource;

            view.Source = awaitedImageSource;
        }
    }
}

Moreover, you might implement the loading activity indicator over the image until the task will be resolved.

Pavlo Datsiuk
  • 1,024
  • 9
  • 17