关闭 'Resource not found' 警告

Turn off 'Resource not found' trace log

Posted by eagleboost on June 16, 2020

1. The problem

When working with WPF applications, it’s common to see logs like below in the Output window of the debugger (Visual Studio, Rider, etc).

1
System.Windows.ResourceDictionary Warning: 9 : Resource not found; ResourceKey='xxxxxx'

Usually developers should pay attention because it means certain resource cannot be found by the WPF infrastructure and it may result in some controls in the UI not being displayed properly.

This ‘Resource not found’ trace log only happens to the resources marked by the DynamicResource markup extension and when the debugger is attached. With DynamicResource the resource is not resolved until runtime when the expression is evaluated.

StaticResource markup extension is different. It causes resolving of the resource happen during loading of the XAML , when a resource cannot be resolved, exceptions would be thrown. It’s static, set once and never change (unless explicitly).

One typical use case of the DynamicResource is implementing theme aware applications. Most of the time switching theme only turns the GUI into different look and feel by changing foreground, background, font etc, so throwing exception may be too much of noises. This possibly could be one of the design considerations of DynamicResource.

Even though developers should pay attention but in some cases they becomes noises in the output when we don’t want to see them, and the problem is now how can we turn it off.

2. Analysis

First of all, there is no official documented way to turn it off. Below is some answers from Microsoft support team can be found on Stack Overflow:

Thanks for the update. I was afraid of that, since my testing found similar results. It seems there is some internal WPF tracing code which does not adhere to the specified settings. In the meantime, We don’t have any suggestions other than finding the Resource Dictionary (or the relevant type) and correcting the issues that the trace output is warning about.

if a debugger was attached, there will always be some WPF tracing emitted regardless of the settings specified in the IDE (or in the app.config). Unfortunately, the output you are receiving appears to fall into this category. Regrettably, there is no way to turn off all the WPF trace output from being emitted

We could certainly file a feature request for the product for this to be considered in a future release, but otherwise I don’t see a way for you to avoid the issue in the current release.

So most likely we have to hack it some how, and let’s find out how.

Because the log starts with System.Windows.ResourceDictionary so we first take a look at the source code of the ResourceDictionary class. It’s obvious to see the tracing is done by TraceResourceDictionary because of the usages like this:

1
2
3
4
if (TraceResourceDictionary.IsEnabled)
{
  TraceResourceDictionary.Trace(...);
}

However, TraceResourceDictionary is an internal class and its source codes are not available on Reference Source. There’re plenty of free disassemble tools out there, my favorite one is the awesome free .net Decompiler and Assembly Browser dotPeek

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
internal static class TraceResourceDictionary
{
  private static AvTrace _avTrace = new AvTrace((GetTraceSourceDelegate) (...);
  
  public static bool IsEnabled
  {
    get
    {
      if (_avTrace != null)
        return _avTrace.IsEnabled;
      return false;
    }
  }

  public static bool IsEnabledOverride
  {
    get
    {
      return _avTrace.IsEnabledOverride;
    }
  }
}

Most of the details are omitted, notice that this static class has two properties IsEnabled and IsEnabledOverride. IsEnabled used in many places but IsEnabledOverride is only used by the FrameworkElement.FindResourceInternal, and it happen to be logging trace messages of ResourceNotFound:

1
2
3
4
5
6
7
8
9
10
11
12
internal static object FindResourceInternal(...)
{
  ...
  if (TraceResourceDictionary.IsEnabledOverride && !isImplicitStyleLookup)
  {
    if (fe != null && fe.IsLoaded || fce != null && fce.IsLoaded)
      TraceResourceDictionary.Trace(TraceEventType.Warning, TraceResourceDictionary.ResourceNotFound, resourceKey);
    else if (TraceResourceDictionary.IsEnabled)
      TraceResourceDictionary.TraceActivityItem(TraceResourceDictionary.ResourceNotFound, resourceKey);
  }
  ...
}

Look further into AvTrace.IsEnabledOverride, it simply does null reference check against a private field _traceSource, which may be created given whether the ManagedTracing flag is enabled in the Windows Registry, or the is the debugger attached, or a flag that indicating explicit refresh is true.

To get what we want to turn the warning trace off, without considering refresh (uncommon) and ManagedTracing being disabled by default in the Windows Registry, the solution is to set _traceSource to null so that TraceResourceDictionary.IsEnabledOverride would return false and trace logs would be skipped.

TraceResourceDictionary.IsEnabled is used for other cases and beyond the scope so we skip discussing it in this article.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
internal class AvTrace
{
  private TraceSource _traceSource;

