I have a LongListSelector which is populated with some items. Each item has a submenu which can be visible or collapsed using a sliding animation. The problem is that the animation is extremely slow depending on which item you tap in the list. At the start and the end it's slow, in the middle it's smooth. I suspect that each animation frame invalidates the longlistselector which means the entire visual tree must update it's layout.
I use a datatrigger to start the animation. Because of that I can see that the triggers on lots of other items also get fired which indicates to me they are being redrawn. It also shows that when you tap in the middle of list a lot less other items get triggered as well. weird...
I hosted the testproject on github:
https://github.com/jessetabak/wpanimationproblem
The LongListSelector:
<phone:LongListSelector x:Name="LongList" Margin="0" Padding="0" ItemsSource="{Binding Items}"
HorizontalAlignment="Stretch" Background="Transparent">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<wegGooiApp2:ItemView />
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
The ItemView:
<UserControl.Resources>
<wegGooiApp2:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<Storyboard x:Key="ShowMenu">
<DoubleAnimation Storyboard.TargetProperty="(Grid.Height)"
Storyboard.TargetName="Submenu"
From="0" To="70" Duration="0:0:0.25" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Submenu"
Storyboard.TargetProperty="(Grid.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="HideMenu">
<DoubleAnimation Storyboard.TargetProperty="(Grid.Height)"
Storyboard.TargetName="Submenu"
From="70" To="0" Duration="0:0:0.25" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Submenu"
Storyboard.TargetProperty="(Grid.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.25">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- TEST ITEM -->
<Border Height="70" BorderBrush="Red" Background="Transparent" BorderThickness="1" HorizontalAlignment="Stretch">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Test Item" HorizontalAlignment="Stretch" FontSize="42" />
<Button Content="v" Grid.Column="1" Tap="Button_Tap" Tag="{Binding}">
</Button>
</Grid>
</Border>
<!-- SUBMENU -->
<Border x:Name="Submenu" Grid.Row="1" BorderBrush="Green" BorderThickness="1"
Visibility="{Binding SubMenuIsVisible, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneTime}"
Background="Green" Height="0" Margin="0 0 0 0">
<i:Interaction.Triggers>
<ec:DataTrigger Binding="{Binding SubMenuIsVisible}" Value="True">
<ec:CallMethodAction MethodName="MenuEnabled"
TargetObject="{Binding ElementName=ThisUserControl, Mode=OneWay}" />
</ec:DataTrigger>
<ec:DataTrigger Binding="{Binding SubMenuIsVisible}" Value="False">
<ec:CallMethodAction MethodName="MenuDisabled"
TargetObject="{Binding ElementName=ThisUserControl, Mode=OneWay}" />
</ec:DataTrigger>
</i:Interaction.Triggers>
<TextBlock Text="SubMenu" FontSize="42" />
</Border>
</Grid>
</UserControl>
The ItemView codebehind:
public partial class ItemView : UserControl
{
private Storyboard _showStoryboard;
private Storyboard _hideStoryboard;
public ItemView()
{
InitializeComponent();
_showStoryboard = (Storyboard) this.Resources["ShowMenu"];
_hideStoryboard = (Storyboard) this.Resources["HideMenu"];
Debug.WriteLine("ItemView CONSTRUCTED");
}
private void Button_Tap(object sender, GestureEventArgs e)
{
var button = (Button)sender;
var viewModelItem = (ItemViewModel)button.Tag;
viewModelItem.SubMenuIsVisible = !viewModelItem.SubMenuIsVisible;
}
public void MenuEnabled()
{
Debug.WriteLine("MENU ENABLED!");
if (Submenu.Visibility == Visibility.Collapsed)
{
_showStoryboard.Begin();
}
}
public void MenuDisabled()
{
Debug.WriteLine("MENU DISABLED!");
if (Submenu.Visibility == Visibility.Visible)
{
_hideStoryboard.Begin();
}
}
private void ThisUserControl_LayoutUpdated(object sender, EventArgs e)
{
//Debug.WriteLine("ITEMVIEW LAYOUT UPDATED!");
}
}
And what it looks like:
/edit 1
I tried turning it into an independent animation using a ScaleTransform, but this won't animate the surrounding ui elements. To fix this you can use a LayoutTransform instead of the standard RenderTransform. After some tweaking this worked quite nice, but the layouttranform turned it back in a slow depenpendent animation...
You are correct that changing the UserControl height causes a large portion of the visual tree to be invalidated, but this is required, and by design. The issue is that you are modifying a controls height in a storyboard to begin with, which is not an independent animation and can't run on the compositor.
Have a read of http://msdn.microsoft.com/en-us/library/windows/apps/jj819807.aspx#dependent although this is for Windows store apps (and there is EnableDependentAnimations flag in SL), the ideas remain the same. You need to figure out a way to expand items using independent animations, probably by using a ScaleTransform.
Related
Issue description
I'm developing an application in which I have a ListBox where, when an element is selected, it's details are shown in an editing control.
I'm binding the SelectedItem to the control and, as I want to apply different DataTemplates, I'm trying to use VM first approach and bind directly to the control contents.
My custom TextBox style displays validation errors with a blue border (for the sake of the example). However, when using this approach, a red validation border is also shown and it's not being removed once the data is correct. This is not the expected behavior, the red border should not show at all.
I don't know if the error is in the style or in the binding.
Example and testing
I've tried different things to try to debug the error. This is not happening with the standard style nor with a DataContext approach. However, I cannot use the DataContext approach as I will need to apply different templates to different types of elements in the list.
See the pictures below.
When the data is invalid (empty) the "VM First + Custom style" option shows both the blue and the red borders:
When I write some text, the red border is not removed:
ViewModels
There are two ViewModels, one for the main window and another for each element in the list:
public class ChildViewModel : ViewModelBase
{
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public override string this[string propertyName]
{
get
{
if (propertyName == nameof(Name))
{
if (string.IsNullOrEmpty(Name))
{
return "The name is mandatory.";
}
}
return base[propertyName];
}
}
}
public class ParentViewModel : ViewModelBase
{
private ChildViewModel _selectedItem;
public ObservableCollection<ChildViewModel> Collection { get; private set; }
public ChildViewModel SelectedItem
{
get => _selectedItem;
set => SetProperty(ref _selectedItem, value);
}
public ParentViewModel()
{
Collection = new ObservableCollection<ChildViewModel>();
Collection.Add(new ChildViewModel());
Collection.Add(new ChildViewModel());
}
}
public abstract class ViewModelBase : INotifyPropertyChanged, IDataErrorInfo
{
public event PropertyChangedEventHandler PropertyChanged;
public string Error => this[null];
public virtual string this[string propertyName] => string.Empty;
protected void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
NotifyPropertyChanged(propertyName);
}
}
}
Views
The MainWindow is just as follows, with no code behind apart from the standard.
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestApp"
Title="MainWindow" Height="300" Width="400">
<Window.DataContext>
<local:ParentViewModel />
</Window.DataContext>
<Window.Resources>
<ResourceDictionary Source="Style.xaml" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox Grid.RowSpan="2" ItemsSource="{Binding Collection, Mode=OneWay}" SelectedItem="{Binding SelectedItem}"/>
<UserControl Grid.Column="1" Grid.Row="0" DataContext="{Binding SelectedItem}">
<StackPanel Orientation="Vertical">
<Label Content="DataContext + No style" />
<TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="DataContext + Custom style" />
<TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TextBoxStyle}" />
</StackPanel>
</UserControl>
<ContentControl Grid.Column="1" Grid.Row="1" Content="{Binding SelectedItem}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:ChildViewModel}">
<StackPanel Orientation="Vertical">
<Label Content="VM First + No style" />
<TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="VM First + Custom style" />
<TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TextBoxStyle}" />
</StackPanel>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</Grid>
</Window>
Style
This is located in a ResourceDictionary named "Style.xaml". The Border and the ValidationErrorElement are separated elements in order to apply different visual states for mouse over, focused...
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Grid x:Name="RootElement">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ValidationStates">
<VisualState x:Name="Valid" />
<VisualState x:Name="InvalidUnfocused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationErrorElement">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="InvalidFocused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationErrorElement">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="Border" BorderThickness="1" BorderBrush="Black" Opacity="1">
<Grid>
<ScrollViewer x:Name="PART_ContentHost" BorderThickness="0" IsTabStop="False" Padding="{TemplateBinding Padding}" />
</Grid>
</Border>
<Border x:Name="ValidationErrorElement" BorderBrush="Blue" BorderThickness="1" Visibility="Collapsed">
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
You are not displaying the error condition correctly.
WPF uses a ControlTemplate with an AdornedElementPlaceholder for this, which is set in the attached Validation.ErrorTemplate property.
A big example with implementation can be found here: https://stackoverflow.com/a/68748914/13349759
Here is a small snippet of XAML from another small example:
<Window.Resources>
<ControlTemplate x:Key="validationFailed">
<StackPanel Orientation="Horizontal">
<Border BorderBrush="Violet" BorderThickness="2">
<AdornedElementPlaceholder />
</Border>
<TextBlock Foreground="Red" FontSize="26" FontWeight="Bold">!</TextBlock>
</StackPanel>
</ControlTemplate>
</Window.Resources>
<Grid>
<TextBox Margin="10"
Validation.ErrorTemplate="{StaticResource validationFailed}" >
<TextBox.Text>
<Binding Path="Age">
<Binding.ValidationRules>
<DataErrorValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
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);
I have a LongListSelector which is populated with some items. Each item has a submenu which can be visible or collapsed using a sliding animation. The problem is the spacing between the items, which should be 0. But when I use the sliding animation a couple of times, the spacing is sometimes a few pixels. When you scroll way down and back up the list will rerender and the spacing is gone.
Here are some screenshots, don't mind the ugly colours, I used them to keep the different elements apart from eachother. Purple is background color if the longlistselector. Each item has a red 1px border.
This is how it should be:
And when I clicked the show/hide button a few times:
And here is my code:
LongListSelector:
<phone:LongListSelector x:Name="LongList" Margin="0" Padding="0" ItemsSource="{Binding Items}"
HorizontalAlignment="Stretch" Background="DarkOrchid">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- 1st ROW -->
<Border BorderBrush="Red" Background="DarkKhaki" BorderThickness="1" HorizontalAlignment="Stretch">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Test Item" FontSize="42" />
<Button Content="v" Grid.Column="1" Tap="Button_Tap" Tag="{Binding}">
<i:Interaction.Triggers>
<ec:DataTrigger Binding="{Binding SubMenuIsVisible}" Value="True">
<eim:ControlStoryboardAction ControlStoryboardOption="Play">
<eim:ControlStoryboardAction.Storyboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Grid.Height)"
Storyboard.TargetName="Submenu"
From="0" To="60" Duration="0:0:0.25" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Submenu"
Storyboard.TargetProperty="(Grid.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</eim:ControlStoryboardAction.Storyboard>
</eim:ControlStoryboardAction>
</ec:DataTrigger>
<ec:DataTrigger Binding="{Binding SubMenuIsVisible}" Value="False">
<eim:ControlStoryboardAction ControlStoryboardOption="Play">
<eim:ControlStoryboardAction.Storyboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Grid.Height)"
Storyboard.TargetName="Submenu"
From="60" To="0" Duration="0:0:0.25" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Submenu"
Storyboard.TargetProperty="(Grid.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.25">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</eim:ControlStoryboardAction.Storyboard>
</eim:ControlStoryboardAction>
</ec:DataTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
</Border>
<!-- SUB Menu -->
<Border x:Name="Submenu" Grid.Row="1" Background="Green" HorizontalAlignment="Stretch" Height="0">
<TextBlock Text="SubMenu" FontSize="42" />
</Border>
</Grid>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
EDIT:
I suspect this problem has something to do with performance issues. I posted a new question for this, you can find it here:
Slow storyboard animation inside LongListSelector
The LongListSelector likes putting arbitrary padding/margins/overlap on stuff without any actual reason. I've found that using a ListBox is typically a more stable and less headachey-prone solution.
I can't see an obvious solution except maybe trying a StackPanel instead of a Grid with an Auto height.
Consider the following usercontrol:
This is a custom usercontrol that I have written that has two nested elements.
FilterContent displays a special type of markup that filters content on the right hand side of the screen
MainContent hosts the filtered content.
The only real purpose of the control is to provide consistent UI and animation across the application, as this filter/content pattern is used frequently.
The (simplified) Xaml of the usercontrol look as follows:
<UserControl>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0" Content="{Binding ElementName=filterControl, Path=FilterControl}" DataContext="{Binding}" />
<ContentPresenter Grid.Column="1" Content="{Binding ElementName=filterControl, Path=MainControl}" DataContext="{Binding}" />
</Grid>
The codebehind is :
public sealed partial class FilterPaneControl : UserControl
{
public static DependencyProperty FilterControlProperty = DependencyProperty.Register("FilterControl", typeof(object), typeof(FilterPaneControl), new PropertyMetadata(default(object), PropertyChangedCallback));
public static DependencyProperty MainControlProperty = DependencyProperty.Register("MainControl", typeof (object), typeof (FilterPaneControl), new PropertyMetadata(default(object)));
public FilterPaneControl()
{
this.InitializeComponent();
}
public object FilterControl
{
get { return (object)GetValue(FilterControlProperty); }
set { SetValue(FilterControlProperty, value); }
}
public object MainControl
{
get { return (object) GetValue(MainControlProperty); }
set { SetValue(MainControlProperty, value); }
}
}
The usage of the control in an implementing page is :
<Generic:FilterPaneControl>
<Generic:FilterPaneControl.FilterControl>
<Grid>
<TextBlock Text="Filter Content here"/>
</Grid>
</Generic:FilterPaneControl.FilterControl>
<Generic:FilterPaneControl.MainControl>
<Grid>
<TextBlock Text="Main Content here"/>
</Grid>
</Generic:FilterPaneControl.MainControl>
</Generic:FilterPaneControl>
That works fine!
The Problem
The problem is when I then want to reference some of the content within the control from the implementing page. A good case for this is visual states for handling snap/portrait (WinRT implementation)
<Generic:FilterPaneControl>
<Generic:FilterPaneControl.FilterControl>
<Grid>
<TextBlock x:Name="filterContent1" Text="Filter Content here"/>
</Grid>
</Generic:FilterPaneControl.FilterControl>
<Generic:FilterPaneControl.MainControl>
<Grid>
<TextBlock Text="Main Content here"/>
</Grid>
</Generic:FilterPaneControl.MainControl>
</Generic:FilterPaneControl>
<VisualStateManager.VisualStateGroups>
<VisualState x:Name="FullScreenPortrait">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="filterContent1" Storyboard.TargetProperty="Width">
<DiscreteObjectKeyFrame KeyTime="0" Value="200"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateManager.VisualStateGroups>
This leads to a run-time exception, as the visualstatemanager cannot find the referenced element 'filterContent1' even though it exists in the Visual Tree.
Additionally, if I try and reference the element directly in an Page.Loaded event handler, filterContent1 is null.
It is as if the nested Xaml doesn't render until later - which is throwing the visualstatemanager too.
Any suggestions?
First, VisualStateManager should be placed in a single panel with the element for which it is done, otherwise it would be an exception. For your case it turns out like this:
<Generic:FilterPaneControl>
<Generic:FilterPaneControl.FilterControl>
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualState x:Name="FullScreenPortrait">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="filterContent1" Storyboard.TargetProperty="Width">
<DiscreteObjectKeyFrame KeyTime="0" Value="200"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateManager.VisualStateGroups>
<TextBlock x:Name="filterContent1" Text="Filter Content here"/>
</Grid>
</Generic:FilterPaneControl.FilterControl>
...
Second, usually VisualStateManager placed in either a Template / Style, or UserControl. The transition to the states is carried out either in code or through XAML (with special techniques). Sample of set state behind code:
VisualStateManager.GoToState(NameOfControl, "State1", true);
Third, in a manner:
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="filterContent1" Storyboard.TargetProperty="Width">
<DiscreteObjectKeyFrame KeyTime="0" Value="200"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
Width not sets, in my case is an exception. We need to use animation something like this:
<Storyboard Storyboard.TargetName="filterContent1" Storyboard.TargetProperty="Width">
<DoubleAnimation To="200" Duration="0:0:1.0"/>
</Storyboard>
As proof of his words, I give an example:
MainWindow
<Window x:Class="VSMinUserControlHelp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VSMinUserControlHelp"
Title="MainWindow" Height="350" Width="525"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<local:UserControl1 x:Name="Control1" Height="118" VerticalAlignment="Top" Margin="50,12,101,0" />
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Button Name="State1Button" Width="75" Click="State1Button_Click">State1</Button>
</StackPanel>
</Grid>
</Window>
Code behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void State1Button_Click(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(Control1, "State1", true);
}
}
UserControl
<UserControl x:Class="VSMinUserControlHelp.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Common1">
<VisualState x:Name="State1">
<Storyboard Storyboard.TargetName="filterContent1" Storyboard.TargetProperty="Width">
<DoubleAnimation To="200" Duration="0:0:1.0"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<TextBlock x:Name="filterContent1" Background="Aqua" Width="100" HorizontalAlignment="Left" Text="Filter Content here"/>
</Grid>
</UserControl>
Note: Example run on VS 2010, Windows XP, not tested under WinRT.
When using the VisualStateManager in WPF there can be a requirement to transition to a VisualState on control initialization. As far as I can tell there is no way to declare an initial state in Xaml, leaving you with the limited option of transitioning to the required state in your code behind after initialization.
Using code behind is not always desirable, and if you are using a Binding to control your VisualStates then not always possible.
So the question is: how do you set an initial VisualState in WPF without setting it in the code behind?
Too long for a comment
Binding "should" make no difference. If it works fine from code-behind it's bound to work from xaml unless there is something really weird in the Bindings.
All of blend's actions can be considered as a xaml helper tool. End result is you get some xaml that blend creates for you. If you do not want to use blend. Just add the xaml yourself in VS.
For this very thing the GoToStateAction can be coded such as
<Window ...
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
...>
...
<Button x:Name="button"
Style="{DynamicResource ButtonStyle1}">
<i:Interaction.Triggers>
<i:EventTrigger>
<ei:GoToStateAction StateName="YourState"
TargetObject="{Binding ElementName=button}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
You'll need the corresponding references in your project as well.
On a side-note do try blend. It has it's advantages in specific places. You prolly would not replace typing xaml directly, but it serves as a good helper tool. Ignoring it completely unless forced to is pointless IMO.
You can directly bind any control with visual state at the time of initialisaton itself in xaml.
You need to create one dependency property to change the state.
hope below code can help you .
<Grid model:StateManager.VisualStateProperty="{Binding VisibilityState}" >
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="97" />
<RowDefinition Height="65" />
<RowDefinition Height="297" />
</Grid.RowDefinitions>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisibleStateGroup">
<VisualState x:Name="VisibleState">
<Storyboard Duration="0:0:0">
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="myGrid" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CollapsedState">
<Storyboard Duration="0:0:0">
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="myGrid" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid Name="myGrid" Grid.Row="0" Grid.ColumnSpan="2" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="383*" />
<ColumnDefinition Width="383*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0,0,15,0" HorizontalAlignment="Right" VerticalAlignment="Center">
<Label Content="MyName"></Label>
</StackPanel>
</Grid>
Dependency property code for visual state change
public class StateManager : DependencyObject
{
public static string GetVisualStateProperty(DependencyObject obj)
{
return (string)obj.GetValue(VisualStatePropertyProperty);
}
public static void SetVisualStateProperty(DependencyObject obj, string value)
{
obj.SetValue(VisualStatePropertyProperty, value);
}
public static readonly DependencyProperty VisualStatePropertyProperty =
DependencyProperty.RegisterAttached(
"VisualStateProperty",
typeof(string),
typeof(StateManager),
new PropertyMetadata((s, e) =>
{
var propertyName = (string)e.NewValue;
var ctrl = s as Grid;
if (ctrl == null)
throw new InvalidOperationException("This attached property only supports types derived from FrameworkElement.");
var transitionWorked = System.Windows.VisualStateManager.GoToElementState(ctrl, (string)e.NewValue, true);
//MessageBox.Show(transitionWorked.ToString());
}));
}