In my previous WPF post I created a simple photo viewer. If you run the application, you'll notice things "lock up" while downloading an image. This is because the images are downloaded on the UI thread. This thread is the same thread that draws updates on the window, so while we are using it no input can be received. Worse, we can't update the status bar to let the user know we haven't locked up.
For the below code to work, you'll need to add three more namespace declarations to the original file:
using System.Net;
using System.IO;
using System.Windows.Threading;
The first thing to change is the TreeView's SelectedItemChanged event, so it fires off a thread:
String selectedLink = String.Empty;
void tvPhotos_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) {
TreeViewItem item = e.NewValue as TreeViewItem;
if (item != null) {
String link = item.Tag as String;
if (link != null) {
selectedLink = link;
sbiMessage.Content = "Loading: " + link;
LoadImageHandler limg = new LoadImageHandler(LoadImage);
limg.BeginInvoke(link, null, null);
}
}
}
This version uses LoadImageHandler to launch another thread and sets a message in the status bar that we loading the image. BeginInvoke returns immediately after launching the thread, allowing the UI updates to process (status bar and highlighting the selected node in the TreeView). I've added a string to the class, selectedLink, and set it to the link that is loading - more on why in a moment. Let's look at LoadImageHandler and LoadImage:
delegate void LoadImageHandler(String link);
void LoadImage(String link) {
Uri uri = new Uri(link);
WebClient web = new WebClient();
Stream imgData = web.OpenRead(uri);
MemoryStream memory = new MemoryStream();
int data = imgData.ReadByte();
while (data != -1) {
memory.WriteByte((byte)data);
data = imgData.ReadByte();
}
memory.Seek(0, SeekOrigin.Begin);
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.StreamSource = memory;
bi.EndInit();
bi.Freeze();
UpdateImageUIHandler upUI = new UpdateImageUIHandler(UpdateImageUI);
object[] args = { link };
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, upUI, bi, args);
web.Dispose();
}
LoadImage is the actual method run, LoadImageHandler is a delegate used to run the method on a separate thread. The first half is straight forward, a call is made on the web to the image, and it's downloaded to a MemoryStream. This MemoryStream is used as the source for the BitmapImage that will be loaded into our Image control. The interesting call here is Freeze() - normally you cannot create UI objects on one thread and use them in another. Classes that descend from Freezable are the exception - when you call Freeze on a Freezable you are saying "I swear under penalty of Exceptions I will make no changes to this object."
It is worth note a Freezable cannot not always be frozen. Check the CanFreeze property to see if you can freeze the object. In writing this example I learned a BitmapImage that uses a Uri instead of a Stream cannot be frozen; hence the code to copy the image to a MemoryStream.
Last, the method create an UpdateImageUIHandler to update the UI with the loaded BitmapImage. The special magic here is the Window.Dispatcher - almost all classes in WPF have a Dispatcher which is a convenient property to cause action to take place on the thread the object was created on. Notice the call to web.Dispose() comes after the call to BeginInvoke - just as before BeginInvoke returns immediately so we can dispose of the WebClient while the UI thread is updating.
public delegate void UpdateImageUIHandler(BitmapImage imgData, String link);
public void UpdateImageUI(BitmapImage imgData, String link) {
if (link.Equals(selectedLink)) {
imgDisplayed.Source = imgData;
sbiMessage.Content = link;
}
}
Not much left to do but update the Image control, but here is the use of selectedLink. If, while we were downloading one image, the user selected another node, we might load the wrong image into view. By checking the user is on the same node as when we started we can avoid this problem. Sadly, when using delegates there isn't a way to stop or kill the background thread that is downloading an old image - for that we would need to use a Thread class, and that's beyond the scope of this post.
Posted By Mike On Thursday, August 09, 2007
Filed under wpf threading |
Comments (2)
Arif
-
Monday, October 19, 2009
5:13:46 PM
I guess U shud look at some basic concepts of Invoke and BeginInvoke.
Check description of these function please. Both of these functions run on the Main UI thread itself. None will span another thread.
Now wat is the difference: The beginInvoke creates a function pointer and places the call on the CPU execute Queue and the process immediate lines after it.When the main thread is free, the cpu picks the function pointer of this beginInvoke and executes the function.
Invoke, calls the function immediately.
U have included System.windows.Threading but where have U used it.
My dear fellow developer, Threading is a deep and complicated concept, please dont confuse ppl
Michael C. Neel
-
Monday, October 19, 2009
7:59:14 PM
There is so much fail in the above comment, I'll just link to the documentation on asynchronous delegates:
http://msdn.microsoft.com/en-us/library/22t547yb.aspx