Access ItemsControl Items and Animate One by One - wpf

Today is a good day since I started with WPF, this for a launcher I'm creating.
Using the following code, I managed to get the result to be seen in the screenshot:
<Grid>
<ItemsControl ItemsSource="{Binding Programs}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Text}" Background="Transparent" Foreground="White" Width="128" Height="150" >
<Button.RenderTransform>
<TransformGroup>
<ScaleTransform />
</TransformGroup>
</Button.RenderTransform>
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Grid.Row="0" Source="{Binding Image}" Height="128" />
<ContentPresenter Grid.Row="1" HorizontalAlignment="Center" Margin="3,10" />
<Rectangle Grid.Row="0" Fill="{TemplateBinding Background}" />
<Rectangle Grid.Row="1" Fill="{TemplateBinding Background}" />
</Grid>
</ControlTemplate>
</Button.Template>
<Button.Resources>
<Storyboard SpeedRatio="4" x:Key="MouseEnterStoryboard" x:Name="MouseEnterStoryboard">
<ColorAnimation Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)" To="#22FFFFFF"></ColorAnimation>
</Storyboard>
<Storyboard SpeedRatio="4" x:Key="MouseLeaveStoryboard" x:Name="MouseLeaveStoryboard">
<ColorAnimation Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)" To="Transparent"></ColorAnimation>
</Storyboard>
<Storyboard Duration="00:00:00.05" x:Key="MouseClickStoryboard" AutoReverse="True">
<DoubleAnimation To="0.8" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"/>
<DoubleAnimation To="0.8" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"/>
</Storyboard>
<Storyboard x:Key="WindowLoadedStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="00:00:01" />
</Storyboard>
</Button.Resources>
<Button.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<BeginStoryboard Storyboard="{StaticResource MouseEnterStoryboard}" />
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
<BeginStoryboard Storyboard="{StaticResource MouseLeaveStoryboard}" />
</EventTrigger>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard Storyboard="{StaticResource MouseClickStoryboard}" />
</EventTrigger>
<EventTrigger RoutedEvent="Window.Loaded">
<BeginStoryboard Storyboard="{StaticResource WindowLoadedStoryboard}"></BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Screenshot:
Now, for each item in the list bound to this control, it will create a button.
How would I access this button programmatically, better yet, how would I access one of the Storyboards programatically since assigning a name (x:to them simply won't do the trick it seems...
Also, how can I animate the buttons one by one? Currently they each fade in at exact the same time (# WindowLoadedStoryboard), but I would like to let each button fade in one by one with a short delay, to create a nice effect. How would I achieve this?
Hope someone can answer these 2 questions for me!
Greetings!

Your problem with accessing the elements defined in the DataTemplate is caused because you defined those elements in a DataTemplate... those elements could be rendered in many different types of UI container controls. You can find the solution in the How to: Find DataTemplate-Generated Elements page from MSDN.
You first need to get hold of the relevant container control that contains the item that has had that DataTemplate applied to it. Next, you need to get the ContentPresenter from that container control and then you can get the DataTemplate from ContentPresenter. Finally, you can access the named elements from the DataTemplate. From the linked page:
// Getting the currently selected ListBoxItem
// Note that the ListBox must have
// IsSynchronizedWithCurrentItem set to True for this to work
ListBoxItem myListBoxItem = (ListBoxItem)(myListBox.ItemContainerGenerator.
ContainerFromItem(myListBox.Items.CurrentItem));
// Getting the ContentPresenter of myListBoxItem
ContentPresenter myContentPresenter = FindVisualChild<ContentPresenter>(myListBoxItem);
// Finding textBlock from the DataTemplate that is set on that ContentPresenter
DataTemplate myDataTemplate = myContentPresenter.ContentTemplate;
TextBlock myTextBlock =
(TextBlock)myDataTemplate.FindName("textBlock", myContentPresenter);
// Do something to the DataTemplate-generated TextBlock
MessageBox.Show("The text of the TextBlock of the selected list item: " +
myTextBlock.Text);

Related

Expander getting stuck after IsExpanded set to true by storyboard

So I have an expander that I want to have the normal functionality (open and close with its own button) but I also want a different button to expand it when pressed (this button is in the header of the expander). I'm using a storyboard in an event trigger for the Button.Click which works, but after it is expanded this way the normal button doesn't work, it just stays expanded. My xaml is below, I would really prefer to keep this all in the xaml, I could come up with a way to do it in the codebehind/viewmodel myself.
<Expander x:Name="IndexExpander" IsExpanded="True" Grid.Row="4" Grid.ColumnSpan="5" Margin="10" MaxHeight="150">
<Expander.Triggers>
<EventTrigger SourceName="btnAddIndex" RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="IndexExpander" Storyboard.TargetProperty="IsExpanded" BeginTime="0:0:0.25" Duration="0:0:0.20" >
<DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Expander.Triggers>
<Expander.Header>
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
<TextBlock Text="Indexes" FontWeight="Bold"/>
<!-- Add/Delete Buttons-->
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*"/>
<ColumnDefinition Width="50*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" x:Name="btnAddIndex" Command="{Binding AddIndexCommand}" Template="{StaticResource AddButtonTemplate}" IsEnabled="{Binding IsEditable}" Margin="0,0,5,0" />
</Grid>
</StackPanel>
</Expander.Header>
Alright, so for anyone following in my footsteps here's what I did. I got the idea from here, and adapted it until it worked correctly.
<Expander.Triggers>
<EventTrigger SourceName="btnAddCol" RoutedEvent="Button.Click">
<BeginStoryboard x:Name="ColumnExpanderStory">
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="ColumnExpander" Storyboard.TargetProperty="IsExpanded">
<DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="ToggleButton.PreviewMouseUp">
<RemoveStoryboard BeginStoryboardName="ColumnExpanderStory" />
</EventTrigger>
</Expander.Triggers>
<Expander.Header>
The problem was that the storyboard overrides any other bindings to the IsExpanded property, so it has to be removed to restore them (read more here). The suggestion was to use the ToggleButton.Checked event to remove the storyboard, but that didn't work for me, only the "Preview" events seemed to have the right timing. I started with PreviewMouseDown, but it would remove the storyboard, then on mouse up toggle the expander, meaning the first click would just flip states back and forth quickly. Using PreviewMouseUp got around that issue.

XAML animation of height of control with dynamic content

I have a panel that should be minimized unless the user hovers the mouse over the panel. It is implemented using a storyboard that lets the height of the panel grow when the use puts the mouse over the control. At the moment the target height is hard coded to 400 which is a bad solution as the content of the panel will be different each time the application starts (it is static during execution).
How do you create an animation that lets the panel grow to the size of the current content?
<Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="500" Width="525">
<Grid>
<Border Margin="10,0" Background="LightGray" HorizontalAlignment="Left" VerticalAlignment="Top" CornerRadius="0,0,8,8">
<Border.Effect>
<DropShadowEffect Opacity="0.5"/>
</Border.Effect>
<Border.Triggers>
<EventTrigger RoutedEvent="Border.MouseEnter">
<BeginStoryboard>
<BeginStoryboard.Storyboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height"
From="25"
To="400"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard.Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Border.MouseLeave">
<BeginStoryboard>
<BeginStoryboard.Storyboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height"
From="400"
To="25"
Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard.Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<StackPanel Margin="5">
<TextBlock Height="25" Text="My items panel" />
<ListBox MinWidth="150" MinHeight="100" ItemsSource="{Binding MyItems}" />
</StackPanel>
</Border>
</Grid>
Edit: I have tried with binding to the Height of the StackPanel but that didn't really help as it didn't take the margins of the stackpanel into account thus making the panel shorter than needed.
<DoubleAnimation Storyboard.TargetProperty="Height"
From="{Binding ElementName=NameOfStackPanel, Path=ActualHeight}"
To="25"
Duration="0:0:0.2" />
You could create a converter to handle adding the margins to the ActualHeight of your StackPanel. You could even use a multivalue convertor so you could bind the margin too and not have to hardcode a fudge factor. Finally, you could probably wrap your stackpanel in another panel (without margins) and bind to the height of that instead.

Can't get (fairly simple) WPF animation working

I'm trying to get a message panel animated in WPF but has so far achieved no success.
This is the situation:
I have a user control with a StackPanel containing an ItemsControl bound to an (observable) collection in the control's View Model object (ViewModel.Messages).
When I need to present the user with messages I ad those (as MessageVM instances) to the observable collection.
The ItemsControl's visibility is bound to an integer property called ViewModel.CountVisibleMessages and there's a converter taking care of translating 0 to Visibility.Hidden and positive values to Visibility.Visible.
This works just fine. When a message gets added to the collection the StackPanel automatically becomes visible and as the user (or a timer) removes the last message it gets hidden. The StackPanel height is automatically adjusted to fit all messages of course.
To make everything look nicer I would prefer it if the StackPanel resized itself using an animation running for, say, 300 ms. (Ultimately I would also like it to accelerate and deccelerate but that's beyond my ambition right now.
I have experimented for a few hours now but I feel I'm not even close.
Below is my current (not even close to working) XAML at the moment:
<StackPanel Orientation="Vertical"
VerticalAlignment="Top"
Visibility="{Binding CountVisibleMessages, Converter={StaticResource IntToVisibility}}"
Height="Auto"
Background="{DynamicResource HmiBackColorLightBrush}">
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding CountVisibleMessagesChanged}" Value="True" ><!-- I suppose I shopuld've used a Routed Event here but I just needed to get it triggered -->
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Margin.Bottom"
From="100" <!-- Just a made up value to test the concept -->
To="0"
Duration="0:0:0:3"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<ItemsControl ItemsSource="{Binding Messages}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Style="{DynamicResource Message}">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="15" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Text}" Margin="3" Style="{Binding MessageType, Converter={StaticResource MessageTypeToStyle}, ConverterParameter={x:Type TextBlock}}" /> <!-- using dynamic styling here -->
<RadioButton Grid.Column="1" Style="{DynamicResource HmiCloseMessageButton}" IsChecked="{Binding IsVisible, Converter={StaticResource BoolToVisibility}, ConverterParameter=true}" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
(I do realize the above XAML won't get the StackPanel to auto-resize slowly. It's just an experiment to get anything happening).
This can't be too difficult I suppose (it's a pretty standard UI behavior in many programs) so I'd appreciate it if anyone could point me in the right directions.
Cheers
Are you sure you want to adjust the bottom margin? The stack panel VerticalAlignment is top. If you want to change the Height then bind your StoryBoard Property to Height. Do you know if your StoryBoard is firing?
The key point is ExitActions are necessary for EnterAction based dataTrigger animations. So the following seems to be working in my case ....
<StackPanel Grid.Row="0" Height="100" x:Name="MyGrid">
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding ElementName=MyCheckBox,
Path=IsChecked}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.Target="{Binding
RelativeSource={RelativeSource
AncestorType={x:Type
StackPanel}},
BindsDirectlyToSource=True}"
Storyboard.TargetProperty="Height"
From="100"
To="200"
Duration="0:0:1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.Target="{Binding
RelativeSource={RelativeSource
AncestorType={x:Type
StackPanel}},
BindsDirectlyToSource=True}"
Storyboard.TargetProperty="Height"
To="100"
Duration="0:0:1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
</StackPanel>
<CheckBox x:Name="MyCheckBox" Grid.Row="1" />
Let me know if this helps.

