WPF Change button style at runtime - wpf

I am trying to turn on or off a style at runtime using a Toggle Switch.
I have added the style to a resource dictionary but im not sure how to make some C# code to load or unload the resource. All of my buttons are using a dynamic resource of "PassiveGlowButton" and when i use a toggle switch i would like it to remove the "PassiveGlowButton" so its using the style of "GlowButton"
The code behind "GlowButton" This is the code i want to apply when the toggle is on. This is in App.Xaml under Application.resources, resourceDictionary:
<ResourceDictionary>
<Style x:Key="GlowButton" TargetType="{x:Type Button}"
BasedOn="{StaticResource AccentedSquareButtonStyle}">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect ShadowDepth="5" Color="WhiteSmoke" BlurRadius="18"/>
</Setter.Value>
</Setter>
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Effect.ShadowDepth"
From="3.0" To="0.0" Duration="0:0:1"
AutoReverse="True" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<!-- Mouse over glow -->
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Effect.BlurRadius"
From="45.0" To="17.0" Duration="0:0:1"
AutoReverse="True" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Effect.BlurRadius"
From="15.0" To="15.0" Duration="0:0:1"
AutoReverse="True" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
Update
I have been able to set the style using a button but it will only apply to a button called Testbttn. Is there a way to change it to apply to Button.Style? If i use this method it also looses the storyboard of the button for some reason
Style style = this.FindResource("PassiveGlowButton") as Style;
TestBttn.Style = style;
Update 2: The solution was to create 3 styles, one the button uses from load and then 2 others, one with a blank button and one with the style i wanted.
I have attached the code i used to swap between the styles.
private void ButtonStyle_Checked(object sender, RoutedEventArgs e)
{
Application.Current.Resources["PassiveGlowButton"] = Application.Current.Resources["PassiveGlowButtonOn"];
}
private void ButtonStyle_UnChecked(object sender, RoutedEventArgs e)
{
Application.Current.Resources["PassiveGlowButton"] = Application.Current.Resources["PassiveGlowButtonOff"];
}

There are several ways of doing this.
What you're asking may be best redesigned to use VisualStateManager.
Another option is redesigning the styles into a StyleViewModel. (I recommend using a enum and typing your styles so that the VM can live / reference separate from the styles themselves) If you do this properly you can change the style type and the styles binding will update.
Finally you can use DynamicResource as the style and make a default style resource that's set else where. Styles, when used as a resource, can have the same key in separate dictionaries. The names overlapped so the last one in (or closest to the control requesting it in the hierarchy) will be the one to get used. You can re-arrange the style order or add / remove them but the controls won't update until the next time they are loaded.
Each is a little tricky to implement and although I like VisualStateManager I'm a fan of the binding fix (option 2) myself. There's a difference between the two; so I don't want this to confuse you or start a debate. I'm just illustrating options.
Here's a quick example of binding styles if you do prefer to go that route which will fix your problem IMO.
Example:
Styles
<Application x:Class="Question_Answer_WPF_App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<Style x:Key="StyleA"
TargetType="Button">
<Setter Property="Background"
Value="Green" />
<Setter Property="Height"
Value="40" />
<Setter Property="Margin"
Value="4" />
</Style>
<Style x:Key="StyleB"
TargetType="Button">
<Setter Property="Background"
Value="Blue" />
<Setter Property="Height"
Value="30" />
</Style>
</Application.Resources>
</Application>
Enum
namespace Question_Answer_WPF_App.ViewModels
{
public enum Styles
{
StyleA,
StyleB
}
}
ViewModel
using System.Windows.Input;
namespace Question_Answer_WPF_App.ViewModels
{
public class StylesViewModel : NotifyModel
{
private Styles selectedStyle;
public StylesViewModel()
{
SelectStyleCommand = new RelayCommand(SelectStyle);
}
public Styles SelectedStyle
{
get { return selectedStyle; }
set
{
selectedStyle = value;
Notify();
}
}
public ICommand SelectStyleCommand { get; }
private void SelectStyle(object obj)
{
if (obj is Styles style) SelectedStyle = style;
}
}
}
Converter
using Question_Answer_WPF_App.ViewModels;
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Question_Answer_WPF_App.Views
{
public class StyleTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var result = Application.Current.Resources["StyleA"];
if (value is Styles style)
{
switch (style)
{
case Styles.StyleB:
result = Application.Current.Resources["StyleB"];
break;
case Styles.StyleA:
default:
result = Application.Current.Resources["StyleA"];
break;
}
}
return result;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> DependencyProperty.UnsetValue;
}
}
View
<UserControl x:Class="Question_Answer_WPF_App.Views.StylesTestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModels="clr-namespace:Question_Answer_WPF_App.ViewModels"
xmlns:local="clr-namespace:Question_Answer_WPF_App.Views">
<UserControl.Resources>
<ViewModels:StylesViewModel x:Key="StylesViewModel" />
<local:StyleTypeConverter x:Key="StyleTypeConverter" />
</UserControl.Resources>
<StackPanel>
<Button Style="{Binding SelectedStyle, Source={StaticResource StylesViewModel}, Converter={StaticResource StyleTypeConverter}}"
Command="{Binding SelectStyleCommand, Source={StaticResource StylesViewModel}}"
CommandParameter="{x:Static ViewModels:Styles.StyleA}"
Content="Select Style A" />
<Button Style="{Binding SelectedStyle, Source={StaticResource StylesViewModel}, Converter={StaticResource StyleTypeConverter}}"
Command="{Binding SelectStyleCommand, Source={StaticResource StylesViewModel}}"
CommandParameter="{x:Static ViewModels:Styles.StyleB}"
Content="Select Style B" />
</StackPanel>
</UserControl>
Results

