Usercontrol - notify property change from ViewModel - c#

I'm trying to implement custom Border, which binds to bool property in ViewModel, and whenever this property changes I want to do some animation with Border.
ViewModel property has OnPropertyChanged interface, It looks like this:
public bool Enable_control
{
get { return _enable_ctl; }
set { _enable_ctl = value; OnPropertyChanged(); }
}
private bool _enable_ctl;
This is how I bind custom Border in xaml:
<cust_border:Border_slide ShowSlide={Binding Enable_control}"/>
And this is my custom border control code:
public class Border_slide : Border
{
public Border_slide()
{
}
public bool ShowSlide
{
get { return (bool)GetValue(ShowSlideProperty); }
set { SetValue(ShowSlideProperty, value);}
}
public static readonly DependencyProperty ShowSlideProperty =
DependencyProperty.Register("ShowSlide", typeof(bool), typeof(Border_slide), new
FrameworkPropertyMetadata(true, new PropertyChangedCallback(ShowSlideChanged)));
private static void ShowSlideChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//I would like to do animation of control when property of ViewModel changes - presumably here,
//but PropertyChangedCallback gets triggered only once - not on every Enable_control change!
}
}
So, question is: how do you properly update UserControl's dependency property from Viewmodel property change, in order to do something with UserControl next?

There was a time when I did something similar to yours.
In my case, I hope it will help since I solved it this way. But it can be different from what you think, so please let me know if there's anything I missed!
And I have uploaded my sample source code to GitHub.
Here sample sourcecode. https://github.com/ncoresoftsource/stackoverflowsample/tree/main/src/answers/dependency-border-animation
ViewModel
public class MainViewModel : ObservableObject
{
private bool _enable_ctl;
public bool Enable_control
{
get { return _enable_ctl; }
set { _enable_ctl = value; OnPropertyChanged(); }
}
}
MainWindow
I used Label instead of Border. Because Border cannot use ControlTemplate, it can be constrained to use and expand animation.
<CheckBox Content="Switch" IsChecked="{Binding Enable_control}"/>
<Label Style="{StaticResource SLIDER}"/>
App.xaml
<Style TargetType="Label" x:Key="SLIDER">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="#AAAAAA"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="100"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<Border x:Name="border" Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Opacity="0">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Enable_control}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0" To="1" Duration="00:00:0.5"
Storyboard.TargetName="border"
Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="1" To="0" Duration="00:00:0.5"
Storyboard.TargetName="border"
Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Solved. Clemens was partially right - nothing was wrong with my code, property did get notified with changes. My problems were elsewhere - with a variable that didn't get value before property changed, and default value of dependency property. I sorted out those things now, and It works as expected.
Here is my full code, might get useful to someone, at least I find It so:
It's a custom Border, which can be used for a sliding animation of left Margin. Meaning you can use this Border to slide some menues to View from left side to desired right location, just add controls inside It and you're ready to go. Code can be used for other controls too (Panel etc.), but I used Border since I can round corners or do other nice UI presentations with it easily.
Logic behind is that If you set MoveTo property, Border will slide to desired left Margin location. If you don't, then Border will slide to center of screen - meaning you can use this Border for sliding menues into Views that are centered or left aligned in yout MainWindow. That was actually whole point of creating It - because you cannot use dynamic values in animation To or From, since these are freezables. So I had to create this custom control, because I couldn't do an animation to screen center (width needs to be calculated first). Anyway, here It is:
public class Border_slide : Border
{
public Border_slide()
{
Loaded += Border_slide_Loaded;
}
private void Border_slide_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
//Save starting Margin.Left
StartX = Margin.Left;
}
///<summary>Property for starting X location</summary>
private double StartX
{
get { return _startX; }
set { _startX = value; }
}
private double _startX;
public static DependencyProperty MoveToProperty =
DependencyProperty.Register("MoveTo", typeof(double), typeof(Border_slide), new PropertyMetadata(default(double)));
///<summary>Property thats sets to what Margin.Left we want move Border</summary>
public double MoveTo
{
get { return (double)GetValue(MoveToProperty); }
set { SetValue(MoveToProperty, value); }
}
public bool ShowSlide
{
get { return (bool)GetValue(ShowSlideProperty); }
set { SetValue(ShowSlideProperty, value); }
}
public static readonly DependencyProperty ShowSlideProperty =
DependencyProperty.Register("ShowSlide", typeof(bool), typeof(Border_slide), new FrameworkPropertyMetadata(true,
new PropertyChangedCallback(ShowSlideChanged)));
private static void ShowSlideChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var brd = d as Border_slide;
//Animate Margin, when Property changes
if (((bool)e.NewValue) == true) //When property is true, Border is allready at desired location, so we move It to first x- location
{
ThicknessAnimation back_to_start_X_location = new ThicknessAnimation
{
BeginTime = TimeSpan.FromSeconds(0),
Duration = TimeSpan.FromSeconds(1),
From = new Thickness(brd.Margin.Left, 0, 0, 0),
To = new Thickness(brd.StartX, 0, 0, 0)
};
brd.BeginAnimation(Border.MarginProperty, back_to_start_X_location);
}
else //If property is False we need to move Border to desired X- location
{
//If we don't set MoveTo property then move Border to center of screen
if (brd.MoveTo == default(double))
{
var X_center = Application.Current.MainWindow.ActualWidth / 2 - brd.Width / 2;
ThicknessAnimation to_center_X_location = new ThicknessAnimation
{
BeginTime = TimeSpan.FromSeconds(0),
Duration = TimeSpan.FromSeconds(1),
From = new Thickness(brd.ZacetniX, 0, 0, 0),
To = new Thickness(X_center, 0, 0, 0)
};
brd.BeginAnimation(Border.MarginProperty, to_center_X_location);
}
else //If MoveTo property is set then move Border to desired X-location
{
ThicknessAnimation to_desired_X_location = new ThicknessAnimation
{
BeginTime = TimeSpan.FromSeconds(0),
Duration = TimeSpan.FromSeconds(1),
From = new Thickness(brd.StartX, 0, 0, 0),
To = new Thickness(brd.MoveTo, 0, 0, 0)
};
brd.BeginAnimation(Border.MarginProperty, to_desired_X_location);
}
}
}
}
So, as you see - Border moves depending on what you set as start Margin.Left (that is a must) and/or MoveTo property, with binding to some bool value from ViewModel. In my case, I slide this Border when I disable all my other controls in View - so that I can make blur effect on View too. If you will use It for different things, than be careful about default value of ShowSlide property (mine is true). And surely you can re-adjust control for different things too...
E.g. usage in XAML:
<my_controls:Border_slide Margin="-500,0,0,0" MoveTo="5" ShowSlide="{Binding Enable_control}">

