Подтвердить что ты не робот

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? Есть ли какие-либо известные объяснения или обходные пути для этой проблемы?

4b9b3361

Ответ 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 (мои переходы выполнялись медленнее, чем они должны были быть, поэтому я оставил его, чтобы попытаться ускорить его). Я думаю, что это свойство работает, чтобы "привязать" переход к данному времени. Когда я добавил свойство обратно к переходам, он исправил мою проблему, и мои анимации будут работать надежно.