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>
Related
I'm trying to make it so that when the window drops below a certain size, the buttons shrink in size.
Here is the code for my Style:
<Style x:Key="AppBarButtonStyle" TargetType="AppBarButton">
<Setter Property="Width" Value="68"/>
</Style>
How can I cause all AppBarButtons to become 64 width when the window drops below 720 pixels?
We've discussed this in your another question, your AppBarSeparators are generated in the Pivot's DataTemplate.
Still you can use DataBinding with Converter to do this, and if the size of the window is changeable during the run-time, you may also need complete your data source class with INotifyPropertyChanged Interface.
For example here:
<Page.Resources>
<local:WidthConverter x:Key="cvt" />
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Pivot x:Name="docPivot" ItemsSource="{x:Bind pivotlist}" SizeChanged="docPivot_SizeChanged">
<Pivot.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<AppBarButton Background="Red" Icon="Accept" Label="Accept" Width="{Binding WindowWidth, Converter={StaticResource cvt}}" />
</StackPanel>
<StackPanel Orientation="Horizontal"
Grid.Row="1">
</StackPanel>
</Grid>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
</Grid>
The other things are the same as my answer in your last question. And the WidthConverter here is like this:
public class WidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
double? width = (double?)value;
if (width <= 720)
return 64;
return 68;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
This should be easy enough, not sure if you can set styles generally, or if you are restricted to named elements though, so I would create a visualstatemanager that does that first, and then explore my options further:
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="WindowStates">
<VisualState x:Name="WideState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Button1.Width" Value="100" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NarrowState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Button1.Width" Value="68" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>
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.
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.
Okay! I did a big edition :)
In a UserControl, I need to use interactions DataTrigger like below. The reason for is that I need a storyboard (MyStory) with a bound key-frame value. (Doing so was discussed here before.)
<UserControl x:Class="WpfApplication1.UserControl2"
...
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:local="clr-namespace:WpfApplication1">
<UserControl.Resources>
<Style x:Key="MyControlStyle" TargetType="UserControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Grid.Resources>
<Storyboard x:Key="MyStory">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
<SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:UserControl2}}, Path=SpecialColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Grid.Resources>
<Border x:Name="brdBase" BorderThickness="1" BorderBrush="Gray">
<TextBox Text="{Binding SpecialText}"/>
</Border>
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding SpecialText}" Value="Fire!">
<ei:ControlStoryboardAction Storyboard="{StaticResource MyStory}" ControlStoryboardOption="Play"/>
</ei:DataTrigger>
</i:Interaction.Triggers>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid x:Name="grdRoot" DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:UserControl2}}}">
<UserControl Style="{DynamicResource MyControlStyle}"/>
</Grid>
</UserControl>
C# code behind:
public partial class UserControl2 : UserControl
{
#region ________________________________________ SpecialColor
public Color SpecialColor
{
get { return (Color)GetValue(SpecialColorProperty); }
set { SetValue(SpecialColorProperty, value); }
}
public static readonly DependencyProperty SpecialColorProperty =
DependencyProperty.Register("SpecialColor",
typeof(Color),
typeof(UserControl2),
new FrameworkPropertyMetadata(Colors.Red));
#endregion
#region ________________________________________ SpecialText
public string SpecialText
{
get { return (string)GetValue(SpecialTextProperty); }
set { SetValue(SpecialTextProperty, value); }
}
public static readonly DependencyProperty SpecialTextProperty =
DependencyProperty.Register("SpecialText",
typeof(string),
typeof(UserControl2),
new FrameworkPropertyMetadata(string.Empty));
#endregion
public UserControl2()
{
InitializeComponent();
}
}
I expect the above code to play MyStory right after SpecialText set to "Fire!". To do so, we can use one of the following ways:
1.
<Grid>
<local:UserControl2 SpecialText="Fire!"/>
</Grid>
2. Typing "Fire!" in the UserControl2 text-box at run-time.
The first way doesn't affect on GUI at design-time. I have some complicated UserControls in my app and really need to solve this issue. There are some DPs that call a storyboard to change layout of the UserControl animatedly. Obviously these storyboards involved with one or more bound key-frames. So I must run my app more and more to check it :(
How can I change a Button template dynamically?
I have a ComboBox where by changing his selected value I want to change a Button Template.
This is what I have been trying to do:
<Window.Resources>
<ControlTemplate x:Key="ButtonControlTemplate1" TargetType="{x:Type Button}">
<Grid>
<Rectangle Fill="#FF2D2D7A" Margin="7.5,9.5,8.5,11" Stroke="Black"
RadiusX="45" RadiusY="45" StrokeThickness="6"/>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="ButtonControlTemplate2" TargetType="{x:Type Button}">
<Grid>
<ed:RegularPolygon Fill="#FFE7F9C9" Height="Auto" InnerRadius="0.47211"
Margin="20.5,16,15.5,8" PointCount="5" Stretch="Fill"
Stroke="Black" StrokeThickness="6" Width="Auto"/>
</Grid>
</ControlTemplate>
</Window.Resources>
<Grid x:Name="LayoutRoot">
<ComboBox Name="GroupBoxHeaderComboBox" ItemsSource="{Binding Path=collection}"
DisplayMemberPath="Key" Height="52" Margin="211.5,60,230.5,0"
VerticalAlignment="Top" SelectedIndex="1"/>
<Button Content="Button" HorizontalAlignment="Left" Height="102" Margin="47.5,0,0,91"
VerticalAlignment="Bottom" Width="132"
Template="{DynamicResource ButtonControlTemplate2}"/>
<Button Content="Button" HorizontalAlignment="Right" Height="112.5" Margin="0,0,27.5,85"
VerticalAlignment="Bottom" Width="153"
Template="{DynamicResource ButtonControlTemplate1}"/>
<Button Content="Button" Height="102" Margin="239.5,0,252.5,13.5"
VerticalAlignment="Bottom"
Template="{Binding ElementName=GroupBoxHeaderComboBox, Path=SelectedItem.Value}"/>
</Grid>
And here are the associated Templates:
<Window.Resources>
<ControlTemplate x:Key="ButtonControlTemplate1" TargetType="{x:Type Button}">
<Grid>
<Rectangle Fill="#FF2D2D7A" Margin="7.5,9.5,8.5,11" Stroke="Black"
RadiusX="45" RadiusY="45" StrokeThickness="6"/>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="ButtonControlTemplate2" TargetType="{x:Type Button}">
<Grid>
<ed:RegularPolygon Fill="#FFE7F9C9" Height="Auto" InnerRadius="0.47211"
Margin="20.5,16,15.5,8" PointCount="5" Stretch="Fill"
Stroke="Black" StrokeThickness="6" Width="Auto"/>
</Grid>
</ControlTemplate>
</Window.Resources>
And the code behind:
public partial class MainWindow : Window
{
public Dictionary<string, string> collection
{
get;
private set;
}
public MainWindow()
{
this.InitializeComponent();
DataContext = this;
collection = new Dictionary<string, string>()
{
{ "DynamicResource ButtonControlTemplate2", "{DynamicResource ButtonControlTemplate2}"},
{ "DynamicResource ButtonControlTemplate1", "{DynamicResource ButtonControlTemplate2}"},
};
// Insert code required on object creation below this point.
}
}
Is there another genric way to acomplish this?... I want that most of the code would be xaml.
EDIT:
Is there a point to do it using a style? Let's say I want more then one object to act, otherwise is there a point to change the style and to do it all from there?
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public Dictionary<string, ControlTemplate> collection
{
get
{
Dictionary<string, ControlTemplate> controlTemplates = new Dictionary<string, ControlTemplate>();
controlTemplates.Add("ButtonControlTemplate1", FindResource("ButtonControlTemplate1") as ControlTemplate);
controlTemplates.Add("ButtonControlTemplate2", FindResource("ButtonControlTemplate2") as ControlTemplate);
return controlTemplates;
}
}
}
Create a ControlTemplate in Windows resource,
<Window.Resources>
<ControlTemplate x:Key="GreenTemplate" TargetType="{x:Type Button}">
<Grid>
<Ellipse Fill="Green"/>
<ContentPresenter Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Window.Resources>
Now in run time you can change the template property of button.
private void Button_Clicked(object sender, RoutedEventArgs e)
{
Button btn = e.OriginalSource as Button;
if (btn != null)
{
btn.Template = FindResource("GreenTemplate") as ControlTemplate;
}
}
You can use a data trigger and do it all in xaml.
This uses a tree but the concept is the same
<Window x:Class="WpfBindingTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfBindingTest"
Title="Window3" Height="300" Width="300" Name="win3" >
<Window.Resources>
<XmlDataProvider x:Key="treeData" XPath="*">
<x:XData>
<Items Name="Items" xmlns="">
<Item1/>
<Item2>
<Item22/>
<Item12/>
<Item13>
<Item131/>
<Item131/>
</Item13>
</Item2>
</Items>
</x:XData>
</XmlDataProvider>
<HierarchicalDataTemplate ItemsSource="{Binding XPath=child::*}" x:Key="template">
<TextBlock Name="textBlock" Text="{Binding Name}"/>
</HierarchicalDataTemplate>
</Window.Resources>
<StackPanel>
<TreeView ItemTemplate="{StaticResource template}"
Name="treeView"
ItemsSource="{Binding Source={StaticResource treeData}}">
<TreeView.ItemContainerStyle>
<!--Using style setter to set the TreeViewItem.IsExpanded property to true, this will be applied
to all TreeViweItems when they are generated-->
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
<Button Width="120" Height="30">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Content" Value="Default" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=treeView, Path=SelectedItem.Name}" Value="Item12">
<Setter Property="Content" Value="Now changed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
from here.
(I just googled to get an example faster)