I have a WPF markup extension in charge of retrieving images by name, returning a BitmapImage
object.
<Image Source="{my:ImageProvider ImageName=myImageName}"></Image>
Since retrieving an image is an operation that can possibly take a few seconds, I'd like to show a default image and display the requested image once it's ready.
What I tried to do is something like this, but as this may change the BitmapImage
object, it won't update the UI (sample code):
BitmapImage img;
public override object ProvideValue(IServiceProvider serviceProvider)
{
img = new BitmapImage(new Uri(@"D:\defaultImage.png", UriKind.Absolute));
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += new DoWorkEventHandler(bw_DoWork);
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
bw.RunWorkerAsync();
return img;
}
void bw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
System.Threading.Thread.Sleep(5000);
}
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
img.UriSource = new Uri(@"D:\actualImage.png", UriKind.Absolute);
}
Is there a way I can update the UI to use the modified BitmapImage
(something like INotifyPropertyChanged) or is there a different approach to achieve this?
I ended up with two ways to do this. Both ways use a class that wraps the image and implements INotifyPropertyChanged
:
class ImageSourceWrapper : ObservableObject
{
private ImageSource _image;
public ImageSource Image
{
get { return _image; }
set
{
if (value != _image)
{
_image = value;
RaiseOnPropertyChanged("Image");
}
}
}
public ImageSourceWrapper(ImageSource image)
{
Image = image;
}
}
Once I have this, I can have my markup extension return an ImageSourceWrapper
object and bind to it, like so
<Image Source="{Binding Source={my:ImageProvider ImageName=myImageName}, Path=Image}" />
I didn't really like this way, since it's pretty messy and involves having to know the ImageSourceWrapper
class rather than working simply with ImageSource
. Then I came up with the second approach.
In this approach I still use the ImageSourceWrapper
class, but instead of having my markup extension return an ImageSourceWrapper
object, I return a binding object which I set up to be bound to an ImageSourceWrapper
object.
The markup extension looks something like this:
private ImageSourceWrapper _imageSourceWrapper;
public override object ProvideValue(IServiceProvider serviceProvider)
{
// Get the object and the property to be bound.
IProvideValueTarget service = IProvideValueTarget)provider.GetService(typeof(IProvideValueTarget));
DependencyObject targetObject = service.TargetObject as DependencyObject;
DependencyProperty targetProperty = service.TargetProperty as DependencyProperty;
// Set up the binding with the default image.
_imageSourceWrapper = new ImageSourceWrapper(DefaultImage);
Binding binding = new Binding("Image");
binding.Source = _imageSourceWrapper;
BindingOperations.SetBinding(targetObject, targetProperty, binding);
// Retrieve the actual image asynchronously.
GetImageAsync();
return binding.ProvideValue(serviceProvider);
}
private void GetImageAsync()
{
// Get the image asynchronously.
// Freeze the image so it could be accessed from all threads regardless
// of which thread it was created on.
newImage.Freeze();
// Got the image - update the _imageSourceWrapper object.
_imageSourceWrapper = newImage;
}
Then I can use it in XAML like this
<Image Source="{my:ImageProvider ImageName=myImageName}" />
This way the default image is displayed first, and once the requested image is retrieved, it will be displayed instead.
Sorry if the code here isn't entirely correct, I'm not near the code at the moment. Hopefully this is enough at the moment to express the main idea.