Related

DataContext binding in Page's Resources

I have a page that gets a datacontext objet in the behind code.
I would like to set an empty value to the TextList[11] when the Trigger variable loses the 1 value. The Trigger "int" and the TextList "ObservableCollection" are booth situated in the Datacontext object. The TextList is initialized 20pcs element before set the page datacontext. I have to solve it in wpf code, code behind excluded. My English is pretty poor, sorry!
<Page x:Class="LayerTemplates.Templates.example"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:LayerTemplates.Templates"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title=""
xmlns:System="clr-namespace:System;assembly=mscorlib">
<Page.Resources>
<DataTemplate x:Key="myDataTemplate" DataType="{x:Type System:String}">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Trigger}" Value="1">
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<StringAnimationUsingKeyFrames Storyboard.TargetName="Page.DataContext" Storyboard.TargetProperty="TextList[11]" Duration="1">
<DiscreteStringKeyFrame KeyTime="0" Value=""></DiscreteStringKeyFrame>
</StringAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Page.Resources>
<Grid>
...
</Grid>
If you just want to hide a text box or label try to use style triggers instead. As you are setting the value of a string to an empty value I think you might be able to use Visibility="Hidden".
In this example I hide the the label by default but whenever the MyIntProperty becomes 1 I change visibility to Visible.
My xaml code looks like this:
<Window x:Class="TestBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Label Content="{Binding MyTextProperty}">
<Label.Style>
<Style TargetType="Label">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<DataTrigger Binding="{Binding MyIntProperty}" Value="1">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</Grid>
Data binding class and codebehind looks like this:
public class MyViewModel
{
public int MyIntProperty { get; set; } = 1;
public string MyTextProperty { get; set; } = "This is my text";
}
public partial class MainWindow : Window
{
MyViewModel model;
public MainWindow()
{
InitializeComponent();
model = new MyViewModel();
this.DataContext = model;
}
}
Note that in order to get the visibility to update automatically you have let the view model class implement INotifyPropertyChanged for the MyIntProperty.

In WPF, how to override an Event Trigger?

