views:

1646

answers:

6

I am trying to load an image in the background and then update the UI. I have been playing with this all day and I don't know what I am missing. I keep getting the following error:

"The calling thread cannot access this object because a different thread owns it."

I've hunted around following example after example, but I cannot seem to find an answer. I also wrapped the code that is touching the UI in another BeginInvoke.

Update 3: The moral of the story. ImageSource is not thread safe for access.

Update 2: This has got to be a simple solution :). I tried the cloning, but that didn't result in success, but I did get a different error: "Exception has been thrown by the target of an invocation."

Update 1: I tried the BackgroundWorker, but I am still getting the same error, but it is occurring on the brush.ImageSource.Height. Am I signaling the UI correctly? Any suggestions?

Here is my XAML:

<Window x:Class="Slideshow.Show"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"&gt;
    <DockPanel>
        <Canvas Background="Black" Name="canvas">
            <Viewbox Name="background" Stretch="Uniform">
                <Rectangle name="background" />
            </Viewbox>
        </Canvas>
    </DockPanel>
</Window>

Here is some of the code behind:

namespace Slideshow
{
    public class Show 
    {
        public Show()
        {
            BackgroundWorker bw = new BackgroundWorker();
            bw.DoWork += new DoWorkEventHandler(bw_DoWork);
            bw.RunWorkerCompleted += 
                new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
            bw.RunWorkerAsync();
        }

        void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            BitmapSource bitmap = e.Result as BitmapSource;

            if (bitmap != null)
            {
                this.Dispatcher.BeginInvoke(DispatcherPriority.Normal 
                    (ThreadStart)delegate()
                {
                    Image image = new Image();
                    image.Source = bitmap;
                    background.Child = image;
                 });
            }
        }

        void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            BitmapSource bitmapSource = CreateBrush(GetRandomFile());
            e.Result = bitmapSource;
         }
    }
}
+1  A: 

You want to incorporate multiple threads in your WPF application. As stated in the article below, WPF forces you to do all UI work on the thread that created the UI.

Check this out: http://pjbelfield.wordpress.com/2007/10/29/worker-threads-and-the-dispatcher-in-wpf/

and for intensive UI work:

http://eprystupa.wordpress.com/2008/07/28/running-wpf-application-with-multiple-ui-threads/

Joshua
A: 

Your code to load the image (.jpg) into memory should be done in the background. Code to load the image from memory into a WPF control must be done in the normal UI WPF thread. Only slow/long running tasks should be put into a background thread. Once the long task is complete, it must signal the UI thread to update the view with the result of the long task.

My favourite way is using the BackgroundWorker class:

var bg = new System.ComponentModel.BackgroundWorker();
bg.DoWork += Work_Function;
bg.RunWorkerCompleted += UI_Function;
bg.RunWorkerAsync();

void Work_Function(object sender, DoWorkEventArgs e) { ... }
void UI_Function(object sender, RunWorkerCompletedEventArgs e) { ... }

When you call RunWorkerAsync(), first your Work_Function() is called. When it is complete, the UI_Function() is called. The work function may place one variable in the DoWorkEventArgs.Result property, which is accessible from the RunWorkerCompletedEventArgs.Result property.

I think you might also be interested in this article: http://www.codeproject.com/KB/WPF/WPF_Explorer_Tree.aspx It shows how to set up Lazy Loading in a tree view in WPF. I think the basic concept (add a dummy node, intercept the display of the dummy node by loading the next image instead) will apply to your work.

vanja.
How do I signal the UI thread?
daub815
I have updated my answer to show an example of how it can be done.
vanja.
Thanks. I will give that a shot.
daub815
I tried the Background Worker, but to no avail. The same issue. :(
daub815
A: 

The error you are getting now is good news, it means that BackgroundWorker is working. You could try cloning it in the RunWorkerCompleted with brush.Clone();.

Try removing the Dispatcher.BeingInvoke part. The whole point of BackgroundWorker is that the event is marshaled back into the UI thread for you automatically so you don't have to worry.

Samuel
Unfortunately, cloning didn't work, but I am getting a different error: "Exception has been thrown by the target of an invocation."
daub815
+6  A: 

What I tend to use the ThreadPool.QueueUserWorkItem to load the image, then when the operation completes I call back to the UI thread using the thread-safe Dispatcher object. The image source is not thread safe, you will have to use something like the JpegBitmapDecoder, there is also a PngBitmapDecoder.

For Example:

public Window()
{
    InitializeComponent();

    ThreadPool.QueueUserWorkItem(LoadImage,
         "http://z.about.com/d/animatedtv/1/0/1/m/simp2006_HomerArmsCrossed_f.jpg");
}

public void LoadImage(object uri)
{
    var decoder = new JpegBitmapDecoder(new Uri(uri.ToString()), BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
    decoder.Frames[0].Freeze();
    this.Dispatcher.Invoke(DispatcherPriority.Send, new Action<ImageSource>(SetImage), decoder.Frames[0]);
}

public void SetImage(ImageSource source)
{
    this.BackgroundImage.Source = source;
}
bendewey
OMG! I have been so frustrated with the ImageSource that I never noticed in the documentation that it was not Thread Safe.
daub815
There ya go. Just like like how Forms/WPF is STA only, except not as well documented.
Samuel
+1  A: 

One more thing we had in our project. Since ImageSource is placed into UI you have to check if it is frozen:

public void SetImage(ImageSource source)
{
   ImageSource src = null;
   if(!source.IsFrozen)
       src = source.GetAsFrozen();
   else
       src = source; 
   this.BackgroundImage.Source = src;
}
Methos
I think I ran into an issue that calling IsFrozen wasn't thread safe. AKA if it wasn't frozen, I got an error. Thanks for the heads up.
daub815
I have updated SetImage method which is used in bendewey's sample. Of course it must be called with Dispatcher.Invoke. And before calling Dispatcher.Invoke you should better call Dispatcher.CheckAccess which will return true if dispatching is needed and false if you can update UI without Dispatcher
Methos
A: 

i'm trying this also, but i'm getting a problem with jpg files but i'm getting "Interface not registered..." anyone know what that's all about?

I would recommend asking the question on Stackflow not as an answer and reference this question.
daub815