  public bool IsEnabledOverride
  {
    get
    {
      return this._traceSource != null;
    }
  }

  private void Initialize()
  {
    if (AvTrace.ShouldCreateTraceSources())
    {
      this._traceSource = this._getTraceSourceDelegate();
      this._isEnabled = AvTrace.IsWpfTracingEnabledInRegistry() || AvTrace._hasBeenRefreshed || this._enabledByDebugger;
    }
    else
    {
      this._clearTraceSourceDelegate();
      this._traceSource = (TraceSource) null;
      this._isEnabled = false;
    }
  }

  private static bool ShouldCreateTraceSources()
  {
    return AvTrace.IsWpfTracingEnabledInRegistry() || AvTrace.IsDebuggerAttached() || AvTrace._hasBeenRefreshed;
  }

3. Implementations

Coding is fairly straightforward with Reflection calls all the way to clear out _traceSource. In terms of timing, since TraceResourceDictionary is accessed pretty early during the initialization of a WPF application so we can call SkipResourceNotFound.Install() right in the Application.OnStartup override method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
////GetNonPublicStaticField and GetNonPublicField are extensions methods that does what their name suggests
public static class SkipResourceNotFound
{
  public static void Install()
  {
    try
    {
      var typeName = "MS.Internal.TraceResourceDictionary";
      ////Get the type of MS.Internal.TraceResourceDictionary
      var traceResDict = FindType(typeName);
      ////Get the static private field of _avTrace
      var avTrace = traceResDict.GetNonPublicStaticField("_avTrace").GetValue(null);
      var avTraceType = avTrace.GetType();
      ////Get private field of _traceSource and set it to null
      var traceSourceField = avTraceType.GetNonPublicField("_traceSource");
      traceSourceField.SetValue(avTrace, null);
    }
    catch
    {
    }
  }

  private static Type FindType(string typeName)
  {
    Type type = null;
    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
      type = Type.GetType(typeName + "." + assembly.GetName());
      if (type != null)
      {
        return type;
      }
    }

    return null;
  }
}

References

1. 问题

在开发WPF应用的过程中,开发者常常会在调试器(Visual StudioRider等)的输出窗口看到下面的输出:

1
System.Windows.ResourceDictionary Warning: 9 : Resource not found; ResourceKey='xxxxxx'

一般来说这表明程序有问题,因为WPF基础库在运行时找不到某个资源,结果可能导致某些空间显示异常,开发者应该引起重视。

值得注意的是“Resource not found”这种警告只在使用DynamicResource的情况下发生,一般来说同时也需要调试器存在,而生产环境并不受影响。使用DynamicResource的目的是告知WPF把资源查询的操作延迟到该资源真正被访问的时候发生。

StaticResource则不同。使用StaticResource的情况下,在把XAML载入内存的时候WPF就会搜索资源系统,找到资源并设置给相应的对象。如果资源找不到就会抛出异常而不是输出警告,因为StaticResource是静态的,除非运行时显示修改,否则设置好之后在相应的界面生命周期中是不变的。

DynamicResource的一个典型应用是用来实现支持换肤的程序。大多数时候换肤只不过是界面的颜色、字体、背景之类发生改变,这种情况下如果抛异常似乎有点过分,毕竟程序的功能不会受影响。DynamicResource的设计思路大概也考虑到了这一点,所以输出警告更为合理。

尽管“Resource not found”警告需要引起开发者的注意,但当我们并不希望看到的时候,它们就成了输出窗口中的噪音。所以本文要讨论的问题就是怎样关掉这种警告。

2. 分析

首先需要明确的是在官方文档中找不到已知方法来关掉这种警告。在Stack Overflow上的一个问题的回答中可以找到以下回答来自微软技术支持部门的回答:

Thanks for the update. I was afraid of that, since my testing found similar results. It seems there is some internal WPF tracing code which does not adhere to the specified settings. In the meantime, We don’t have any suggestions other than finding the Resource Dictionary (or the relevant type) and correcting the issues that the trace output is warning about.

if a debugger was attached, there will always be some WPF tracing emitted regardless of the settings specified in the IDE (or in the app.config). Unfortunately, the output you are receiving appears to fall into this category. Regrettably, there is no way to turn off all the WPF trace output from being emitted

We could certainly file a feature request for the product for this to be considered in a future release, but otherwise I don’t see a way for you to avoid the issue in the current release.

所以我们只能靠自力更生,看下面的分析:

注意到输出以”System.Windows.ResourceDictionary“开头,所以先来看看ResourceDictionary的源代码。很容易发现很多类似于下面的调用,显然真正的日志输出操作是由TraceResourceDictionary来完成的:

1
2
3
4
if (TraceResourceDictionary.IsEnabled)
{
  TraceResourceDictionary.Trace(...);
}

TraceResourceDictionary是在Reference Source上没有源代码的内部类,我们可以用反编译工具来查看其代码。类似工具很多,我最常用的是来自JetBrains的免费工具dotPeek

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
internal static class TraceResourceDictionary
{
  private static AvTrace _avTrace = new AvTrace((GetTraceSourceDelegate) (...);
  
  public static bool IsEnabled
  {
    get
    {
      if (_avTrace != null)
        return _avTrace.IsEnabled;
      return false;
    }
  }

  public static bool IsEnabledOverride
  {
    get
    {
      return _avTrace.IsEnabledOverride;
    }
  }
}

上面的代码略去了大多数实现细节,重点是两个属性IsEnabledIsEnabledOverride. IsEnabled在很多地方被调用到,但是IsEnabledOverride的使用者只有一处——FrameworkElement.FindResourceInternal,碰巧跟ResourceNotFound相关:

1
2
3
4
5
6
7
8
9
10
11
12
internal static object FindResourceInternal(...)
{
  ...
  if (TraceResourceDictionary.IsEnabledOverride && !isImplicitStyleLookup)
  {
    if (fe != null && fe.IsLoaded || fce != null && fce.IsLoaded)
      TraceResourceDictionary.Trace(TraceEventType.Warning, TraceResourceDictionary.ResourceNotFound, resourceKey);
    else if (TraceResourceDictionary.IsEnabled)
      TraceResourceDictionary.TraceActivityItem(TraceResourceDictionary.ResourceNotFound, resourceKey);
  }
  ...
}

再进一步可以看到AvTrace.IsEnabledOverride属性只是简单查询_traceSource字段是否为空,_traceSource在某些情况下会被创建,比如Windows注册表中打开了ManagedTracing开关,或者当前处在调试器环境中,或者一个标志显示刷新的静态变量为真。

要达到关闭“Resource not found”警告的,在不考虑显示刷新(刷新并不常见)以及ManagedTracing默认关闭的情况下,只需要想办法把_traceSource设置为空即可。

TraceResourceDictionary.IsEnabled的用途超出本文讨论的范围,此处略去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
internal class AvTrace
{
  private TraceSource _traceSource;