Related

c# uwp tooltip placement property not updating

In C# UWP I am creating custom tooltip style.
I have changed the default style of tooltip as below.
<Style TargetType="ToolTip">
<Setter Property="Foreground" Value="White" />
<Setter Property="Background" Value="{ThemeResource SystemControlBackgroundChromeMediumLowBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource SystemControlForegroundChromeHighBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource ToolTipBorderThemeThickness}" />
<Setter Property="FontSize" Value="{ThemeResource ToolTipContentThemeFontSize}" />
<Setter Property="Padding" Value="40,40,40,35"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Grid Background="Transparent">
<Grid
MinWidth="100"
MinHeight="90"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Padding="15"
Background="Transparent">
<local:ArrowDown x:Name="arrowDown" TooltipPlacement="{TemplateBinding Placement}"/>
And my custom control ArrowDown is getting information of ToolTip placement, so I can show it depends if tooltip is under or above control.
In the ArrowDown control I have added a DependencyProperty as below:
public PlacementMode TooltipPlacement
{
get { return (PlacementMode)GetValue(TooltipPlacementProperty); }
set { SetValue(TooltipPlacementProperty, value); }
}
public static readonly DependencyProperty TooltipPlacementProperty =
DependencyProperty.Register("TooltipPlacement", typeof(PlacementMode), typeof(ArrowDown), new PropertyMetadata(null, TooltipPlacementChangedCallback));
private static void TooltipPlacementChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = (ArrowDown)d;
self.CalculateArrowVisibility();
}
// Method to show or hide arrow
private void CalculateArrowVisibility()
{
}
And the problem is that the CalculateArrowVisibility is fire only the first time when tooltip is shown, and it always returns Top for TooltipPlacement, no matter if tooltip is shown below or above control.
I need CalculateArrowVisibility to be fired whenever the tooltip is shown, and I need TooltipPlacement property to show if tooltip is Under or Above control.
Anyone have the idea about this?
The fact is that you cannot use the ToolTipService attached properties (e.g. <Button ToolTipService.Placement="Bottom" ToolTipService.ToolTip="!!!" />) to define the tooltip and it placement. This way the Placement is not set on the actual ToolTip control itself, and that's why it will always return Top.
In order to have the ToolTip pass down its Placement value to your custom dependency property, you will have to attach it like the following -
<Button>
<ToolTipService.ToolTip>
<ToolTip Placement="Bottom" Content="Hahaha..." />
</ToolTipService.ToolTip>
</Button>
Update
Turns out that even though the app Window pushes the tooltip above or below its parent, its Placement value is never changed, what's changed is its horizontal & vertical offsets.
So, in your case, if we could work out its exact vertical offset, we would be able to determine whether the tooltip is above or below (its parent).
Given we have a ToolTip Style in place, we can create an attached property of type ToolTip and attach it to the Grid that contains the ArrowDown control.
<Grid MinWidth="100"
MinHeight="90"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Padding="15"
Background="Transparent"
local:ToolTipHelper.ToolTip="{Binding RelativeSource={RelativeSource TemplatedParent}}">
Because the TemplatedParent of the Grid is the ToolTip, we can use RelativeSource binding to link the ToolTip on the screen with our attached property, as shown above.
Now, we have a reference to the actual ToolTip, let's find its offsets. After some digging, I've found that the offsets of the ToolTip are always 0, they are useless; however, the offsets of its parent - a Popup, sometimes gives me the correct values, but not always. This is because I was using the Opened event where those values weren't yet populated; as soon as I changed it to SizeChanged, they have been giving me the expected values.
public static class ToolTipHelper
{
public static ToolTip GetToolTip(DependencyObject obj)
{
return (ToolTip)obj.GetValue(ToolTipProperty);
}
public static void SetToolTip(DependencyObject obj, ToolTip value)
{
obj.SetValue(ToolTipProperty, value);
}
public static readonly DependencyProperty ToolTipProperty =
DependencyProperty.RegisterAttached("ToolTip", typeof(ToolTip), typeof(ToolTipHelper),
new PropertyMetadata(null, (s, e) =>
{
var panel = (Panel)s; // The Grid that contains the ArrowDown control.
var toolTip = (ToolTip)e.NewValue;
// We need to monitor SizeChanged instead of Opened 'cause the offsets
// are yet to be properly set in the latter event.
toolTip.SizeChanged += (sender, args) =>
{
var popup = (Popup)toolTip.Parent; // The Popup that contains the ToolTip.
// Note we have to use the Popup's offset here as the ToolTip's are always 0.
var arrowDown = (ArrowDown)panel.FindName("arrowDown");
arrowDown.TooltipPlacement = popup.VerticalOffset > 0
? PlacementMode.Bottom
: PlacementMode.Top;
};
}));
}
Now, with this approach, you should be able to use the ToolTipService attached properties too. So the following XAML would work.
<Button ToolTipService.ToolTip="!!!" Content="Hover Me" />
Hope this helps!

