wpf - VisualStateManager no funciona como se anuncia
silverlight animation (4)
El siguiente problema me ha estado molestando durante días, pero solo he podido destilarlo a su forma más simple. Considera el siguiente 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>
Simplemente remodela el control CheckBox
para que su fondo dependa de su estado:
- Marcado = Verde
- No seleccionado = rojo
- Comprobación (transición) = Verde claro
- Desmarcar (transición) = Luz roja
Por lo tanto, cuando marca una de las casillas de verificación, espera que se ilumine en verde durante un corto período y luego se vuelva verde. De manera similar, cuando se desactiva, se espera que se vuelva de color rojo claro durante un período corto y luego se vuelva de color rojo.
Y normalmente hace exactamente eso. Pero no siempre.
Juega con el programa el tiempo suficiente (puedo conseguirlo en unos 30 segundos) y verás que la animación de transición a veces triunfa en el estado visual. Es decir, la casilla de verificación seguirá apareciendo de color verde claro cuando se selecciona, o de color rojo claro cuando no está seleccionada. Aquí hay una captura de pantalla que ilustra lo que quiero decir, tomada bien después de los 3 segundos que la transición está configurada para tomar:
Cuando esto ocurre, no es porque el control no haya realizado una transición exitosa al estado objetivo. Se pretende estar en el estado correcto. Verifiqué esto verificando lo siguiente en el depurador (para el caso específico documentado por la captura de pantalla anterior):
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;
Si habilito el seguimiento de animaciones, obtengo el siguiente resultado cuando la transición se completa con éxito:
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 :
Y obtengo el siguiente resultado cuando la transición no se completa correctamente:
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 :
Las primeras 12 líneas son exactamente las mismas que cuando la transición tiene éxito, ¡pero las 10 líneas finales faltan completamente!
He leído toda la documentación de VSM que pude encontrar y no he podido encontrar una explicación para este comportamiento errático.
¿Debo suponer que esto es un error en el VSM? ¿Hay alguna explicación conocida o solución para este problema?
Este problema levantó su cabeza fea para mí recientemente en WPF 4.5. En mi caso, parece que mi transición fue recolectando basura mientras estaba activa, por lo que a veces nunca se activó el evento Completado y nunca se restablecieron sus animaciones. Debido a que mi CheckState VisualState básicamente llamó a todas las mismas propiedades nuevamente para "arreglarlas" en sus puntos finales de transición, parecía que este estado se había activado parcialmente, pero no creo que lo haya hecho nunca.
Solución: había omitido la propiedad GeneratedDuration en mis VisualTransitions (mis transiciones se ejecutaban más lentamente de lo que deberían haber sido, así que lo dejé para intentar acelerarlo). Creo que esta propiedad funciona para "anclar" la transición por el tiempo dado. Cuando agregué la propiedad a las transiciones, solucionó mi problema y mis animaciones funcionaban de manera confiable.
He podido identificar y solucionar el problema de la siguiente manera:
En primer lugar, actualicé mi proyecto de reproducción a .NET 3.5 y obtuve el código fuente de WPF Toolkit de CodePlex . Agregué el proyecto WPF Toolkit a mi solución y le agregué una referencia desde el proyecto Repro.
A continuación, ejecuté la aplicación y me aseguré de que todavía podía reproducir el problema. Efectivamente, era fácil hacerlo.
Luego abrí el archivo VisualStateManager.cs y comencé a agregar algunos diagnósticos en lugares clave que me dirían qué código se estaba ejecutando y qué no. Al agregar estos diagnósticos y comparar el resultado de una buena transición a una mala transición, pude identificar rápidamente que el siguiente código no se estaba ejecutando cuando el problema se manifestó:
// Hook up generated Storyboard''s 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;
};
Así que la naturaleza del error cambió de un problema en VSM a un problema en el Storyboard.Completed
evento completado no siempre se plantea. Este es un problema que he experimentado antes y parece ser una fuente de gran angustia para cualquier desarrollador de WPF que haga algo incluso fuera de lo común cuando se trata de animaciones.
A lo largo de este proceso estuve publicando mis hallazgos en el grupo de Google Discípulos de WPF , y fue en este punto que Pavan Podila respondió con esta gema:
Kent,
He tenido problemas en el pasado para que los guiones gráficos no disparen sus eventos completos. Lo que me he dado cuenta es que si reemplaza un Guión gráfico directamente, sin detenerlo primero, puede ver algunos eventos Completados fuera de orden. En mi caso, estaba aplicando nuevos Storyboards al mismo FrameworkElement, sin detener el Storyboard anterior y eso me estaba dando algunos problemas. No estoy seguro si su caso es similar pero pensé que compartiría este tidbit.
Pavana
Armado con esta idea, cambié esta línea en VisualStateManager.cs :
group.StartNewThenStopOld(element, transition.Storyboard, dynamicTransition);
A esto:
var masterStoryboard = new Storyboard();
if (transition.Storyboard != null)
{
masterStoryboard.Children.Add(transition.Storyboard);
}
masterStoryboard.Children.Add(dynamicTransition);
group.StartNewThenStopOld(element, masterStoryboard);
Y - he aquí que mi reprro que antes estaba fallando intermitentemente, ahora estaba trabajando cada vez!
Entonces, realmente esto funciona alrededor de un error o comportamiento extraño en el subsistema de animación de WPF.
No sé si eso está relacionado con su problema, pero también me topé con problemas con AnimationClock. Completado no se dispara de manera confiable cuando se reemplaza una animación en ejecución con otra. Pensé que era una cuestión de recolección de basura y referencias / enraizamiento. Cuando un AnimationClock todavía se está ejecutando pero ya no se hace referencia a él de alguna manera, puede ser recolectado como basura en cualquier momento. Si se llega al final antes de que se produzca la recolección de basura, se completa Completado, de lo contrario no. Lo que hace para un comportamiento muy impredecible.
Mi solución es agregar inicialmente mi reloj a alguna colección (para forzar su raíz y así evitar la recolección de basura) y eliminarlo de la colección una vez que esté Completado, luego Completado se dispara el 100% del tiempo y no hay pérdidas de memoria.
Sólo mis dos centavos ...
Parece como si el culpable fuera la configuración de Duration="0"
en los storyboards Checked y Unchecked. Quitarlo soluciona el problema. No estoy seguro de entender por qué, a menos que el guión gráfico esté vinculado a la transición correspondiente de alguna manera.
Sin embargo, creo que encontré una solución más limpia para ti de todos modos. Si cambia su ControlTemplate a esto, entonces logra lo mismo sin las Transiciones ...
<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>