Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are Freezables already frozen and have a null Dispatcher (same with Styles) when stored in Application.Resources?

Tags:

c#

.net

wpf

xaml

I'm very confused by this and it's starting to make me question my whole understanding of the WPF resource system

I have a multi-window application where each Window-derived object runs on a separate thread with separate dispatcher.

Thread t = new Thread(() => {
    Window1 win = new Window1();
    win.Show();
    System.Windows.Threading.Dispatcher.Run();
});
t.SetApartmentState(ApartmentState.STA);
t.Start();

I have a Dictionary1.xaml resource dictionary with a named Style object inside it (it just sets the Background property to Red and is targetted at a TextBox). In my App.xaml I reference Dictionary1.xaml via the ResourceDictionary.MergedDictionaries collection. In the XAML of my other windows I have a StaticResource to the style key in a textbox control, which works.

I'm able to open multiple windows but shouldn't I be getting cross-threading errors? In the constructor of one of the window classes I did this:

Style s = (Style)TryFindResource("TestKey");
Console.WriteLine(((Setter)s.Setters[0]).Property.Name);    // no problem
s.Dispatcher == this.Dispatcher    // false

Since a Style object is derived from DispatcherObject, doesn't that mean it's only accessible to the thread that owns it? And if an object is defined in a ResourceDictionary, doesn't that mean that by default, it's a static instance? How is this even able to work? Why aren't I getting a cross-threading error?

(I erroneously reported a question I since deleted about a cross threading error that was caused by something else)

I'm very confused by this - I thought only frozen Freezable objects were shareable across threads. Why am I allowed to access a DispatcherObject on other threads?

like image 785
blue18hutthutt Avatar asked Nov 20 '12 07:11

blue18hutthutt


2 Answers

So I have an answer finally - I dug through alot of the .NET framework code and came to the following conclusions:

When a resource dictionary is either an Application-level dictionary, a theme dictionary or read-only, all items stored in the resource dictionary will be "sealed"

if (this.IsThemeDictionary || this._ownerApps != null || this.IsReadOnly)
{
    StyleHelper.SealIfSealable(value);
}

... 

internal static void SealIfSealable(object value)
{
ISealable sealable = value as ISealable;
if (sealable != null && !sealable.IsSealed && sealable.CanSeal)
{
    sealable.Seal();
}
}

"Sealing" an object essentially renders it immutable and is implemented via the ISealable dictionary - in fact Freezable implements it's sealing behavior by calling Freeze()! Styles implement it too and its implementation prevents the Setters or Triggers collections from being modified. ISealable is implemented by many classes!

public abstract class Freezable : DependencyObject, ISealable
public class Style : DispatcherObject, INameScope, IAddChild, ISealable, IHaveResources, IQueryAmbient

More importantly, every implementation of Seal() in every WPF class I've seen (so far) calls DispatcherObject.DetachFromDispatcher() which sets the dispatcher to null! (Freeze() internally calls this too)

"Sealing" appears to implement the immutability behavior that is often advertised as being exclusive to Freezable, but objects implementing ISealable will exhibit the same behavior and be thread-safe - Freezables have additional behavior that distinguishes it (eg sub-property notification change)

So to conclude, "sealing" an object, in effect, enables it to be shared across threads due to each object being detached from its dispatcher automatically if it is present in an Application-level or theme resource dictionary or in a read-only resource dictionary

To verify this conclusion, reflect over ResourceDictionary and look at the AddOwner() method that sets the logical parent of the ResourceDictionary (eg a FrameworkElement, FrameworkContentElement or Application)

This is why the brushes and Style object were accessible from other threads, because the resource dictionaries were merged into the Application.Resources and were thus all automatically sealed - not surprisingly putting these resources at the Window-level will cause the usual cross-threading exception.

I guess you can generalize that ISealable is what enables WPF objects to be shareable across threads and read-only, although technically detaching from the Dispatcher is not a requirement of the protocol (just like DispatcherObjects are not required to make VerifyAccess() calls in every property) since it's technically up to every object to implement it's own Seal() behavior and there is no immediate correlation between ISealable and Dispatcher

like image 109
blue18hutthutt Avatar answered Oct 06 '22 00:10

blue18hutthutt


I am also very confused with your "issue". There is no magic with dispatcher object, the thread affinity should be coded in the class inheriting from DispatcherObject: Each accessor that shouldn't be multi threading compliant should call the base.VerifyAccess() method provided by DispatcherObject. And here is the way the getter of Style.Setters property is defined:

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public SetterBaseCollection Setters
{
    get
    {
        base.VerifyAccess();
        if (this._setters == null)
        {
            this._setters = new SetterBaseCollection();
            if (this._sealed)
            {
                this._setters.Seal();
            }
        }
        return this._setters;
    }
}

So in your case, an exception should indeed been thrown...

Maybe you could try to call the s.CheckAccess() method. You could also try for dispatcher comparison Object.ReferenceEquals(A,B) method to ensure the fact the equal operator has not been overloaded (although I didn't find any overloading...).

This subject is very interesting, keep us informed!

like image 40
Charles HETIER Avatar answered Oct 06 '22 00:10

Charles HETIER