Alerting User By Changing Background of Control C#

I'm programming in WPF(C#). I want to alert users about filling empty text box (or any other controls). I want to flash control to alert him/her. This is the codes that I used them but it does not change color:
static void AlertByChangingBackground(Control control)
{
Action a = () =>
{
control.Background = System.Windows.Media.Brushes.Red;
Thread.Sleep(500);
control.Background = System.Windows.Media.Brushes.White;
};
control.Dispatcher.Invoke(a);
}
As it can be seen, I also use Action but it does not work. I also use control.UpdateLayout() before Sleep method but it does not working, too. How can I fix the problem.
Update 1:
Now, I use codes illustrated below. But the problem is when the function is called several times (specially when it is called continuously in short times) the color of text does not back to its first color. For example my control may be remain at red color. How can I fix it?
public static void AlertByChangingBackground(Control control)
{
Action a = () =>
{
ColorAnimation animation;
animation = new ColorAnimation();
animation.From = Colors.Red;
animation.To = ToColor(control.Background);
animation.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 330));
RepeatBehavior rb = new RepeatBehavior(3);
animation.RepeatBehavior = rb;
control.Background = new SolidColorBrush(Colors.Red);
control.Background.BeginAnimation(SolidColorBrush.ColorProperty, animation);
};
control.Dispatcher.BeginInvoke(a);
}
I note that I want to start animation from current background of my control, not from white or any predefined color.
You can use triggers or animations to alert the user rather than using thread,
you can add xmlns:sys="clr-namespace:System;assembly=mscorlib" namespace for checking the string is empty or not.
<Style TargetType="{x:Type TextBox}" x:Key="AlertStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding Text,RelativeSource={RelativeSource Mode=Self}}" Value="{x:Static sys:String.Empty}">
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
use this style for your TextBox control like follows,
<TextBox Width="100" Height="25" Style="{StaticResource AlertStyle}">

Run two Commands and Animation on Button Click

