Is it possible to have nested visual states. What I mean is if a ParentControl has a ChildControl and both have their own visual states, is it possible by setting the state of the ParentControl to change the state of the ChildControl accordingly.
You need to call the GoToState method to change the visual state of the child control.
Since you need to call a method you can't use Storyboards in visual state manager of a parent control since these can only animate properties.
Hence you need to write some code in the child control. To monitor the state of the parent and respond appropriately.
There are a number of different ways to do this but the crucial nugget of info is to use the VisualStateManager.GetVisualStateGroups method to find the VisualStateGroup on the parent that you are interested in, then attach to that group's CurrentStateChanging event. Hence code in the Child control can be notified when a state its interested in is being transitioned to by the parent and can call GoToState appropriately against itself.
I'm just going to declare a new dependency property:
public static readonly DependencyProperty StateProperty =
DependencyProperty.Register("State",
typeof( string ),
typeof( TextBlockControl ),
new PropertyMetadata("Top",
new PropertyChangedCallback(StateChanged)));
[Category("DigItOut"), Description("State")]
public string State
{
get
{
return this.GetValue(StateProperty).ToString();
}
set
{
this.SetValue(StateProperty, value);
}
}
private static void StateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
if (!String.IsNullOrEmpty(args.NewValue.ToString()))
VisualStateManager.GoToState(sender as TextBlockControl, args.NewValue.ToString(), true);
}
And then just set it from it's parents state:
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="States">
<VisualState x:Name="Reverse">
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="textBlockControl" Storyboard.TargetProperty="(TextBlockControl.State)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<System:String>Bottom</System:String>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Straight"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
But if I still want control over the use of transitions, then I'll have to find another solution. Probably second property.
Related
Trying to port to WPF some behavior from an old Silverlight application that allowed users to configure their view by moving/minimizing/maximizing various UserControls. Several StoryBoards were declared in UserControl.Resources and later accessed from code via their x:Name.
<Storyboard x:Name="MaximizeStoryboard" Completed="MaximizeStoryboard_Completed" Storyboard.TargetName="WholeControl">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(FrameworkElement.Width)">
<SplineDoubleKeyFrame x:Name="MaximizeWidth" KeyTime="00:00:00.3000000" Value="0"/>
</DoubleAnimationUsingKeyFrames>
...
In WPF, after changing x:Name to x:Key, I can access the Storyboard via:
Storyboard maximizeStoryboard = (Storyboard)Resources["MaximizeStoryboard"];
Previously used
MaximizeWidth.Value = Container.Contents.ActualWidth;
to set the restore width (and other values) from code, before beginning the animation. That doesn't work in WPF. What is the proper way to get access so those values can be set from code?
One of the possible way can be go through the children of Storyboard, find DoubleAnimationUsingKeyFrames child and then get the first KeyFrame.
Storyboard maximizeStoryboard = (Storyboard)Resources["MaximizeStoryboard"];
var doubleAnimationUsingKeyFrames = maximizeStoryboard.Children.OfType<DoubleAnimationUsingKeyFrames>().FirstOrDefault();
if (doubleAnimationUsingKeyFrames != null)
{
if (doubleAnimationUsingKeyFrames.KeyFrames.Count > 0)
{
var keyFrame = doubleAnimationUsingKeyFrames.KeyFrames[0] as SplineDoubleKeyFrame;
if(keyFrame !=null)
{
keyFrame.Value = Container.Contents.ActualWidth;
}
}
}
I have a a custom Panel that raises a RoutedEvent which is defined globally in a static class :
public class CompressItemsToFitStackPanel : StackPanel
{
protected override Size ArrangeOverride(Size arrangeSize)
{
// some logic
// Raise Attached Event
CustomEventManager.RaiseArrangeEvent(this);
return base.ArrangeOverride(arrangeSize);
}
}
My attached Event:
public static class CustomEventManager
{
public static readonly RoutedEvent ArrangeEvent = EventManager.RegisterRoutedEvent("Arrange",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(CustomEventManager));
internal static void RaiseArrangeEvent(UIElement target)
{
var args = new RoutedEventArgs(ArrangeEvent);
target.RaiseEvent(args);
}
}
this Panel is the items panel for an Items control ,
the ItemsTemplate as an EventTrigger which i wan't to fire when the attached event is raised:
<DataTemplate DataType="{x:Type local:Checker}" x:Key="CheckerTempalte">
<Ellipse x:Name="ellipse" Style="{StaticResource checkerStyle}">
<Ellipse.Triggers>
<EventTrigger RoutedEvent="local:CustomEventManager.Arrange">
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)"
Storyboard.TargetName="ellipse"
>
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="{Binding Val}" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
The Event trigger is not triggered ,
Maybe i'm not using AttachedEvents correctly or it is not Declared correctly
i need the event from the panel to propagate and trigger the EventTriggers in the Child elements ,
any ideas what i'm doing wrong ?
EDIT :
After dkozl's insight i came to the conclusion that i need an AddXXXHandler and RemoveXXXHandler in order for the XAML to add/ remove the handler for the EventTrigger
public static void AddArrangeHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.AddHandler(CustomEventManager.ArrangeEvent, handler);
}
}
public static void RemoveArrangeHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.RemoveHandler(CustomEventManager.ArrangeEvent, handler);
}
}
but still nothing happens , and i never reach these methods .
EDIT 2 :
thanks to dkozl's comments below ,
the Event is raised for each child element , since he the ellipses are down the
Visual Tree .
protected override Size ArrangeOverride(Size arrangeSize)
{
foreach (UIElement child in children)
{
CustomEventManager.RaiseArrangeEvent(child);
}
}
but still nothing happens , iv'e also tested the 'StoryBoard' by giving the 'EventTrigger'
the 'MouseEnter' event and moving my mouse over the element , it works fine.
still , even raising the event on each Ellipse still does not work ...
any ideas ?
Another point of interest is that the child elements are actually the 'Checker' type
and not the Ellipse which the DataTemplate represents , i don't get how 'Checker' is considered a UIElement .?
What you've created is standard RoutedEvent, not an attached one. Add/Remove handler is different for attached events. You'll need two separate methods (AddArrangeChildrenHandler and RemoveArrangeChildrenHandler). It has been explained on MSDN site
UPDATE:
I've copied updated Ellipse definition and CustomEventManager class into my application, added test button which calls CustomEventManager.RaiseArrangeEvent(ellipse); when clicked and it works for me.
I also had to add Ellipse.RenderTransorm of TransformGroup, fourth transformation being TranslateTransform to make it work like in the example
UPDATE2: Event is raised on the panel where the Ellipse is placed which means that bubbling event would never reach it as it will start from the panel and go up the visual tree to Window never reaching children of the panel
You should maybe consider a code only approach to this problem. If you wish to define the animations in XAML you can possibly StaticResource them into properties on the CompressToFitStackPanel. But if your DataTemplate is referring to a specific transform by index you are definitely losing the power and abstraction of DataTemplate'ing.
Reevaluate your problem space and what the design is supposed to solve. Use ILSpy or Reflector to analyze how other Panels solved problems with properties or Attached Dependency Properties (Grid.Column, Grid.Row).
Place more of the burden on the new layout panel and less on those who use it.
Good luck.
My application has a few background worker, each doing different work. When I click the 'Start' button, all backgroundworker will start simultaneously.
in my xaml, I had defined my animation of a rotate image:
<window.Resources>
<Storyboard x:Key="imageRotate">
<DoubleAnimation Storyboard.TargetName="transRotate"
Storyboard.TargetProperty="(Image.RenderTransform).(RotateTransform.Angle)"
From="0" To="360"
Duration="0:0:0.5"
AutoReverse="False"
RepeatBehavior="Forever"/>
</Storyboard>
</window.Resources>
I want the animation to begin when all the backgroundworker started, and stop ONLY after ALL background worker stopped.
I have a property call AreWorkersBusy:
private bool _areWorkerBusy;
public bool AreWorkerBusy
{
get
{
return _areWorkerBusy;
}
set
{
bool isBusy = false;
foreach(BackgroundWorker worker in BackgroundWorkerList)
{
if(worker.IsBusy)
isBusy = true;
}
_areWorkerBusy = isBusy;
}
}
but it's not dependencyProperty, so I can't bind to my animation's DataTrigger.
Any workaround???
Help!
One of the possible ways,
You can implement INotifyPropertyChanged to notify 'AreWorkerBusy' changes, create a dependency property in the control and bind 'AreWorkerBusy' with it.
Create two routed events one to start animation and another to stop animation.
In the property changed handler for your DP, raise the specific routed event.
In your control write event triggers, and based on the event start of stop the animation.
I had written a similar experience, http://keepitsimpleengineer.blogspot.com/2010/09/wpf-circular-progress-control-part-2.html
There's something fundamental about VisualStateGroups that I'm not understanding. Everything I've read has led me to believe that they are orthogonal. That is, a state change in one group won't affect other groups. Indeed, they would be rather pointless if this were not the case.
However, in an attempt to understand some odd behavior I was encountering, I put together a simple example that shows that a state change in one group can trigger an animation in another. I'm trying to understand how this can be.
All I want is a ToggleButton-based control to have one appearance when toggled (IsChecked == true) regardless of whether it has focus or whether the mouse is over it, for example.
Firstly, I have a very simple control that transitions between custom states in a custom group:
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
[TemplateVisualState(Name = normalState, GroupName = activationGroupName)]
[TemplateVisualState(Name = hoverState, GroupName = activationGroupName)]
[TemplateVisualState(Name = activatedState, GroupName = activationGroupName)]
public class CustomControl : ToggleButton
{
private const string activationGroupName = "Activation";
private const string normalState = "Normal";
private const string hoverState = "Hover";
private const string activatedState = "Activated";
public CustomControl()
{
this.DefaultStyleKey = typeof(CustomControl);
this.Checked += delegate
{
this.UpdateStates();
};
this.Unchecked += delegate
{
this.UpdateStates();
};
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.UpdateStates();
}
protected override void OnMouseEnter(MouseEventArgs e)
{
base.OnMouseEnter(e);
this.UpdateStates();
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
this.UpdateStates();
}
private void UpdateStates()
{
var state = normalState;
if (this.IsChecked.HasValue && this.IsChecked.Value)
{
state = activatedState;
}
else if (this.IsMouseOver)
{
state = hoverState;
}
VisualStateManager.GoToState(this, state, true);
}
}
Secondly, I have a template for this control that changes the background color based on the current state:
<Style TargetType="local:CustomControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:CustomControl">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Activation">
<VisualStateGroup.Transitions>
<VisualTransition To="Normal" GeneratedDuration="00:00:0.2"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal"/>
<VisualState x:Name="Hover">
<Storyboard>
<ColorAnimation Duration="00:00:0.2" To="Red" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Activated">
<Storyboard>
<ColorAnimation Duration="00:00:0.2" To="Blue" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="grid" Background="White">
<TextBlock>ToggleButton that should be white normally, red on hover, and blue when checked</TextBlock>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
For the most part it works, but when the control loses focus it transitions back to the normal state.
I can get around this by explicitly handling focus changes in my control and enacting a state update:
public CustomControl()
{
this.DefaultStyleKey = typeof(CustomControl);
this.Checked += delegate
{
this.UpdateStates();
};
this.Unchecked += delegate
{
this.UpdateStates();
};
// explicitly handle focus changes
this.GotFocus += delegate
{
this.UpdateStates();
};
this.LostFocus += delegate
{
this.UpdateStates();
};
}
However, it makes no sense to me why I have to do this.
Why is a state change in one VisualStateGroup causing an animation defined by another to execute? And what is the simplest, most correct way for me to achieve my stated goal?
Thanks
Note how the GotoState method simply takes the state name without any qualification as to which group it belongs to. Whilst the groups separate the set of states that a control can hold at any one time (one state per group) the state names are expected to be unique across all states in the manager.
Note also that the ToggleButton from which you derive has the following attribute associated with the class:-
[TemplateVisualStateAttribute(Name = "Normal", GroupName = "CommonStates")]
This indicates that the logic you have inherit will at some point call GotoState with the state "Normal". Typically controls only have one UpdateStates method (as you have) and at any point where any of the states may have changed it will be called. This function in turn will call GotoState multiple times with one state name from each group. Hence at the point when the control loses focus the ToggleButton calls GotoState to set the "Unfocused" state but along with that will also call it with "Normal" and either "Checked" or "Unchecked".
Now you've got your own template where the states "Unfocused", "Checked" are not present so those calls to GotoState from ToggleButton don't do anything. However you do have "Normal". The VSM has no way of knowing that the ToggleButton use of "Normal" is intended to choose from its default template's "CommonStates" group. Instead it just finds a VisualState with the name "Normal" which in this case is found in your "Activiation" group. Hence your existing state of "Activated" is replaced with your "Normal".
Change your "Normal" to "Blah" and things work as you expect.
(It helps to remember that the group names don't really do a lot despite their presence in the TemplateVisualStateAttribute, their only use I can think of is in the Blend UI)
I have the following storyboard:
<Window.Resources>
<Storyboard x:Key="ButtonsAnim">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="topRightButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-100"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="topRightButton" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="100"/>
...
It basically moves some buttons around in a canvas.
This is the code that starts the animation:
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Storyboard sb = (Storyboard)Resources["ButtonsAnim"];
storyBoard = sb;
storyBoard.Begin(this, true);
}
What I am trying to do is to reset the animation when I click a button which hides the window. When the window reappears the animation should start from the beginning.
I tried using storyBoard.Begin(this, true) when the application reappears but for this first milliseconds the buttons are at their last position.
I then tried storyBoard.seek(TimeSpan.Zero) before hiding the window but it fails:
System.Windows.Media.Animation
Warning: 6 : Unable to perform action
because the specified Storyboard was
never applied to this object for
interactive control.; Action='Seek';
Storyboard='System.Windows.Media.Animation.Storyboard';
Storyboard.HashCode='24901833';
Storyboard.Type='System.Windows.Media.Animation.Storyboard';
TargetElement='System.Windows.Media.Animation.Storyboard';
TargetElement.HashCode='24901833';
TargetElement.Type='System.Windows.Media.Animation.Storyboard'
I also tried storyBoard.remove(this) before hiding the window, same effect: the buttons are at their last position.
Any ideas?
Thank you.
To use StoryBoard.Remove() this way - you should keep reference to your storyboard object.
Like this:
Storyboard myStoryBoard;
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
myStoryBoard = (Storyboard)Resources["myStoryBoard"];
myStoryBoard.Begin();
}
void sbRemoveEvent()
{
myStoryBoard.Remove();
}
I think Storyboard.Stop() should work here. But if you not find anything elegant, you can try reset buttons' transform after you've hided the window. E.g.:
((TranslateTransform)((TransformGroup)topRightButton.RenderTransform)[3]).X = 0;
((TranslateTransform)((TransformGroup)topRightButton.RenderTransform)[3]).Y = 0;
Hope I didn't make any mistake while casting.
NB: You may also find useful this example from MSDN: How to: Control a Storyboard After It Starts