I have a StackPanel with multiple buttons. I want all the buttons except one to trigger an animation when the user clicks on them, so in the StackPanel.Triggers I have added this code:
<StackPanel.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource animationName}" />
</EventTrigger>
</StackPanel.Triggers>
In the particular button I have added this code:
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource anotherAnimation}" />
</EventTrigger>
</Button.Triggers>
When clicking the button, both animations begin, so it appears that the second EventTrigger is just added to the first one and not override it.
How can I override the first EventTrigger so only the second one will be triggered when clicking on that particular button?
Note: I need the answer to be in pure XAML without any code-behind involved.
EDIT: Here is the storyboard:
<Storyboard x:Key="animationName">
<DoubleAnimation
Storyboard.TargetName="PageFrame"
Storyboard.TargetProperty="Opacity"
To="0" Duration="0:0:0.25" />
</Storyboard>
Just use x:Key property for necessary buttons. For example:
<Window>
<Window.Resources>
<Style x:Key="myStyle" TargetType="Button">
<Setter Property="Background" Value="Green"/>
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource animationName}" />
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Button Style="{StaticResource myStyle}">Styles are cool!</Button>
<Button>No Animation:)</Button>
<Button Style="{StaticResource myStyle}">Yes to animation!</Button>
</StackPanel>
</Window>
Update:
If you want to avoid use Style just for a few buttons, just create Style for all Button controls and set Style="{x:Null}" to controls where you want to avoid animation. See the following example:
<Window>
<Window.Resources>
<!--This style will be applied to all Buttons, except where Style="{x:Null}"-->
<Style TargetType="Button">
<Style.Resources>
<Storyboard x:Key="animationName">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0" Duration="0:0:0.25" />
</Storyboard>
</Style.Resources>
<Setter Property="Background" Value="Green"/>
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource animationName}" />
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Button Content="Yes to Animation"/>
<Button Content="No Animation:)" Style="{x:Null}"/>
<Button Content="Yes to Animation"/>
</StackPanel>
</Window>
Update 1:
you have deleted the TargetName, but I really need to set it so the animation will be applied to the correct element.
Since a style can be reused in multiple places in a WPF application, we can't reference to a UIElement from within the style. This behavior is by design.
As promised I took #RayBurns answer from this link and modified it, to answer your question. The ConditionalEventTrigger is now looking like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Markup;
namespace Trigger
{
[ContentProperty("Actions")]
public class ConditionalEventTrigger : FrameworkContentElement
{
private static readonly RoutedEvent TriggerActionsEvent = EventManager.RegisterRoutedEvent("", RoutingStrategy.Direct, typeof(EventHandler), typeof(ConditionalEventTrigger));
public RoutedEvent RoutedEvent { get; set; }
public static readonly DependencyProperty ExcludedSourceNamesProperty = DependencyProperty.Register(
"ExcludedSourceNames", typeof (List<string>), typeof (ConditionalEventTrigger), new PropertyMetadata(new List<string>()));
public List<string> ExcludedSourceNames
{
get { return (List<string>) GetValue(ExcludedSourceNamesProperty); }
set { SetValue(ExcludedSourceNamesProperty, value); }
}
public static readonly DependencyProperty ActionsProperty = DependencyProperty.Register(
"Actions", typeof (List<TriggerAction>), typeof (ConditionalEventTrigger), new PropertyMetadata(new List<TriggerAction>()));
public List<TriggerAction> Actions
{
get { return (List<TriggerAction>) GetValue(ActionsProperty); }
set { SetValue(ActionsProperty, value); }
}
// "Triggers" attached property
public static ConditionalEventTriggerCollection GetTriggers(DependencyObject obj) { return (ConditionalEventTriggerCollection)obj.GetValue(TriggersProperty); }
public static void SetTriggers(DependencyObject obj, ConditionalEventTriggerCollection value) { obj.SetValue(TriggersProperty, value); }
public static readonly DependencyProperty TriggersProperty = DependencyProperty.RegisterAttached("Triggers", typeof(ConditionalEventTriggerCollection), typeof(ConditionalEventTrigger), new PropertyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
// When "Triggers" is set, register handlers for each trigger in the list
var element = (FrameworkElement)obj;
var triggers = (List<ConditionalEventTrigger>)e.NewValue;
foreach (var trigger in triggers)
element.AddHandler(trigger.RoutedEvent, new RoutedEventHandler((obj2, e2) =>
trigger.OnRoutedEvent(element, e2)));
}
});
// When an event fires, check the condition and if it is true fire the actions
void OnRoutedEvent(FrameworkElement element, RoutedEventArgs args)
{
var originalSender = args.OriginalSource as FrameworkElement;
if(originalSender == null) return;
DataContext = element.DataContext; // Allow data binding to access element properties
if (!ExcludedSourceNames.Any(x=>x.Equals(originalSender.Name)))
{
// Construct an EventTrigger containing the actions, then trigger it
var dummyTrigger = new EventTrigger { RoutedEvent = TriggerActionsEvent };
foreach (var action in Actions)
dummyTrigger.Actions.Add(action);
element.Triggers.Add(dummyTrigger);
try
{
element.RaiseEvent(new RoutedEventArgs(TriggerActionsEvent));
}
finally
{
element.Triggers.Remove(dummyTrigger);
}
}
}
}
public class ConditionalEventTriggerCollection: List<ConditionalEventTrigger>{}
}
It can be used in your XAML like this. Take care that all SourceNames you don´t want to be recognized on execution of your actions are inside the ExcludedSourceNames section.:
<trigger:ConditionalEventTrigger.Triggers>
<trigger:ConditionalEventTriggerCollection>
<trigger:ConditionalEventTrigger RoutedEvent="Button.Click">
<trigger:ConditionalEventTrigger.ExcludedSourceNames>
<system:String>buttonTriggeringAnotherAnimation</system:String>
</trigger:ConditionalEventTrigger.ExcludedSourceNames>
<BeginStoryboard Storyboard="{StaticResource Storyboard1}"></BeginStoryboard>
</trigger:ConditionalEventTrigger>
</trigger:ConditionalEventTriggerCollection>
</trigger:ConditionalEventTrigger.Triggers>
To give you an ready to start example here is a window:
<Window x:Class="ConditionalEventTriggerExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ConditionalEventTriggerExample"
xmlns:trigger="clr-namespace:Trigger;assembly=Trigger"
xmlns:system="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Storyboard x:Key="Storyboard1">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="rectangle">
<EasingColorKeyFrame KeyTime="0:0:1" Value="#FF5151FD"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Storyboard2">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="rectangle1">
<EasingColorKeyFrame KeyTime="0:0:1" Value="#FFFF7400"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
<StackPanel>
<StackPanel.Triggers>
<EventTrigger RoutedEvent="Button.Click" SourceName="buttonTriggeringAnotherAnimation">
<BeginStoryboard Storyboard="{StaticResource Storyboard2}"/>
</EventTrigger>
</StackPanel.Triggers>
<trigger:ConditionalEventTrigger.Triggers>
<trigger:ConditionalEventTriggerCollection>
<trigger:ConditionalEventTrigger RoutedEvent="Button.Click">
<trigger:ConditionalEventTrigger.ExcludedSourceNames>
<system:String>buttonTriggeringAnotherAnimation</system:String>
</trigger:ConditionalEventTrigger.ExcludedSourceNames>
<BeginStoryboard Storyboard="{StaticResource Storyboard1}"></BeginStoryboard>
</trigger:ConditionalEventTrigger>
</trigger:ConditionalEventTriggerCollection>
</trigger:ConditionalEventTrigger.Triggers>
<Button x:Name="button" Content="Button"/>
<Button x:Name="button1" Content="Button"/>
<Button x:Name="buttonTriggeringAnotherAnimation" Content="triggering another animation"/>
<Button x:Name="button3" Content="Button"/>
<Button x:Name="button4" Content="Button"/>
<Button x:Name="button5" Content="Button"/>
<Rectangle x:Name="rectangle" Fill="#FFF4F4F5" Height="100" Stroke="Black"/>
<Rectangle x:Name="rectangle1" Fill="#FFF4F4F5" Height="100" Stroke="Black"/>
</StackPanel>
If you don´t get it to work I can upload the solution on GitHub.

