WPF: Threading basics in WPF

imageIn 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 (7)

Submit this story to DotNetKicks   

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

epidemicz - Tuesday, July 12, 2011 12:14:44 PM

Very interesting, I like this method and I appreciate you taking the time to share it with us.

Arif does have a point that System.Threading does not look like it is necessary, but that's not really a big deal.

I think it's also worth mentioning that with BeginInvoke it seems like you're going to be limited to by the available threads in the Thread Pool. As you said though, that is probably out of the scope of this article as this code works rather well.

Thanks again!

tom cat - Sunday, August 28, 2011 4:41:44 PM

yeah begininvoke will just use the main UI thread

Mike - Sunday, August 28, 2011 9:51:47 PM

From the docs linked: "If the BeginInvoke method is called, the common language runtime (CLR) queues the request and returns immediately to the caller. The target method is called asynchronously on a thread from the thread pool."

If you guys had actually tried out what you've assumed, you would see an exception "The calling thread cannot access this object because a different thread owns it."

Adam - Monday, March 19, 2012 5:40:01 AM

@Mike Don't worry, not all of us are complete morons.

@Arif @epidemicz The use of System.Windows.Threading is not pointless as it contains the DispatcherPriority enumeration that has been used.

@Arif @tom cat It is common knowledge that Control.BeginInvoke of old and Dispatcher.BeginInvoke of new calls an arbitrary Thread Pool thread to offload the work.

Adam - Monday, March 19, 2012 5:43:18 AM

@All Apologies Control.BeginInvoke and Dispatcher.BeginInvoke actually marshal calls back onto the UI thread asynchronously from wherever they are called. Got the wrong end of the stick there.

However, BeginInvoke on the delegate will use an arbitrary Thread Pool thread.

Leave a comment



Your name:
 

Your email (not shown):
 
Will display your Gravatar image.

Your website (optional):



About Michael

Michael C. Neel, born 1976 in Houston, TX and now live in Knoxvile, TN. Software developer, currently .Net focused. Board member of ETNUG and organizes CodeStock, East Tennessee's annual developers conference. .Net speaker, a Microsoft ASP.NET MVP and ASPInsider. Co-Founder of FuncWorks, LLC and GameMarx.

Proud father of two amazing girls, Rachel and Hannah, and loving husband to Cicelie who inflates and pops his ego as necessary.

 Subscribe to ViNull.com |  Comments

Follow me on Twitter | Contact Me

Related Posts

Ada Lovelace Day: Mary Everest Boole and Beautiful Math

October 16th, 2012 is technically Ada Lovelace Day, a day for celebrating women in science, technology, engineering, and math (STEM), but as long as it's ... Read more

SilverLight 2.0: Setting the Background of a Button

I know, this hardly seems a topic worthy of a blog post.  To create a Button in SilverLight like the first in the picture is just: <Button Content="Stock ... Read more

Code Camp Room Planner

I just finished creating a project on CodePlex for the Code Camp Room Planner, a little app I wrote to help me plan the schedule at CodeStock 2009.  ... Read more

WPF: Using Viewbox and Canvas to create a virtual resolution workspace

First, for you SilverLight 2.0 junkies, you can skip over this.  SilverLight 2.0 doesn't (yet?) support the WPF ViewBox. The problem: when you start ... Read more

WPF: Dive into WPF

For this first post on WPF I've decided it would be best to start off with a "real" application.  This means we'll need those thing almost all applications ... Read more

XNA 3D Primer by Michael C. Neel

XNA 3D Primer by Michael C. Neel
Buy Now: [ Amazon ] [ Wrox ]

GameMarx

CodeStock

ASPInsiders Member

ETNUG Member