In my app, I have a Left Grid and a Right Grid. Initially, the Left Grid is fully expanded (GridLength = 7*) while the Right Grid is not visible (Width = 0*).
If a Button in the LeftGrid is clicked and the Right Grid is NOT expanded, the Right Grid should expand to a Width of 3*.
If the Right Grid is expanded and a Button in the LeftGrid is clicked twice successively, then the RightGrid should shrink back to a Width of 0*.
These expansions/contractions should be Animated.
When a Button is clicked, three things must happen.
1) The id of the selected Button should be stored in a Property in the ViewModel.
2) The Width to which the RightGrid will be set, is stored in a Property in the ViewModel. This Command takes care of the two successive click case.
3) Finally, the Animation should run.
I am experiencing a few issues:
1) How do I bind the two Commands to a single Button?
<Button DockPanel.Dock="Top"
Command="{Binding DataContext.SetSelectedItemCommand,
RelativeSource={RelativeSource
AncestorType={x:Type ItemsControl}}}"
CommandParameter="{Binding id}"
Command="{Binding ElementName=root, Path=DataContext.ChangeRightGridWidthCommand}"
>
This is not allowed.
I've been looking at this article (http://www.codeproject.com/Articles/25808/Aggregating-WPF-Commands-with-CommandGroup).
However, I'm not certain if this will work for me as I am defining my Commands in my ViewModel.
2) I am using a Custom GridLength Animation class as I need to use * for my Widths.
public class GridLengthAnimation : AnimationTimeline
{
public static readonly DependencyProperty ToProperty =
DependencyProperty.Register(
"To", typeof(GridLength), typeof(GridLengthAnimation));
public GridLength To
{
get { return (GridLength)GetValue(ToProperty); }
set { SetValue(ToProperty, value); }
}
public override Type TargetPropertyType
{
get { return typeof(GridLength); }
}
protected override Freezable CreateInstanceCore()
{
return new GridLengthAnimation();
}
public override object GetCurrentValue(
object defaultOriginValue, object defaultDestinationValue,
AnimationClock animationClock)
{
return new GridLength(To.Value,
GridUnitType.Star);
}
}
I plan on using something like this in my XAML:
<proj:GridLengthAnimation
Storyboard.TargetName="rightGrid"
Storyboard.TargetProperty="Width"
To="RightGridWidth" Duration="0:0:2" />
<Grid.ColumnDefinitions>
<ColumnDefinition Name="leftGrid" Width="7*"/>
<ColumnDefinition Name="rightGrid" Width="{Binding RightGridWidth}"/>
</Grid.ColumnDefinitions>
Should I put the above XAML code within:
<EventTrigger RoutedEvent="Button.Click">
If so, will my Commands or the EventTrigger be run first? I need my Commands to be run first as only then will the To value have been set correctly when the Storyboard runs.
I'm quite confused as to how to put all this together.
Any help/advice would be greatly appreciated!!
Quick and dirty with what you already have, i.e. the EventTrigger that runs the animation.
Add a RightGridWidth property (with change notification) to your view model:
public GridLength RightGridWidth
{
get { return rightGridWidth; }
set
{
rightGridWidth = value;
OnPropertyChanged("RightGridWidth");
}
}
Toggle the property value in the SetSelectedItemCommand's execute handler:
if (RightGridWidth.Value == 0d)
{
RightGridWidth = new GridLength(3d, GridUnitType.Star);
}
else
{
RightGridWidth = new GridLength(0d, GridUnitType.Star);
}
Run the animation in the Click EventTrigger:
<Button Content="Click" Command="{Binding SetSelectedItemCommand}">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<local:GridLengthAnimation
Storyboard.Target="{Binding ElementName=col2}"
Storyboard.TargetProperty="Width"
Duration="0:0:1" To="{Binding RightGridWidth}"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Finally, use a working implementation of the animation's GetCurrentValue, which has to return a value that depends on the CurrentProgress:
public override object GetCurrentValue(
object defaultOriginValue, object defaultDestinationValue,
AnimationClock animationClock)
{
var from = (GridLength)defaultOriginValue;
if (from.GridUnitType != To.GridUnitType ||
!animationClock.CurrentProgress.HasValue)
{
return from;
}
var p = animationClock.CurrentProgress.Value;
return new GridLength(
(1d - p) * from.Value + p * To.Value,
from.GridUnitType);
}
An entirely different and perhaps better approach would be the use of visual states. See VisualStateManager.

Trigger ExitAction on IsVisible equals true [duplicate]

