I am trying to create a Menu Custom Control that contains some Animations to make things look a little more "Fluid". This is where I am running into Problems, I would like to have the Width of my Custom Control be driven by a animation to emulate the expanding of the menu. I can achieve this by using Triggers in the Template but using this method I can't seem to find a way to bind the "to", I had read that this isn't allowed because of Thread Safety so I figured I would just use the "Click" event and do the Animation in the Code Behind.
Well when I tried to add my class to the Resource Dictionary I end up getting a bunch of errors saying '"DefaultStyleKeyProperty" does not exist in the current context' in the Class file. I need to add my class to the Resource Dictionary in order to be able to get to any of the UI Objects. So I am a little stuck on how to achieve this, or am I going about this the wrong way?
Here is what I have so far:
NavMenu.xaml
<ResourceDictionary x:Class="Happ.UI.Controls.NavMenu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Happ.UI.Controls">
<Style TargetType="{x:Type local:NavMenu}">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Background="Yellow" Orientation="Vertical"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:NavMenu}">
<Border Background="{TemplateBinding Background}" BorderBrush="Transparent">
<ToggleButton x:Name="btnMenu"
Margin="10" Padding="4" Background="Transparent"
HorizontalAlignment="Left" VerticalAlignment="Top"
IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:NavMenu}}, Path=IsExpanded}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
NavMenu.cs
namespace Happ.UI.Controls {
public partial class NavMenu : Menu {
static NavMenu() {
DefaultStyleKeyProperty.OverrideMetadata( typeof( NavMenu ), new FrameworkPropertyMetadata( typeof( NavMenu ) ) );
}
public bool IsExpanded {
get {
return (bool)GetValue( IsExpandedProperty );
}
set {
TimeSpan tsTime = new TimeSpan( 0, 0, 0, 0, 500 );
DoubleAnimation mnuAnim = new DoubleAnimation( MinWidth, tsTime );
btnMenu.BeginAnimation( Width, mnuAnim );
SetValue( IsExpandedProperty, value );
}
}
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.Register( "IsExpanded", typeof( bool ), typeof( NavMenu ), new PropertyMetadata( true ) );
}
}
}
Generic.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Happ.UI.Controls">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Happ.UI.Controls;component/Templates/NavMenu.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
I tried adding the xClass into the Generic.xaml also with no luck, when I did that I received another additional error: "Partial declarations of 'NavMenu' must no specify different base classes".
Edit
I knew there had to be a better way of doing this, using the Interactivity Library. What I ended up doing is creating a new class specifically for the Animations. Then I can attach the new TriggerAction to my Control, one thing that I did figure out is I need to have a ListContainer for the target animation element. This is because there is no way to achieve the animation from 0 to "Auto", the Panel provides a way to get the children and calculate the "Auto" width in code. Here are my updates:
NavMenu.cs
namespace Happ.UI.Controls {
public class NavMenu : Menu {
static NavMenu() {
DefaultStyleKeyProperty.OverrideMetadata( typeof( NavMenu ), new FrameworkPropertyMetadata( typeof( NavMenu ) ) );
}
/// <summary>
/// Flag to Tell if the Menu is Expanded
/// </summary>
public bool IsExpanded {
get {
return (bool)GetValue( IsExpandedProperty );
}
set {
SetValue( IsExpandedProperty, value );
}
}
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.Register( "IsExpanded", typeof( bool ), typeof( NavMenu ), new PropertyMetadata( true ) );
}
}
NavMenu.Animations.cs
namespace Happ.UI.Controls {
public class NavMenuAnimations : System.Windows.Interactivity.TriggerAction<UIElement> {
/// <summary>
/// Sets the Target Element for the Animation
/// </summary>
public Panel TargetPanel {
get {
return (Panel)GetValue( TargetPanelProperty );
}
set {
SetValue( TargetPanelProperty, value );
}
}
public static readonly DependencyProperty TargetPanelProperty =
DependencyProperty.Register( "TargetPanel", typeof( Panel ), typeof( NavMenuAnimations ),
new PropertyMetadata( null ) );
/// <summary>
/// Sets the Target Element for the Animation
/// </summary>
public double Seconds {
get {
return (double)GetValue( SecondsProperty );
}
set {
SetValue( SecondsProperty, value );
}
}
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register( "Seconds", typeof( double ), typeof( NavMenuAnimations ),
new PropertyMetadata( (double)0.5 ) );
/// <summary>
/// This is the Main Animation Method that is called each time the Trigger occurs
/// </summary>
/// <param name="parameter"></param>
protected override void Invoke( object parameter ) {
double maxWidth = 0;
//Make sure that we have a Target Element
if( TargetPanel == null ) {
throw new Exception( "No Target Element specified for the animation. (NavMenu Animations)" );
}
//Make sure the Min Width and Height are Set
TargetPanel.MinWidth = ( Width > 0 ) ? Width : TargetPanel.MinWidth;
//Check if the MaxWidth has been set
if( Double.IsInfinity( TargetPanel.MaxWidth ) ) {
//Loop through the Children and Get Width
foreach( UIElement elem in TargetPanel.Children ) {
//Update the Max Width of the Panel
maxWidth += elem.RenderSize.Width;
}
//Check if we found a MaxWidth. if not return
if( maxWidth == 0 )
return;
//Assign the new MaxWidth
TargetPanel.MaxWidth = maxWidth;
}
//Check if Width is at Minumum (Shrunk)
if( TargetPanel.Width == TargetPanel.MinWidth ) {
TargetPanel.BeginAnimation( FrameworkElement.WidthProperty, new DoubleAnimation( TargetPanel.MaxWidth, TimeSpan.FromSeconds( Seconds ) ) );
}
else {
TargetPanel.BeginAnimation( FrameworkElement.WidthProperty, new DoubleAnimation( TargetPanel.MinWidth, TimeSpan.FromSeconds( Seconds ) ) );
}
}
}
}
NavMenuTemplate.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:Happ.UI.Controls">
<Style TargetType="{x:Type local:NavMenu}">
<Setter Property="Width" Value="50"/>
<Setter Property="Padding" Value="10" />
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel x:Name="navMenu" Orientation="Vertical">
</StackPanel>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:NavMenu}">
<Border Background="{TemplateBinding Background}"
BorderBrush="Transparent"
Padding="{TemplateBinding Padding}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Viewbox Grid.Row="0"
HorizontalAlignment="Left" VerticalAlignment="Top"
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:NavMenu}}, Path=IconSize}"
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:NavMenu}}, Path=IconSize}">
<ToggleButton x:Name="btnMenu" Grid.Row="0"
Content="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:NavMenu}}, Path=MenuIcon}"
Background="Transparent"
Padding="0"
IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:NavMenu}}, Path=IsExpanded}">
</ToggleButton>
</Viewbox>
<ItemsPresenter Grid.Row="1" MinWidth="50">
<i:Interaction.Triggers>
<i:EventTrigger SourceName="btnMenu" EventName="Click" >
<local:NavMenuAnimations TargetPanel="{Binding ElementName=navMenu}" Technique="ExpandWidth" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ItemsPresenter>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
This is getting very close to what I need, I only have one problem remaining. Because of the orientation of my Elements I needed to define a ItemsPanelTemplate which is the Element that I need to Animate. The problem is however I can not figure out how to locate the "navMenu" element. I know this is simply a Context issue I just don't know how to get to the right place. I have tried using the RelativeParent={RelativeParent TemplatedParent} but I can not use the assigned name with it. Can anyone tell me how I can pass a reference to the ItemsPanelTemplate?
Why is the NavMenu defined as partial in the first place? This should only be class wihout any XAML except for the default template that you defined in your NavMenu.xaml file. You should not have any NavMenu.xaml.cs file.
Also your IsExpanded CLR wrapper should only set the value of the dependency property.
To get a reference to the "btnMenu" ToggleButton that is defined in the template of the custom control you should override the OnApplyTemplate method:
public class NavMenu : Menu
{
static NavMenu()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NavMenu), new FrameworkPropertyMetadata(typeof(NavMenu)));
}
public bool IsExpanded
{
get
{
return (bool)GetValue(IsExpandedProperty);
}
set
{
SetValue(IsExpandedProperty, value);
}
}
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.Register("IsExpanded", typeof(bool), typeof(NavMenu), new PropertyMetadata(true));
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
System.Windows.Controls.Primitives.ToggleButton btnMenu = Template.FindName("btnMenu", this) as System.Windows.Controls.Primitives.ToggleButton;
TimeSpan tsTime = new TimeSpan(0, 0, 0, 0, 500);
DoubleAnimation mnuAnim = new DoubleAnimation(0.0, MinWidth, tsTime);
btnMenu.BeginAnimation(WidthProperty, mnuAnim);
}
}
Related
i created a custom Canvas that inhirat from Canvas, i declared a new Dependency Property "NewMouseOver" that i want to affect via Setter in Trigger.
public class CanvaNetwork : Canvas
{
public CanvaNetwork() { }
public bool NewMouseOver
{
get { return (bool)GetValue(NewMouseOverProperty); }
set { SetValue(NewMouseOverProperty, value); }
}
public static readonly DependencyProperty NewMouseOverProperty =
DependencyProperty.Register("NewMouseOver", typeof(bool),
typeof(CanvaNetwork), new PropertyMetadata(false));
}
and here is my XAML :
<DataTemplate DataType="{x:Type local:Node}">
<local:CanvaNetwork x:Name="ItemCanvas_Node"
NewMouseOver="{Binding MyMouseOver}"
Background="Transparent">
<Path x:Name="Path_NodeProcess"
Stroke="Green"
Fill="Gray"
Stretch="None"
Data="{Binding Path =Geometryform}"
Visibility="{Binding Path=Visibility}">
</Path>
<local:CanvaNetwork.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="NewMouseOver" Value="True" />
</Trigger>
</local:CanvaNetwork.Triggers>
</local:CanvaNetwork>
</DataTemplate>
hera is my Node Class :
Public Node :DependencyObject
{
public static readonly DependencyProperty MyMouseOverProperty =
DependencyProperty.Register("MyMouseOver", typeof(bool), typeof(NodeProcess), new PropertyMetadata(true,new PropertyChangedCallback(On_MyMouseOver)));
private static void On_MyMouseOver(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//..some code
}
public bool MyMouseOver
{
get { return (bool)GetValue(MyMouseOverProperty); }
set { SetValue(MyMouseOverProperty, value); }
}
}
what i want is :
1-i have DependencyProperty : NewMouseOver (has get and set not like IsMouseOver in the original Canvas Class).
2-acces to NewMouseOver via Trigger/Setter and change the state of NewMouseOver .
3-via XAML : set a binding betwin : NewMouseOver (in CanvaNetwork) & MyMouseOver (in Node Class)
4-after that i'll use On_MyMouseOver (in Node Class) and MyMouseOver to make some stuff.
I think that I can answer the question about how to update the DependencyProperty in your canvas object.
To test it, I would define an "on changed" method for the dependency property. You can put a breakpoint here to verify that the dependency property is set.
class CanvaNetwork : Canvas
{
public CanvaNetwork ( ) { }
public static readonly DependencyProperty NewMouseOverProperty
= DependencyProperty.Register ( "NewMouseOver",
typeof (bool),
typeof (CanvaNetwork),
new PropertyMetadata (false, OnNewMouseOverChanged)) ;
public bool NewMouseOver
{
get { return (bool)GetValue (NewMouseOverProperty); }
set { SetValue (NewMouseOverProperty, value); }
}
public static void OnNewMouseOverChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e )
{
}
}
In the DataTemplate, you have to define the triggers within a Style.
<DataTemplate DataType="{x:Type local:Node}">
<local:CanvaNetwork x:Name="ItemCanvas_Node"
Background="red" Height="100" Width="100">
<Path x:Name="Path_NodeProcess"
Stroke="Green"
Fill="Gray"
Stretch="None"
Data="{Binding Path =Geometryform}"
Visibility="{Binding Path=Visibility}">
</Path>
<local:CanvaNetwork.Style>
<Style>
<Setter Property="local:CanvaNetwork.NewMouseOver" Value="False" />
<Style.Triggers>
<Trigger Property="Canvas.IsMouseOver" Value="True">
<Setter Property="local:CanvaNetwork.NewMouseOver" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
</local:CanvaNetwork.Style>
</local:CanvaNetwork>
</DataTemplate>
You can only update a property with a trigger, if the default property is set within the style.
For this to work I have removed your attritute NewMouseOver="{Binding MyMouseOver}". From your list, points 1 and 2 work, but removing this attribute means that point 3 does not work.
However, I think that you are probably taking the wrong approach anyway. Wouldn't it be better to hook up the MouseOver event to a command property in your Node class, as described here:
How to make MouseOver event in MVVM?
I'm working towards making click and drag-able spline curves while learning WPF. I've been able to successfully work with pure Line segments, but making the jump to a polyline is proving difficult. I have a class for interpolating the spline curves that I used to use in WinForms, so I'm using a few input clicks from the mouse, and those will be the thumbs to click and drag. The interpolated points have a high enough resolution that a WPF Polyline should be fine for display. To clarify, I need the higher resolution output, so using a WPF Beizer is not going to work.
I have the outline pretty well setup- but the particular issue I'm having, is that dragging the thumbs does not either a) the two way binding is not setup correctly, or b) the ObservableCollection is not generating notifications. I realize that the ObservableCollection only notifies when items are added/removed/cleared, etc, and not that the individual indices are able to produce notifications. I have spent the last few hours searching- found some promising ideas, but haven't been able to wire them up correctly. There was some code posted to try inherit from ObservableCollection and override the OnPropertyChanged method in the ObservableCollection, but that's a protected virtual method. While others used a method call into the OC to attach PropertyChanged event handlers to each object, but I'm unsure where to inject that logic. So I am a little stuck.
MainWindow.xaml:
There is an ItemsControl hosted in a mainCanvas. ItemsControl is bound to a property on the ViewModel
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Menu>
<MenuItem x:Name="menuAddNewPolyline" Header="Add Polyline" Click="MenuItem_Click" />
</Menu>
<Canvas x:Name="mainCanvas" Grid.Row="1">
<ItemsControl x:Name="polylinesItemsControl"
ItemsSource="{Binding polylines}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</Grid>
MainWindow.Xaml.cs:
Pretty simple- initializes a new view model, and it's set as the DataContext. There is a menu with a Add Polyline item, which in turn, initializes a new PolylineControl, and generates three random points (using Thread.Sleep, otherwise they were the same, between the calls) within the ActualHeight and ActualWidth of the window. The new PolylineControl is added to the ViewModel in an ObservableCollection This is a stand in until I get to accepting mouse input.
public partial class MainWindow : Window
{
private ViewModel viewModel;
public MainWindow()
{
InitializeComponent();
viewModel = new ViewModel();
DataContext = viewModel;
}
private Point GetRandomPoint()
{
Random r = new Random();
return new Point(r.Next(0, (int)this.ActualWidth), r.Next(0, (int)this.ActualHeight));
}
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
var newPolyline = new PolylineControl.Polyline();
newPolyline.PolylinePoints.Add(GetRandomPoint());
Thread.Sleep(100);
newPolyline.PolylinePoints.Add(GetRandomPoint());
Thread.Sleep(100);
newPolyline.PolylinePoints.Add(GetRandomPoint());
viewModel.polylines.Add(newPolyline);
}
}
ViewModel.cs:
Absolutely noting fancy here
public class ViewModel
{
public ObservableCollection<PolylineControl.Polyline> polylines { get; set; }
public ViewModel()
{
polylines = new ObservableCollection<PolylineControl.Polyline>();
}
}
**The PolylineControl:
Polyline.cs:**
Contains DP's for an ObservableCollection of points for the polyline. Eventually this will also contain the interpolated points as well as the input points, but a single collection of points will do for the demo. I did try to use the INotifyPropertyChanged interface to no avail.
public class Polyline : Control
{
public static readonly DependencyProperty PolylinePointsProperty =
DependencyProperty.Register("PolylinePoints", typeof(ObservableCollection<Point>), typeof(Polyline),
new FrameworkPropertyMetadata(new ObservableCollection<Point>()));
public ObservableCollection<Point> PolylinePoints
{
get { return (ObservableCollection<Point>)GetValue(PolylinePointsProperty); }
set { SetValue(PolylinePointsProperty, value); }
}
static Polyline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Polyline), new FrameworkPropertyMetadata(typeof(Polyline)));
}
}
Generic.xaml
Contains a canvas with a databound Polyline, and an ItemsControl with a DataTemplate for the ThumbPoint control.
<local:PointCollectionConverter x:Key="PointsConverter"/>
<Style TargetType="{x:Type local:Polyline}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Polyline}">
<Canvas Background="Transparent">
<Polyline x:Name="PART_Polyline"
Stroke="Black"
StrokeThickness="2"
Points="{Binding Path=PolylinePoints,
RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource PointsConverter}}"
>
</Polyline>
<ItemsControl x:Name="thumbPoints"
ItemsSource="{Binding PolylinePoints, RelativeSource={RelativeSource TemplatedParent}}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<tc:ThumbPoint Point="{Binding Path=., Mode=TwoWay}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
PointsCollectionConverter.cs:
Contains a IValueConverter to turn the ObservableCollection into a PointsCollection.
public class PointCollectionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.GetType() == typeof(ObservableCollection<Point>) && targetType == typeof(PointCollection))
{
var pointCollection = new PointCollection();
foreach (var point in value as ObservableCollection<Point>)
{
pointCollection.Add(point);
}
return pointCollection;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
And finally, the ThumbPointControl:
ThumbPoint.cs:
Contains a single DP for the center of the point, along with the DragDelta functionality.
public class ThumbPoint : Thumb
{
public static readonly DependencyProperty PointProperty =
DependencyProperty.Register("Point", typeof(Point), typeof(ThumbPoint),
new FrameworkPropertyMetadata(new Point()));
public Point Point
{
get { return (Point)GetValue(PointProperty); }
set { SetValue(PointProperty, value); }
}
static ThumbPoint()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ThumbPoint), new FrameworkPropertyMetadata(typeof(ThumbPoint)));
}
public ThumbPoint()
{
this.DragDelta += new DragDeltaEventHandler(this.OnDragDelta);
}
private void OnDragDelta(object sender, DragDeltaEventArgs e)
{
this.Point = new Point(this.Point.X + e.HorizontalChange, this.Point.Y + e.VerticalChange);
}
}
Generic.xaml:
Contains the style, and an Ellipse bound which is databound.
<Style TargetType="{x:Type local:ThumbPoint}">
<Setter Property="Width" Value="8"/>
<Setter Property="Height" Value="8"/>
<Setter Property="Margin" Value="-4"/>
<Setter Property="Background" Value="Gray" />
<Setter Property="Canvas.Left" Value="{Binding Path=Point.X, RelativeSource={RelativeSource Self}}" />
<Setter Property="Canvas.Top" Value="{Binding Path=Point.Y, RelativeSource={RelativeSource Self}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ThumbPoint}">
<Ellipse x:Name="PART_Ellipse"
Fill="{TemplateBinding Background}"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Window after the Add Polyline menu item is pressed
The code works to add the polyline with three random points.
Thumbs moved away from poly line
However, once you move the thumbs, the polyline does not update along with it.
I have a working example of just a single line segment (added to the view model as many times as you click the add segment button) so it seems the logic should all be correct, but something broke down with the introduction of the ObservableCollection to host the multiple points required for a polyline.
Any help is appreciated
Following on from Clemens suggestions, I was able to make it work.
I renamed the Polyline.cs control to eliminate confusion with the standard WPF Polyline Shape class to DynamicPolyline. The class now implements INotifyPropertyChanged, and has DP for the PolylinePoints and a seperate ObservableCollection for a NotifyingPoint class which also implements INotifyPropertyChanged. When DynamicPolyline is initialized, it hooks the CollectionChanged event on the ObserableCollection. The event handler method then either adds an event handler to each item in the collection, or removes it based on the action. The event handler for each item simply calls SetPolyline, which in turn cycles through the InputPoints adding them to a new PointCollection, and then sets the Points property on the PART_Polyline (which a reference to is created in the OnApplyTemplate method).
It turns out the Points property on a Polyline does not listen to the INotifyPropertyChanged interface, so data binding in the Xaml was not possible. Probably will end up using a PathGeometery in the future, but for now, this works.
To address Marks non MVVM concerns.. It's a demo app, sorry I had some code to test things in the code behind. The point is to be able to reuse these controls, and group them with others for various use cases, so it makes more sense for them to be on their own vs repeating the code.
DynmicPolyline.cs:
public class DynamicPolyline : Control, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
}
public static readonly DependencyProperty PolylinePointsProperty =
DependencyProperty.Register("PoilylinePoints", typeof(PointCollection), typeof(DynamicPolyline),
new PropertyMetadata(new PointCollection()));
public PointCollection PolylinePoints
{
get { return (PointCollection)GetValue(PolylinePointsProperty); }
set { SetValue(PolylinePointsProperty, value); }
}
private ObservableCollection<NotifyingPoint> _inputPoints;
public ObservableCollection<NotifyingPoint> InputPoints
{
get { return _inputPoints; }
set
{
_inputPoints = value;
OnPropertyChanged();
}
}
private void SetPolyline()
{
if (polyLine != null && InputPoints.Count >= 2)
{
var newCollection = new PointCollection();
foreach (var point in InputPoints)
{
newCollection.Add(new Point(point.X, point.Y));
}
polyLine.Points = newCollection;
}
}
private void InputPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
var point = item as NotifyingPoint;
point.PropertyChanged += InputPoints_PropertyChange;
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems)
{
var point = item as NotifyingPoint;
point.PropertyChanged -= InputPoints_PropertyChange;
}
}
}
private void InputPoints_PropertyChange(object sender, PropertyChangedEventArgs e)
{
SetPolyline();
}
public DynamicPolyline()
{
InputPoints = new ObservableCollection<NotifyingPoint>();
InputPoints.CollectionChanged += InputPoints_CollectionChanged;
SetPolyline();
}
static DynamicPolyline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DynamicPolyline), new FrameworkPropertyMetadata(typeof(DynamicPolyline)));
}
private Polyline polyLine;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
polyLine = this.Template.FindName("PART_Polyline", this) as Polyline;
}
NotifyingPoint.cs
Simple class that raises property changed events when X or Y is updated from the databound ThumbPoint.
public class NotifyingPoint : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
}
public event EventHandler ValueChanged;
private double _x = 0.0;
public double X
{
get { return _x; }
set
{
_x = value;
OnPropertyChanged();
ValueChanged?.Invoke(this, null);
}
}
private double _y = 0.0;
public double Y
{
get { return _y; }
set
{
_y = value;
OnPropertyChanged();
}
}
public NotifyingPoint()
{
}
public NotifyingPoint(double x, double y)
{
X = x;
Y = y;
}
public Point ToPoint()
{
return new Point(_x, _y);
}
}
And finally, for completeness, here is the Generic.xaml for the control. Only change in here was the bindings for X and Y of the NotifyingPoint.
<Style TargetType="{x:Type local:DynamicPolyline}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:DynamicPolyline}">
<Canvas x:Name="PART_Canvas">
<Polyline x:Name="PART_Polyline"
Stroke="Black"
StrokeThickness="2"
/>
<ItemsControl x:Name="PART_ThumbPointItemsControl"
ItemsSource="{Binding Path=InputPoints, RelativeSource={RelativeSource TemplatedParent}}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<tc:ThumbPoint X="{Binding Path=X, Mode=TwoWay}" Y="{Binding Path=Y, Mode=TwoWay}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I dropped my Spline class in to the SetPolyline method, and got the result I was after:
Two working click and drag able spline curves
I'm attemping to study from Material Design for XAML source code.
Here is their GitHub:
MaterialDesignInXamlToolkit
Here is the the code I'm looking into:
PackIcon
Here is the helper class for PackIcon:
PackIconBase
PackIconDataFactory
Currently, I'm looking at their icon pack example and doing a quick test on it.
Here is the test class:
public class PackIconTest : Control
{
static PackIconTest()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(PackIconTest), new FrameworkPropertyMetadata(typeof(PackIconTest)));
}
public PackIconTest()
{
Data = "M4.93,4.93C3.12,6.74 2,9.24 2,12C2,14.76 3.12,17.26 4.93,19.07L6.34,17.66C4.89,16.22 4,14.22 4,12C4,9.79 4.89,7.78 6.34,6.34L4.93,4.93M19.07,4.93L17.66,6.34C19.11,7.78 20,9.79 20,12C20,14.22 19.11,16.22 17.66,17.66L19.07,19.07C20.88,17.26 22,14.76 22,12C22,9.24 20.88,6.74 19.07,4.93M7.76,7.76C6.67,8.85 6,10.35 6,12C6,13.65 6.67,15.15 7.76,16.24L9.17,14.83C8.45,14.11 8,13.11 8,12C8,10.89 8.45,9.89 9.17,9.17L7.76,7.76M16.24,7.76L14.83,9.17C15.55,9.89 16,10.89 16,12C16,13.11 15.55,14.11 14.83,14.83L16.24,16.24C17.33,15.15 18,13.65 18,12C18,10.35 17.33,8.85 16.24,7.76M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z";
}
private static readonly DependencyPropertyKey DataPropertyKey =
DependencyProperty.RegisterReadOnly(nameof(Data), typeof(string), typeof(PackIconTest), new PropertyMetadata(""));
public static readonly DependencyProperty DataProperty = DataPropertyKey.DependencyProperty;
[TypeConverter(typeof(GeometryConverter))]
public string Data
{
get { return (string)GetValue(DataProperty); }
private set { SetValue(DataPropertyKey, value); }
}
}
Here is the XAML usage:
<local:PackIconTest Width="200" Height="200"/>
The icon doesn't show. What am I missing?
I solved this propblem by making a style applying for PackIconTest target type.
Here is the xaml code:
<Window.Resources>
<Style TargetType="local:PackIconTest">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Path Data="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Data}"
Stroke="Black"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
I have two custom controls/visuals and I need an Orientation property on both. In both cases, the default should be "Horizontal" but the user of the control/visual should be able to specify Orientation="Vertical" to arrange the components of the control/visual vertically. What I have works great on my ImageButton control, but not so well on my HeaderedLabel visual. Although both of them compile fine, Intellisense doesn't like one of them. Here's an example of their use...
<Visuals:ImageButton Image="Icons/ok.png" Content="Normal Content"/>
<Visuals:ImageButton Image="Icons/ok.png" Content="Vertical Content" Orientation="Vertical"/>
<Visuals:HeaderedLabel Header="Normal Header" Content="Normal Content"/>
<Visuals:HeaderedLabel Header="Vertical Header" Content="Vertical Content" Orientation="Vertical"/>
...which produces the following when rendered inside a vertical StackPanel:
So it does what I want, but the problem is this: While Intellisense recognizes the possible options for Orientation for the ImageButton, it does not recognize the possible options for Orientation for the HeaderedLabel. And while the code compiles & runs fine, there's a persistent error in the Visual Studio "Error List" pane: "'Vertical' is not a valid value for property 'Orientation'.", and there's a blue squiggly line under the text Orientation="Vertical" for the second HeaderedLabel in my xaml example above.
Here are the relevant files:
// File 'ImageButton.cs'
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Visuals
{
public class ImageButton : Button
{
static ImageButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ImageButton),
new FrameworkPropertyMetadata(typeof(ImageButton)));
}
public ImageSource Image
{
get { return (ImageSource)GetValue(ImageProperty); }
set { SetValue(ImageProperty, value); }
}
public Orientation Orientation
{
get { return (Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
// Note that for ImageButton, I can just say 'Orientation.Horizontal' and
// the compiler resolves that to System.Windows.Controls.Orientation...
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register("Orientation", typeof(Orientation),
typeof(ImageButton), new UIPropertyMetadata(Orientation.Horizontal));
public static readonly DependencyProperty ImageProperty =
DependencyProperty.Register("Image", typeof(ImageSource),
typeof(ImageButton), new UIPropertyMetadata(null));
}
}
.
// File 'HeaderedLabel.cs'
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Visuals
{
public class HeaderedLabel : Control
{
static HeaderedLabel()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(HeaderedLabel),
new FrameworkPropertyMetadata(typeof(HeaderedLabel)));
}
public object Header
{
get { return (object)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
public object Content
{
get { return (object)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public object Orientation
{
get { return (object)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(object), typeof(HeaderedLabel),
new UIPropertyMetadata(null));
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(object), typeof(HeaderedLabel),
new UIPropertyMetadata(null));
// Note that for HeaderedLabel, unlike ImageButton, I have to specify the fully-
// qualified name of 'Orientation.Horizontal', otherwise the compiler resolves it
// to Visuals.HeaderedLabel.Orientation and gives a compiler error...
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register("Orientation", typeof(System.Windows.Controls.Orientation),
typeof(HeaderedLabel), new UIPropertyMetadata(System.Windows.Controls.Orientation.Horizontal));
}
}
.
<!-- File 'Generic.xaml' -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Visuals"
xmlns:bind="clr-namespace:Visuals.BindingConverters">
<Style TargetType="{x:Type local:ImageButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ImageButton}">
<Button>
<StackPanel Orientation="{TemplateBinding Orientation}">
<Image Source="{TemplateBinding Image}"/>
<ContentPresenter/>
</StackPanel>
</Button>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type local:HeaderedLabel}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:HeaderedLabel}">
<StackPanel Orientation="{TemplateBinding Orientation}">
<StackPanel Orientation="Horizontal">
<ContentControl Content="{TemplateBinding Header}" />
<TextBlock Text=":" />
</StackPanel>
<ContentControl Content="{TemplateBinding Content}"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Any ideas why the compiler would resolve Orientation.Horizontal to System.Windows.Controls.Orientation.Horizontal for the ImageButton, but not for the HeaderedLabel? And more importantly, any ideas why Intellisense can't figure out the options for HeaderedLabel.Orientation?
BTW, I'm using VisualStudio 2012 and .NET Framework 4.0.
All of your properties, including the Orientation property, are declared as having the type object.
You should have this instead:
public Orientation Orientation
{
get { return (Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
The XAML editor should be able to correctly accept values of type Orientation if you declare the property correctly. Otherwise, it will attempt to assign a string value of "Vertical" to the property, which when passed to the SetValue() method will fail, because the DependencyProperty object itself was initialized with Orientation as the valid type and it has no way to convert from the string value to an Orientation value.
If you declare the property correctly, then WPF will understand automatically that it needs to convert the string value shown in the XAML to an Orientation value for the property (i.e. parse the string value as the appropriate enum type), and in that case the initialization should work.
I have a label control where I use a converter to switch its styles based on a bool property, IsCheckedOut on my viewmodel, like so:
<UserControl.Resources>
<Converters:BooleanStyleConverter
x:Key="BooleanStyleConverter "
StyleFalse="{StaticResource HeaderLabelStyle}"
StyleTrue="{StaticResource HeaderLabelHighlightedStyle}" />
</UserControl.Resources>
<Label Style="{Binding Path=IsCheckedOut,
Converter={StaticResource BooleanStyleConverter}}">
some content here
</Label>
The converter simply returns one of the two styles:
public class BooleanStyleConverter : IValueConverter
{
public Style StyleFalse { get; set; }
public Style StyleTrue { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((bool)value)
{
return StyleTrue;
}
return StyleFalse;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And the styles look something like this:
<Style x:Key="HeaderLabelHighlightedStyle" TargetType="{x:Type Label}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<Border Background="{StaticResource RedGradient}">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="HeaderLabelHighlightedStyle" TargetType="{x:Type Label}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<Border Background="{StaticResource BlueGradient}">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
So when IsCheckedOut is true the label gets a red background, and when it's false it gets a blue background (well, the styles are a bit more complicated, but you get the idea).
Now, I'd like to have a transition between the styles, so that the new colors fade in when IsCheckedOut changes.
Does anyone know how I can accomplish this?
Sorry, but you're doing it wrong.
You do get bonus points for being extremely creative and ambitious in your solution. But you've taken the proverbial 5kg hammer down on a thumbtack.
The correct solution in this situation is to use Storyboards nested in VSM States.
It looks like you essentially have 2 States for your UI: One where some business logic value is true and another state for when it's false. Note that the aforementioned distinction is 100% technology independent. In any technology, whatever it is you're trying to achieve would be considered 2 states for your UI.
In Silverlight/WPF, instead of hacking together something that mimics UI states, you could actually create VisualStateManager states.
Technically it would work in the following way:
1. Your UserControl would have 1 VisualStateGroup that has 2 VisualStates (one for true and another for false).
2. Those VSM states each represent 1 storyboard.
3. That storyboard would change the template or any other properties you feel are appropriate.
To learn the basics of VSM I strongly suggest you spend the next 30 minutes watching the following VSM videos: http://expression.microsoft.com/en-us/cc643423.aspx (Under "How Do I?")
Seriously, these videos are phenomenally successful in explaining VSM. The one that most pertinent to your dilemma is "Add States to a Control" but I'll suggest you watch all of them.
In WPF, you could use the VisualStateManager from the WPF Toolkit.
As Justin said, you are doing something wrong, and you might want to do five steps back and reconsider your approach...
But I really liked this puzzle :). I've solved it without VSM, just to demonstrate how flexible WPF is. Basic principle here is using Dependency properties value coercion. We track all style changes, but we don't let the new value go out from Coerce() function, until we complete transition animation.
To simplify your testing, just copy/paste the following code and check if it works for you :). If you want to get into details - feel free to ask additional questions.
XAML:
<Window x:Class="WpfApplication5.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:WpfApplication5"
Title="Fade Styles"
Width="320"
Height="240">
<Window.Resources>
<SolidColorBrush x:Key="RedGradient" Color="Red" />
<SolidColorBrush x:Key="BlueGradient" Color="Blue" />
<Style x:Key="HeaderLabelStyle" TargetType="{x:Type Label}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<Border Background="{StaticResource RedGradient}">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="HeaderLabelHighlightedStyle" TargetType="{x:Type Label}">
<Setter Property="Foreground" Value="White" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<Border Background="{StaticResource BlueGradient}">
<ContentPresenter />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<loc:BooleanStyleConverter x:Key="BooleanStyleConverter"
StyleFalse="{StaticResource HeaderLabelStyle}"
StyleTrue="{StaticResource HeaderLabelHighlightedStyle}" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Style="{Binding IsChecked, ElementName=chkToggle, Converter={StaticResource BooleanStyleConverter}}"
loc:StyleAnimation.IsActive="True"
Content="Some content here" />
<CheckBox Grid.Row="1" Name="chkToggle" Content="Use Blue" />
</Grid>
</Window>
Take a look here on loc:StyleAnimation.IsActive="True".
C#
using System;
using System.Collections;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media.Animation;
namespace WpfApplication5
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
}
public class StyleAnimation : DependencyObject
{
private const int DURATION_MS = 200;
private static readonly Hashtable _hookedElements = new Hashtable();
public static readonly DependencyProperty IsActiveProperty =
DependencyProperty.RegisterAttached("IsActive",
typeof(bool),
typeof(StyleAnimation),
new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsActivePropertyChanged)));
public static bool GetIsActive(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (bool)element.GetValue(IsActiveProperty);
}
public static void SetIsActive(UIElement element, bool value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(IsActiveProperty, value);
}
static StyleAnimation()
{
// You can specify any owner type, derived from FrameworkElement.
// For example if you want to animate style for every Control derived
// class - use Control. If Label is your single target - set it to label.
// But be aware: value coercion will be called every time your style is
// updated. So if you have performance problems, probably you should
// narrow owner type to your exact type.
FrameworkElement.StyleProperty.AddOwner(typeof(Control),
new FrameworkPropertyMetadata(
null, new PropertyChangedCallback(StyleChanged), CoerceStyle));
}
private static object CoerceStyle(DependencyObject d, object baseValue)
{
var c = d as Control;
if (c == null || c.Style == baseValue)
{
return baseValue;
}
if (CheckAndUpdateAnimationStartedFlag(c))
{
return baseValue;
}
// If we get here, it means we have to start our animation loop:
// 1. Hide control with old style.
// 2. When done set control's style to new one. This will reenter to this
// function, but will return baseValue, since CheckAndUpdateAnimationStartedFlag()
// will be true.
// 3. Show control with new style.
var showAnimation = new DoubleAnimation
{
Duration =
new Duration(TimeSpan.FromMilliseconds(DURATION_MS)),
To = 1
};
var hideAnimation = new DoubleAnimation
{
Duration = new Duration(TimeSpan.FromMilliseconds(DURATION_MS)),
To = 0
};
hideAnimation.Completed += (o, e) =>
{
// To stress it one more: this will trigger value coercion again,
// but CheckAndUpdateAnimationStartedFlag() function will reture true
// this time, and we will not go to this loop again.
c.CoerceValue(FrameworkElement.StyleProperty);
c.BeginAnimation(UIElement.OpacityProperty, showAnimation);
};
c.BeginAnimation(UIElement.OpacityProperty, hideAnimation);
return c.Style; // Return old style this time.
}
private static void StyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// So what? Do nothing.
}
private static void OnIsActivePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var fe = d as FrameworkElement;
if (fe == null)
{
return;
}
if (GetIsActive(fe))
{
HookStyleChanges(fe);
}
else
{
UnHookStyleChanges(fe);
}
}
private static void UnHookStyleChanges(FrameworkElement fe)
{
if (_hookedElements.Contains(fe))
{
_hookedElements.Remove(fe);
}
}
private static void HookStyleChanges(FrameworkElement fe)
{
_hookedElements.Add(fe, false);
}
private static bool CheckAndUpdateAnimationStartedFlag(Control c)
{
var hookedElement = _hookedElements.Contains(c);
if (!hookedElement)
{
return true; // don't need to animate unhooked elements.
}
var animationStarted = (bool)_hookedElements[c];
_hookedElements[c] = !animationStarted;
return animationStarted;
}
}
public class BooleanStyleConverter : IValueConverter
{
public Style StyleFalse { get; set; }
public Style StyleTrue { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((bool)value)
{
return StyleTrue;
}
return StyleFalse;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Cheers :)
I too got a solution becuase I as well liked your problem :)
I solved the puzzle using ColorAnimations on the gradient. Have a look:
<Window x:Class="WpfTest___App.DoEvents.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" MinHeight="300" MinWidth="300"
Name="Window">
<Grid DataContext="{Binding ElementName=Window}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Label Content="Some content here" Grid.Row="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<Label.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="Blue" Offset="0"/>
<GradientStop Color="LightBlue" Offset="0.4"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</Label.Background>
<Label.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsCheckedOut}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="Background.GradientStops[0].Color"
To="Red" Duration="0:0:5"/>
<ColorAnimation Storyboard.TargetProperty="Background.GradientStops[1].Color"
To="Orange" Duration="0:0:5"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="Background.GradientStops[0].Color"
To="Blue" Duration="0:0:5"/>
<ColorAnimation Storyboard.TargetProperty="Background.GradientStops[1].Color"
To="LightBlue" Duration="0:0:5"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<Button Content="Change IsCheckedOut" Click="Button_Click" Grid.Row="1"/>
</Grid>
In the code behind I created a Dependency property for testing and a listener to the Click event of the button:
public partial class Window1 : Window
{
public bool IsCheckedOut
{
get { return (bool)GetValue(IsCheckedOutProperty); }
set { SetValue(IsCheckedOutProperty, value); }
}
public static readonly DependencyProperty IsCheckedOutProperty = DependencyProperty.Register("IsCheckedOut", typeof(bool), typeof(Window1), new PropertyMetadata(false));
public Window1()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
IsCheckedOut = !IsCheckedOut;
}
}
This should solve your problem as well :)