scott
scott

Reputation: 435

Async loading images in WPF value converter

I have a WPF listbox, which will bind a lot of images. Each image may will from local disk or get icon from Exe itself.

I put all those parse codes in MultiValueConverter. But it now seems block the UI. How to make that async?

Code Sample: https://github.com/qianlifeng/Wox/blob/master/Wox/Converters/ImagePathConverter.cs#L53

Upvotes: 0

Views: 6833

Answers (2)

Harsh Baid
Harsh Baid

Reputation: 7249

First you should provide some sample code so it easy to reproduce for answerers.

IsAsync will not help

Using IsAsync binding will not help because of below reasons:

  1. I have observed that when loading images dynamically with IsAsync it will lead to memory leaks at times. So avoid this sugar candy IsAsync property
  2. IsAsync will not always help as one problem case mentioned in the question here where OP is trying to load image from web but wpf is resolving DNS on main thread so again application hangs for while

Solution is Use Attached Binding Property in WPF

Details:

  1. You should put some thumbnail image on Source in XAML
  2. Write one Attached Property class to load the image in background and Update Image Source in when its available (sample code below for similar kind of use case)
<Image my:ImageAsyncHelper.SourceUri="{Binding Author.IconUrl}" />

Attached Property class

public class ImageAsyncHelper : DependencyObject
{
  public static Uri GetSourceUri(DependencyObject obj) { return (Uri)obj.GetValue(SourceUriProperty); }
  public static void SetSourceUri(DependencyObject obj, Uri value) { obj.SetValue(SourceUriProperty, value); }
  public static readonly DependencyProperty SourceUriProperty = DependencyProperty.RegisterAttached("SourceUri", typeof(Uri), typeof(ImageAsyncHelper), new PropertyMetadata
  {
    PropertyChangedCallback = (obj, e) =>
    {
      ((Image)obj).SetBinding(Image.SourceProperty,
        new Binding("VerifiedUri")
        {
          Source = new ImageAsyncHelper { GivenUri = (Uri)e.NewValue },
          IsAsync = true,
        });
    }
  });

  Uri GivenUri;
  public Uri VerifiedUri
  {
    get
    {
      try
      {
        Dns.GetHostEntry(GivenUri.DnsSafeHost);
        return GivenUri;
      }
      catch(Exception)
      {
        return null;
      }

    } 
  } 
}

And if you need to use IMultiValueConverter with attached property defined above then it should go like below xaml code:

Attached Property with IMultiValueConverter

<Image>
    <my:ImageAsyncHelper.SourceUri>
        <MultiBinding Converter="{StaticResource MyImageMultiValueConverter}">
            <Binding Source="Author" Path="IconUrl"/> <!-- Binding Parameters -->
            <Binding Path="ImageType"/> <!-- Binding Parameters -->
            <Binding Path="MyParameterToConverter"/> <!-- Binding Parameters -->
        </MultiBinding>
    </my:ImageAsyncHelper.SourceUri>
</Image>

Reference links

  1. How can I keep a WPF Image from blocking if the ImageSource references an unreachable Url?
  2. Using multibinding to set custom attached property in WPF

Upvotes: -2

pushpraj
pushpraj

Reputation: 13669

You can leverage IsAsync property of Binding

From MSDN:

Use the IsAsync property when the get accessor of your binding source property might take a long time. One example is an image property with a get accessor that downloads from the Web. Setting IsAsync to true avoids blocking the UI while the download occurs.

example

<Image Source="{Binding MyImage,IsAsync=True, Converter={StaticResource MyConverter}}" />

more on Binding.IsAsync


Async Converter

I managed to create a async converter

namespace CSharpWPF
{
    class AsyncConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return new AsyncTask(() =>
             {
                 Thread.Sleep(4000); //long running job eg. download image.
                 return "success";
             });
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        public class AsyncTask : INotifyPropertyChanged
        {
            public AsyncTask(Func<object> valueFunc)
            {
                AsyncValue = "loading async value"; //temp value for demo
                LoadValue(valueFunc);  
            }

            private async Task LoadValue(Func<object> valueFunc)
            {
                AsyncValue =  await Task<object>.Run(()=> 
                    {
                        return valueFunc();
                    });
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("AsyncValue"));
            }

            public event PropertyChangedEventHandler PropertyChanged;

            public object AsyncValue { get; set; }
        }
    }
}

this converter will return an instance of AsyncTask which will encapsulate the long running job within

class AsyncTask will execute the task asynchronously and will set the result to AsyncValue as it also implements INotifyPropertyChanged hence using the notification to update the UI

usage

<Grid xmlns:l="clr-namespace:CSharpWPF">
    <Grid.Resources>
        <l:AsyncConverter x:Key="AsyncConverter" />
    </Grid.Resources>
    <TextBlock DataContext="{Binding MyProperty,Converter={StaticResource AsyncConverter}}"
               Text="{Binding AsyncValue}" />
</Grid>

Idea is to bind the DataContext of the element to the converter and the desired property to the AsyncValue of the new data context

above example is using Text property of a text block for easy demo

exmaple is made for a IValueConverter same approach can be used for IMultiValueConverter too.

Upvotes: 7

Related Questions