I have a scenario where I want to reduce the margin of a Button control when it receives focus. This is because because I wan't to increase the border property of that Button control and at the same time wan't the button to stay same size (height and width). So a little bit of effort on web guided me to write a ValueConverter and get the margin reduced. But I am still not able to put up the working code till yet. Here is what I have jumbled up
Xaml
<ControlTemplate.Triggers> <!--ControlTemplate for the Button-->
<Trigger Property="IsFocused" Value="True">
<Setter Property="BorderBrush" TargetName="bdrButton" Value="Wheat"/>
<Setter Property="BorderThickness" TargetName="bdrButton" Value="2"/>
<Setter Property="Margin" TargetName="bdrButton" >
<Setter.Value>
<Binding ElementName="bdrButton" Path="Margin" Converter="{StaticResource N_MarginReducer}">
<Binding.ConverterParameter>
<Thickness>1,1,1,1</Thickness>
</Binding.ConverterParameter>
</Binding>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
Converter:
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
Thickness newMargin = new Thickness();
Thickness margin = (Thickness)value;
Thickness reduceBy = (Thickness)parameter;
newMargin.Left = margin.Left - reduceBy.Left;
newMargin.Top = margin.Top - reduceBy.Top;
newMargin.Right = margin.Right - reduceBy.Right;
newMargin.Bottom = margin.Bottom - reduceBy.Bottom;
return newMargin;
}
The above code results in StackOverFlowException for the Margin.Left being called recursively. Anyone has better idea or implementation for the scenario that I am trying to achieve.
You will want to use a ThicknessAnimation for the Margin and a DoubleAnimation for the height/width, rather than using a converter.
http://msdn.microsoft.com/en-us/library/system.windows.media.animation.thicknessanimation.aspx
http://msdn.microsoft.com/en-us/library/system.windows.media.animation.doubleanimation.aspx
Example:
<StackPanel>
<StackPanel.Resources>
<Thickness x:Key="unfocusedThickness" Top="15" Left="15" Right="15" Bottom="15"/>
<Thickness x:Key="focusedThickness" Top="5" Left="5" Right="5" Bottom="5"/>
</StackPanel.Resources>
<Button Height="100" Width="100" Margin="15">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.GotFocus">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="100" To="120" Duration="0:0:0"/>
<DoubleAnimation Storyboard.TargetProperty="Width" From="100" To="120" Duration="0:0:0"/>
<ThicknessAnimation Storyboard.TargetProperty="Margin" From="{StaticResource unfocusedThickness}" To="{StaticResource focusedThickness}" Duration="0:0:0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.LostFocus">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="120" To="100" Duration="0:0:0"/>
<DoubleAnimation Storyboard.TargetProperty="Width" From="120" To="100" Duration="0:0:0"/>
<ThicknessAnimation Storyboard.TargetProperty="Margin" To="{StaticResource unfocusedThickness}" From="{StaticResource focusedThickness}" Duration="0:0:0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<TextBox />
</StackPanel>
Related
I'm brand spanking new to WPF and XAML, so I'm sorry if this is a silly question:
I am trying to start a StoryBoard on a TextBlock that changes the text. I want to start this animation when the TextBlock becomes visible. It looks like the only events you can trigger inside of a TextBlock.Triggers bracket is an EventTrigger. If this is so, as far as I can see an EventTrigger needs a routed event, but IsVisibilityChanged isn't one. Since I can't use that, any ideas as to what I should do instead?
I have attached a sample of my code that isn't working (doesn't compile), just to illustrate what it is that I am trying to do:
<TextBlock Foreground="LightGray" Text="Payfast Running" Name="AnimatedTextBlock">
<TextBlock.Triggers>
<Trigger Property="Visibility" Value="Visible">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<StringAnimationUsingKeyFrames
Storyboard.TargetProperty="(TextBlock.Text)"
Duration="0:0:1.5"
Storyboard.TargetName="AnimatedTextBlock"
RepeatBehavior="Forever">
<DiscreteStringKeyFrame Value="Payfast Running" KeyTime="0:0:0"/>
<DiscreteStringKeyFrame Value="Payfast Running." KeyTime="0:0:0:5"/>
<DiscreteStringKeyFrame Value="Payfast Running.." KeyTime="0:0:1"/>
<DiscreteStringKeyFrame Value="Payfast Running..." KeyTime="0:0:1:5"/>
</StringAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</TextBlock.Triggers>
</TextBlock>
Lastly, I need to do this in markup, not CodeBehind, if possible.
You might need to wrap that all up into a style.
<TextBlock Foreground="LightGray" Text="Payfast Running" Name="AnimatedTextBlock">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Visibility, RelativeSource={RelativeSource TemplatedParent}}" Value="Visible">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<StringAnimationUsingKeyFrames
Storyboard.TargetProperty="(TextBlock.Text)"
Duration="0:0:1.5"
Storyboard.TargetName="AnimatedTextBlock"
RepeatBehavior="Forever">
<DiscreteStringKeyFrame Value="Payfast Running" KeyTime="0:0:0"/>
<DiscreteStringKeyFrame Value="Payfast Running." KeyTime="0:0:0:5"/>
<DiscreteStringKeyFrame Value="Payfast Running.." KeyTime="0:0:1"/>
<DiscreteStringKeyFrame Value="Payfast Running..." KeyTime="0:0:1:5"/>
</StringAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
"I want to start this animation when the TextBlock becomes visible."
You could bind your TextBlock's Visibility to a boolean property (note that you have to add a resource for boolean-to-visibility conversion):
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Window.Resources>
...
<TextBlock Visibility="{Binding Path=ReadyToPlay, Converter={StaticResource BoolToVisConverter}}"/>
And then, in your ReadyToPlay property's setter method, you could add an if statement that checks for whether or not to start your Storyboard:
if (value) {
// start Storyboard:
// find Storyboard instance and call Begin() method on it
}
By doing this, when ReadyToPlay becomes true, the TextBlock will become visible and the Storyboard will begin simultaneously.
Context: I have two Storyboards in my UserControl. One of them is for sliding the UserControl in and one is for sliding it out.
All I'm doing for the slide in/out is setting the margin to a negative value for sliding out and to zero for sliding in.
Now, I want the sbShowLeftMenu storyboard to be executed when the UserControl's visibility is set to Visible. Also, I want to be able to manually slide the UserControl in/out using the Buttons BtnHide & BtnShow.
Now if the UserControl becomes visible the sbShowLeftMenu is activated and the UserControls gets moved in. Switching the visibility between Collapsed and Visible this behaviour continues, as I want it to.
Now if I hit the BtnHide to move the UserControl out of viewport everything works fine until I start switching the Visibility of the UserControl again. Now the Storyboard doesn't work anymore. I can still move the UserControls in/out with the Buttons but the 'Visible' Trigger does not start the Storyboard.
Here are gif examples of the behavior:
With the click on 'Database Search' I set the Visibility of the UserControl to Visible (because its bound to the 'IsExpanded' property of the ExpanderControl) and it works just fine:
Here I demonstrate what happens after I manually click the 'BtnHide':
This is the code for the UserControl:
<UserControl.Resources>
<Style x:Key="TextBlockStyle">
<Setter Property="TextBlock.FontSize" Value="10"></Setter>
<Setter Property="TextBlock.Margin" Value="1"></Setter>
<Setter Property="TextBlock.VerticalAlignment" Value="Center"></Setter>
</Style>
<Storyboard x:Key="sbShowLeftMenu">
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="BtnShow" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.01" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="BtnHide" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.01" Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
<ThicknessAnimation Storyboard.TargetProperty="Margin" Storyboard.TargetName="pnlLeftMenu" From="-650,0,0,0" To="0,0,0,0" DecelerationRatio=".9" Duration="0:0:1" />
</Storyboard>
<Storyboard x:Key="sbHideLeftMenu">
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="BtnHide" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.01" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="BtnShow" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.01" Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
<ThicknessAnimation Storyboard.TargetProperty="Margin" Storyboard.TargetName="pnlLeftMenu" From="0,0,0,0" To="-650,0,0,0" AccelerationRatio=".9" Duration="0:0:1" />
</Storyboard>
</UserControl.Resources>
<UserControl.Template>
<ControlTemplate>
<Grid Background="Red">
<StackPanel Panel.ZIndex="2" Name="pnlLeftMenu" Orientation="Horizontal" HorizontalAlignment="Left" Margin="-650,0,0,0" Height="500">
<!-- Content -->
<Border>The Content is in here</Border>
<Grid>
<Button x:Name="BtnShow" Height="25" Width="25" VerticalAlignment="Top" HorizontalAlignment="Left" >
<Button.Content>
<Path Stroke="Black"
StrokeThickness="2"
Data="M 0,0 L 0.5,0.5 L 0,1"
Stretch="Uniform"></Path>
</Button.Content>
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource sbShowLeftMenu}"></BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<Button x:Name="BtnHide" Height="25" Width="25" VerticalAlignment="Top" HorizontalAlignment="Left" Visibility="Collapsed" >
<Button.Content>
<Path Stroke="Black"
StrokeThickness="2"
Data="M 1,1 L 0.5,0.5 L 1,0"
Stretch="Uniform"></Path>
</Button.Content>
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource sbHideLeftMenu}"></BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
</Grid>
</StackPanel>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsVisible"
Value="True"
my:TriggerTracing.TriggerName="BoldWhenMouseIsOver"
my:TriggerTracing.TraceEnabled="True">
<Trigger.EnterActions>
<BeginStoryboard Name="sbShowLeftMenu" Storyboard="{StaticResource sbShowLeftMenu}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Name="xy" Storyboard="{StaticResource sbHideLeftMenu}"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</UserControl.Template>
You seem to have some complex logic which is interfering with the operations of the story boards.
I would recommend that you not try do this in triggers but instead in code behind. Create a state machine operation and then open/close/make visible/make invisible depending on the state.
The following code is what I use for similar logic of moving a panel based on boolean state via storyboards. You can expand it to handle visibility as well with other states.
private bool moveRight = true; // Start out on the left side, then move right.
public void MoveRight()
{
try
{
if (moveRight)
{
(Resources["MoveToOpen"] as Storyboard)?.Begin(this, false);
(Resources["FlipArrowClose"] as Storyboard)?.Begin(this, false);
}
else
{
(Resources["MoveToClose"] as Storyboard)?.Begin(this, false);
(Resources["FlipArrowOpen"] as Storyboard)?.Begin(this, false);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
moveRight = !moveRight;
}
In short my quetion is: Can I use databinding for the duration property of a storyboard?
My intention was to design a circular progressbar where i have a simple counter. Now based on the counter I wanted to change the duration of the animation. Is there any way I can achieve this by databinding or is there any other way.
I am using an Arc to create the progressbar effect as you can see below. When the DependencyProperty PauseAnimation becomes false, my animation starts. When it is set to true, it resets(I couldnt find a way to Pause the animation so I am resetting it to start from begining). The below code runs perfectly but I am having trouble setting the duration of the storyboard. I want to change that based on a property in my control. Can I achieve this?
<ed:Arc x:Name="AnimatingArc" ArcThickness="2" ArcThicknessUnit="Pixel" StartAngle="0" EndAngle="360" Fill="{DynamicResource ApplicationPrimaryColour}" HorizontalAlignment="Left" Height="25" Stretch="None" Stroke="Transparent" VerticalAlignment="Top" Width="25" Margin="8,47,0,0">
<ed:Arc.Style>
<Style TargetType="{x:Type ed:Arc}">
<Style.Triggers>
<DataTrigger Binding="{Binding PauseAnimation}" Value="false">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StartAngle" From="0" To="360" Duration="0:0:30" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StartAngle" To="0" Duration="0:0:10" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</ed:Arc.Style>
I'm yet again a bit stumped, and was hoping someone could please help.
Apologies in advance to the long code.
Issue - I have a DataTrigger that starts off firing as expected, but it eventually fails and I can't work out why. In the example provided, it moves a rectangle around a Canvas. Each click of the button moves it in this sequence; N, NE, E, SE, S, SW, W, NW. Then it starts the sequence again, starting with N. Once the first sequence is completed, it won't move North. It will only ever move NW again (ie the last successful move).
The property that triggers the DataTrigger is updating.
thanks
XAML;
<Window.Resources>
<Style x:Key="TestRectStyle" TargetType="{x:Type Rectangle}">
<Style.Triggers>
<DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="North">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="0" Duration="0:0:0.8" AutoReverse="False" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="NorthEast">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="50" Duration="0:0:0.8" AutoReverse="False" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="East">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="0" Duration="0:0:0.8" AutoReverse="False" />
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="50" Duration="0:0:0.8" AutoReverse="False" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="SouthEast">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="50" Duration="0:0:0.8" AutoReverse="False" />
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="50" Duration="0:0:0.8" AutoReverse="False" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="South">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="50" Duration="0:0:0.8" AutoReverse="False" />
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="0" Duration="0:0:0.8" AutoReverse="False" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="SouthWest">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="50" Duration="0:0:0.8" AutoReverse="False" />
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="West">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="0" Duration="0:0:0.8" AutoReverse="False" />
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="NorthWest">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="TestDataTemplate01" DataType="BO:MyPerson">
<Canvas Width="1000" Height="1000" Background="Transparent">
<Rectangle Width="50" Height="50" Fill="Red" Style="{StaticResource TestRectStyle}" Canvas.Top="300" Canvas.Left="300" />
</Canvas>
</DataTemplate>
</Window.Resources>
<Canvas Width="1000" Height="1000">
<ItemsControl Name="ic_People" ItemTemplate="{StaticResource TestDataTemplate01}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Width="1000" Height="1000" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<Button Canvas.Right="0" Click="Button_Click_1" Width="120">Next Move</Button>
</Canvas>
Codebehind;
public partial class Window1 : Window
{
private ObservableCollection<MyPerson> _personList = new ObservableCollection<MyPerson>();
public Window1()
{
InitializeComponent();
MyPerson person1 = new MyPerson();
_personList.Add(person1);
ic_People.ItemsSource = _personList;
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
DoNextMove();
}
private int debugDirection = 0;
private void DoNextMove()
{
if (debugDirection > 15)
debugDirection = 0;
_personList[0].MoveOneTile(debugDirection);
debugDirection += 2; // increase by 2 to as I've not implemented the odd numbers yet
}
}
MyPerson code;
public class MyPerson : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
private string _dirMov = "";
public string UI_DirectionOfMovement
{
get { return _dirMov; }
set
{
_dirMov = value;
OnPropertyChanged("UI_DirectionOfMovement");
}
}
public void MoveOneTile(int directionToMove)
{
this.UI_DirectionOfMovement = "clear"; // clearing it first forces an update
switch (directionToMove)
{
case 0: { this.UI_DirectionOfMovement = "North"; break; }
case 2: { this.UI_DirectionOfMovement = "NorthEast"; break; }
case 4: { this.UI_DirectionOfMovement = "East"; break; }
case 6: { this.UI_DirectionOfMovement = "SouthEast"; break; }
case 8: { this.UI_DirectionOfMovement = "South"; break; }
case 10: { this.UI_DirectionOfMovement = "SouthWest"; break; }
case 12: { this.UI_DirectionOfMovement = "West"; break; }
case 14: { this.UI_DirectionOfMovement = "NorthWest"; break; }
default: { throw new Exception(); }
}
}
public MyPerson()
{
}
}
The problem here ultimately comes down to the fact that animations don't actually change the values of the Canvas.Left and Canvas.Top properties. They only appear to do so, as values obtained from animation override values obtained via data binding.
After each animation finishes, the animation 'holds' the value of the Canvas.Left or Canvas.Top dependency properties at its final value. This 'held' value is what is returned if you get the value of the dependency property, and it overrides any value set via data binding. As you start a second animation, the dependency property's value is obtained by working from the previous animation's held value. As more and more animations happen, WPF has to determine the location of the rectangle by going back through a chain of more and more animations.
I can't say why only the last (NW) animation runs after you've been run all of them. It quite probably has something to do with dependency property value precedence. This page doesn't say what happens if there are multiple animations on a dependency property, but in this situation I would assume that the last animation to start on that property takes precedence. I suspect that the DataTriggers are firing, but the WPF dependency property system for some reason is ignoring values coming from the 'overridden' animations.
I would recommend avoiding having a chain of animations like this. Instead, change your Person objects to keep track of where they are on the canvas, for example, by adding Left and Top properties. You can then bind Canvas.Left and Canvas.Top to these properties. Your DoNextMove method should also set the values of these properties to where the animation moves them to. Do this after changing the value of theUI_DirectionOfMovement property. Finally, stop your animations from 'holding' their final values by setting FillBehavior="Stop" on each DoubleAnimation.
As animated values take precedence over locally-set values, it's not a problem to set the property values for Left and Top at the start of the animation. While the animation is running, the animated values for Canvas.Left and Canvas.Top take precedence over any value set via data binding. As the animation finishes, the animation releases its hold over the Canvas.Left and Canvas.Top dependency properties, and the location of the rectangle reverts to being obtained via data binding. With any luck, this location will be the same as at the end of the animation.
I simply want to open up the WPF Popup with a delay, sort of like a ToolTip.
How can I achieve this?
And, by the way, Popup.PopupAnimation = PopupAnimation.Fade ... fades in too quickly. I want at least half a second in there.
You can create a style to be applied to the Popup in the following way:
<Style x:Key="TooltipPopupStyle" TargetType="Popup">
<Style.Triggers>
<DataTrigger Binding="{Binding PlacementTarget.IsMouseOver, RelativeSource={RelativeSource Self}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="OpenPopupStoryBoard" >
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsOpen" FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame KeyTime="0:0:0.25" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<PauseStoryboard BeginStoryboardName="OpenPopupStoryBoard"/>
<BeginStoryboard x:Name="ClosePopupStoryBoard">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsOpen" FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame KeyTime="0:0:1" Value="False"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<PauseStoryboard BeginStoryboardName="ClosePopupStoryBoard" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<PauseStoryboard BeginStoryboardName="OpenPopupStoryBoard"/>
<ResumeStoryboard BeginStoryboardName="ClosePopupStoryBoard" />
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
Then, whenever you want to use it, you would write markup similar to this (notice the binding for the PlacementTarget):
<TextBlock x:Name="TargetControl" Text="Hover over me!" />
<Popup PlacementTarget="{Binding ElementName=TargetControl}" Style="{StaticResource TooltipPopupStyle}">
<Border BorderBrush="Red" BorderThickness="1" Background="White">
<TextBlock Text="This is a Popup behaving somewhat like the tooltip!" Margin="10" />
</Border>
</Popup>
The answer cplotts pasted is good but may not apply in your case because it leaves the animation attached to the IsOpen property, effectively locking it in place and preventing it from being changed via direct property setting, binding, and other ways. This may make it difficult to use with your code, depending on how you are using it.
If that is the case, I would switch to starting a DispatcherTimer when you want to open a popup after some delay, like this:
_popupTimer = new DispatcherTimer(DispatcherPriority.Normal);
_popupTimer.Interval = TimeSpan.FromMilliseconds(100);
_popupTimer.Tick += (obj, e) =>
{
_popup.IsOpen = true;
};
_popupTimer.Start();
For a ToolTip-like behavior this could be done on MouseEnter. If you want to cancel the popup opening for some reason (such as if the mouse leaves the control before the popup appears), just:
_popupTimer.Stop();
Update
As cplotts obseved in the comment, you will also want to set _popup.IsOpen = false in some situations in the MouseLeave event, depending on your logic for handling the mouse enter / exit events between your control and the popup. Be aware that you usually don't want to blindly set IsOpen=false on every MouseLeave event, because it may do so when the popup appears over it. This would in some situations lead to a flickering popup. So you'll need some logic there.
First off ... the credit for this answer goes to Eric Burke. He answered this very question posted in the WPF Disciples group. I thought it would be useful to put this answer out on StackOverflow too.
Basically, you need to animate the IsOpen property of the Popup with with a DiscreteBooleanKeyFrame.
Check out the following xaml (which can easily be pasted into Kaxaml or another loose xaml editing utility):
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<ContentPresenter>
<ContentPresenter.ContentTemplate>
<DataTemplate>
<Grid>
<CheckBox
x:Name="cb"
Width="100"
Height="40"
Content="Hover Over Me"
/>
<Popup
x:Name="popup"
Placement="Bottom"
PlacementTarget="{Binding ElementName=cb}"
>
<Border Width="400" Height="400" Background="Red"/>
</Popup>
</Grid>
<DataTemplate.Triggers>
<Trigger SourceName="cb" Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard x:Name="bsb">
<Storyboard>
<BooleanAnimationUsingKeyFrames
Storyboard.TargetName="popup"
Storyboard.TargetProperty="IsOpen"
FillBehavior="HoldEnd"
>
<DiscreteBooleanKeyFrame KeyTime="0:0:0.5" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<StopStoryboard BeginStoryboardName="bsb"/>
</Trigger.ExitActions>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ContentPresenter.ContentTemplate>
</ContentPresenter>
</Page>
Please note that I modified his original solution slightly ... to trigger the IsOpen on mouse over versus checking the CheckBox as he had it. All in the attempt to make Popup behave a little like ToolTip.
System.Windows.Controls.ToolTip tp = new System.Windows.Controls.ToolTip();
System.Windows.Threading.DispatcherTimer tooltipTimer =
new System.Windows.Threading.DispatcherTimer
(
System.Windows.Threading.DispatcherPriority.Normal
);
private void TooltipInvalidCharacter()
{
tp.Content =
"A flie name cannot contain any of the following character :" +
"\n" + "\t" + "\\ / : * ? \" < > |";
tooltipTimer.Interval = TimeSpan.FromSeconds(5);
tooltipTimer.Tick += new EventHandler(tooltipTimer_Tick);
tp.IsOpen = true;
tooltipTimer.Start();
}
void tooltipTimer_Tick(object sender, EventArgs e)
{
tp.IsOpen = false;
tooltipTimer.Stop();
}
You can extend the XAML for this solution so the popup stays open as long as the mouse is over it, then disappears automatically.
I modified the sample as follows:
Create a "ClosePopop" animation which sets IsOpen to False after 0.5 seconds. I made this a resource because it's used twice.
For the control's IsMouseOver trigger, add an ExitAction that starts the ClosePopup animation. This gives the user a chance to move the mouse over the popup before it closes. I named this animation"bxb"
Add a Trigger to the popup's IsMouseOver property. On mouseover, stop (but don't remove) the original "bxb" ClosePopup animation. This leaves the popup visible; removing the animation here will make the popup close.
On the popup's mouseout, start a new ClosePopup animation then remove the "bxb" animation. The last step is critical because otherwise the first, stopped "bxb" animation will keep the popup open.
This version turns the pop blue while the mouse is over it so you can see the sequence of events with Kaxaml.
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DataTemplate x:Key="TooltipPopup">
<Grid>
<CheckBox
x:Name="cb"
Width="100"
Height="40"
Content="Hover Over Me"/>
<Popup
x:Name="popup"
Placement="Bottom"
PlacementTarget="{Binding ElementName=cb}">
<Border x:Name="border" Width="400" Height="400" Background="Red"/>
</Popup>
</Grid>
<DataTemplate.Resources>
<Storyboard x:Key="ClosePopup">
<BooleanAnimationUsingKeyFrames
Storyboard.TargetName="popup"
Storyboard.TargetProperty="IsOpen"
FillBehavior="Stop">
<DiscreteBooleanKeyFrame KeyTime="0:0:0.5" Value="False"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</DataTemplate.Resources>
<DataTemplate.Triggers>
<Trigger SourceName="cb" Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard x:Name="bsb" >
<Storyboard>
<BooleanAnimationUsingKeyFrames
Storyboard.TargetName="popup"
Storyboard.TargetProperty="IsOpen"
FillBehavior="HoldEnd">
<DiscreteBooleanKeyFrame KeyTime="0:0:0.5" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<StopStoryboard BeginStoryboardName="bsb"/>
<BeginStoryboard x:Name="bxb" Storyboard="{StaticResource ClosePopup}"/>
</Trigger.ExitActions>
</Trigger>
<Trigger SourceName="popup" Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="Blue"/>
<Trigger.EnterActions>
<StopStoryboard BeginStoryboardName="bxb"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource ClosePopup}"/>
<RemoveStoryboard BeginStoryboardName="bxb"/>
</Trigger.ExitActions>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</Page>