How to copy DispatcherObject (BitmapSource) into different thread?
Asked Answered
C

4

5

I am trying to figure out how can I copy DispatcherObject (in my case BitmapSource) into another thread.

Use case:
I have a WPF app that needs to show window in a new thread (the app is actually Outlook addin and we need to do this because Outlook has some hooks in the main UI thread and is stealing certain hotkeys that we need to use - 'lost in translation' in interop of Outlook, WPF (which we use for UI), and Winforms (we need to use certain microsoft-provided winforms controls)).

With that, I have my implementation of WPFMessageBox, that is configured by setting some static properties - and and one of them is BitmapSource for icon. This is used so that in startup I can set WPFMessageBox.Icon once, and since then, every WPFMessageBox will have the same icon.

The problem is that BitmapSource, which is assigned into icon, is a DispatcherObject, and when read, it will throw InvalidOperationException: "The calling thread cannot access this object because a different thread owns it.".

How can I clone that BitmapSource into the actual thread? It has Clone() and CloneCurrentValue() methods, which don't work (they throw the same exception as well). It also occured to me to use originalIcon.Dispatcher.Invoke( do the cloning here ) - but the BitmapSource's Dispatcher is null, and still - I'd create a copy on a wrong thread and still couldnt use it on mine. BitmapSource.IsFrozen == true.

Any idea on how to copy the BitmapSource into different thread (without completely reconstructing it from an image file in a new thread)?

EDIT: So, freezing does not help: In the end I have a BitmapFrame (Window.Icon doesn't take any other kind of ImageSource anyway), and when I assign it as a Window.Icon on a different thread, even if frozen, I get InvalidOperationException: "The calling thread cannot access this object because a different thread owns it." with a following stack trace:

    WindowsBase.dll!System.Windows.Threading.Dispatcher.VerifyAccess() + 0x4a bytes 
    WindowsBase.dll!System.Windows.Threading.DispatcherObject.VerifyAccess() + 0xc bytes    
    PresentationCore.dll!System.Windows.Media.Imaging.BitmapDecoder.Frames.get() + 0xe bytes    
    PresentationFramework.dll!MS.Internal.AppModel.IconHelper.GetIconHandlesFromBitmapFrame(object callingObj = {WPFControls.WPFMBox.WpfMessageBoxWindow: header}, System.Windows.Media.Imaging.BitmapFrame bf = {System.Windows.Media.Imaging.BitmapFrameDecode}, ref MS.Win32.NativeMethods.IconHandle largeIconHandle = {MS.Win32.NativeMethods.IconHandle}, ref MS.Win32.NativeMethods.IconHandle smallIconHandle = {MS.Win32.NativeMethods.IconHandle}) + 0x3b bytes   
>   PresentationFramework.dll!System.Windows.Window.UpdateIcon() + 0x118 bytes  
    PresentationFramework.dll!System.Windows.Window.SetupInitialState(double requestedTop = NaN, double requestedLeft = NaN, double requestedWidth = 560.0, double requestedHeight = NaN) + 0x8a bytes  
    PresentationFramework.dll!System.Windows.Window.CreateSourceWindowImpl() + 0x19b bytes  
    PresentationFramework.dll!System.Windows.Window.SafeCreateWindow() + 0x29 bytes 
    PresentationFramework.dll!System.Windows.Window.ShowHelper(object booleanBox) + 0x81 bytes  
    PresentationFramework.dll!System.Windows.Window.Show() + 0x48 bytes 
    PresentationFramework.dll!System.Windows.Window.ShowDialog() + 0x29f bytes  
    WPFControls.dll!WPFControls.WPFMBox.WpfMessageBox.ShowDialog(System.Windows.Window owner = {WPFControlsTest.MainWindow}) Line 185 + 0x10 bytes  C#
Crucial answered 3/5, 2010 at 15:18 Comment(0)
C
3

The key is to create the bitmap on the thread you want to use. So you can't cache your icon in some static field/property, bud load it (from file, resource, stream or whatever) every time you're opening a new window on new thread.

BitmapFrame can be used on the thread it was created only.

Even cloning doesn't work here, as you correctly stated (which just sucks).

I had exactly the same problem and solved it just by loading icon every time, in my particular case simply by calling

// get your stream somewhere - 
window.Icon = BitmapFrame.Create(stream)

And this is how you can get your icon from resource in WPF:

var streamResourceInfo = Application.GetResourceStream(new Uri(@"pack://application:,,,/YourAssembly;relative path to the icon", UriKind.RelativeOrAbsolute));
// use streamResourceInfo.Stream 
Constantan answered 30/12, 2010 at 2:40 Comment(0)
B
8

Once you call Freeze, it should work on multiple threads.

Babarababassu answered 3/5, 2010 at 15:20 Comment(3)
Thank you - I tried that before, but other conditions have thrown the exception, after cleaning the code, Freeze() is enough.Modlin
Ok, sorry - it seems that freezing BitmapFrame doesn't allow it to be used in a different thread as a window icon - see my edit of question.Modlin
I was trying to generate a bitmap in one thread, and save it in another, and in that case, Freeze() works.Valine
E
5

bitmapSourceForOtherThread = new WriteableBitmap(previousBitmapSource);

This comes at a price, but it's pretty cheap comparing to serializing.

Long answer.

Estoppel answered 14/12, 2011 at 18:32 Comment(6)
For my case, this was the only solution that worked. I needed to feed a BitmapFrame into a TransformedBitmap. Didn't have control over which thread the BitmapFrame was created on as it is provided by a library (and BitmapFrame's Dispatcher property is null). Even querying the CanFreeze property caused an exception. I had two options: major overhaul of the source code of mine and others, or use WriteableBitmap. Guess which one i chose...Emotive
I was unable to use new WriteableBitmap to clone a WriteableBitmap that was created on another thread.Valine
@noa: what was the error/exception? Did you look at the code on the blog?Estoppel
The exception is this: System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it. I did look at the code. It looks like your original bitmapSource is loaded from a file.Valine
By the way, the exception was raised on the call to new WriteableBitmap.Valine
Can you make a minimal example of new WriteableBitmap failing in your scenario?Estoppel
C
3