How to properly execute a command upon animation is completed?

I have a button as follows:
<Button x:Name ="Btn_Import" Grid.Row="33" Grid.Column="15" Grid.ColumnSpan="36" Grid.RowSpan="36" >
<Button.Template>
<ControlTemplate>
<Grid RenderTransformOrigin="0.5,0.5" x:Name="bg">
<Image x:Name ="import_image" Source="{Binding ImportBtnBaseImagePath}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="import_image" Property="Source" Value="{Binding ImportBtnOverImagePath}" />
</Trigger>
<Trigger Property="ButtonBase.IsPressed" Value ="True">
<!-- press effect -->
<Setter TargetName="bg" Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.9" ScaleY="0.9"/>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
<Button.Triggers>
<EventTrigger RoutedEvent="PreviewMouseDown" >
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Studio" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:2" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Completed">
<i:InvokeCommandAction Command="{Binding NavigateCommand}" CommandParameter="ImportButtonClickParmeters" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
I want this button to triger an animation on some other control to fade out for 2 seconds, and then once the animation is completed to navigate to some other view through 'NavigateCommand'. But I get the following error:
Additional information: Specified value of type
'System.Windows.Interactivity.EventTrigger' must have IsFrozen set to
false to modify.
Your issue depends on a well know bug. Unluckly I found that the common solution does not properly work in this case.
Anyway if you wish to keep your application MVVM compliant, I suggest you to create a "fake" animation, whose task is to execute a command. Of course this animation has to be the last one in your storyboard.
This is the CommandFakeAnimation code:
public class CommandFakeAnimation : AnimationTimeline
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandFakeAnimation), new UIPropertyMetadata(null));
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object), typeof(CommandFakeAnimation), new PropertyMetadata(null));
public CommandFakeAnimation()
{
Completed += new EventHandler(CommandAnimation_Completed);
}
public ICommand Command
{
get
{
return (ICommand)GetValue(CommandProperty);
}
set
{
SetValue(CommandProperty, value);
}
}
public object CommandParameter
{
get
{
return GetValue(CommandParameterProperty);
}
set
{
SetValue(CommandParameterProperty, value);
}
}
private void CommandAnimation_Completed(object sender, EventArgs e)
{
if (Command != null && Command.CanExecute(CommandParameter))
{
Command.Execute(CommandParameter);
}
}
protected override Freezable CreateInstanceCore()
{
return new CommandFakeAnimation();
}
public override Type TargetPropertyType
{
get
{
return typeof(Object);
}
}
public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock)
{
return defaultOriginValue;
}
}
As you can see you can apply this animation to whatever dependecy property that you wish, since it does not change its value. It just execute a command when it is completed.
Now we can use the new animation in the XAML:
<Button.Triggers>
<EventTrigger RoutedEvent="PreviewMouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:2" />
<local:CommandFakeAnimation Duration="0:0:0" Command="{Binding Path=YourCommand, Mode=OneWay}"
CommandParameter="{Binding Path=YourParameter, Mode=OneWay}"
Storyboard.TargetProperty="Opacity" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
I hope it can help you.

