当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 | 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 14 15 | 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 15 16 17 18 | 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这个补丁,一样没有用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述