I have a button style with a control template in a resource directory that triggers a storyboard animation when IsPressed is equal to true.
The problem is, the button's Command is fired before the animation is complete (the animation is about two seconds long). This causes a choppy delay in the animation as the Command is executed. Is there a way to trigger the command when the storyboard is complete? I'd like to keep this in the button style if possible, since I have six buttons that will use this behavior.
Below is my style for reference:
<Style x:Key="ButtonStyle1" TargetType="{x:Type Button}">
<Setter Property="Foreground" Value="White" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<ControlTemplate.Resources>
<Storyboard x:Key="Storyboard1">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="grid" Storyboard.TargetProperty="(FrameworkElement.Width)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="42"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="36"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="42"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="grid" Storyboard.TargetProperty="(FrameworkElement.Height)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="42"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="36"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="42"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="360"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>
<Grid x:Name="grid" Width="42" Height="42">
<Ellipse x:Name="ellipse" Opacity="1" StrokeThickness="2" Stroke="Transparent" Width="Auto" Height="Auto" >
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.7,0.7">
<GradientStop Color="#FF0D384B" Offset="0"/>
<GradientStop Color="#FF51919D" Offset="1"/>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Path x:Name="ellipse_Copy" Stretch="Fill" Stroke="Transparent" StrokeThickness="2" Width="Auto" Height="Auto" Opacity="0.5" Data="M41,21 C41,29.918156 29.87824,23.911434 25.375,20.625245 20.75,17.250203 16.281082,18.126696 11.375,21.250502 10,22.125994 1,32.045695 1,21 1,9.954305 9.954305,1 21,1 32.045695,1 41,9.954305 41,21 z" Margin="0,0,0,14.821">
<Path.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Offset="1" Color="#26FFFFFF"/>
<GradientStop Color="White"/>
</LinearGradientBrush>
</Path.Fill>
</Path>
<Ellipse Stroke="#FF6A83A2" StrokeThickness="2" d:IsLocked="True" />
<ContentPresenter x:Name="contentPresenter" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" RecognizesAccessKey="True" RenderTransformOrigin="0.5,0.5">
<ContentPresenter.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</ContentPresenter.RenderTransform>
</ContentPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True"/>
<Trigger Property="IsDefaulted" Value="True"/>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Fill" TargetName="ellipse">
<Setter.Value>
<RadialGradientBrush GradientOrigin="0.7,0.7">
<GradientStop Color="#FF4B260D" Offset="0"/>
<GradientStop Color="#FFFFAD00" Offset="1"/>
</RadialGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource Storyboard1}" />
</Trigger.EnterActions>
</Trigger>
<Trigger Property="IsEnabled" Value="False"/>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
| | John Hamm | Hi,
you could use an Attached Behaviour for this case. An attached behaviour is basically an attached property that registers event handlers to its principal dependencyobject in its OnPropertyChanged callback. You could create a class that exposes a DelayedCommand attached property. When this property is set (even through the style), you will have access to the control it is set on, can cast it to the appropriate type, look for the animation and register an eventhandler to theStoryboard's Completed event that executes the command. The Command property of the button would be set to a dummy command object that only determines whether the actual command can be executed, while the DelayedCommand does the real work.
There might be an issue about the order in which a style sets properties. If the DelayedCommand is set before the storyboard, this approach gets stuck, since it needs the storyboard to be in place before it can subscribe to its Completed event. But I believe setters are executed from beginning to end, and if not, one can still register a handler for the loaded event instead and do the real setup there. - Marked As Answer byJim Zhou - MSFTModerator23 hours 52 minutes ago
-
| | hbarck | One thing you can consider is to perform the calculations of the button in a background thread so the UI thread can continue with the animation. What are you doing when the command is invoked?
Geert van Horrik - CatenaLogic
Visit my blog: http://blog.catenalogic.com
Looking for a way to deploy your updates to all your clients? Try Updater! | | Geert van Horrik | Geert,
My app uses the MVVM model. The button's command activates the Update method in my viewmodel. I tried using a BackgroundWorker class to run the calculation code in a background thread, and while that worked to get rid of the animation delay with my test code (Console.Beep!), I couldn't figure out how to go about calculating the real code in the background worker thread, since the real code populates the viewmodel, which is on the UI thread!
It doesn't seem to be the length of the calculations that causes the delay since Console.Beep caused the same delay.
Here is the relevant code:
public class DashboardViewModel: ViewModelBase {
private Dashboard _dashboard;
private DashboardDataBroker _dashboardDataBroker;
// ...
RelayCommand _updateCommand;
public ICommand UpdateCommand {
get {
if (_updateCommand == null)
_updateCommand = new RelayCommand(param => this.Update(), param => true);
return _updateCommand;
}
}
public void Update() {
_dashboardDataBroker.UpdateDashboard();
OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs(string.Empty));
}
}
public class DashboardDataBroker {
private DateTime _lastUpdate;
private Random _r;
// ...
public void UpdateDashboard() {
_lastUpdate = DateTime.Now;
GetRandomData(_dashboard);
}
private void GetRandomData(Dashboard d) {
double totalminutes = DateTime.Now.Subtract(_lastUpdate).TotalMinutes;
int attempts = _r.Next(totalminutes);
decimal postedtoday = Convert.ToDecimal(_r.Next(50));
_dashboard.Performance.PostedToday += postedtoday;
_dashboard.Performance.Posted += postedtoday;
_dashboard.Positions.Attempts += attempts;
_dashboard.Positions.Touched += _r.Next(attempts);
_dashboard.Positions.Worked += _r.Next(attempts) / 5;
_dashboard.Positions.Contacted += _r.Next(attempts) / 2;
}
}
From DashboardView.xaml:
<Button Command="{Binding UpdateCommand}" Style="{DynamicResource ButtonStyle1}">
And the ButtonStyle1 source code has already been posted. ButtonStyle1 lives in Default.xaml, which is a ResourceDictionary.
Here is the attempt where I used a BackgroundWorker thread - this got rid of the short pause in the animation:
public class DashboardViewModel: ViewModelBase {
private Dashboard _dashboard;
private DashboardDataBroker _dashboardDataBroker;
private BackgroundWorker bgw;
// ...
RelayCommand _updateCommand;
public ICommand UpdateCommand {
get {
if (_updateCommand == null)
_updateCommand = new RelayCommand(param => this.Update(), param => true);
return _updateCommand;
}
}
public void Update() {
bgw.RunWorkerAsync();
//_dashboardDataBroker.UpdateDashboard();
//OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs(string.Empty));
}
void bgw_DoWork(object sender, DoWorkEventArgs e) {
Console.Beep();
}
}
Then I ran into the delay again when I dispatched the Beep back to the main thread. The reason I did this is that my actual real calculations populate the properties of
my viewmodel, which are bound to the XAML view. This brought the slight pause in animation back, even when I set the dispatcher priority to SystemIdle!
public class DashboardViewModel: ViewModelBase {
private Dashboard _dashboard;
private DashboardDataBroker _dashboardDataBroker;
private BackgroundWorker bgw;
// ...
RelayCommand _updateCommand;
public ICommand UpdateCommand {
get {
if (_updateCommand == null)
_updateCommand = new RelayCommand(param => this.Update(), param => true);
return _updateCommand;
}
}
public void Update() {
bgw.RunWorkerAsync(Dispatcher.CurrentDispatcher);
//_dashboardDataBroker.UpdateDashboard();
//OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs(string.Empty));
}
void bgw_DoWork(object sender, DoWorkEventArgs e) {
Dispatcher d = e.Argument as Dispatcher;
Action a = () => Console.Beep();
d.BeginInvoke(a, DispatcherPriority.ApplicationIdle);
}
}
It seems that the smallest operation (Console.Beep) causes a slight pause in the animation of the button press. I seem to think that the only solution is to wait until the animation is done, and somehow tie into the Storyboard's Completed event. But the Storyboard is not accessible from code since it is in the style's controltemplate, and the style is in a ResourceDictionary. - Edited byJohn Hamm Saturday, September 19, 2009 4:44 PMUpdated code
-
| | John Hamm | Hi,
you could use an Attached Behaviour for this case. An attached behaviour is basically an attached property that registers event handlers to its principal dependencyobject in its OnPropertyChanged callback. You could create a class that exposes a DelayedCommand attached property. When this property is set (even through the style), you will have access to the control it is set on, can cast it to the appropriate type, look for the animation and register an eventhandler to theStoryboard's Completed event that executes the command. The Command property of the button would be set to a dummy command object that only determines whether the actual command can be executed, while the DelayedCommand does the real work.
There might be an issue about the order in which a style sets properties. If the DelayedCommand is set before the storyboard, this approach gets stuck, since it needs the storyboard to be in place before it can subscribe to its Completed event. But I believe setters are executed from beginning to end, and if not, one can still register a handler for the loaded event instead and do the real setup there. - Marked As Answer byJim Zhou - MSFTModerator23 hours 52 minutes ago
-
| | hbarck | hbarck,
Great suggesetion! SOOO CLOSE! This almost worked. I successfully used an attached property to my button's style. I was able to find the storyboard. However, when I try to set its Completed event, I get the following exception;
InvalidOperationException was unhandled by user code
Specified value of type 'System.Windows.Media.Animation.Storyboard' must have IsFrozen set to false to modify.
Apparently I am unable to set the Completed event from a template's setter.
Here is the button's new style. You'll notice that the only thing changed is the second to last line where I set the attached property value:
<Style x:Key="BubbleButton" TargetType="{x:Type Button}">
<Setter Property="Foreground" Value="White" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<ControlTemplate.Resources>
<Storyboard x:Key="Storyboard1">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="grid" Storyboard.TargetProperty="(FrameworkElement.Width)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="42"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="36"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="42"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="grid" Storyboard.TargetProperty="(FrameworkElement.Height)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="42"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="36"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="42"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="360"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>
<Grid x:Name="grid" Width="42" Height="42">
<Ellipse x:Name="ellipse" Opacity="1" StrokeThickness="2" Stroke="Transparent" Width="Auto" Height="Auto" >
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.7,0.7">
<GradientStop Color="#FF0D384B" Offset="0"/>
<GradientStop Color="#FF51919D" Offset="1"/>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Path x:Name="ellipse_Copy" Stretch="Fill" Stroke="Transparent" StrokeThickness="2" Width="Auto" Height="Auto" Opacity="0.5" Data="M41,21 C41,29.918156 29.87824,23.911434 25.375,20.625245 20.75,17.250203 16.281082,18.126696 11.375,21.250502 10,22.125994 1,32.045695 1,21 1,9.954305 9.954305,1 21,1 32.045695,1 41,9.954305 41,21 z" Margin="0,0,0,14.821">
<Path.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Offset="1" Color="#26FFFFFF"/>
<GradientStop Color="White"/>
</LinearGradientBrush>
</Path.Fill>
</Path>
<Ellipse Stroke="#FF6A83A2" StrokeThickness="2" d:IsLocked="True" />
<ContentPresenter x:Name="contentPresenter" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" RecognizesAccessKey="True" RenderTransformOrigin="0.5,0.5">
<ContentPresenter.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</ContentPresenter.RenderTransform>
</ContentPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True"/>
<Trigger Property="IsDefaulted" Value="True"/>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Fill" TargetName="ellipse">
<Setter.Value>
<RadialGradientBrush GradientOrigin="0.7,0.7">
<GradientStop Color="#FF4B260D" Offset="0"/>
<GradientStop Color="#FFFFAD00" Offset="1"/>
</RadialGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource Storyboard1}" />
</Trigger.EnterActions>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" TargetName="grid" Value="0.25"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="views:Attachments.DelayedCommand" Value="{Binding UpdateCommand}" />
</Style>
And here is the source code for my attached property class:
public static class Attachments {
public static ICommand GetDelayedCommand(DependencyObject obj) {
return (ICommand)obj.GetValue(DelayedCommandProperty);
}
public static void SetDelayedCommand(DependencyObject obj, ICommand value) {
obj.SetValue(DelayedCommandProperty, value);
}
// Using a DependencyProperty as the backing store for DelayedCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DelayedCommandProperty =
DependencyProperty.RegisterAttached("DelayedCommand", typeof(ICommand), typeof(Attachments), new UIPropertyMetadata(OnCommandChanged));
private static void OnCommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) {
if (e.NewValue == null) return;
var b = obj as Button;
var sb = b.FindResource("Storyboard1") as Storyboard;
sb.Completed += (o, evt) => ((ICommand)e.NewValue).Execute(null);
}
}
| | John Hamm | Hi,
ok, one more idea: you could try to set the x:Shared attribute on the Storyboard to false. If I understood it right, this should give you an unfrozen, individual copy for each instance of the style, but better read up on it in the documentation yourself, I didn't test it. Will create some memory overhead as well, but I don't expect this to be a problem... | | hbarck |
|