6

我对此感到非常困惑,它开始让我质疑我对 WPF 资源系统的整体理解

我有一个多窗口应用程序,其中每个 Window 派生对象在具有单独调度程序的单独线程上运行。

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

我有一个 Dictionary1.xaml 资源字典,其中包含一个命名的 Style 对象(它只是将 Background 属性设置为 Red 并针对 TextBox)。在我的 App.xaml 中,我通过 ResourceDictionary.MergedDictionaries 集合引用 Dictionary1.xaml。在我的其他窗口的 XAML 中,我有一个静态资源到文本框控件中的样式键,它可以工作。

我可以打开多个窗口,但不应该出现跨线程错误吗?在其中一个窗口类的构造函数中,我这样做了:

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

由于 Style 对象是从 DispatcherObject 派生的,这是否意味着它只能由拥有它的线程访问?如果在 ResourceDictionary 中定义了一个对象,这是否意味着默认情况下它是一个静态实例?这怎么可能工作?为什么我没有收到跨线程错误?

(我错误地报告了一个我后来删除的关于由其他原因引起的跨线程错误的问题)

我对此感到非常困惑 - 我认为只有冻结的 Freezable 对象可以跨线程共享。为什么允许我访问其他线程上的 DispatcherObject?

4

3 回答 3

4

所以我终于有了一个答案——我挖掘了很多 .NET 框架代码并得出以下结论:

当资源字典是应用程序级字典、主题字典或只读时,存储在资源字典中的所有项目都将被“密封”

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();
}
}

“密封”一个对象本质上使其不可变,并通过 ISealable 字典实现 - 实际上 Freezable 通过调用 Freeze() 来实现它的密封行为!Styles 也实现了它,它的实现防止了 Setters 或 Triggers 集合被修改。ISealable 由许多类实现!

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

更重要的是,我(到目前为止)见过的每个 WPF 类中的 Seal() 的每个实现都调用 DispatcherObject.DetachFromDispatcher() ,它将调度程序设置为 null!(Freeze() 在内部也调用它)

“密封”似乎实现了通常被宣传为 Freezable 独有的不变性行为,但实现 ISealable 的对象将表现出相同的行为并且是线程安全的 - Freezables 具有区分它的其他行为(例如子属性通知更改)

因此,总而言之,“密封”一个对象实际上可以跨线程共享,因为如果每个对象存在于应用程序级或主题资源字典或只读资源中,则它会自动与其调度程序分离字典

为了验证这个结论,反思 ResourceDictionary 并查看设置 ResourceDictionary 的逻辑父级(例如 FrameworkElement、FrameworkContentElement 或应用程序)的 AddOwner() 方法

这就是为什么可以从其他线程访问画笔和 Style 对象的原因,因为资源字典被合并到 Application.Resources 中,因此全部自动密封 - 毫不奇怪,将这些资源放在窗口级别会导致通常的跨线程异常.

我想您可以概括地说,ISealable 使 WPF 对象能够跨线程共享和只读,尽管从技术上讲,与 Dispatcher 分离并不是协议的要求(就像 DispatcherObjects 不需要在每个属性),因为从技术上讲,实现它自己的 Seal() 行为取决于每个对象,并且 ISealable 和 Dispatcher 之间没有直接关联

于 2012-11-26T04:49:04.780 回答
2

我也对你的“问题”感到很困惑。调度程序对象没有什么神奇之处,线程亲和性应该在继承自 DispatcherObject 的类中编码:每个不应该与多线程兼容的访问器都应该调用 DispatcherObject 提供的 base.VerifyAccess() 方法。这是 Style.Setters 属性的 getter 的定义方式:

[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;
    }
}

所以在你的情况下,确实应该抛出一个异常......

也许您可以尝试调用 s.CheckAccess() 方法。您还可以尝试调度程序比较 Object.ReferenceEquals(A,B) 方法,以确保等于运算符没有被重载(尽管我没有发现任何重载......)。

这个主题很有趣,请随时通知我们!

于 2012-11-20T09:17:18.587 回答
0

很好的问题,有两点我想提一下,

第一点是,每次您请求样式时,它都会为您提供一个新的样式对象,因此样式中的对象永远不会在多个控件之间共享,因为样式就像包含样式信息的类的一部分。

第二点是,为什么调度程序应该抛出异常,因为您的调度程序总是在单线程上运行。因此,当您更改控件的调度程序时,线程关联规则使用调度程序并在 GUI 线程上呈现事物。

这个信息有帮助吗。

于 2012-11-20T09:47:39.277 回答