Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Image handling in WPF not thread safe?

I am creating a Window in a different thread that's marked STA this window has a few controls and images.

I than go and close this window and open another window in the main UI Thread in this I have a print dialog and use the following code to get a FixedDocumentSequence:

var tempFileName = System.IO.Path.GetTempFileName();
File.Delete(tempFileName);

using (var xpsDocument = new XpsDocument(tempFileName, FileAccess.ReadWrite, CompressionOption.NotCompressed))
{
    var writer = XpsDocument.CreateXpsDocumentWriter(xpsDocument);

    writer.Write(this.DocumentPaginator);
}

using (var xpsDocument = new XpsDocument(tempFileName, FileAccess.Read, CompressionOption.NotCompressed))
{
    var xpsDoc = xpsDocument.GetFixedDocumentSequence();
    return xpsDoc;
}

On the line:

writer.Write(this.DocumentPaginator);

I am getting a InvalidOperationException from a internal call to VerifyAccess, this is the StackTrace:

bei System.Windows.Threading.Dispatcher.VerifyAccess()
bei System.Windows.Threading.DispatcherObject.VerifyAccess()
bei System.Windows.Media.Imaging.BitmapDecoder.get_IsDownloading()
bei System.Windows.Media.Imaging.BitmapFrameDecode.get_IsDownloading()
bei System.Windows.Media.Imaging.BitmapSource.FreezeCore(Boolean isChecking)
bei System.Windows.Freezable.Freeze(Boolean isChecking)
bei System.Windows.PropertyMetadata.DefaultFreezeValueCallback(DependencyObject d, DependencyProperty dp, EntryIndex entryIndex, PropertyMetadata metadata, Boolean isChecking)
bei System.Windows.Freezable.FreezeCore(Boolean isChecking)
bei System.Windows.Media.Animation.Animatable.FreezeCore(Boolean isChecking)
bei System.Windows.Freezable.Freeze()
bei System.Windows.Media.DrawingDrawingContext.DrawImage(ImageSource imageSource, Rect rectangle, AnimationClock rectangleAnimations)
bei System.Windows.Media.DrawingDrawingContext.DrawImage(ImageSource imageSource, Rect rectangle)
bei System.Windows.Media.DrawingContextDrawingContextWalker.DrawImage(ImageSource imageSource, Rect rectangle)
bei System.Windows.Media.RenderData.BaseValueDrawingContextWalk(DrawingContextWalker ctx)
bei System.Windows.Media.DrawingServices.DrawingGroupFromRenderData(RenderData renderData)
bei System.Windows.UIElement.GetDrawing()
bei System.Windows.Media.VisualTreeHelper.GetDrawing(Visual reference)
bei System.Windows.Xps.Serialization.VisualTreeFlattener.StartVisual(Visual visual)
bei System.Windows.Xps.Serialization.ReachVisualSerializer.SerializeTree(Visual visual, XmlWriter resWriter, XmlWriter bodyWriter)
bei System.Windows.Xps.Serialization.ReachVisualSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.DocumentPageSerializer.SerializeChild(Visual child, SerializableObjectContext parentContext)
bei System.Windows.Xps.Serialization.DocumentPageSerializer.PersistObjectData(SerializableObjectContext serializableObjectContext)
bei System.Windows.Xps.Serialization.ReachSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.DocumentPageSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.DocumentPaginatorSerializer.PersistObjectData(SerializableObjectContext serializableObjectContext)
bei System.Windows.Xps.Serialization.DocumentPaginatorSerializer.SerializeObject(Object serializedObject)
bei System.Windows.Xps.Serialization.XpsSerializationManager.SaveAsXaml(Object serializedObject)
bei System.Windows.Xps.XpsDocumentWriter.SaveAsXaml(Object serializedObject, Boolean isSync)
bei System.Windows.Xps.XpsDocumentWriter.Write(DocumentPaginator documentPaginator)

Since the StackTrace does some call to BitmapSource/BitmapDecoder, I thought about trying to remove the Images and set the Source of the in-place Image controls to null

<Image Source={x:Null} />

After I did this with all of my Images my code was running smoothly and no more exception was firing.

I tried to make a customimage to solve this problem with the following:

