当Generic.xaml遇上BitmapImage:发现一个疑似WPF Bug而又不似Bug的问题

发现这个有点像 Bug 又不太像 Bug 的东西的过程是这样的:

我继承自 ContentControl 写了一个 MyContentControl ,在其中定义了一个叫做 IconProperty
的依赖属性及其对应的 CLR 属性并且在其静态构造中调用了 DefaultStyleKeyProperty.OverrideMetadata
方法,代码很少,看起来是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyContentControl: ContentControl {

static MyContentControl() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyContentControl), new FrameworkPropertyMetadata(typeof(MyContentControl)));
}

public ImageSource Icon {
get {
return (ImageSource) GetValue(IconProperty);
}
set {
SetValue(IconProperty, value);
}
}

public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(ImageSource), typeof(MyContentControl));
}

其中的 Icon 属性声明类型为 ImageSource ,目的简单明了,当然就是给这个控件加个图标了。

然后再给这个自定义控件定义一个放在 Generic.xaml 里的 Template ,一样很简单,只是用一个 StackPanel 把它的
Icon 和 Content 包起来,代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Style TargetType="{x:Type local:MyContentControl}">
<Style.Setters>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyContentControl}">
<StackPanel>
<Image Source="{TemplateBinding Icon}" Stretch="Fill"/>
<ContentPresenter Content="{TemplateBinding Content}"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>

然后定义一个窗体,其中有一个 Canvas 和一个 Button ,点击 Button 时把 Canvas 清空然后再向其中加 500
个位置随机的自定义控件,这部分代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
canvas.Children.Clear();
Random random = new Random();

for (int i = 0; i < 500; i++) {
MyContentControl marker = new MyContentControl {
Content = i, Icon = bitmap
};

marker.SetValue(Canvas.LeftProperty, (double) random.Next(0, (int) canvas.ActualWidth));

marker.SetValue(Canvas.TopProperty, (double) random.Next(0, (int) canvas.ActualHeight));
canvas.Children.Add(marker);
}

其中的 bitmap 是窗体的一个私有字段,它关联了一张小箭头式的 png 图片(这个 bitmap 是窗体的私有字段或者是方法中的局部变量会对结果有影响,这点稍后说)。

再然后运行程序玩一下吧,点一下 Button 之后是这样的:

看起来蛮正常的。

但是试着多点几次 Button 之后发现不对劲了,怎么几乎每一次都比上一次慢呢?

于是就又在加入 500 个控件的地方监视了一下时间,代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Stopwatch watch = new Stopwatch();
watch.Start();

this.Dispatcher.BeginInvoke(new Action(() => {
this.Title = watch.ElapsedMilliseconds.ToString();

if (count <= 10) {
ClearAndAddMarkers();
} else {
count = 0;
}

count++;
}), DispatcherPriority.Loaded);

计时器在 canvas 的 Children 填充之后开始,在 Dispatcher 的 Loaded 时停止,这样确保记录下来的时间是用来
Render 的时间而不是填充集合的时间。把这个清空、填充、计时的过程连续跑十次,把记录下来的时间写到窗体的 Title 上。

在运行一下,点 Button ,观察一下 Window 的 Title ,先是 400 多毫秒,然后 600 多, 800 多
…… 最后一次用了 1300 多。当然,如果您机子配置太好的话得把加入自定义控件的数量调大一点。

好奇怪啊好奇怪,我开始认定是代码写的有问题(确实也是有一点,不过不是关键),但是找来找去找不到。于是试着把 Generic.xaml
改了下名字,不让它自动应用,然后在窗体里面引用这个改了名的资源字典。结果,怪事发生了,每次的时间稳定在了 400 毫秒左右。

这样看起来好像是 WPF 对 Generic.xaml 这种方式的处理有问题了,可以疑似为是个 Bug 。那为什么标题又说不似 Bug 呢?

这就涉及到前面说的 bitmap 了,如果去掉这个私有字段而是在填充 canvas 的 Children 的时候每次 new 一个新的
BitmapImage 来赋值给每一个自定义控件的 Icon 的话,也可以把每次的时间维持在 400 毫秒左右,所以又说它不太像个 Bug 。

如果有哪位遇到了类似的问题不妨试一下不要用 Generic.xaml ,改用自己命名的普通资源字典来试一下;又或者是不要让窗体 hold 住
bitmap 这个资源不放手,每次 new 一个 BitmapImage 试一下。

但是无论如何,同一个 Template 定义在 Generic.xaml
中自动应用和定义在普通资源字典中手动引用这两种方式会导致程序的性能不同终究是个奇怪的问题,希望能有高手给出更好的解决方案和解释。

PS :我试过了在 .NET 3.5 和 .NET 4 下分别用 Debug 和 Release 编译,也试过了在 VS 中运行和脱离
VS 独立运行,都是有问题的。

另外,还试过了打微软发布的 KB981107 这个补丁,一样没有用。

下载代码