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
так, чтобы его фон зависел от его состояния:
- Проверено = Зеленый
- Unchecked = Red
- Проверка (переход) = светло-зеленый
- Отмена (переход) = светло-красный
Итак, когда вы проверяете один из флажков, вы ожидаете, что он на короткое время станет светло-зеленым, а затем станет зеленым. Точно так же, при снятии флажка, вы ожидаете, что он кратковременно загорится красным светом, а затем станет красным.
И это обычно делает именно это. Но не всегда.
Играйте с программой достаточно долго (я могу получить ее примерно за 30 секунд), и вы обнаружите, что анимация перехода иногда превосходит ее в визуальном состоянии. То есть флажок будет оставаться светло-зеленым, если он выбран, или светло-красный, если он не выбран. Вот скриншот, иллюстрирующий то, что я имею в виду, хорошо принятый после 3 секунд, на который сконфигурирован переход:
![enter image description here]()
Когда это происходит, это происходит не потому, что элемент управления не успешно переходит в целевое состояние. Он должен находиться в правильном состоянии. Я проверил это, проверив следующее в отладчике (для конкретного случая, описанного в приведенном выше снимке экрана):
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? Есть ли какие-либо известные объяснения или обходные пути для этой проблемы?
Ответы
Ответ 1
Я смог определить и исправить проблему следующим образом:
Во-первых, я понизил мой проект воспроизведения до .NET 3.5 и взял исходный код WPF Toolkit из CodePlex. Я добавил проект WPF Toolkit к моему решению и добавил ссылку на него из проекта Repro.
Затем я запустил приложение и убедился, что все еще могу воспроизвести проблему. Конечно же, это было легко сделать.
Затем я распаковал файл VisualStateManager.cs и начал добавлять некоторые диагностические данные в ключевые места, которые могли бы рассказать мне, какой код работал, а что нет. Добавив эту диагностику и сравнив результат с хорошим переходом на плохой переход, я быстро смог определить, что следующий код не работал, когда проблема проявилась:
// Hook up generated Storyboard Completed event handler
dynamicTransition.Completed += delegate
{
if (transition.Storyboard == null ||
transition.ExplicitStoryboardCompleted)
{
if (ShouldRunStateStoryboard(control, element, state, group))
{
group.StartNewThenStopOld(element, state.Storyboard);
}
group.RaiseCurrentStateChanged(element, lastState, state,
control);
}
transition.DynamicStoryboardCompleted = true;
};
Таким образом, характер ошибки, смещенной от проблемы в VSM, к проблеме в событии Storyboard.Completed
не всегда поднимается. Это проблема, с которой я столкнулся раньше, и, похоже, является источником большой тоски для любого разработчика WPF, делающего что-то даже немного необычное, когда дело касается анимации.
В течение этого процесса я публиковал свои результаты в WPF Disciples google group, и именно в этот момент Паван Подола ответил этим камнем:
Kent,
У меня были проблемы в прошлом, когда раскадровки не стреляли в их завершенные события. Что я понял заключается в том, что если вы замените раскадровку прямо, без предварительной остановки, вы можете увидеть некоторые из-за порядка Завершенные мероприятия. В моем случае я был применяя новые раскадровки к тем же FrameworkElement, не останавливая Ранняя раскадровка, и это было давая мне некоторые вопросы. Не уверен если ваш случай похож, но я думал, что буду поделитесь этим лакомым кусочком.
Паван
Вооружившись этим пониманием, я изменил эту строку в VisualStateManager.cs:
group.StartNewThenStopOld(element, transition.Storyboard, dynamicTransition);
Для этого:
var masterStoryboard = new Storyboard();
if (transition.Storyboard != null)
{
masterStoryboard.Children.Add(transition.Storyboard);
}
masterStoryboard.Children.Add(dynamicTransition);
group.StartNewThenStopOld(element, masterStoryboard);
И - вот и вот - мой рев, который раньше прерывался с перерывами, теперь работал каждый раз!
Итак, действительно, это работает вокруг ошибки или нечетного поведения в подсистеме анимации WPF.
Ответ 2
Похоже, что установкой Duration="0"
в расписанных и проверенных и непроверенных раскадках была ошибка. Исправление проблемы устраняет проблему. Я не уверен, что понимаю, почему, если раскадровка так или иначе связана с соответствующим переходом.
Однако, я думаю, что я нашел для вас более чистое решение. Если вы измените свой ControlTemplate на это, тогда он выполнит то же самое без переходов...
<ControlTemplate TargetType="CheckBox">
<Grid x:Name="Root">
<Grid.Background>
<SolidColorBrush x:Name="brush" Color="White"/>
</Grid.Background>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CheckStates">
<VisualState Name="Checked">
<Storyboard x:Name="CheckedStoryboard">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="0" Value="LightGreen"/>
<DiscreteColorKeyFrame KeyTime="00:00:03" Value="Green"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="Unchecked">
<Storyboard x:Name="UncheckedStoryboard">
<ColorAnimationUsingKeyFrames Storyboard.TargetName="brush" Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="0" Value="LightSalmon"/>
<DiscreteColorKeyFrame KeyTime="00:00:03" Value="Red"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter/>
</Grid>
</ControlTemplate>
Ответ 3
Не знаю, связано ли это с вашей проблемой, но я также наткнулся на проблемы с AnimationClock.Completed не надежно срабатывает при замене запущенной анимации другой. Я понял, что это вопрос сбора мусора и ссылок/укоренения. Когда AnimationClock все еще работает, но больше не ссылается, это может быть сбор мусора в любой момент времени. Если конец достигнут до сбора мусора, то Завершено уволено, иначе нет. Это приводит к очень непредсказуемому поведению.
Мое обходное решение состоит в том, чтобы изначально добавить мои часы в какую-нибудь коллекцию (чтобы заставить ее укорениться и, таким образом, предотвратить сбор мусора) и удалить ее из коллекции после завершения, тогда Завершенные будут уволены в 100% случаев, и нет утечки памяти.
Только мои два цента...
Ответ 4
Эта проблема недавно подняла для меня уродливую голову в WPF 4.5. В моем случае похоже, что мой переход собирал мусор, когда он активен, поэтому он иногда никогда не запускал событие Completed, и он никогда не reset его анимации. Поскольку мой Checked VisualState в основном называл все те же свойства снова, чтобы "исправить" их в конечных точках перехода, казалось, что это состояние было частично запущено, но я не верю, что это когда-либо было.
Решение. Я отказался от свойства GeneratedDuration в моих VisualTransitions (мои переходы выполнялись медленнее, чем они должны были быть, поэтому я оставил его, чтобы попытаться ускорить его). Я думаю, что это свойство работает, чтобы "привязать" переход к данному времени. Когда я добавил свойство обратно к переходам, он исправил мою проблему, и мои анимации будут работать надежно.