If I've got some images in my Silverlight app then I probably don't want to display certain parts of the app before those images are fully loaded from the web.
The Downloader provides a way to do this except that it manages (as far as I know) a single file at a time whereas what I want is to be able to wait until all my images are ready in order to progress (it might be that I have multiple zip files that I need to download).
Additionally, I'd like to be able to specify on an Image where it is getting its image from in XAML even if I'm actually going to get that image from a downloader at a later point. That is, I really want something like;
<Image Source="{Downloader images1.zip image1.png}"/>
Now, as far as I'm aware, trying to build something like this isn't going to work because if I use the Source property like that then Silverlight will throw an error when it encounters the bad value and even if I handle the ImageFailed event it doesn't seem like there's a way to get rid of that error.
Also, I don't think you can derive from Image in order to produce a "different image" because the Image class is sealed.
Also, there's no way (AFAIK) to hook into the XAML parsing process to say something like;
"Hey, when you see something that looks like {XXX} then just call this component over here and it'll sort it out for you"
And, similarly, I don't think I can add some kind of dependency property such as Downloader.ImageSource which would then call off into my code to say "Hey, someone just set your property on this image over here".
So...what can you do? You could certainly do something like produce a DownloaderImage control which "looked just like an image" and aggregated an Image to do its work. This would work out ok but it means building a control that looks just like Image and delegates all the properties down to properties on Image with the exception of the Source property which it handles differently. It seems a little painful to have to go down that route but I can't really see another way of making it work so I had a try.
I built a simple control called DownloaderImage. I haven't copied all the properties of Image, just enough to get started (if i wanted to really use this, I'd have to end up copying all the properties of Image across to it which is a bit painful as previously mentioned :-)).
The DownloaderImage Control - XAML
<Canvas xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Image x:Name="wrappedImage" Stretch="Fill"/>
</Canvas>
The DownloadImage Control - Source
using System.Windows.Controls;
namespace SilverlightProject15
{
public class DownloaderImage : Control
{
public DownloaderImage()
{
System.IO.Stream s = this.GetType().Assembly.GetManifestResourceStream(
"SilverlightProject15.DownloaderImage.xaml");
canvas = (Canvas)this.InitializeFromXaml(
new System.IO.StreamReader(s).ReadToEnd());
wrappedImage = (Image)canvas.FindName("wrappedImage");
this.Loaded += OnLoaded;
}
void OnLoaded(object sender, System.EventArgs e)
{
SizeControls();
}
public new double Width
{
get
{
return (base.Width);
}
set
{
base.Width = value;
SizeControls();
}
}
public new double Height
{
get
{
return (base.Width);
}
set
{
base.Height = value;
SizeControls();
}
}
public string ImageSource
{
get
{
return (imageSource);
}
set
{
imageSource = value;
// Simplistic parsing to say the least :-)
string[] pieces = imageSource.Split(' ');
DownloadHelper.RegisterForDownload(wrappedImage, pieces[0], pieces[1]);
}
}
private void SizeControls()
{
canvas.Width = wrappedImage.Width = base.Width;
canvas.Height = wrappedImage.Height = base.Height;
}
private Canvas canvas;
private Image wrappedImage;
private string imageSource;
}
}
This makes use of a helper static class that I added to manage downloaders for the various zip files that I might want to download across my app. This is a bit simplistic and probably riddled with bugs as I just sort of threw it together but here it is;
The DownloadHelper Class
The idea of this class is that an image can call DownloaderHelper.RegisterForDownload and pass across the zipFile they're waiting for, and the image they want from it. If the helper already has that Downloader then it can straight away call SetSource on the Image with it but, otherwise, it'll have to go and get a Downloader for that particular zipFile, download it and remember to call SetSource when it's done. This is why this class ends up with a few lists of things kicking around (as I say, it might well be a bit buggy :-));
using System;
using System.Windows.Controls;
using System.Windows;
using System.Collections.Generic;
namespace SilverlightProject15
{
public enum DownloaderStatus
{
InProgress,
Completed,
Errored
};
public class DownloadNotificationEventArgs : EventArgs
{
public DownloaderStatus Status { get; set; }
public string ZipFile { get; set; }
}
// AFAIK, we're single threaded otherwise this lot won't work well.
public static class DownloadHelper
{
class ImageInfo
{
public Image Image { get; set; }
public string ImageName { get; set; }
public void SetSource(Downloader downloader)
{
Image.SetSource(downloader, ImageName);
}
}
class DownloaderInfo
{
public DownloaderStatus Status { get; set; }
public Downloader Downloader { get; set; }
public List<ImageInfo> WaitList { get; set; }
}
static DownloadHelper()
{
downloaders = new Dictionary<string, DownloaderInfo>();
}
public static void RegisterForDownload(Image i,
string zipFile, string imageName)
{
if (downloaders.ContainsKey(zipFile))
{
DownloaderInfo info = downloaders[zipFile];
switch (info.Status)
{
case DownloaderStatus.InProgress:
info.WaitList.Add(new ImageInfo() { Image = i, ImageName = imageName });
break;
case DownloaderStatus.Completed:
i.SetSource(downloaders[zipFile].Downloader, imageName);
break;
case DownloaderStatus.Errored:
throw new InvalidOperationException("Zip file download has failed");
break;
default:
break;
}
}
else
{
CreateDownloader(i, zipFile, imageName);
}
}
private static void CreateDownloader(Image i, string zipFile, string imageName)
{
DownloaderInfo info = new DownloaderInfo()
{
Status = DownloaderStatus.InProgress,
WaitList = new List<ImageInfo>(),
Downloader = new Downloader()
};
info.Downloader.Open("GET", new Uri(zipFile, UriKind.RelativeOrAbsolute));
info.Downloader.DownloadFailed += OnDownloadFailed;
info.Downloader.Completed += OnDownloadCompleted;
info.Downloader.Send();
info.WaitList.Add(new ImageInfo() { Image = i, ImageName = imageName });
downloaders.Add(zipFile, info);
}
static void OnDownloadFailed(object sender, ErrorEventArgs e)
{
Downloader d = (Downloader)sender;
DownloaderInfo info = downloaders[d.Uri.ToString()];
info.Status = DownloaderStatus.Errored;
info.WaitList = null;
FireDownloadNotification(new DownloadNotificationEventArgs()
{
Status = DownloaderStatus.Errored,
ZipFile = d.Uri.ToString()
});
}
static void OnDownloadCompleted(object sender, EventArgs e)
{
Downloader d = (Downloader)sender;
DownloaderInfo info = downloaders[d.Uri.ToString()];
info.Status = DownloaderStatus.Completed;
ClearWaitList(info);
FireDownloadNotification(new DownloadNotificationEventArgs()
{
Status = DownloaderStatus.Completed,
ZipFile = d.Uri.ToString()
});
}
static void ClearWaitList(DownloaderInfo downloaderInfo)
{
foreach (ImageInfo imageInfo in downloaderInfo.WaitList)
{
imageInfo.SetSource(downloaderInfo.Downloader);
}
downloaderInfo.WaitList = null;
}
static void FireDownloadNotification(DownloadNotificationEventArgs args)
{
if (DownloadNotification != null)
{
DownloadNotification(null, args);
}
}
public static event EventHandler<DownloadNotificationEventArgs> DownloadNotification;
private static Dictionary<string, DownloaderInfo> downloaders;
}
}
So, with that in place I can now go and use this in a XAML file.
Usage: The Page.XAML file
I ended up writing a little XAML file;
<Canvas x:Name="parentCanvas"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Loaded="Page_Loaded"
xmlns:ctl="clr-namespace:SilverlightProject15;assembly=ClientBin/SilverlightProject15.dll"
x:Class="SilverlightProject15.Page;assembly=ClientBin/SilverlightProject15.dll"
Width="640"
Height="480"
>
<Canvas x:Name="loadedCanvas" Width="640" Height="480" Background="Black">
<ctl:DownloaderImage x:Name="image1" ImageSource="images1.zip kermit.png" Width="100" Height="100"/>
<ctl:DownloaderImage x:Name="image2" ImageSource="images2.zip piggy.png" Canvas.Left="120" Width="100" Height="100"/>
</Canvas>
<Canvas x:Name="loadingCanvas" Width="640" Height="480" Background="Gray">
<TextBlock Canvas.Left="20" Canvas.Top="20" FontSize="96">Loading...</TextBlock>
</Canvas>
</Canvas>
What we've got here are 2 canvases. One is just a "Loading...." screen and the other is hosting 2 of my DownloaderImage controls. Each of these is reaching into a different zip file (images1.zip and images2.zip) in order to load up an image from there. I've also got a clunky bit of code for switching between the 2 when the Downloader has done downloading all of its zip files;
namespace SilverlightProject15
{
public partial class Page : Canvas
{
public void Page_Loaded(object o, EventArgs e)
{
InitializeComponent();
DownloadHelper.DownloadNotification += OnDownloadDone;
}
void OnDownloadDone(object sender, DownloadNotificationEventArgs e)
{
// We assume success here and don't check much...
if (--downloadCount <= 0)
{
loadingCanvas.Visibility = Visibility.Collapsed;
}
}
int downloadCount = 2;
}
}
And that's it. I've dropped the project file here (it's a web project and a Silverlight project).
As always with anything on my blog - it's just some thoughts and it probably turns out that there's perhaps a really simple way of doing this in Silverlight V1.1 already but I'm not aware of it and hence the post :-)