当Generic.xaml遇上BitmapImage:发现一个疑似WPF Bug而又不似Bug的问题
发现这个有点像Bug又不太像Bug的东西的过程是这样的:
我继承自ContentControl写了一个MyContentControl,在其中定义了一个叫做IconProperty的依赖属性及其对应的CLR属性并且在其静态构造中调用了DefaultStyleKeyProperty.OverrideMetadata方法,代码很少,看起来是这个样子的:
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包起来,代码是这样的:
<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个位置随机的自定义控件,这部分代码是这样的:
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个控件的地方监视了一下时间,代码是这样的:
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这个补丁,一样没有用。