Achieve "slide down" animation in WPF

I am attempting to create my own template for an Expander control. When the control is expanded, I want the content to slide down slowly.
The desired height of the content is not known at compile time.
I thought we could define the slide down as an animation:
<Storyboard x:Key="ExpandContent">
<DoubleAnimation
Storyboard.TargetName="_expanderContent"
Storyboard.TargetProperty="Height"
From="0.0"
To="{Binding ElementName=_expanderContent,Path=DesiredHeight}"
Duration="0:0:1.0" />
</Storyboard>
But Unfortunately not. We get an error
Cannot freeze this Storyboard timeline tree for use across threads.
It appears that we cannot use binding when defining animation parameters. (Discussed also in this question.)
Does anyone have any ideas on how I can approach this? I'm wary of using LayoutTransform.ScaleY, because that would create a distorted image.
This is similar to this question, but this question has an answer involved writing code-behind, which I don't think is possible in a control template.
I'm wondering if a XAML-based solution is achievable.
For what it's worth, here is the current state of my control template.
<ControlTemplate x:Key="ExpanderControlTemplate" TargetType="{x:Type Expander}">
<ControlTemplate.Resources>
<!-- Here are the storyboards which don't work -->
<Storyboard x:Key="ExpandContent">
<DoubleAnimation
Storyboard.TargetName="_expanderContent"
Storyboard.TargetProperty="Height"
From="0.0"
To="{Binding ElementName=_expanderContent,Path=DesiredHeight}"
Duration="0:0:1.0" />
</Storyboard>
<Storyboard x:Key="ContractContent">
<DoubleAnimation
Storyboard.TargetName="_expanderContent"
Storyboard.TargetProperty="Height"
From="{Binding ElementName=_expanderContent,Path=DesiredHeight}"
To="0.0"
Duration="0:0:1.0" />
</Storyboard>
</ControlTemplate.Resources>
<Grid Name="MainGrid" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Name="ContentRow" Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter ContentSource="Header" />
<ToggleButton Template="{StaticResource ProductButtonExpand}"
Grid.Column="1"
IsChecked="{Binding Path=IsExpanded,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
/>
<Rectangle Grid.ColumnSpan="2" Fill="#FFDADADA" Height="1" Margin="8,0,8,2" VerticalAlignment="Bottom"/>
</Grid>
</Border>
<ContentPresenter Grid.Row="1" HorizontalAlignment="Stretch" Name="_expanderContent">
</ContentPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="_expanderContent" Property="Height" Value="{Binding ElementName=_expanderContent,Path=DesiredHeight}" />
<!-- Here is where I would activate the storyboard if they did work -->
<Trigger.EnterActions>
<!--<BeginStoryboard Storyboard="{StaticResource ExpandContent}"/>-->
</Trigger.EnterActions>
<Trigger.ExitActions>
<!--<BeginStoryboard x:Name="ContractContent_BeginStoryboard" Storyboard="{StaticResource ContractContent}"/>-->
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="_expanderContent" Property="Height" Value="0" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
This is kind of an old question but I had problems with this today, so I guess posting my solution would be worth it:
I had to animate the Height property of a grid row (sliding up and down), but needed dynamic binding so that the row would slide again to the same position as before.
I found this answer to be very helpful (after fruitlessly battling XAML):
http://go4answers.webhost4life.com/Question/found-solution-work-protected-override-190845.aspx
Sometimes doing things in the code-behind is just simpler:
Storyboard sb = new Storyboard();
var animation = new GridLengthAnimation
{
Duration = new Duration(500.Milliseconds()),
From = this.myGridRow.Height,
To = new GridLength(IsGridRowVisible ? GridRowPreviousHeight : 0, GridUnitType.Pixel)
};
// Set the target of the animation
Storyboard.SetTarget(animation, this.myGridRow);
Storyboard.SetTargetProperty(animation, new PropertyPath("Height"));
// Kick the animation off
sb.Children.Add(animation);
sb.Begin();
The GridLengthAnimation class can be found here:
http://social.msdn.microsoft.com/forums/en-US/wpf/thread/da47a4b8-4d39-4d6e-a570-7dbe51a842e4/
/// <summary>
/// Animates a grid length value just like the DoubleAnimation animates a double value
/// </summary>
public class GridLengthAnimation : AnimationTimeline
{
/// <summary>
/// Returns the type of object to animate
/// </summary>
public override Type TargetPropertyType
{
get
{
return typeof(GridLength);
}
}
/// <summary>
/// Creates an instance of the animation object
/// </summary>
/// <returns>Returns the instance of the GridLengthAnimation</returns>
protected override System.Windows.Freezable CreateInstanceCore()
{
return new GridLengthAnimation();
}
/// <summary>
/// Dependency property for the From property
/// </summary>
public static readonly DependencyProperty FromProperty = DependencyProperty.Register("From", typeof(GridLength),
typeof(GridLengthAnimation));
/// <summary>
/// CLR Wrapper for the From depenendency property
/// </summary>
public GridLength From
{
get
{
return (GridLength)GetValue(GridLengthAnimation.FromProperty);
}
set
{
SetValue(GridLengthAnimation.FromProperty, value);
}
}
/// <summary>
/// Dependency property for the To property
/// </summary>
public static readonly DependencyProperty ToProperty = DependencyProperty.Register("To", typeof(GridLength),
typeof(GridLengthAnimation));
/// <summary>
/// CLR Wrapper for the To property
/// </summary>
public GridLength To
{
get
{
return (GridLength)GetValue(GridLengthAnimation.ToProperty);
}
set
{
SetValue(GridLengthAnimation.ToProperty, value);
}
}
/// <summary>
/// Animates the grid let set
/// </summary>
/// <param name="defaultOriginValue">The original value to animate</param>
/// <param name="defaultDestinationValue">The final value</param>
/// <param name="animationClock">The animation clock (timer)</param>
/// <returns>Returns the new grid length to set</returns>
public override object GetCurrentValue(object defaultOriginValue,
object defaultDestinationValue, AnimationClock animationClock)
{
double fromVal = ((GridLength)GetValue(GridLengthAnimation.FromProperty)).Value;
//check that from was set from the caller
if (fromVal == 1)
//set the from as the actual value
fromVal = ((GridLength)defaultDestinationValue).Value;
double toVal = ((GridLength)GetValue(GridLengthAnimation.ToProperty)).Value;
if (fromVal > toVal)
return new GridLength((1 - animationClock.CurrentProgress.Value) * (fromVal - toVal) + toVal, GridUnitType.Star);
else
return new GridLength(animationClock.CurrentProgress.Value * (toVal - fromVal) + fromVal, GridUnitType.Star);
}
}
If you can use Interactions with FluidLayout (Blend 4 SDK) you are in luck, it's really useful for those fancy animation things.
First set the content CP's Height to 0:
<ContentPresenter Grid.Row="1"
HorizontalAlignment="Stretch"
x:Name="_expanderContent"
Height="0"/>
To animate this, the Height just needs to be animated to NaN in the VisualState that represents the expanded state (non-discrete animations would not let you use NaN):
xmlns:is="http://schemas.microsoft.com/expression/2010/interactions"
<Grid x:Name="MainGrid" Background="White">
<VisualStateManager.CustomVisualStateManager>
<is:ExtendedVisualStateManager/>
</VisualStateManager.CustomVisualStateManager>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ExpansionStates" is:ExtendedVisualStateManager.UseFluidLayout="True">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:1"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="Expanded">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Height)"
Storyboard.TargetName="_expanderContent">
<DiscreteDoubleKeyFrame KeyTime="0" Value="NaN"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Collapsed"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!-- ... --->
That should be all that is necessary, the fluid layout will create the transition for you from there.
If you have a code-behind solution that would be fine, you can even use code-behind in dictionaries like this:
<!-- TestDictionary.xaml -->
<ResourceDictionary x:Class="Test.TestDictionary"
...>
//TestDictionary.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
namespace Test
{
partial class TestDictionary : ResourceDictionary
{
//Handlers and such here
}
}
There is a ready-to-use and XAML-only solution on CodeProject:
The Styles:
<local:MultiplyConverter x:Key="MultiplyConverter" />
<Style TargetType="Expander" x:Key="VerticalSlidingEmptyExpander">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<ScrollViewer x:Name="ExpanderContentScrollView"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Top"
>
<ScrollViewer.Tag>
<system:Double>0.0</system:Double>
</ScrollViewer.Tag>
<ScrollViewer.Height>
<MultiBinding Converter="{StaticResource MultiplyConverter}">
<Binding Path="ActualHeight" ElementName="ExpanderContent"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</ScrollViewer.Height>
<ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>
</ScrollViewer>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="1"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="0"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="Expander" x:Key="HorizontalSlidingEmptyExpander">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<ScrollViewer x:Name="ExpanderContentScrollView"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
HorizontalContentAlignment="Left"
VerticalContentAlignment="Stretch"
>
<ScrollViewer.Tag>
<system:Double>0.0</system:Double>
</ScrollViewer.Tag>
<ScrollViewer.Width>
<MultiBinding Converter="{StaticResource MultiplyConverter}">
<Binding Path="ActualWidth" ElementName="ExpanderContent"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</ScrollViewer.Width>
<ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>
</ScrollViewer>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="1"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ExpanderContentScrollView"
Storyboard.TargetProperty="Tag"
To="0"
Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
MultiplyConverter:
public class MultiplyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
double result = 1.0;
for (int i = 0; i < values.Length; i++)
{
if (values[i] is double)
result *= (double)values[i];
}
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new Exception("Not implemented");
}
}
I duplicated the Style to have a horizontal and vertical version and omitted the ToggleButtons, but you can easily get that from the original post.