How would I make a control fade in/out when it becomes Visible.
Below is my failed attempt:
<Window x:Class="WadFileTester.Form1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="MyWindow" Title="WAD File SI Checker" Height="386" Width="563" WindowStyle="SingleBorderWindow" DragEnter="Window_DragEnter" DragLeave="Window_DragLeave" DragOver="Window_DragOver" Drop="Window_Drop" AllowDrop="True">
<Window.Resources>
<Style TargetType="ListView" x:Key="animatedList">
<Style.Triggers>
<DataTrigger Binding="{Binding Visibility}" Value="Visible">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
From="0.0" To="1.0" Duration="0:0:5"
/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
From="1.0" To="0.0" Duration="0:0:5"
/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<ListView Name="listView1" Style="{StaticResource animatedList}" TabIndex="1" Margin="12,41,12,12" Visibility="Hidden">
</ListView>
</Grid>
</Window>
I don't know how to do both animations (fade in and fade out) in pure XAML. But simple fade out can be achieved relatively simple. Replace DataTriggers with Triggers, and remove ExitActions since they makes no sense in Fade out scenario. This is what you will have:
<Style TargetType="FrameworkElement" x:Key="animatedList">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0.0" To="1.0" Duration="0:0:0.2"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
But hey, don't give up. If you want to support both animations I can suggest small coding behind the XAML. After we do a trick, we will get what you want by adding one line of code in XAML:
<Button Content="Fading button"
x:Name="btn"
loc:VisibilityAnimation.IsActive="True"/>
Every time we change btn.Visibility from Visible to Hidden/Collapsed button will fade out. And every time we change Visibility back the button will fade in. This trick will work with any FrameworkElement (including ListView :) ).
Here is the code of VisibilityAnimation.IsActive attached property:
public class VisibilityAnimation : 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(VisibilityAnimation),
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 VisibilityAnimation()
{
UIElement.VisibilityProperty.AddOwner(typeof(FrameworkElement),
new FrameworkPropertyMetadata(Visibility.Visible, new PropertyChangedCallback(VisibilityChanged), CoerceVisibility));
}
private static void VisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// So what? Ignore.
}
private static void OnIsActivePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var fe = d as FrameworkElement;
if (fe == null)
{
return;
}
if (GetIsActive(fe))
{
HookVisibilityChanges(fe);
}
else
{
UnHookVisibilityChanges(fe);
}
}
private static void UnHookVisibilityChanges(FrameworkElement fe)
{
if (_hookedElements.Contains(fe))
{
_hookedElements.Remove(fe);
}
}
private static void HookVisibilityChanges(FrameworkElement fe)
{
_hookedElements.Add(fe, false);
}
private static object CoerceVisibility(DependencyObject d, object baseValue)
{
var fe = d as FrameworkElement;
if (fe == null)
{
return baseValue;
}
if (CheckAndUpdateAnimationStartedFlag(fe))
{
return baseValue;
}
// If we get here, it means we have to start fade in or fade out
// animation. In any case return value of this method will be
// Visibility.Visible.
var visibility = (Visibility)baseValue;
var da = new DoubleAnimation
{
Duration = new Duration(TimeSpan.FromMilliseconds(DURATION_MS))
};
da.Completed += (o, e) =>
{
// This will trigger value coercion again
// but CheckAndUpdateAnimationStartedFlag() function will reture true
// this time, and animation will not be triggered.
fe.Visibility = visibility;
// NB: Small problem here. This may and probably will brake
// binding to visibility property.
};
if (visibility == Visibility.Collapsed || visibility == Visibility.Hidden)
{
da.From = 1.0;
da.To = 0.0;
}
else
{
da.From = 0.0;
da.To = 1.0;
}
fe.BeginAnimation(UIElement.OpacityProperty, da);
return Visibility.Visible;
}
private static bool CheckAndUpdateAnimationStartedFlag(FrameworkElement fe)
{
var hookedElement = _hookedElements.Contains(fe);
if (!hookedElement)
{
return true; // don't need to animate unhooked elements.
}
var animationStarted = (bool) _hookedElements[fe];
_hookedElements[fe] = !animationStarted;
return animationStarted;
}
}
The most important thing here is CoerceVisibility() method. As you can see we do not allow changing this property until fading animation is completed.
This code is neither thread safe nor bug free. Its only intention is to show the direction :). So feel free to improve, edit and get reputation ;).
You can't directly use the Visibility property for a fade out because setting a trigger on it will first Hide/Collapse the control, THEN animate it. So basically you'll got an animation on a collapsed control => nothing.
One "reliable" way would be to introduce a new Dependency Property (attached or not), say IsOpen and setting a property trigger IsOpen=True on it with:
EnterAction:
Make sure Visibility is set to Visible
Fade in the Opacity from 0 to 1
ExitAction:
Visibility set to Visible at keyframe 0 and Collapsed/Hidden at the last Keyframe
Fade out the Opacity from 1 to 0.
Here's an example:
<Style TargetType="{x:Type local:TCMenu}">
<Style.Resources>
<Storyboard x:Key="FadeInMenu">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="{x:Null}">
<EasingDoubleKeyFrame KeyTime="0" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="{x:Null}">
<DiscreteObjectKeyFrame KeyTime="0:0:0.0" Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="FadeOutMenu">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="{x:Null}">
<EasingDoubleKeyFrame KeyTime="0" Value="1"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="{x:Null}">
<DiscreteObjectKeyFrame KeyTime="0:0:0.0" Value="{x:Static Visibility.Visible}"/>
<DiscreteObjectKeyFrame KeyTime="0:0:0.2" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</Style.Resources>
<Style.Triggers>
<Trigger Property="IsOpen" Value="true">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource FadeInMenu}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource FadeOutMenu}"/>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
I've been coming at this a slightly different way - I've got an extended version of Ray's answer to this question which adds a FadeIn() and FadeOut() extension method to everything that collapses or shows the element as appropriate, then instead of making objects visible I can just call FadeIn() and FadeOut() on them - and it will work an any element without any specific animation code.
public static T FadeFromTo(this UIElement uiElement, double fromOpacity,
double toOpacity, int durationInMilliseconds, bool loopAnimation,
bool showOnStart, bool collapseOnFinish)
{
var timeSpan = TimeSpan.FromMilliseconds(durationInMilliseconds);
var doubleAnimation =
new DoubleAnimation(fromOpacity, toOpacity,
new Duration(timeSpan));
if (loopAnimation)
doubleAnimation.RepeatBehavior = RepeatBehavior.Forever;
uiElement.BeginAnimation(UIElement.OpacityProperty, doubleAnimation);
if (showOnStart)
{
uiElement.ApplyAnimationClock(UIElement.VisibilityProperty, null);
uiElement.Visibility = Visibility.Visible;
}
if (collapseOnFinish)
{
var keyAnimation = new ObjectAnimationUsingKeyFrames{Duration = new Duration(timeSpan) };
keyAnimation.KeyFrames.Add(new DiscreteObjectKeyFrame(Visibility.Collapsed, KeyTime.FromTimeSpan(timeSpan)));
uiElement.BeginAnimation(UIElement.VisibilityProperty, keyAnimation);
}
return uiElement;
}
public static T FadeIn(this UIElement uiElement, int durationInMilliseconds)
{
return uiElement.FadeFromTo(0, 1, durationInMilliseconds, false, true, false);
}
public static T FadeOut(this UIElement uiElement, int durationInMilliseconds)
{
return uiElement.FadeFromTo(1, 0, durationInMilliseconds, false, false, true);
}
I realize this Question is a bit old, but I have only read it now and I have tweaked the code given by Anvaka. It supports binding to Visibility (only when binding mode is set to TwoWay). It also supports 2 different duration values for FadeIn and FadeOut.
Here is the class:
public class VisibilityAnimation : DependencyObject
{
#region Private Variables
private static HashSet<UIElement> HookedElements = new HashSet<UIElement>();
private static DoubleAnimation FadeAnimation = new DoubleAnimation();
private static bool SurpressEvent;
private static bool Running;
#endregion
#region Attached Dependencies
public static readonly DependencyProperty IsActiveProperty = DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(VisibilityAnimation), 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);
}
public static readonly DependencyProperty FadeInDurationProperty = DependencyProperty.RegisterAttached("FadeInDuration", typeof(double), typeof(VisibilityAnimation), new PropertyMetadata(0.5));
public static double GetFadeInDuration(UIElement e)
{
if (e == null) throw new ArgumentNullException("element");
return (double)e.GetValue(FadeInDurationProperty);
}
public static void SetFadeInDuration(UIElement e, double value)
{
if (e == null) throw new ArgumentNullException("element");
e.SetValue(FadeInDurationProperty, value);
}
public static readonly DependencyProperty FadeOutDurationProperty = DependencyProperty.RegisterAttached("FadeOutDuration", typeof(double), typeof(VisibilityAnimation), new PropertyMetadata(1.0));
public static double GetFadeOutDuration(UIElement e)
{
if (e == null) throw new ArgumentNullException("element");
return (double)e.GetValue(FadeOutDurationProperty);
}
public static void SetFadeOutDuration(UIElement e, double value)
{
if (e == null) throw new ArgumentNullException("element");
e.SetValue(FadeOutDurationProperty, value);
}
#endregion
#region Callbacks
private static void VisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// So what? Ignore.
// We only specified a property changed call-back to be able to set a coercion call-back
}
private static void OnIsActivePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// Get the framework element and leave if it is null
var fe = d as FrameworkElement;
if (fe == null) return;
// Hook the element if IsActive is true and unhook the element if it is false
if (GetIsActive(fe)) HookedElements.Add(fe);
else HookedElements.Remove(fe);
}
private static object CoerceVisibility(DependencyObject d, object baseValue)
{
if (SurpressEvent) return baseValue; // Ignore coercion if we set the SurpressEvent flag
var FE = d as FrameworkElement;
if (FE == null || !HookedElements.Contains(FE)) return baseValue; // Leave if the element is null or does not belong to our list of hooked elements
Running = true; // Set the running flag so that an animation does not change the visibility if another animation was started (Changing Visibility before the 1st animation completed)
// If we get here, it means we have to start fade in or fade out animation
// In any case return value of this method will be Visibility.Visible
Visibility NewValue = (Visibility)baseValue; // Get the new value
if (NewValue == Visibility.Visible) FadeAnimation.Duration = new Duration(TimeSpan.FromSeconds((double)d.GetValue(FadeInDurationProperty))); // Get the duration that was set for fade in
else FadeAnimation.Duration = new Duration(TimeSpan.FromSeconds((double)d.GetValue(FadeOutDurationProperty))); // Get the duration that was set for fade out
// Use an anonymous method to set the Visibility to the new value after the animation completed
FadeAnimation.Completed += (obj, args) =>
{
if (FE.Visibility != NewValue && !Running)
{
SurpressEvent = true; // SuppressEvent flag to skip coercion
FE.Visibility = NewValue;
SurpressEvent = false;
Running = false; // Animation and Visibility change is now complete
}
};
FadeAnimation.To = (NewValue == Visibility.Collapsed || NewValue == Visibility.Hidden) ? 0 : 1; // Set the to value based on Visibility
FE.BeginAnimation(UIElement.OpacityProperty, FadeAnimation); // Start the animation (it will only start after we leave the coercion method)
return Visibility.Visible; // We need to return Visible in order to see the fading take place, otherwise it just sets it to Collapsed/Hidden without showing the animation
}
#endregion
static VisibilityAnimation()
{
// Listen for visibility changes on all elements
UIElement.VisibilityProperty.AddOwner(typeof(FrameworkElement), new FrameworkPropertyMetadata(Visibility.Visible, new PropertyChangedCallback(VisibilityChanged), CoerceVisibility));
}
}
This is best done using a behavior
class AnimatedVisibilityFadeBehavior : Behavior<Border>
{
public Duration AnimationDuration { get; set; }
public Visibility InitialState { get; set; }
DoubleAnimation m_animationOut;
DoubleAnimation m_animationIn;
protected override void OnAttached()
{
base.OnAttached();
m_animationIn = new DoubleAnimation(1, AnimationDuration, FillBehavior.HoldEnd);
m_animationOut = new DoubleAnimation(0, AnimationDuration, FillBehavior.HoldEnd);
m_animationOut.Completed += (sender, args) =>
{
AssociatedObject.SetCurrentValue(Border.VisibilityProperty, Visibility.Collapsed);
};
AssociatedObject.SetCurrentValue(Border.VisibilityProperty,
InitialState == Visibility.Collapsed
? Visibility.Collapsed
: Visibility.Visible);
Binding.AddTargetUpdatedHandler(AssociatedObject, Updated);
}
private void Updated(object sender, DataTransferEventArgs e)
{
var value = (Visibility)AssociatedObject.GetValue(Border.VisibilityProperty);
switch (value)
{
case Visibility.Collapsed:
AssociatedObject.SetCurrentValue(Border.VisibilityProperty, Visibility.Visible);
AssociatedObject.BeginAnimation(Border.OpacityProperty, m_animationOut);
break;
case Visibility.Visible:
AssociatedObject.BeginAnimation(Border.OpacityProperty, m_animationIn);
break;
}
}
}
This is specifically being applied to a border - I haven't tried a user control but I expect the same applies.
To use it, you need the Blend Interactivity namespace:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
And use this markup on the Border that you want the behavior on:
<i:Interaction.Behaviors>
<Interactivity:AnimatedVisibilityFadeBehavior AnimationDuration="0:0:0.3" InitialState="Collapsed" />
</i:Interaction.Behaviors>
You'll need to add in the namespace for the behavior class too..
Quite old now, but could you not just chain the DoubleAnimations?
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
From="0.0" To="1.0" Duration="0:0:5"
/>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
From="1.0" To="0.0" Duration="0:0:5"
/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
You may want to try AutoReverse property ... though I am not sure if it works the way you want it to.
This is what I found on MSDN :
When a timeline's AutoReverse property is set to true and its RepeatBehavior property causes it to repeat, each forward iteration is followed by a backward iteration. This makes one repetition. For example, a timeline with an AutoReverse value of true with an iteration Count of 2 would play forward once, then backwards, then forwards again, and then backwards again.
I prefer Nock's XAML-only solution. Combined with the Mike Nakis' comment it gave me the perfect solution. I myself was struggling a bit with the DataTrigger. That's why I want to share it here:
<TextBlock x:Name="Text1" Text="Hello World!" FontSize="30" d:Visibility="Visible">
<TextBlock.Style>
<Style>
<Style.Resources>
<Storyboard x:Key="FadeInMenu">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="{x:Null}">
<EasingDoubleKeyFrame KeyTime="0" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="{x:Null}">
<DiscreteObjectKeyFrame KeyTime="0:0:0.0" Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="FadeOutMenu">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="{x:Null}">
<EasingDoubleKeyFrame KeyTime="0" Value="1"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="{x:Null}">
<DiscreteObjectKeyFrame KeyTime="0:0:0.0" Value="{x:Static Visibility.Visible}"/>
<DiscreteObjectKeyFrame KeyTime="0:0:0.2" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</Style.Resources>
<Style.Triggers>
<DataTrigger Binding="{Binding IsOpen}" Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource FadeInMenu}"/>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource FadeOutMenu}"/>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
<Setter Property="TextBlock.Visibility" Value="Collapsed" />
</Style>
</TextBlock.Style>
</TextBlock>
Also do not forget to implement INotifyPropertyChanged for the Property IsOpen in code behind.

