I have an enum on a viewmodel which represents one of 5 colors, and I am using a ValueConverter to convert these values into actual colors.
Specifically it's for the color of each listbox item's background to change to as it is hovered over.
I have a custom control with a visual state manager and a mouseover group which uses a SplineColorKeyFrame to animate the hover color (This is set in the xaml of control template). The custom control just has a dependency property on it for the hover color.
This works great if the Value of the SplineColorKeyFrame is from a resource, or set in the xaml as a fixed color. However it just animates to transparent when I set the Value to "{TemplateBinding HoverColor}"
Even setting the DependencyProperty in the xaml to a color, and trying to use the TemplateBinding in the control to read the color causes the problem, the animation won't use the right color if I tell it to get it from a binding or template binding.
I've snooped the app and can see that the dependency property has the correct value, but it doesn't seem to be picking up that value in the animation.
Can anyone suggest how to get around this?
Here's my control template:
<Style TargetType="{x:Type local:MyCustomControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyCustomControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" x:Name="border">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="MouseOverGroup">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.2"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)" Storyboard.TargetName="border">
<SplineColorKeyFrame KeyTime="0" Value="{TemplateBinding HoverColor}" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Normal"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<VisualStateManager.CustomVisualStateManager>
<ic:ExtendedVisualStateManager/>
</VisualStateManager.CustomVisualStateManager>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseEnter">
<ic:GoToStateAction x:Name="MouseOverAction" StateName="MouseOver"/>
</i:EventTrigger>
<i:EventTrigger EventName="MouseLeave">
<ic:GoToStateAction x:Name="NormalAction" StateName="Normal"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here's my custom control code:
public class MyCustomControl : Control
{
public static DependencyProperty HoverColorProperty = DependencyProperty.Register("HoverColor", typeof (Color),
typeof (MyCustomControl));
public Color HoverColor
{
get
{
return (Color)GetValue(HoverColorProperty);
}
set
{
SetValue(HoverColorProperty, value);
}
}
static MyCustomControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl), new FrameworkPropertyMetadata(typeof(MyCustomControl)));
}
}
Here's the main window xaml:
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Test="clr-namespace:Test" Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<Test:ViewModel/>
</Window.DataContext>
<Grid>
<Test:MyCustomControl HoverColor="Yellow"
HorizontalAlignment="Center" VerticalAlignment="Center" Width="150" Height="150"
Background="Bisque">
</Test:MyCustomControl>
</Grid>
</Window>
I found the answer to this problem. It turns out that, for some reason, the binding doesn't work if the StoryBoard is inline in the Xaml. If you put the Storyboard in a resource and refer to the resource in the VisualStateManager, then everything works.
Related
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.
I have some controls in a DataTemplate and I'd like to control it's pressed state behaviour. I did the following where I just put in VisualStateManager in the DataTemplate but it doesn't seem to work. I think it's possible to understand what I'm trying to do below. Is it possible to do it inline inside the DataTemplate tags?
<ItemsControl ItemsSource="{Binding Items}">
....
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid ...>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
...
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderThickness" Storyboard.TargetName="GridItemBorder">
<DiscreteObjectKeyFrame KeyTime="0" Value="3"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="Border" ...>
...
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The short answer is that there is no "Pressed" visual state for the control type you're targeting -- so while you can reference any state in the Visual State Manager, it won't matter, because the control's code will never put it into that state.
You can see which visual states a control supports by looking at its definition (they're declared using the TemplateVisualState attribute), or by looking at this section on MSDN.
The way to go here might be to use the Button (or an override of [ButtonBase][2] that you write), since that has the "Pressed" visual state built in. You'd just have to write a Control Template for it that provides the layouts/styles that you're after.
Edit Here's an example:
Control template (resources section). This is a control template for the Button control, but it's not really a button. I'm just using it to take advantage of the "Pressed" visual state functionality.
<ControlTemplate x:Key="MyButtonTemplate" TargetType="Button">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderThickness)" Storyboard.TargetName="GridItemBorder">
<DiscreteObjectKeyFrame KeyTime="0" Value="3"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="GridItemBorder" BorderBrush="Orange" BorderThickness="1" Background="White">
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</Grid>
</ControlTemplate>
Items control
Define the item template as a "Button" which uses the above ControlTemplate.
<ItemsControl ItemsSource="{Binding SelectedItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Template="{StaticResource MyButtonTemplate}" Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I want to create a ControlTemplate with predefined VisualStates. I want to use them with GoToStateActions and DataTriggers.
I don't know what exactly is going wrong here. It seems to me, that the Binding isn't established in that way I suppose it is.
namespace ControlTemplateVisualState
{
using System.Windows.Controls;
public class MyControl : ContentControl
{
public MyControl()
{
this.DefaultStyleKey = typeof(MyControl);
}
}
}
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ControlTemplateVisualState="clr-namespace:ControlTemplateVisualState"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions">
<ControlTemplate x:Key="MyControlTemplate" TargetType="{x:Type ControlTemplateVisualState:MyControl}">
<Grid x:Name="grid" Background="Red">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualState x:Name="IsDisabledState">
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)" Storyboard.TargetName="grid">
<EasingColorKeyFrame KeyTime="0" Value="#FF2BFF00"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding IsEnabled, ElementName=grid}" Value="False">
<ei:GoToStateAction StateName="IsDisabledState"/>
</ei:DataTrigger>
</i:Interaction.Triggers>
</Grid>
</ControlTemplate>
<Style TargetType="{x:Type ControlTemplateVisualState:MyControl}">
<Setter Property="Background" Value="#FF0010FF"/>
<Setter Property="Template" Value="{StaticResource MyControlTemplate}"/>
</Style>
</ResourceDictionary>
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlTemplateVisualState.MainWindow"
x:Name="Window"
xmlns:local="clr-namespace:ControlTemplateVisualState"
Title="MainWindow"
Width="640" Height="480">
<Grid x:Name="LayoutRoot">
<local:MyControl IsEnabled="False"/>
</Grid>
</Window>
You could give this a go:
<i:Interaction.Behaviors>
<si:DataStateBehavior Binding='{Binding IsLoggedIn}' Value='True'
TrueState='LoggedInState' FalseState='LoggedOutState'/>
</i:Interaction.Behaviors>
It's slightly different, but works even with Silverlight, see: http://expressionblend.codeplex.com/wikipage?title=Behaviors%20and%20Effects&referringTitle=Documentation
Just make sure to get the fixed version if you use WPF4: http://expressionblend.codeplex.com/workitem/8148
I don't think Blend SDK triggers and behaviors can be used in control templates -- only in UserControls: there's no concrete object to which the trigger can be attached when the XAML is parsed. (I'm not 100% certain of this explanation, but I do know that if you have multiple control templates, you can't put Blend behaviors in all of them.) You'll need code behind to invoke the VSM -- that's just how custom controls work. You'll use something like this code from the Silverlight Toolkit:
public static void GoToState(Control control, bool useTransitions, params string[] stateNames)
{
foreach (var name in stateNames)
{
if (VisualStateManager.GoToState(control, name, useTransitions))
break;
}
}
How do you implement validation in custom controls? I am looking to replicate the standard validation logic you would see with a TextBox data-bound to a model or view-model that exposes IDataErrorInfo or INotifyDataErrorInfo.
To implement validation you should add the "ValidationStates" group to the VisualStateManager of the control.
I will illustrate the simple custom control TestControl with the TestProperty property.
Style in the Generic.xaml, depending on the state displays the blue text or the red border with the first error message:
<Style TargetType="local:TestControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TestControl">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ValidationStates">
<VisualState x:Name="Valid" />
<VisualState x:Name="InvalidFocused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="InvalidBorder" Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="InvalidUnfocused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="InvalidBorder" Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<TextBlock Text="{TemplateBinding TestProperty}" Foreground="Blue" />
<Border x:Name="InvalidBorder" BorderBrush="Red" BorderThickness="2" Visibility="Collapsed">
<TextBlock Text="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource TemplatedParent}}" Foreground="Red" FontWeight="Bold" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
There are 3 states:
Valid - No validation errors.
InvalidFocused - Applied when you set the focus to the control in the invalid state. Default controls display the red popup as well as the red border in this state, but in my particular example I don't display it for simplicity. Users can invoke this state by using the Tab keyboard button or by clicking a focusable inner control like TextBox.
InvalidUnfocused - Applied when the control in the invalid state but isn't focused.
Here is the code of the control, it contains only one property:
public class TestControl : Control
{
public TestControl()
{
this.DefaultStyleKey = typeof(TestControl);
}
public string TestProperty
{
get { return (string)GetValue(TestPropertyProperty); }
set { SetValue(TestPropertyProperty, value); }
}
public static readonly DependencyProperty TestPropertyProperty =
DependencyProperty.Register("TestProperty", typeof(string), typeof(TestControl), new PropertyMetadata(null));
}
After that if you use the IDataErrorInfo, the correct xaml is:
<local:TestControl TestProperty="{Binding SomeModelProperty, ValidatesOnDataErrors=True}" />
For the INotifyDataErrorInfo, the correct xaml is even simplier:
<local:TestControl TestProperty="{Binding SomeModelProperty}" />
I'm trying to create a custom combobox that shows a loading animation when a bool property is set on it (e.g. IsLoading).
I've created a control template in Blend based on the combobox, and added a textblock inside the togglebutton template. In the code behind I'm calling VisualStateManager.GoToState but it always returns false. I was trying to move to a custom state initially, but I can't even move to states such as Disabled or MouseOver.
I have a usercontrol that just contains a combobox and the style is set to my combobox style containing the control template. I presume GoToState fails because the state is not on the control itself, but inside the combobox. How can I access this?
I'm at a loss as to how to debug this as there are no errors.
thanks
I had a similar problem! I had the Visual States defined within a grid in the ControlTemplate.
<Style x:Key="Image3TextRowButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="{StaticResource ForegroundBrush}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid x:Name="RootGrid" Background="{StaticResource BackgroundBrush}">
...
<VisualStateManager.VisualStateGroups>
...
<VisualStateGroup x:Name="ActiveStates">
<VisualState x:Name="Active">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background)" Storyboard.TargetName="RootGrid">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource BackgroundActiveBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="NotActive" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And the user control:
<UserControl x:Class="Griesser.Presentation.ContactCenterClient.Client.Control.Image3TextRowButtonUC"
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"
mc:Ignorable="d"
x:Name="Image3TextRowButton">
<Grid>
<Button x:Name="btn" Command="{Binding Command, ElementName=Image3TextRowButton}" Style="{StaticResource Image3TextRowButtonStyle}" />
</Grid>
In the code behind I am looking first for the RootGrid and then use GoToElementState:
private void ChangeVisualActiveState(bool useTransitions)
{
// Search RootGrid, because the Visual States are defined in the ControlTemplate!
FrameworkElement dt = btn.Template.FindName("RootGrid", btn) as FrameworkElement;
if (IsActive)
{
VisualStateManager.GoToElementState(dt, "Active", useTransitions);
}
else
{
VisualStateManager.GoToElementState(dt, "NotActive", useTransitions);
}
}