public class CustomImage : Image
{
    public CustomImage()
    {
        this.Loaded += CustomImage_Loaded;
        this.SourceUpdated += CustomImage_SourceUpdated;
    }

    private void CustomImage_SourceUpdated(object sender, System.Windows.Data.DataTransferEventArgs e)
    {
        FreezeSource();
    }

    private void CustomImage_Loaded(object sender, System.Windows.RoutedEventArgs e)
    {
        FreezeSource();
    }

    private void FreezeSource()
    {
        if (this.Source == null)
            return;

        var freeze = this.Source as Freezable;
        if (freeze != null && freeze.CanFreeze && !freeze.IsFrozen)
            freeze.Freeze();
    }
}

But I am still getting the error. Preferable I am searching for a solution that works on all images in my WPF application.

Hope I made my self clear, since this is rather odd to explain with the 2 threads and a random exception at some point.

Edit: After some further testing I am now able to present to you a reproducible application with the problem at hand, hope its clearer with this.

You need 3 windows, 1 folder and 1 image. In my case its MainWindow.xaml Window1.xaml Window2.xaml

Images is the name of the folder, and in there is an image called "plus.png".

MainWindow.xaml:

<StackPanel Orientation="Vertical">
    <StackPanel.Resources>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Margin"
                    Value="0,0,0,5"></Setter>
        </Style>
    </StackPanel.Resources>
    <Button Content="Open Window 1" Click="OpenWindowInNewThread" />
    <Button Content="Open Window 2" Click="OpenWindowInSameThread" />
</StackPanel>

MainWindow.xaml.cs:

private void OpenWindowInNewThread(object sender, RoutedEventArgs e)
{
    var th = new Thread(() =>
    {
        SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));

        var x = new Window1();
        x.Closed += (s, ec) => Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Background);

        x.Show();

        System.Windows.Threading.Dispatcher.Run();
    });
    th.SetApartmentState(ApartmentState.STA);
    th.IsBackground = true;
    th.Start();
}

private void OpenWindowInSameThread(object sender, RoutedEventArgs e)
{
    var x = new Window2();
    x.Show();
}

Window1.xaml:

<StackPanel Orientation="Horizontal">
    <ToggleButton Template="{StaticResource PlusToggleButton}" />
</StackPanel>

Window1.xaml.cs: No Code in there just constructor...

Window2.xaml:

<StackPanel Orientation="Horizontal">
    <ToggleButton Template="{StaticResource PlusToggleButton}" />
    <Button Content="Print Me" Click="Print"></Button>
</StackPanel>

Window2.xaml.cs:

public void Print(object sender, RoutedEventArgs e)
{
    PrintDialog pd = new PrintDialog();
    pd.PrintVisual(this, "HelloWorld");
}

App.xaml:

<ControlTemplate x:Key="PlusToggleButton"
                    TargetType="{x:Type ToggleButton}">
    <Image Name="Image"
            Source="/WpfApplication1;component/Images/plus.png"
            Stretch="None" />
</ControlTemplate>

Steps to reproduce:

  1. In the MainWindow click on the button saying "Open Window 1".
  2. A window will popup in a second UI-Thread, close this Window.
  3. Click the button saying "Open Window 2"
  4. A window will popup in the main UI-Thread
  5. Hit the button saying "Print Me", application should crash

Hope its now easier to help me out.

Edit2:

Added the missing code part, sorry about this mistake.

Another info that might help solve the problem is that when you click the buttons in reversed order - first Window 2 and than Window 1 - and than go try to print no exception will fire, so I still believe that its some image caching problem, that when the image first gets loaded into main UI-Thread printing works if not it will fail.

like image 794
Rand Random Avatar asked Jan 16 '15 19:01

Rand Random


1 Answers

You're using an UI object that was created by Window1 on Window2.

Essentially, parts of ControlTemplate are shared between threads, which should not happen (namely BitmapImage, as I've read from your callstack).

You can explictly tell that no sharing:

<ControlTemplate x:Key="PlusToggleButton"
                    TargetType="{x:Type ToggleButton}"
                    x:Shared="False">
like image 92
Erti-Chris Eelmaa Avatar answered Oct 06 '22 07:10

Erti-Chris Eelmaa