I have a problem with PopupButton in WPF.
When I press it once the popup appears with some items to select (ElementTreeControl). After i press the same PopupButton it should close - yet it closes and opens again.
I solved that and it was working but when i click outside of this control and it closes (StayOpen = false) I have problem with reopening it again - need to press PopupButton two times.
Is there some property or workaround on how to detect when the control was pressed and when it was area outside of it?
I want the PopupButton to be:
When closed:
opens with one click
When open:
closed with 2nd click on popupButton
closed with click on the outside area
Popup button click action:
private Popup rtWindowBoundsPopup;
private async void ButtonClickAsync(object pmSender, RoutedEventArgs pmE)
{
if (pmSender is PopupButton lcButton)
{
if (lcButton.Tag is StarElement lcStarElement)
{
if (!string.IsNullOrEmpty(DatabaseName))
{
lcStarElement = await rtStarHelper.AssureMetaData(lcStarElement, DatabaseName);
}
else
{
StarDim lcStarDim = rtStarHelper.GetDimFromDimId(lcStarElement.Dim.DimId, true);
lcStarElement.Dim = await rtStarHelper.AssureMetaData(lcStarDim);
}
ShowTreeViewPopup(lcButton, lcStarElement);
}
}
}
private void ShowTreeViewPopup(PopupButton pmButton, StarElement pmStarElement)
{
ElementTreeControl lcElementTreeControl;
if (rtWindowBoundsPopup == null)
{
rtWindowBoundsPopup = new Popup();// { IsLightDismissEnabled = true };
rtWindowBoundsPopup.Opened += WindowBoundsPopupOpened;
}
if (rtWindowBoundsPopup.Child is ElementTreeControl lcTreeControl)
{
lcElementTreeControl = lcTreeControl;
lcElementTreeControl.HideAddionalCols();
}
else
{
lcElementTreeControl = new ElementTreeControl { Tag = pmButton };
rtWindowBoundsPopup.Child = lcElementTreeControl;
lcElementTreeControl.SelectionChanged += PopupListBoxSelectionChangedAsync;
}
Point lcPoint = UiHelper.CalcOffsets(pmButton);
Rect lcCurrentwindowbounds = CurrentWindow.RestoreBounds;
if (lcPoint.Y < lcCurrentwindowbounds.Height / 2)
{
lcElementTreeControl.MaxHeight = lcCurrentwindowbounds.Height - lcPoint.Y - pmButton.ActualHeight;
}
else
{
lcElementTreeControl.MaxHeight = lcPoint.Y - pmButton.ActualHeight;
}
lcElementTreeControl.Width = Math.Max(pmButton.ActualWidth, 400);
lcElementTreeControl.MaxWidth = lcCurrentwindowbounds.Width;
lcElementTreeControl.MinHeight = 150;
lcElementTreeControl.Init(rtStarCube, pmStarElement, rtStarHelper);
lcElementTreeControl.CaptionColWidth = lcElementTreeControl.Width;
rtWindowBoundsPopup.PlacementTarget = pmButton;
rtWindowBoundsPopup.Placement = PlacementMode.Bottom;
rtWindowBoundsPopup.StaysOpen = false;//false;
rtWindowBoundsPopup.Closed -= WindowBoundsPopupOnClosed;
rtWindowBoundsPopup.Closed += WindowBoundsPopupOnClosed;
rtWindowBoundsPopup.IsOpen = true;
}
In WindowBoundsPopupOnClosed nothing happens, i have tried to make it work there but didn't menage to do so.
Where do you actually close the Popup? I only see you setting IsOpen to true. Current behavior: the first click on PopupButton will open the Popup. Now because StaysOpen is set to false, clicking the button (which is outside the Popup) a second time will close the Popup, as the popup has lost focus, since it is moved from Popup to the PopupButton. IsOpen returns now false. This second click then invokes the event handler ButtonClickAsync which sets IsOpen back to true again, which reopens the Popup.
Your code is too complicated, because you are using C# instead of XAML.
The PopupButton should be a ToggleButton or based on it.
<Window>
<StackPanel>
<ToggleButton x:Name="PopupButton" />
<Popup IsOpen="{Binding ElementName=PopupButton, Path=IsChecked}">
<ElementTreeControl Tag="pmButton" />
</Popup>
</StackPanel>
</Window>
Using EventTrigger
An alternative approach is to use EventTrigger. This can be easier in situations, where you don't have access to the triggering control 'e.g., the PopupButton, as it might be defined out of scope e.g inside some other template. This example still assumes that PopupButton is derived from ToggleButton:
The Window, which hosts the Popup
(The ToggleButton, which opens/closes the Popup, is defined in a separate control ControlWithPopupButton, see below)
<Window>
<Window.Triggers>
<!--
EventTriggers must be defined in the scope of the Popup "RtWindowBoundsPopup"
and in the routing path of the raised event.
-->
<EventTrigger RoutedEvent="ToggleButton.Unchecked"
Sourcename="PopupButton">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="RtWindowBoundsPopup"
Storyboard.TargetProperty="IsOpen"
Duration="0">
<DiscreteBooleanKeyFrame Value="False" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="ToggleButton.Checked"
Sourcename="PopupButton">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="RtWindowBoundsPopup"
Storyboard.TargetProperty="IsOpen"
Duration="0">
<DiscreteBooleanKeyFrame Value="True" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<StackPanel>
<ControlWithPopupButton />
<Popup x:Name="RtWindowBoundsPopup">
<ElementTreeControl Tag="pmButton" />
</Popup>
</StackPanel>
</Window>
The UserControl, which contains the PopupButton
<ControlWithPopupButton>
<!--
PopupButton must derive from ToggleButton
or raise both Routed Events ToggleButon.Check and ToggleButton.Unchecked.
-->
<PopupButton x:Name="PopupButton" />
</ControlWithPopupButton>
Remarks
Then methods like ElementTreeControl.Init, should be called from the ElementTreeControl.Loaded event handler inside the ElementTreeControl class. I don't now what StarElement is, but it should bind to a DependencyProperty of ElementTreeControl. I don't have enough context, but I guess you should add an override of ElementTreeControl.OnSelectionChanged, so that you can move the code of PopupListBoxSelectionChangedAsync to the ElementTreeControl.
Related
There is a control that has animation DoubleAnimationUsingPath, the animation is triggered by the event RoutedEvent = "ui:CheckBoxProgressCircle.Check"
<EventTrigger RoutedEvent="ui:CheckBoxProgressCircle.Check">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingPath Storyboard.TargetName="AnimatedTranslateTransform"
Storyboard.TargetProperty="X"
PathGeometry="{StaticResource CheckAnimationPath}"
Source="X"
Duration="0:0:.1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
In the code, I register the event:
static CheckBoxProgressCircle()
{
CheckEvent =
EventManager.RegisterRoutedEvent("Check", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(CheckBoxProgressCircle));
}
public event RoutedEventHandler Check
{
add
{
AddHandler(CheckEvent, value);
}
remove
{
RemoveHandler(CheckEvent, value);
}
}
Then I call the event:
var newEventArgs = new RoutedEventArgs(CheckEvent);
RaiseEvent(newEventArgs);
If the control is on a tab that is active, everything works without errors, if the control is on a tab that has never become active, I get an error:
''AnimatedTranslateTransform' name cannot be found in the name scope of 'System.Windows.Controls.ControlTemplate'.'
I think this is due to the fact that the control did not render, because was on an inactive tab and does not have AnimatedTranslateTransform, how I can force wpf to render this element before the tab will become active? Or if I'm wrong, what can I do about it?
The solution turned out to be quite simple: you can call the ApplyTemplate () method; for this control.
https://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.applytemplate.aspx
I found a good explanation here on SO of how to bind the Duration property of a ColorAnimation to the Value property of a Slider. One uses a converter to convert the Double value from the slider to a Duration, and a Binding to have that set the Duration of the ColorAnimation. Here, abbreviated, is how that works:
<Window.Resources>
<local:DoubleToDurationConverter x:Key="DoubleToDurationConverter" />
</Window.Resources>
<Slider x:Name="slider" />
<Button Content="Click me for an animation">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<ColorAnimation To="Green"
Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)"
FillBehavior="Stop"
Duration="{Binding ElementName=slider,
Path=Value,
Mode=OneWay,
Converter={StaticResource DoubleToDurationConverter}}" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
I tried that and it worked fine for me. But what I want to do is bind the Duration to a dependency property called FadeTime I've added to my custom control. So, in that control's ControlTemplate I have this:
<ControlTemplate.Triggers>
<Trigger Property="IsLit" Value="true">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="glow"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="{Binding FadeTime, Mode=OneWay,
RelativeSource={RelativeSource TemplatedParent}}"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</ControlTemplate.Triggers>
This compiles, but gives me an error message at run-time:
InvalidOperationException: Cannot freeze this Storyboard timeline tree
for use across threads.
How can I bind my DoubleAnimation's Duration to a dependency variable in a custom control's ControlTemplate?
Thanks!
UPDATE
Data-binding is actually gross overkill for what I want to do. Real data-binding would allow for the property's value to change at run-time. All I really want is a way for the developer who is using my custom control to be able to set the Duration of the DoubleAnimation at design time, without having to edit the ControlTemplate. It's okay if the value the developer chooses never changes at run time.
Instead of defining the animation in your XAML markup, you could define it programmatically in the PropertyChangedCallback for the IsLit property.
You could simply define another property that lets the consumer of the control specify the duration of the animation.
Here is an example for you.
Control:
public class MyCustomControl : Control
{
private UIElement glow;
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(TimeSpan),
typeof(MyCustomControl), new PropertyMetadata(TimeSpan.FromSeconds(1)));
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(DurationProperty); }
set { SetValue(DurationProperty, value); }
}
public static readonly DependencyProperty IsLitProperty =
DependencyProperty.Register("IsLit", typeof(bool),
typeof(MyCustomControl), new PropertyMetadata(false, new PropertyChangedCallback(OnIsLitChanged)));
public bool IsLit
{
get { return (bool)GetValue(IsLitProperty); }
set { SetValue(IsLitProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
glow = Template.FindName("glow", this) as UIElement;
if (glow != null && IsLit)
Animate(glow);
}
private static void OnIsLitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
bool newValue = (bool)e.NewValue;
if(newValue)
{
MyCustomControl c = d as MyCustomControl;
if(c != null && c.glow != null)
{
c.Animate(c.glow);
}
}
}
private void Animate(UIElement glow)
{
DoubleAnimation animation = new DoubleAnimation();
animation.To = 1;
animation.Duration = Duration;
glow.BeginAnimation(OpacityProperty, animation);
}
}
Template:
<ControlTemplate x:Key="ct" TargetType="local:MyCustomControl">
<Border x:Name="glow" Width="100" Height="100" Background="Red" Opacity="0.1">
</Border>
</ControlTemplate>
Usage:
<local:MyCustomControl Template="{StaticResource ct}" Duration="0:0:5" IsLit="True" />
Basically, you can't use normal bindings inside the storyboard of a control template. Since you just want a way for developers to change the value, one of the following options might work for you:
(1) Use StaticResource: Place a duration object somewhere outside the control template, where it's easier to change for developers. However, it needs to be somewhere statically accessible to the control template, since DynamicResource won't work in this place.
<Duration x:Key="MyCustomDuration">0:0:1</Duration>
... then later
Duration="{StaticResource MyCustomDuration}"
(2) Use a static code behind field with x:Static:
public static class SettingsClass
{
public static Duration MyCustomDuration = new Duration(new TimeSpan(0, 0, 1));
}
and use:
Duration="{x:Static local:SettingsClass.MyCustomDuration}"
I'm trying to create a storyboard in XAML that animates a property of one of the child elements of an element which raises an event. But I can't seem to get it to work without using Names, which is something I can't really do in this specific situation.
I'm basically trying something like this (much simplified of course):
<Canvas>
<Canvas.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Children[0].(Canvas.Left)" From="0" To="400" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Canvas.Triggers>
<Button Canvas.Left="20" Canvas.Top="20">A</Button>
<Button Canvas.Left="40" Canvas.Top="20">B</Button>
</Canvas>
Any ideas on how this could be achieved?
Providing that the UIElement you are indexing in the animation exists (i.e. already present on the Canvas) then you can do the following:
<Canvas x:Name="MyCanvas">
<Button x:Name="btn" Canvas.Left="20" Canvas.Top="20">A</Button>
<Button Canvas.Left="40" Canvas.Top="20">B</Button>
<Canvas.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.Target="{Binding ElementName=MyCanvas, Path=Children[0]}"
Storyboard.TargetProperty="(Canvas.Left)" From="0" To="400" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Canvas.Triggers>
</Canvas>
Notice how I have moved the addition of the Buttons above the Trigger. If the Buttons are below the Trigger as in your question, trying to access Children[0] will throw an ArgumentOutOfRangeException because there are no children at this point.
To use the Storyboard.TargetProperty in the animation, it should always be a dependency property. Children property gets a UIElementCollection of child elements of this Panel (Canvas). Therefore, the following construction Children [n] return UIElement, which should lead to a certain type, to access its dependency property.
This can be done in the code as follows:
Button MyButton = (Button)MyCanvas.Children[0];
MessageBox.Show(MyButton.Width.ToString());
All of these actions missing in the animation by default, this is your construction will not work.
I propose to create animations in the code where this conversion possible.
To demonstrate this, I created a Canvas, in the event Loaded having registered animation. Element number is set via an attached dependency property (of course, the example can be implemented in various ways). Below is my example:
XAML
<Grid>
<local:MyCanvas x:Name="MyCanvas" local:ClassForAnimation.Children="1">
<Button Canvas.Left="20" Canvas.Top="20">A</Button>
<Button Canvas.Left="40" Canvas.Top="20">B</Button>
</local:MyCanvas>
</Grid>
Code behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
public class MyCanvas : Canvas
{
public MyCanvas()
{
this.Loaded += new RoutedEventHandler(MyCanvas_Loaded);
}
private void MyCanvas_Loaded(object sender, RoutedEventArgs e)
{
MyCanvas myCanvas = sender as MyCanvas;
// Get No. of children
int children = ClassForAnimation.GetChildren(myCanvas);
// Get current Button for animation
Button MyButton = (Button)myCanvas.Children[children];
if (myCanvas != null)
{
DoubleAnimation doubleAnimation = new DoubleAnimation();
doubleAnimation.From = 0;
doubleAnimation.To = 400;
MyButton.BeginAnimation(Button.WidthProperty, doubleAnimation);
}
}
}
public class ClassForAnimation : DependencyObject
{
public static readonly DependencyProperty ChildrenProperty;
public static void SetChildren(DependencyObject DepObject, int value)
{
DepObject.SetValue(ChildrenProperty, value);
}
public static int GetChildren(DependencyObject DepObject)
{
return (int)DepObject.GetValue(ChildrenProperty);
}
static ClassForAnimation()
{
PropertyMetadata MyPropertyMetadata = new PropertyMetadata(0);
ChildrenProperty = DependencyProperty.RegisterAttached("Children",
typeof(int),
typeof(ClassForAnimation),
MyPropertyMetadata);
}
}
Note: Access to the items in the Canvas should only be done in the event Loaded, or when it ended. Otherwise, the items are not available because they are not loaded.
I am making an WPF application following MVVM pattern. I have one button and textblock. TextBlock is only shown when its text is not empty. On start of application text is empty to textblock is not shown. When i click button sample text is set and textblock is shown. And when i click again button text is set to empty and textblock hides.
Now what I want is that when text is set there start animation (fading) opacity changes from 0 to 1 in 5 seconds.
Here is my XAML
<TextBlock Text="{Binding StatusMessage}" Visibility="{Binding IsStatusMessageVisible}" />
<Button Content="UpdateText" Command="{Binding UpdateTextCommand}" />
And here is my ViewModel.
private string _statusMessage;
public string StatusMessage
{
get { return _statusMessage ?? (_statusMessage = string.Empty); }
set
{
_statusMessage = value;
NotifyOfPropertyChange(() => IsStatusMessageVisible);
NotifyOfPropertyChange(() => StatusMessage);
}
}
public System.Windows.Visibility IsStatusMessageVisible
{
get
{
return (string.IsNullOrEmpty(StatusMessage))
? System.Windows.Visibility.Collapsed
: System.Windows.Visibility.Visible;
}
}
public void UpdateText()
{
if (string.IsNullOrEmpty(StatusMessage))
StatusMessage = Properties.Resources.WaitMessageStatus;
else
StatusMessage = string.empty;
}
I just want that when StatusMessage text is set animation runs.
Follow these steps:
declare a TextChanged event in your ViewModel
in the set method of your StatusMessage property, before the line "_statusMessage = value;" raise your TextChanged event if (_statusMessage != value && !string.IsNulOrEmpty(value));
in your XAML, create a StoryBoard to change the opacity of the TextBlock
in you XAML, add a ControlStoryBoardBehavior to the TextBlock, and select your TextChanged event and your StoryBoard
First, get rid of the "Visibility" property on your viewmodel, that doesn't belong there...it should be a boolean. Then, create a style for your TextBlock. In that style, add DataTrigger to bind to the boolean "IsVisible" property. Insdide of the DataTrigger, create a storyboard:
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<!--Animation code in here />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
For an example of how to do opacity animation, just Google it...but here's one way
One way to run an animation on a TextChanged in a TextBlock is the following:
<TextBlock
Text="{Binding MyText, NotifyOnTargetUpdated=True}">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="(TextBlock.Opacity)"
From="0.0"
To="1.0"
Duration="0:0:0.300" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
NB: This code is only for learning purposes: the animation itself is not very useful or nice.
I have an application where there is a MainWindow at the top level with multiple UserControls housed inside it. I have a Button on one UserControls and want to trigger an animation in another UserControls. How to go about it? I have tried with Blend but the timeline does not allow me to access the other UserControls.
In short, I want to display a UserControl (say X) beside my existing application that will fade in on a button click. The button click is in another user control say Y, and both the UserControl X and the UserControl Y are inside MainWindow. I hope I have made myself clear.
An example:
<local:TimeBox x:Name="timeBox">
<local:TimeBox.RenderTransform>
<TranslateTransform />
</local:TimeBox.RenderTransform>
</local:TimeBox>
<local:CustomComboBox>
<local:CustomComboBox.Triggers>
<EventTrigger RoutedEvent="local:CustomComboBox.ApplyClick">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.Target="{x:Reference timeBox}"
Storyboard.TargetProperty="RenderTransform.X"
From="-500" To="0" Duration="0:0:1">
<DoubleAnimation.EasingFunction>
<ExponentialEase Exponent="5" EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</local:CustomComboBox.Triggers>
</local:CustomComboBox>
Notes:
1 - The TranslateTransform cannot have a name so you need to navigate to it starting from the UserControl using RenderTransform.X
2 - The event that should trigger the animation needs to be a RoutedEvent, here is the code for the one i have:
public static RoutedEvent ApplyClickEvent = EventManager.RegisterRoutedEvent("ApplyClick",
RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(CustomComboBox));
public event RoutedEventHandler ApplyClick
{
add { AddHandler(ApplyClickEvent, value); }
remove { RemoveHandler(ApplyClickEvent, value); }
}
//Pipes the event from an internal button.
private void Button_Apply_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(ApplyClickEvent, this));
}