I'm looking for the way to code-behind launch an animation on a control contained in an ControlTemplate.
In my app I have custom templated buttons (playing menu role) created from an ObservableCollection :
MainMenuViewModel :
/// <summary>
/// Menu items list
/// </summary>
private ObservableCollection<MenuItem> _items;
....
/// <summary>
/// Menu items list property
/// </summary>
public ObservableCollection<MenuItem> Items
{
get { return _items; }
set { _items = value; }
}
MainMenuView :
<UserControl x:Class="OfficeTourismeBrantome.Views.MainMenuView"
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"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="300">
<UserControl.Resources>
<Style x:Key="MenuItemButtonStyle" TargetType="Button">
<Setter Property="FontSize" Value="60" />
<Setter Property="FontFamily" Value="Segoe" />
<Setter Property="FontWeight" Value="UltraLight" />
<Setter Property="Foreground" Value="#FFEBEDEA" />
<!--<Setter Property="Height" Value="{Binding MenuLayout.MenuItemSize.Height}" />-->
<Setter Property="HorizontalContentAlignment" Value="Right" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard Name="themeSelectionAnimation">
<DoubleAnimation
Storyboard.TargetName="coloredRectangle"
Storyboard.TargetProperty="Width"
From="30.0"
To="250.0"
Duration="0:0:0.3" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</ControlTemplate.Triggers>
<Canvas HorizontalAlignment="Stretch" ClipToBounds="False" >
<ContentPresenter Canvas.Left="{Binding MenuLayout.MenuItemLeftMargin}" HorizontalAlignment="Center"
VerticalAlignment="Center" Canvas.ZIndex="1"/>
<TextBlock
Text="{Binding SecondaryText}"
Canvas.Top="50"
Canvas.Left="10"
FontSize="30"
FontWeight="ExtraLight"
FontStyle="Italic"
Canvas.ZIndex="1"
/>
<Rectangle
Canvas.Top="30"
Canvas.Left="10"
Name="coloredRectangle"
Width="30"
Height="10"
Canvas.ZIndex="0"
Fill="{Binding Color}"/>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Storyboard x:Key="themeUnselectionAnimation">
<DoubleAnimation
Storyboard.TargetProperty="Width"
From="250.0"
To="30.0"
Duration="0:0:0.15" />
</Storyboard>
</UserControl.Resources>
<ItemsControl Name="menuButtonContainer" ItemsSource="{Binding Items}" Margin="{Binding MenuLayout.MenuMargin}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Style="{StaticResource ResourceKey=MenuItemButtonStyle}"
Margin="{Binding ElementName=menuButtonContainer,
Path=DataContext.MenuLayout.MenuItemMargin}"
Height="{Binding ElementName=menuButtonContainer,
Path=DataContext.MenuLayout.MenuItemSize.Height}"
Content="{Binding Text}"
Command="{Binding ElementName=menuButtonContainer,
Path=DataContext.ChangeThemeCommand}"
CommandParameter="{Binding Id}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>
As you can see in the code above, I got an animation automatically triggered on button click. I want to play it reverse when another button in the collection is clicked (another menu entry is selected). The animation to play is the one called themeUnselectionAnimation.
First question : is there a way to do it only in XAML? I'm not sure as another button need to be pressed to trigger it.
Below is what I thought :
In the Button (which is my menu item) command action, send a message to let subscribers that menu item is changing.
Register to it in MainMenuView code-behind.
Launch animation from there.
My problem so far is to set the target control for the animation. To do that, I need to find the Rectangle named coloredRectangle in ControlTemplate. How to do that ?
Here below is my code corresponding to above steps :
Step 1 : send message (I'm using MVVM Light framework)
/// <summary>
/// Delegates that handles theme change process and tasks
/// </summary>
/// <param name="themeId">the new active theme</param>
private void ChangeTheme(int themeId)
{
// Set current active theme as inactive, if one is selected.
// Exception use because of Single implementation that throw an InvalidOperationException if not item is found
try
{
MenuItem currentTheme = Items.Single(x => x.IsActive == true);
// Check if this is current theme. If it is, we do nothing.
if(currentTheme.Id == themeId)
return;
// If current theme is set and new theme id is not the same, disable the old one
currentTheme.IsActive = false;
// Set new theme as active
Items.Single(x => x.Id == themeId).IsActive = true;
// Finally, launch unselection animation
// Send message and register to it in view code behind
// Create inner message
ThemeChangeNotification innerMessage = new ThemeChangeNotification();
innerMessage.NewThemeId = themeId;
innerMessage.OldThemeId = currentTheme.Id;
NotificationMessage<ThemeChangeNotification> message =
new NotificationMessage<ThemeChangeNotification>(innerMessage, "");
// Send message
Messenger.Default.Send(message);
}
catch (InvalidOperationException exception)
{
// Set first theme selection as active
Items.Single(x => x.Id == themeId).IsActive = true;
}
}
Step 2 : Register to message
Messenger.Default.Register<NotificationMessage<ThemeChangeNotification>>(this, ChangeThemeAnimation);
Step 3 : reach Button from index/id and launch animation (not working)
/// <summary>
/// Theme change message delegate
/// </summary>
/// <param name="e">The ThemeChangeNotification message</param>
private void ChangeThemeAnimation(NotificationMessage<ThemeChangeNotification> message)
{
var buttonTheme = menuButtonContainer.ItemContainerGenerator.ContainerFromIndex(message.Content.OldThemeId) as FrameworkElement;
var rectangle = buttonTheme.FindName("coloredRectangle") as Rectangle;
Storyboard sb = this.FindResource("themeUnselectionAnimation") as Storyboard;
Storyboard.SetTarget(sb, rectangle);
sb.Begin();
}
Thank you very much for your answers !
Surely, you can just create another Style based on the first one that uses the other Storyboard instead... then you could just apply the reverse Style on whichever Button(s) that you want to start that Storyboard:
<Style x:Key="ReverseMenuItemButtonStyle" TargetType="Button">
<Setter Property="FontSize" Value="60" />
<Setter Property="FontFamily" Value="Segoe" />
<Setter Property="FontWeight" Value="UltraLight" />
<Setter Property="Foreground" Value="#FFEBEDEA" />
<Setter Property="HorizontalContentAlignment" Value="Right" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard Name="themeUnselectionAnimation">
<DoubleAnimation
Storyboard.TargetName="coloredRectangle"
Storyboard.TargetProperty="Width"
From="30.0"
To="250.0"
Duration="0:0:0.3" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</ControlTemplate.Triggers>
...
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I've got to be fully honest... I didn't totally understand your question, so if that didn't answer it, it seems as though you also want to know how to start a Storyboard from a view model. In this case, you just need a bool property which will start the animation when set to true in the view model. You can do that using the DataTrigger.EnterActions:
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding SomeBooleanPropertyInViewModel}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard ... />
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
UPDATE >>>
Again... I still don't really know what you're after... I'd suggest working on your question asking skills before posting another one. However, this much I could work out:
You're getting an TargetName cannot be used on Style Setter error and you want to target your coloredRectangle element.
The usual fix for this error is simply to move your Trigger to the element that you are trying to target. So try this instead:
<Rectangle Canvas.Top="30" Canvas.Left="10" Name="coloredRectangle" ... >
<Rectangle.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding SomeBooleanProperty}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard ... />
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
Related
I have a Tabcontrol that I'm trying to add actions based on triggers.
Using Routed Events I can get my storyboard to trigger and the my actions respond as intended. Now I'm trying to prevent my storyboard actions from triggering for a specific tab header if that header is selected.
This has forced me to move away from routed events since I can't use a property condition and a routed event to determine if my storyboard will execute.
Fast forward and now I have finally been able to get IsMouseOver to respond as I wish to the mouse. (My background properties changes and changes back as the mouse enters and leaves the tabitem header. I'm almost there I think but as soon as I add the exact same storyboard as before my code decides to be lazy and skip it for some reason. The background setter is still being triggered but the storyboard remains silent.
I tried removing the setter but the storyboard still does not trigger.
End of the Day: I looking to set styles that transition nicely between each other for all possible combinations of Is Mouse Over and Is Selected.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Tenant_Tool_Analytics_Module.Resources.Components"
xmlns:controls="clr-namespace:Tenant_Tool_Analytics_Module.Resources.Components">
<Style TargetType="{x:Type controls:NavigationElement}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:NavigationElement}">
<Border BorderBrush="Black" BorderThickness=".7" x:Name="Bd">
<Grid Width="150" Height="50" Opacity="0.75" x:Name="NavigationElementGrid">
<Grid.Background >
<RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="0.5" RadiusY="2.0">
<GradientStop Color="#006A4D" Offset="1.0"/>
<GradientStop Color="#56be88" Offset=".2"/>
</RadialGradientBrush >
</Grid.Background>
<ContentControl Grid.Column="0" Content="{TemplateBinding Icon}"/>
<TextBlock x:Name="NavigationElementText" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="{TemplateBinding LabelText}" FontFamily="Futura XBlkIt BT" FontSize="12"/>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
</Grid.RowDefinitions>
</Grid>
</Border>
<!--
Trigger related to when the mouse is over the header
I would like it to Execute the doubleanimations
-->
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<!-- Missing condition: If selected = false -->
</MultiTrigger.Conditions>
<!-- Start BUG the bellow code does not execute -->
<MultiTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="NavigationElementGrid" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.3"/>
<DoubleAnimation Storyboard.TargetName="NavigationElementText" Storyboard.TargetProperty="FontSize" To="14" Duration="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</MultiTrigger.EnterActions>
<!-- End BUG -->
<!-- The triggers are fireing becuase this is being set. -->
<Setter Property="Background" TargetName="Bd" Value="Blue"/>
</MultiTrigger>
<!--
When the mouse leaves I want it to return to it's original
state.
-->
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="False"/>
<!-- Missing condition: If selected = false -->
</MultiTrigger.Conditions>
<!-- Start BUG the bellow code does not execute -->
<MultiTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="NavigationElementGrid" Storyboard.TargetProperty="Opacity" To=".75" Duration="0:0:0.3"/>
<DoubleAnimation Storyboard.TargetName="NavigationElementText" Storyboard.TargetProperty="FontSize" To="12" Duration="0:0:0.3"/>
</Storyboard>
</BeginStoryboard>
</MultiTrigger.EnterActions>
<!-- End BUG -->
<!-- The triggers are fireing becuase this is being set. -->
<Setter Property="Background" TargetName="Bd" Value="Red"/>
</MultiTrigger>
<!-- Missing two more MultiTriggers (very similar to above) for the cases of if the tab is selected.-->
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Code behind
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Tenant_Tool_Analytics_Module.Resources.Components
{
public class NavigationElement : Control
{
public string LabelText
{
get
{
return (string)GetValue(LabelTextProperty);
}
set
{
SetValue(LabelTextProperty, value);
}
}
public static readonly DependencyProperty LabelTextProperty =
DependencyProperty.Register("LabelText", typeof(string), typeof(NavigationElement), new PropertyMetadata(string.Empty));
public object Icon
{
get
{
return (object)GetValue(IconProperty);
}
set
{
SetValue(IconProperty, value);
}
}
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register("Icon", typeof(object), typeof(NavigationElement), new PropertyMetadata(string.Empty));
public System.Windows.Media.Brush BackgroundColour
{
get
{
return (System.Windows.Media.Brush)GetValue(BackgroundColourProperty);
}
set
{
SetValue(BackgroundColourProperty, value);
}
}
public static readonly DependencyProperty BackgroundColourProperty =
DependencyProperty.Register("BackgroundColour", typeof(System.Windows.Media.Brush), typeof(NavigationElement), new PropertyMetadata(Brushes.Black));
}
}
Implementation code
<Window x:Class="Tenant_Tool_Analytics_Module.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:Tenant_Tool_Analytics_Module"
xmlns:views="clr-namespace:Tenant_Tool_Analytics_Module.Views"
xmlns:controls="clr-namespace:Tenant_Tool_Analytics_Module.Resources.Components"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<DockPanel LastChildFill="True">
<views:HeaderView x:Name="HeaderView" DockPanel.Dock="Top"/>
<TabControl DockPanel.Dock="Left" TabStripPlacement="Left" Margin="0, 0, 0, 10">
<TabItem Padding="0">
<TabItem.Header>
<controls:NavigationElement LabelText="Stacking Plan" Icon="{StaticResource StackPlanIcon}"/>
</TabItem.Header>
<Button Content="HI"/>
</TabItem>
<TabItem Padding="0">
<TabItem.Header>
<controls:NavigationElement LabelText="Tenant Profile" Icon="{StaticResource TenantProfileIcon}"/>
</TabItem.Header>
<Button Content="HI"/>
</TabItem>
<TabItem Padding="0">
<TabItem.Header>
<controls:NavigationElement LabelText="Submarket" Icon="{StaticResource SubmarketIcon}"/>
</TabItem.Header>
<Button Content="HI"/>
</TabItem>
<TabItem Padding="0">
<TabItem.Header>
<controls:NavigationElement LabelText="Industry" Icon="{StaticResource IndustryIcon}"/>
</TabItem.Header>
<Button Content="HI"/>
</TabItem>
</TabControl>
</DockPanel>
</Window>
I think you weren't aware of the fact that MultiDataTrigger objects have both EnterActions and ExitActions. Instead of having one MultiDataTrigger triggering on true with only EnterActions and another MultiDataTrigger triggering on false with only EnterActions, you can use only one MultiDataTrigger triggering on true with both EnterActions (to change the object to an abnormal state) and ExitActions to transition it back to its normal state.
The Triggers collection now works as expected and becomes, as a bonus, easier to read:
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<!-- Missing condition: If selected = false -->
</MultiTrigger.Conditions>
<MultiTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="NavigationElementGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.3"/>
<DoubleAnimation Storyboard.TargetName="NavigationElementText"
Storyboard.TargetProperty="FontSize"
To="14" Duration="0:0:0.3"/>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="BorderBackgroundBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="0" Value="Blue"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</MultiTrigger.EnterActions>
<MultiTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="NavigationElementGrid"
Storyboard.TargetProperty="Opacity"
To=".75"
Duration="0:0:0.3"/>
<DoubleAnimation Storyboard.TargetName="NavigationElementText"
Storyboard.TargetProperty="FontSize"
To="12"
Duration="0:0:0.3"/>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="BorderBackgroundBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="0" Value="Red"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</MultiTrigger.ExitActions>
</MultiTrigger>
<!-- Missing two more MultiTriggers (very similar to above) for the cases of if the tab is selected.-->
</ControlTemplate.Triggers>
Also notice how I've used a ColorAnimationUsingKeyFrames to change the Border.Background property without needing a Setter in another Trigger. This way, all changes are performed in the same Storyboard. For this to work, you just need to assign a named SolidColorBrush to your "Bd" Border:
<Border.Background>
<SolidColorBrush x:Name="BorderBackgroundBrush" Color="Red"></SolidColorBrush>
</Border.Background>
To prevent the Storyboard to play if the ancestor TabItem is selected, I suggest you add a boolean IsSelected DependencyProperty to your NavigationElement, so that you can bind this property to its TabItem ancestor by adding a Setter in your Style like this:
<Setter Property="IsSelected" Value="{Binding IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TabItem}}"/>
And you just have to add the condition in your MultiDataTrigger (but you already figured that out):
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
Sidenote: I recommend you wrap and indent your XAML attributes to avoid long XAML lines that force you to scroll. Besides the increased readability, having each XAML attribute on a new line is more version control-friendly because one attribute change only impacts one line.
I need to add backdrop on some element when dropdown is open.
What I mean - in Bootstrap when you open modal window - backround become dark and not clickable (for default settings) (Live example)
Should I add trigger on dropDown open event which will add some background (backdrop) on some element and make this element disabled?
Here is a very robust mimic of effect you want.
xaml:
<Grid>
<TextBlock Text="SomeContent" />
<Button Content="Click"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Click="Button_Click">
<Button.ContextMenu>
<ContextMenu Opened="ContextMenu_Opened"
Closed="ContextMenu_Closed">
<MenuItem Header="Test" />
</ContextMenu>
</Button.ContextMenu>
</Button>
<Grid x:Name="overlay"
Visibility="Collapsed"
Background="#50000000" />
</Grid>
cs:
void Button_Click(object sender, RoutedEventArgs e) => ((Button)sender).ContextMenu.IsOpen = true;
void ContextMenu_Opened(object sender, RoutedEventArgs e) => overlay.Visibility = Visibility.Visible;
void ContextMenu_Closed(object sender, RoutedEventArgs e) => overlay.Visibility = Visibility.Collapsed;
Here is how it works:
Idea is to create semi-transparent layer (backdrop?) which is shown when menu is opened.
It should be easy to improve this: use bindings, add effects, etc.
I found solution.
I've just added some Grid with ZIndex and Background.
This Grid become visible when Toggle Button is checked and unvisible in another case(when click on this GRID (Overlay) - then I force Toggle Button being clicked - this is how I've managed to hide GRID... something simple to Bootstrap behaviour of Modal Backdrop).
<Grid x:Name="Overlay" Panel.ZIndex="2" Grid.Row="1">
<Grid.Background>
<SolidColorBrush Color="Black" Opacity=".3"/>
</Grid.Background>
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=FiltersButton}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseDown">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="FiltersButton" Storyboard.TargetProperty="(ToggleButton.IsChecked)">
<DiscreteBooleanKeyFrame KeyTime="0:0:00.0" Value="False" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Grid.Triggers>
</Grid>
Maybe there are another way (more simple or with less code) to bind logic of visibility...
I have a custom template to my button that is as follow:
<!--Button Template-->
<Style TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="buttonBorderOuter"
BorderBrush="#DBDBDB"
BorderThickness="1"
Background="#00ECECEC"
CornerRadius="5"
Margin="1">
<Border x:Name="buttonBorderInner">
<TextBlock Text="{TemplateBinding Content}"
TextWrapping="Wrap"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="{TemplateBinding Padding}">
<!--<ContentPresenter ContentSource="Content" />-->
</TextBlock>
</Border>
<Border.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#FFECECEC"
Duration="0:0:0" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#00ECECEC"
Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed"
Value="True">
<Setter Property="Background"
Value="#ECECEC"
TargetName="buttonBorderOuter" />
<Setter Property="BorderBrush"
Value="#FDFDFD"
TargetName="buttonBorderInner" />
<Setter Property="BorderThickness"
Value="1"
TargetName="buttonBorderInner" />
<Setter Property="CornerRadius"
Value="4"
TargetName="buttonBorderInner" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The button is working fine in almost any case. I can hover it and see the animations and I can click it and see the animations. The only thing that throws an error is when I click the button and remove it from the view.
I'm writing an WPF application (as you maybe could tell from the XAML in above code) That has the following 'Command':
public ICommand AddEquipmentFilterToList
{
get
{
return new RelayCommand(
() =>
{
Equipment equipment = SegmentRequirementEquipment[SelectedEquipmentFilterIndex];
Button button = new Button();
button.Content = equipment.ID;
button.Padding = new Thickness(2.5);
button.Click += (o, i) =>
{
EquipmentFilters.Remove(button);
};
EquipmentFilters.Add(button);
});
}
}
Here I add a button to an 'ObservableCollection' that is linked to this control in the view:
<ItemsControl ItemsSource="{Binding EquipmentFilters}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
As you can see in my C# code, when I click on the button it should be removed from the list and therefore from the view. When I do this (clicking the button) I get the following error:
'Background' property does not point to a DependencyObject in path '(0).(1)'
I assume it is because the 'MouseLeave' event triggers but can't find a 'Button' to change its background.
Is there a way to fix this error or is my custom template poorly designed? This is the first time I try to edit my visual presentation and would love to learn how to do it better.
Not a decent way (in code) but it should work.
Start the animation in code, not triggers; so you have a chance to check if the button has been removed, in which case, do not start the animation.
button.MouseEnter += (s, e) => { /*begin Storyboard*/};
button.MouseLeave += (s, e) =>
{
if (EquipmentFilters.Contains(button))
{
//begin storyboard
}
};
button.Click += (o, i) =>
{
EquipmentFilters.Remove(button);
};
I have a WPF application which contains a UserControl, whose border is animated:
<UserControl x:Class="CarSystem.CustomControls.AlarmItem"
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:cs="clr-namespace:CarSystem.CustomControls"
mc:Ignorable="d"
DataContext="{Binding Path=Alarm, RelativeSource={RelativeSource Self}}">
<UserControl.Resources>
<Style TargetType="{x:Type cs:AlarmItem}">
<Setter Property="IsFlashing" Value="False" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Border HorizontalAlignment="Center"
Margin="5"
Height="100"
Name="Border"
VerticalAlignment="Center"
Width="100">
<Border.Resources>
<Storyboard x:Key="FlashingStoryboard"
AutoReverse="True"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:00.5"
Storyboard.TargetName="Border"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)">
<DiscreteColorKeyFrame KeyTime="00:00:00.25" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Border.Resources>
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="2" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="FlashStates">
<VisualState x:Name="FlashingOn"
Storyboard="{StaticResource ResourceKey=FlashingStoryboard}" />
<VisualState x:Name="FlashingOff" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Grid.Row="0"
Name="AlarmImage"
Source="{Binding Path=Image, RelativeSource={RelativeSource AncestorType={x:Type cs:AlarmItem}}}"
Stretch="Fill" />
<cs:ResponseTimer Expired="Timer_Expired"
Grid.Row="1"
HideIfExpired="True"
IsTabStop="False"
MinHeight="10"
x:Name="TheTimer"
TimeoutPeriod="00:02:30"
VerticalAlignment="Bottom" />
</Grid>
</Border>
</UserControl>
The application receives data from a proprietary device my company manufactures. The objects are received and loaded into a View Model class instance. A new instance of this control is created for each object received, a reference to the view model is put into the new control instance's DataContext property, and the new control is added to a ListBox in a Window.
The animation is supposed to run while the object's status property is a particular value. There's a second status value where the animation is also supposed to run, and the status changes to that value after a fixed time interval in which the user has not responded to that item. The animation is only supposed to stop when the status takes on a value that can only be set by user interaction.
When the first object is received & displayed, the animation works fine. If no further objects are received, the animation keeps running & the border color changes as intended when the timer expires.
However, the animation just stops, on its own, after 2 or more objects are received. This is both before the timer that changes the status that causes the border color to change expires and before any user action is taken. Note that it doesn't always stop on the second object, it sometimes takes receiving 3 or 4 objects before the animation stops.
Does anyone have any idea why the animation stops? How do I keep each one running to the end? Is there a better way to get the same effect that doesn't have this problem?
The closest scenario I've had to this is triggering an animation based upon a value in the view model. I'm taking the 'better way' option here so apologies if this doesn't fit with your solution, I'm just basing it on what I see.
Now you mention a new control is created for each alert that comes from your external device? So I'm going to assume this control is bound to a single instance and presented in something like a listbox.
If you're still with me, your control instance has it's own view model instance and by the looks of it, your triggering from an IsPending and IsExpired property such that when either is true, make it flash.
First thing I'd do is simplify that binding from the ViewModel adding as IsAlertRequired property which you set when you require the alert - can be updated in the setter for you existing properties. The reason here is that a single trigger is way easier than a multibinding.
Then use DataTrigger to start your storyboard. In your app add a reference to Microsoft.Expression.Interactions and then import them in your XAML:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
Then add a trigger before your layoutroot element:
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding ViewModel.IsAlertRequired}" Value="True">
<ei:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource FlashingAnimation}"/>
</ei:DataTrigger>
That should start your animation based upon a view model data binding.
Assuming you get that far and it works for starting your animation, if it still stops then I would look into whether something within your viewmodel or external component is blocking the UI thread.
Hope this helps.
Thanks to information from kidshaw, I was able to figure out a solution that works. Essentially, the animation is supposed to switch the Border control's BorderBrush back and forth between two colors, black and a color that depends upon the properties of the object in the DataContext. This logic is in the code behind because the logic depends on two different properties of the data context object and I couldn't come up with a XAML trigger that would work properly. It wasn't included in the original question because it didn't seem pertinent.
As mentioned in the comments to kidshaw's answer, the problem was that the animation was losing track of what color the BorderBrush was supposed to be when additional alerts came into the window. Instead of switching back & forth between Red & Black, for example, it would think it had to switch between black & black or red & red. So it looked like no animation was taking place, when it actually was.
To fix the problem, and because of the need to choose the color based on properties of the object in the data context, I ended up adding a second DiscreteColorKeyFrame to the animation for the other color. I tried to use a bind the Value property of the DiscreteColorKeyFrame to a DependencyProperty I added to the control which would be set by the code behind logic, but that didn't work. The animation kept switching back & forth between black & transparent, while the Output window in VS kept recording errors about CanFreeze being false.
So I ended up making one animation for each different color and adding one VisualState to the VisualStateManager for each color. There were changes in the code behind, too, bu tI got it all working.
Here's the XAML:
<UserControl x:Class="CarSystem.CustomControls.AlarmItem"
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:cs="clr-namespace:CarSystem.CustomControls"
mc:Ignorable="d"
DataContext="{Binding Path=Alarm, RelativeSource={RelativeSource Self}}">
<UserControl.Resources>
<Style TargetType="{x:Type cs:AlarmItem}">
<Setter Property="IsFlashing" Value="False" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Border HorizontalAlignment="Center"
Margin="5"
Height="100"
Name="Border"
VerticalAlignment="Center"
Width="100">
<Border.BorderBrush>
<SolidColorBrush x:Name="AnimatedBrush" Color="Black" />
</Border.BorderBrush>
<Border.Resources>
<Storyboard x:Key="ExpiredAnimation"
AutoReverse="False"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:01"
Storyboard.TargetName="AnimatedBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="00:00:00" Value="#FFFFFF78" />
<DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="HistoricalAnimation"
AutoReverse="False"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:01"
Storyboard.TargetName="AnimatedBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="00:00:00" Value="#FFFFFF78" />
<DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="PendingAnimation"
AutoReverse="False"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:01"
Storyboard.TargetName="AnimatedBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="00:00:00" Value="Red" />
<DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="WhiteListAnimation"
AutoReverse="False"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:01"
Storyboard.TargetName="AnimatedBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="00:00:00" Value="#FF5819" />
<DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Border.Resources>
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderThickness" Value="2" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="FlashStates">
<VisualState x:Name="ExpiredState" Storyboard="{StaticResource ResourceKey=ExpiredAnimation}" />
<VisualState x:Name="HistoricalState" Storyboard="{StaticResource ResourceKey=HistoricalAnimation}" />
<VisualState x:Name="PendingState" Storyboard="{StaticResource ResourceKey=PendingAnimation}" />
<VisualState x:Name="WhiteListState" Storyboard="{StaticResource ResourceKey=WhiteListAnimation}" />
<VisualState x:Name="FlashingOff" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Grid.Row="0"
Name="AlarmImage"
Source="{Binding Path=Image, RelativeSource={RelativeSource AncestorType={x:Type cs:AlarmItem}}}"
Stretch="Fill" />
<cs:ResponseTimer Expired="Timer_Expired"
Grid.Row="1"
HideIfExpired="True"
IsTabStop="False"
MinHeight="10"
x:Name="TheTimer"
TimeoutPeriod="00:02:30"
VerticalAlignment="Bottom" />
</Grid>
</Border>
</UserControl>
And here's the code from the backend that picks & starts the correct animation.
private void StartStatusAnimation() {
if ( condition1 ) {
// It is. Display the WhiteListAnimation.
if ( !VisualStateManager.GoToElementState( Border, "WhiteListState", true ) ) {
// Log error
}
} else if ( condition2 ) {
if ( !VisualStateManager.GoToElementState( Border, "ExpiredState", true ) ) {
// Log error
}
} else if ( condition3 ) {
if ( !VisualStateManager.GoToElementState( Border, "HistoricalState", true ) ) {
// Log error
}
} else if ( condition4 ) {
if ( !VisualStateManager.GoToElementState( Border, "PendingState", true ) ) {
// Log error
}
} else {
// We don't know what state this is. Stop flashing now
if ( !VisualStateManager.GoToElementState( Border, "FlashingOff", true ) ) {
// Log error
}
}
}
Note that the values of the data context objects properties can change due to user interaction, or a timer expiring, which would either stop the flashing all together, in the first case, or cause the current animation to stop and another one to start, in the second. In the second case, the VisualSTateManager is just set to the FlashingOff state when the trigger in the Style sets the control's IsFlashing to false, and in the second, the above method is called again when the timer expires.
So I am trying to create a simple animation that takes the background from the initial color to the new color and back.
The original issue I had is that I created a trigger on the MouseDownEvent that triggered the animation, but the user could trigger another animation before the first one finished. This new animation would animate from the current shade it was at to the new color and back. By progressively restarting the animation while the animation is going, the original color is lost.
The easiest way to solve this is probably if i use the completed event for the animation. However, I don't like this solution because I want my animations to be in a custom style in resource dictionary and not part of the control itself. If the animation is in a custom style in a resource dictionary, then it won't have access to the code behind of the control itself. Is there a good way to get an animation working while maintaining separation between the style and the control?
I then had a different idea. The error is caused because I was animating from the border.background.color to a new color and back, thus if i started a new animation while the old one was going, the new animation started from whatever color value the prior animation was in. But if I set the animation to go back to some saved property value of the original background color then I won't have the issue even if the user restarts the animation.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Components="clr-namespace:DaedalusGraphViewer.Components"
xmlns:Converters="clr-namespace:DaedalusGraphViewer.Components.Converters"
>
<Converters:ThicknessToLeftThicknessConverter x:Key="ThicknessToLeftThicknessConverter" />
<Style x:Key="SearchBoxListViewItemStyle" TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="{x:Type Components:SearchBox}" TargetType="{x:Type Components:SearchBox}">
<Style.Resources>
</Style.Resources>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Components:SearchBox}">
<Border x:Name="Border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid x:Name="LayoutGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ScrollViewer
x:Name="PART_ContentHost"
Grid.Column="0"
VerticalAlignment="Center"
/>
<Label
x:Name="DefaultTextLabel"
Grid.Column="0"
Foreground="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TextColor}"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=LabelText}"
VerticalAlignment="Center"
FontStyle="Italic"
/>
<Popup x:Name="RecentSearchesPopup"
IsOpen="False"
>
<ListView
x:Name="PreviousSearchesListView"
ListView.ItemContainerStyle="{StaticResource SearchBoxListViewItemStyle}"
>
</ListView>
</Popup>
<Border
x:Name="PreviousSearchesBorder"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="LightGray"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness,
Converter={StaticResource ThicknessToLeftThicknessConverter}}"
>
<Image
x:Name="PreviousSearchesIcon"
ToolTip="Previous Searches"
Width="15"
Height="15"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Source="pack://application:,,,/DaedalusGraphViewer;component/Images/Previous.png"
/>
</Border>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="HasText" Value="True">
<Setter Property="Visibility" TargetName="DefaultTextLabel" Value="Hidden" />
</Trigger>
<Trigger
SourceName="DefaultTextLabel"
Property="IsMouseOver"
Value="True"
>
<Setter Property="Cursor" Value="IBeam" />
</Trigger>
<!--<EventTrigger RoutedEvent="Mouse.MouseDown" SourceName="PreviousSearchesBorder">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
Duration="0:0:0.2"
Storyboard.TargetName="PreviousSearchesBorder"
Storyboard.TargetProperty="(Border.Background).Color"
To="Black"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>-->
<Trigger Property="IsPopupOpening" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="PreviousSearchesBorder"
Storyboard.TargetProperty="(Border.Background).Color"
>
<LinearColorKeyFrame KeyTime="0:0:0.0" Value="{x:Static Components:SearchBox.DefaultRecentSearchesButtonColor}" />
<LinearColorKeyFrame KeyTime="0:0:0.2" Value="Black" />
<LinearColorKeyFrame KeyTime="0:0:0.4" Value="{x:Static Components:SearchBox.DefaultRecentSearchesButtonColor}" />
</ColorAnimationUsingKeyFrames>
<!--<ColorAnimation
AutoReverse="True"
Duration="0:0:0.2"
Storyboard.TargetName="PreviousSearchesBorder"
Storyboard.TargetProperty="(Border.Background).Color"
To="Black"
/>-->
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
However, in order to do this I need to store the original background property and I have yet to get that working. I cannot using binding because properties in Animations must be freezeable, so I tried creating a static property on the control that gets set to the original value on the control's loaded event.
I set the color to the background color in the code behind, but the style does not reflect that property.
Is my static reference in the xaml correct? if so, then isn't onapplytemplate when the style should load the color from the static reference?
Well, you can't use a DynamicResource without getting a Freeze error.
What if you loaded the Xaml file at runtime and added the StaticResource to the Xaml file?
Here is an example. (I use some quick and dirty parsing of the Xaml file, but it works for the proof of concept.)
Change the properties of the StoryBoardResourceDictionary.xaml to Content and Copy if newer and remove the MSBuild:Compile setting.
StoryBoardResourceDictionary.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="WindowWithTrigger" TargetType="Window">
<Style.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseDown">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)" >
<EasingColorKeyFrame KeyTime="0:0:2" Value="White"/>
<!-- OriginalBackground is added at runtime -->
<EasingColorKeyFrame KeyTime="0:0:4" Value="{StaticResource OriginalBackground}"/> <!-- Load at runtime -->
</ColorAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
MainWindow.xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="BackgroundAnimationBlend.MainWindow"
x:Name="Window"
Title="MainWindow"
Width="640" Height="480"
Style="{DynamicResource WindowWithTrigger}"
Background="DarkBlue"> <!--Change background to whatever color you want -->
</Window>
MainWindow.xaml.cs
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Markup;
namespace BackgroundAnimationBlend
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var rd2 = LoadFromFile();
this.Resources.MergedDictionaries.Add(rd2);
}
public ResourceDictionary LoadFromFile()
{
const string file = "Styles/StoryBoardResourceDictionary.xaml";
if (!File.Exists(file))
return null;
using (var fs = new StreamReader(file))
{
string xaml = string.Empty;
string line;
bool replaced = false;
while ((line = fs.ReadLine()) != null)
{
if (!replaced)
{
if (line.Contains("OriginalBackground"))
{
xaml += string.Format("<Color x:Key=\"OriginalBackground\">{0}</Color>", Background);
replaced = true;
continue;
}
}
xaml += line;
}
// Read in an EnhancedResourceDictionary File or preferably an GlobalizationResourceDictionary file
return XamlReader.Load(new MemoryStream(Encoding.UTF8.GetBytes(xaml))) as ResourceDictionary;
}
}
}
}
I have no idea how to scale this right now. So maybe it is a crazy idea. But loading the style at runtime and injecting the current background as a xaml string into the style before loading it is the only idea I had that worked.
I have made a small example to illustrate how you can solve your problem (by what i have understood).
<Window x:Class="WpfApplication1.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>
<Color x:Key="background">yellow</Color>
<SolidColorBrush x:Key="backgroundBrush" Color="{StaticResource background}"/>
<Storyboard x:Key="Storyboard1">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)" Storyboard.TargetName="grid">
<SplineColorKeyFrame KeyTime="0:0:0.3" Value="Black"/>
<SplineColorKeyFrame KeyTime="0:0:0.6" Value="{StaticResource background}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
<Grid x:Name="grid" Background="{StaticResource backgroundBrush}">
<Button x:Name="button" Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" RenderTransformOrigin="0.5,0.5">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource Storyboard1}" Name="Storyboard1"/>
</EventTrigger>
</Button.Triggers>
</Button>
</Grid>
I have defined a Color background as StaticResource to use it as Background-Color for the Grid and as Value for the Animation.
In the example above the Grid has a yellow background, that gets animated to black and then back to yellow when the button is clicked.
So all I ended up doing was using FillBehavior = Stop. I was new to animations so I didn't know how to prevent the reactivation of the animation easily without access to the completed event (which would have to be attached in the control, thus defeating the purpose of having a style with animations in a separate resource file). Regardless, I'm giving the bounty to rhyous because his answer actually does solve my problem as well, though in a more difficult manner. I knew that what i wanted to do had to be simple because there was no way that no one had wanted to animate a color and back from a style, yet I couldn't find any Stack Overflow questions on the issue