DataTrigger not firing when property value changes multiple times within a short window

I've found an interesting problem in my WPF app related to a MultiDataTrigger not starting a StoryBoard to animate a data grid cell. I have a WPF data grid control which is bound to an ObservableCollection containing POCOs that implement INotifyPropertyChanged.
What I want to achieve
A real time data grid which flashes updates as values change. When the value increases I want the cell to flash green; when the value decreases I want the cell to flash red. The animation simply animates the background colour of a cell from solid colour to transparent over a 1 second interval.
Problem
The storyboard isn't started by the MultiDataTrigger after the first time. The MultiDataTrigger is monitoring changes to two properties: IsPositive and HasValueChanged. Initially HasValueChanged is false and later changes from false to true as soon as the Value property is set, and the animation works the first time. Thereafter HasValueChanged is pulsed from false to true to trigger a change notification, but the animation is not started.
Here's the XAML style I'm applying to each data grid cell:
<Style TargetType="{x:Type TextBlock}">
<Style.Setters>
<Setter Property="Background"
Value="Aqua" />
</Style.Setters>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=HasValueChanged}"
Value="True" />
<Condition Binding="{Binding Path=IsPositive}"
Value="True" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.EnterActions>
<RemoveStoryboard BeginStoryboardName="PositiveValueCellStoryboard" />
<RemoveStoryboard BeginStoryboardName="NegativeValueCellStoryboard" />
<BeginStoryboard Name="PositiveValueCellStoryboard"
Storyboard="{StaticResource PositiveValueCellAnimation}"
HandoffBehavior="SnapShotAndReplace" />
</MultiDataTrigger.EnterActions>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=HasValueChanged}"
Value="True" />
<Condition Binding="{Binding Path=IsPositive}"
Value="False" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.EnterActions>
<RemoveStoryboard BeginStoryboardName="PositiveValueCellStoryboard" />
<RemoveStoryboard BeginStoryboardName="NegativeValueCellStoryboard" />
<BeginStoryboard Name="NegativeValueCellStoryboard"
Storyboard="{StaticResource NegativeValueCellAnimation}"
HandoffBehavior="SnapShotAndReplace" />
</MultiDataTrigger.EnterActions>
</MultiDataTrigger>
</Style.Triggers>
</Style>
Here's the XAML for the animations:
<Storyboard x:Key="NegativeValueCellAnimation">
<ColorAnimation Storyboard.TargetProperty="Background.(SolidColorBrush.Color)"
Timeline.DesiredFrameRate="10"
RepeatBehavior="1x"
From="Red"
To="Transparent"
Duration="0:0:1" />
</Storyboard>
<Storyboard x:Key="PositiveValueCellAnimation">
<ColorAnimation Storyboard.TargetProperty="Background.(SolidColorBrush.Color)"
Timeline.DesiredFrameRate="10"
RepeatBehavior="1x"
From="Green"
To="Transparent"
Duration="0:0:1" />
</Storyboard>
Here's the code for the POCO object that's bound to each cell:
using System;
using System.Threading;
using Microsoft.Practices.Prism.ViewModel;
namespace RealTimeDataGrid
{
public class Cell : NotificationObject
{
public Cell(int ordinal, int value)
{
Ordinal = ordinal;
_value = value;
LastUpdated = DateTime.MaxValue;
}
public void SetValue(int value)
{
Value = value;
// Pulse value changed to get WPF to fire DataTriggers
HasValueChanged = false;
Thread.Sleep(100);
HasValueChanged = true;
}
private int _value;
public int Value
{
get { return _value; }
private set
{
if (_value == value)
return;
_value = value;
// Performance optimization, using lambdas here causes performance issues
RaisePropertyChanged("IsPositive");
RaisePropertyChanged("Value");
}
}
private bool _hasValueChanged;
public bool HasValueChanged
{
get { return _hasValueChanged; }
set
{
if (_hasValueChanged == value)
return;
_hasValueChanged = value;
// Performance optimization, using lambdas here causes performance issues
RaisePropertyChanged("HasValueChanged");
}
}
public int Ordinal { get; set; }
public DateTime LastUpdated { get; set; }
public bool IsPositive
{
get { return Value >= 0; }
}
public TimeSpan TimeSinceLastUpdate
{
get { return DateTime.Now.Subtract(LastUpdated); }
}
}
}
Apparent fix
Adding a Thread.Sleep(100) in between setting HasValueChanged twice within the SetValue method appears to fix the problem of the MultiDataTrigger not firing, but has undesired side effects.
Videos of the problem
Click here to see a video of the broken version.
Click here to see a video of the fixed version.
The 'fixed' version isn't ideal because the Thread.Sleep causes the cells to update in an apparently sequential fashion rather than simultaneously as in the broken version. Besides, having a Thread.Sleep in there makes me feel bad :)
First of all; am I going about this all wrong? Is there a simpler / better way to achieve what I want? If not, what is the solution to this problem without having to resort to adding a Thread.Sleep (code smell!)?
Why doesn't WPF cause the DataTrigger to fire when values are changed quickly? Is there something that causes WPF to 'miss' property changes; or does WPF simply ignore changes that go from one value to another and then back to the original value within a certain timeframe?
Thanks for any help!
Using a property like an event seems like a worse abuse than the Thread.Sleep to me, i would suggest the use of two RoutedEvents (to differentiate between Changed+Positive & Changed+Negative) in combination with EventTriggers instead.
If you want to stick with the Thread.Sleep you should probably do that in the background:
// IDE-free code, may be broken
HasValueChanged = false;
new Thread((ThreadStart)(() =>
{
Thread.Sleep(100);
HasValueChanged = true;
})).Start();

Categories