Wpf Toolkit Chart Invert Highlighting

How can I highlight other pieces (columns, bars etc.) in a chart created with wpf toolkit. I am using a control template to style my own chart. So far I used a trigger to get a fading effect on the element on which the mouse is residing. I want to invert this; to fade other elements (a popular charting visual gimmick) on to which mouse is not pointing. Following image shows the selected column Faded, I want it to be the other way around.
Just set the default value to faded and use the trigger to bring it up to full opacity. You have done some other styling but here is an example based on the default style:
<Grid>
<Grid.Resources>
<PointCollection x:Key="sampleData">
<Point>1,20</Point>
<Point>2,40</Point>
<Point>3,30</Point>
</PointCollection>
<Style x:Key="dimEffectStyle" TargetType="{x:Type charting:ColumnDataPoint}" BasedOn="{StaticResource {x:Type charting:ColumnDataPoint}}">
<Setter Property="Opacity" Value="0.25"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.25" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<charting:Chart>
<charting:ColumnSeries
Title="A"
ItemsSource="{StaticResource sampleData}"
IndependentValueBinding="{Binding X}"
DependentValueBinding="{Binding Y}"
DataPointStyle="{StaticResource dimEffectStyle}"
/>
</charting:Chart>
</Grid>
Edit:
If you want to change all the other data points except the data point the mouse is over, that is a bit harder and can't be done simply by restyling the controls. But you can create your own series control that has that capability. Here is a chart with an unstyled column series class called MouseNotOverColumnSeries with a new MouseNotOverOpacity property:
<Grid.Resources>
<PointCollection x:Key="sampleData">
<Point>1,20</Point>
<Point>2,40</Point>
<Point>3,30</Point>
</PointCollection>
</Grid.Resources>
<charting:Chart Name="chart1">
<local:MouseNotOverColumnSeries
Title="A"
ItemsSource="{StaticResource sampleData}"
IndependentValueBinding="{Binding X}"
DependentValueBinding="{Binding Y}"
MouseNotOverOpacity="0.5"
/>
</charting:Chart>
Here is the MouseNotOverColumnSeries class:
public class MouseNotOverColumnSeries : ColumnSeries
{
public double MouseNotOverOpacity { get; set; }
protected override void OnDataPointsChanged(IList<DataPoint> newDataPoints, IList<DataPoint> oldDataPoints)
{
base.OnDataPointsChanged(newDataPoints, oldDataPoints);
foreach (var dataPoint in oldDataPoints)
{
dataPoint.MouseEnter -= new MouseEventHandler(dataPoint_MouseEnter);
dataPoint.MouseLeave -= new MouseEventHandler(dataPoint_MouseLeave);
}
foreach (var dataPoint in newDataPoints)
{
dataPoint.MouseEnter += new MouseEventHandler(dataPoint_MouseEnter);
dataPoint.MouseLeave += new MouseEventHandler(dataPoint_MouseLeave);
}
}
void dataPoint_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
foreach (var dataPoint in ActiveDataPoints)
if (e.OriginalSource != dataPoint) dataPoint.Opacity = MouseNotOverOpacity;
}
void dataPoint_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
foreach (var dataPoint in ActiveDataPoints)
dataPoint.Opacity = 1;
}
}
We just pay attention to when the data points change and register mouse enter/leave handlers that manipulate the opacity of all the other data points that the mouse is not over. This could be expanded to support storyboards, etc.

Resources