The key is to create the bitmap on the thread you want to use. So you can't cache your icon in some static field/property, bud load it (from file, resource, stream or whatever) every time you're opening a new window on new thread.

BitmapFrame can be used on the thread it was created only.

Even cloning doesn't work here, as you correctly stated (which just sucks).

I had exactly the same problem and solved it just by loading icon every time, in my particular case simply by calling

// get your stream somewhere - 
window.Icon = BitmapFrame.Create(stream)

And this is how you can get your icon from resource in WPF:

var streamResourceInfo = Application.GetResourceStream(new Uri(@"pack://application:,,,/YourAssembly;relative path to the icon", UriKind.RelativeOrAbsolute));
// use streamResourceInfo.Stream 
Constantan answered 30/12, 2010 at 2:40 Comment(0)
R
2

One workaround that does work, although not very performant, is creating a memory stream from the image data, then reconstructing the image on the thread you want to use it on.

Example for BitmapSource:

Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)delegate()
{
    //serialize image on UI thread
    imageStream = GetImageBytes(cameraImage);
}

...
//reconstruct image on a different thread:
Bitmap bitmap = new Bitmap(imageStream); 

private MemoryStream GetImageBytes(BitmapSource image)
{
    MemoryStream ms = new MemoryStream();
    BitmapEncoder encoder = new PngBitmapEncoder();
    encoder.Frames.Add(BitmapFrame.Create(image));
    encoder.Save(ms);
    ms.Seek(0, SeekOrigin.Begin);
    return ms;
}
Ray answered 30/12, 2010 at 2:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.