VisualStateManager 没有像宣传的那样工作
以下问题已经困扰我好几天了,但我只能将其提炼成最简单的形式。考虑以下 XAML:
<Window x:Class="VSMTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="CheckBox">
<Setter Property="Margin" Value="3"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Grid x:Name="Root">
<Grid.Background>
<SolidColorBrush x:Name="brush" Color="White"/>
</Grid.Background>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CheckStates">
<VisualStateGroup.Transitions>
<VisualTransition To="Checked" GeneratedDuration="00:00:03">
<Storyboard Name="CheckingStoryboard">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="0" Value="LightGreen"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition To="Unchecked" GeneratedDuration="00:00:03">
<Storyboard Name="UncheckingStoryboard">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="0" Value="LightSalmon"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState Name="Checked">
<Storyboard Name="CheckedStoryboard" Duration="0">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="0" Value="Green"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="Unchecked">
<Storyboard Name="UncheckedStoryboard" Duration="0">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="0" Value="Red"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel>
<CheckBox x:Name="cb1">Check Box 1</CheckBox>
<CheckBox x:Name="cb2">Check Box 2</CheckBox>
<CheckBox x:Name="cb3">Check Box 3</CheckBox>
</StackPanel>
</Window>
它只是重新模板化 CheckBox
控件,以便其背景取决于其状态:
- 选中 = 绿色
- 未选中 = 红色
- 选中(转换)= 浅绿色
- 取消选中(转换)= 浅红色
因此,当您选中其中一个复选框时,您会期望它会在短时间内变成浅绿色,然后再变成绿色。同样,取消选中时,您会期望它会在短时间内变成浅红色,然后变成红色。
它通常就是这样做的。 但并非总是如此。
使用该程序足够长的时间(我可以在大约 30 秒内完成它),您会发现过渡动画有时胜过视觉状态。也就是说,复选框在选中时将继续显示浅绿色,而在未选中时将继续显示浅红色。下面的屏幕截图说明了我的意思,是在转换配置为进行 3 秒后拍摄的:
When发生这种情况,不是,因为控件没有成功转换到目标状态。它声称处于正确的状态。我通过在调试器中检查以下内容来验证这一点(对于上面屏幕截图记录的特定情况):
var vsgs = VisualStateManager.GetVisualStateGroups(VisualTreeHelper.GetChild(this.cb2, 0) as FrameworkElement);
var vsg = vsgs[0];
// this is correctly reported as "Unselected"
var currentState = vsg.CurrentState.Name;
如果我启用动画跟踪,则当转换成功完成时我会得到以下输出:
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='44177654'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='6148812'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='8261103'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36205315'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='18626439'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='44177654'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36893403'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckingStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='49590434'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='<null>'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36893403'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckingStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 :
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='49590434'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='<null>'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='16977025'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='16977025'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='16977025'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
当转换失败时我会得到以下输出成功完成:
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='44177654'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='6148812'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='8261103'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36205315'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='18626439'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 3 : Storyboard has been removed; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='44177654'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='UncheckedStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'
System.Windows.Media.Animation Stop: 3 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='36893403'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='CheckingStoryboard'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
System.Windows.Media.Animation Start: 1 : Storyboard has begun; Storyboard='System.Windows.Media.Animation.Storyboard'; Storyboard.HashCode='49590434'; Storyboard.Type='System.Windows.Media.Animation.Storyboard'; StoryboardName='<null>'; TargetElement='System.Windows.Controls.Grid'; TargetElement.HashCode='41837403'; TargetElement.Type='System.Windows.Controls.Grid'; NameScope='<null>'
System.Windows.Media.Animation Stop: 1 :
前12行与转换成功时完全相同,但最后10行完全缺失!
我已经通读了我能找到的所有 VSM 文档,但无法对这种不稳定的行为给出解释。
我是否可以假设这是 VSM 中的错误?此问题有任何已知的解释或解决方法吗?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
我已经能够按如下方式识别并修复问题:
首先,我将重现项目降级到 .NET 3.5 并从 CodePlex。我将 WPF Toolkit 项目添加到我的解决方案中,并添加了 Repro 项目中对其的引用。
接下来,我运行该应用程序并确保我仍然可以重现该问题。果然,这件事很容易做到。
然后我破解了 VisualStateManager.cs 文件,并开始在关键位置添加一些诊断信息,这些诊断信息会告诉我哪些代码正在运行,哪些代码没有运行。通过添加这些诊断并比较良好转换和不良转换的输出,我很快就能识别出当问题出现时以下代码没有运行:
因此,错误的性质从 VSM 中的问题转变为
Storyboard.Completed
事件中的问题并不总是被引发。这是我以前经历过的一个问题,对于任何在动画方面做任何稍微不寻常的事情的 WPF 开发人员来说,这似乎都是一个令人焦虑的根源。在整个过程中,我将我的发现发布在 WPF Disciples google 群组,就在此时,Pavan Podila 回应了这个宝石:
这种洞察力,我在 VisualStateManager.cs:
对此:
并且 - 你瞧 - 我的重现以前间歇性失败,现在每次都可以工作!
所以,这实际上是为了解决 WPF 动画子系统中的错误或奇怪行为。
I've been able to identify and fix the issue as follows:
Firstly, I downgraded my repro project to .NET 3.5 and grabbed the WPF Toolkit source code from CodePlex. I added the WPF Toolkit project to my solution and added a reference to it from the Repro project.
Next, I ran the app and made sure I could still reproduce the issue. Sure enough, it was easy to do so.
Then I cracked open the VisualStateManager.cs file and started adding some diagnostics in key places that would tell me what code was running and what was not. By adding these diagnostics and comparing the output from a good transition to a bad transition, I was quickly able to identify that the following code was not running when the problem manifested itself:
So the nature of the bug shifted from a problem in VSM to a problem in the
Storyboard.Completed
event not always being raised. This is an issue I've experienced before and seems to be a source of much angst for any WPF developer doing anything even slightly out of the ordinary when it comes to animations.Throughout this process I was posting my findings on the WPF Disciples google group, and it was at this point that Pavan Podila responded with this gem:
Armed with this insight, I changed this line in VisualStateManager.cs:
To this:
And - lo and behold - my repro that was previously failing intermittently was now working every time!
So, really this works around a bug or odd behavior in WPF's animation sub-system.
似乎在“已选中”和“未选中”故事板上设置
Duration="0"
是罪魁祸首。删除它可以解决问题。我不确定我明白为什么,除非故事板以某种方式链接到相应的过渡。不过,我想我还是为你找到了一个更干净的解决方案。如果您将 ControlTemplate 更改为此,那么它可以在没有转换的情况下完成相同的事情......
It appears as though setting
Duration="0"
on the Checked and Unchecked storyboards was the culprit. Removing it fixes the problem. I'm not sure I understand why, unless the storyboard is linked to the corresponding transition in some way.However, I think I found a cleaner solution for you anyway. If you change your ControlTemplate to this then it accomplishes the same thing without the Transitions...
不知道这是否与您的问题有关,但我也偶然发现了 AnimationClock.Completed 在用另一个动画替换正在运行的动画时无法可靠触发的问题。我认为这是垃圾收集和引用/生根的问题。当 AnimationClock 仍在运行但不再以某种方式被引用时,它可能会在任何时间点被垃圾收集。如果在垃圾收集发生之前到达末尾,则会触发 Completed,否则不会。这导致了非常不可预测的行为。
我的解决方法是首先将我的时钟添加到某个集合中(以强制其扎根,从而防止垃圾收集)并在 Completed 时将其从集合中删除,然后 Completed 会在 100% 的情况下被触发,并且不会出现内存泄漏。
只是我的两分钱...
Don't know if that is at all related with your problem, but I also stumbled onto problems with AnimationClock.Completed not reliably firing when replacing a running animation with another. I figured that it was a matter of garbage collection and references/rooting. When an AnimationClock is still running but no longer referenced somehow, it may be garbage collected at any point in time. If the end is reached before garbage collection happens, Completed is fired, otherwise not. Which makes for a very unpredictable behavior.
My workaround is to initially add my clock to some collection (to force it to be rooted and thus prevent garbage collection) and remove it from the collection upon Completed, then Completed gets fired 100% of the time, and there are no memory leaks.
Just my two cents...
这个问题最近在 WPF 4.5 中向我提出了丑陋的想法。就我而言,看起来我的过渡在活动时被垃圾收集,因此有时它从未触发 Completed 事件,也从未重置其动画。由于我的 Checked VisualState 基本上再次调用所有相同的属性以在其转换端点“修复”它们,因此似乎此状态已部分触发,但我不相信它曾经这样做过。
解决方案:我在 VisualTransitions 中省略了 generatedDuration 属性(我的转换运行速度比应有的速度慢,所以我将其省略以尝试加快速度。)。我认为这个属性可以“锚定”给定时间的过渡。当我将该属性添加回过渡时,它解决了我的问题,并且我的动画可以可靠地工作。
This problem raised its ugly head for me recently in WPF 4.5. In my case, it looks like my transition was getting garbage collected while active, so it sometimes never fired the Completed event and it never reset its animations. Since my Checked VisualState basically called all the same properties again to "fix" them at their transition end-points, it seemed like this state had partially fired, but I don't believe it ever did.
Solution: I had left off the GeneratedDuration property in my VisualTransitions (my transitions were running slower than they should have been so I left it off to try and speed it up.). I think this property works to "anchor" the transition for the given time. When I added the property back to the transitions it fixed my problem, and my animations would work reliably.