Improve animation smoothness (moving of controls)

I have implemented the animation of moving of a grid control in the following manner:
<Grid
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger
Binding="{Binding ElementName=rootLayout, Path=IsVisible}"
Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ThicknessAnimation
Storyboard.TargetProperty="Margin"
From="-500,0,0,0"
To="0,0,0,0"
Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Border
Grid.RowSpan="2"
Background="Black"
CornerRadius="6" >
<Border.Effect>
<DropShadowEffect />
</Border.Effect>
</Border>
<TextBlock
Grid.Row="0"
Width="400"
Height="200"
Margin="20,20,20,10"
Text="{Binding Path=MessageText}" />
<Button
Grid.Row="1"
Margin="20,5,20,15"
HorizontalAlignment="Right"
Width="75"
Content="OK"
Command="{Binding Path=CloseDialogCommand}" />
</Grid>
The animation works fine, but it's ugly. It is shaky / jittery / jerky and it really looks unprofessional. Is there a way to improve this? Am I using the right approach with animating the value change on the Margin property in order to move the grid? I've read about RenderTransform, but I don't know how to use it in my case.
Also, the animation looks unnatural. I know this can be improved but I don't know how. What are these properties and can they help me enhance my animation:
AccelerationRatio
DecelerationRatio
EasingFunction
IsAdditive
IsCumulative
SpeedRatio
Thanks for helping!
P.S. I am trying to put as much code as possible in XAML, so I'd prefer that approach, but really, if there's anything to improve this...
Use easing functions, a simple DoubleAnimation and RenderTransform, e.g.
<Button Content="Test">
<Button.RenderTransform>
<TranslateTransform/>
</Button.RenderTransform>
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="RenderTransform.X"
From="-500" To="0">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
This should be quite smooth, check out this overview on easing functions to get an idea of how they affect the animation.
Also note that the duration has a strong effect on what an animation looks like, depending on what easing function you use setting high duration values is needed because they slow down significantly at the end.