  public bool IsEnabledOverride
  {
    get
    {
      return this._traceSource != null;
    }
  }

  private void Initialize()
  {
    if (AvTrace.ShouldCreateTraceSources())
    {
      this._traceSource = this._getTraceSourceDelegate();
      this._isEnabled = AvTrace.IsWpfTracingEnabledInRegistry() || AvTrace._hasBeenRefreshed || this._enabledByDebugger;
    }
    else
    {
      this._clearTraceSourceDelegate();
      this._traceSource = (TraceSource) null;
      this._isEnabled = false;
    }
  }

  private static bool ShouldCreateTraceSources()
  {
    return AvTrace.IsWpfTracingEnabledInRegistry() || AvTrace.IsDebuggerAttached() || AvTrace._hasBeenRefreshed;
  }

3. 实现

解决方案清楚之后实现则顺理成章——使用反射找到类型及字段,清除即可。至于调用时机,由于TraceResourceDictionaryWPF初始化的过程中很早就被访问,所以只需要重载Application.OnStartup并在其中调用SkipResourceNotFound.Install()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
////GetNonPublicStaticField和GetNonPublicField是两个扩展方法,其功能如其名称所示
public static class SkipResourceNotFound
{
  public static void Install()
  {
    try
    {
      var typeName = "MS.Internal.TraceResourceDictionary";
      ////通过类名找到相应的类 MS.Internal.TraceResourceDictionary
      var traceResDict = FindType(typeName);
      ////取得_avTrace字段
      var avTrace = traceResDict.GetNonPublicStaticField("_avTrace").GetValue(null);
      var avTraceType = avTrace.GetType();
      ////取得_avTrace._traceSource字段并设置为null
      var traceSourceField = avTraceType.GetNonPublicField("_traceSource");
      traceSourceField.SetValue(avTrace, null);
    }
    catch
    {
    }
  }

  private static Type FindType(string typeName)
  {
    Type type = null;
    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
      type = Type.GetType(typeName + "." + assembly.GetName());
      if (type != null)
      {
        return type;
      }
    }

    return null;
  }
}

参考资料