Animation from within DataTemplate

Below is the data template I'm using for a listbox's ItemTemplate. It shows some simple data, and a button, which ideally should animate a Popup, also contained within the DataTemplate. Unfortunately the whole thing blow up at runtime. The error says line 52 is wrong, which is:
<EventTrigger RoutedEvent="Button.Click">
Here's the whole DataTemplate. I've used this popup before, with the same exact content templates and even the same animation elsewhere. It's only blowing up when I try to put it into a DataTemplate. I assume the animation is having difficulty finding the right animation target - I'm hoping someone here would know more.
<DataTemplate x:Key="ItemTemplate2">
<Border Width="100" Height="100" BorderThickness="4" BorderBrush="Red">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Price}"/>
<Popup x:Name="popupContent" IsOpen="True" Margin="10,0,0,0" Grid.Row="0" >
<Popup.Child>
<Thumb x:Name="thumbContent" DragDelta="Thumb_DragDelta" Width="0" Height="0">
<Thumb.Template>
<ControlTemplate>
<local:thumbTemplate Margin="0" x:Name="df" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Popup.Child>
</Popup>
<Button Content="Show">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<BeginStoryboard.Storyboard>
<Storyboard x:Name="sbShowPopup">
<DoubleAnimation Duration="0:0:1" To="200" Storyboard.TargetProperty="(FrameworkElement.Width)" Storyboard.TargetName="popupContent" d:IsOptimized="True"/>
<DoubleAnimation Duration="0:0:1" To="80" Storyboard.TargetProperty="(FrameworkElement.Height)" Storyboard.TargetName="popupContent" d:IsOptimized="True"/>
</Storyboard>
</BeginStoryboard.Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
</StackPanel>
</Border>
</DataTemplate>
You may have used this before but not in Silverlight. The only supported value for RoutedEvent in Silverlight is "FrameworkElement.LoadedEvent".
You will need the BlendSDK to do this sort of thing in Silverlight.

Resources