Best way to "group" items "virtually"? - wpf

I'm working on a custom items based WPF control. For the "normal" layout, think vertical StackPanel, so my xaml would look something like:
<mycontrol>
<item1 />
<item2 />
<item3 />
</mycontrol>
In this case, its simple, 3 item containers are created and all is good. The control will look like:
[item1]
[item2]
[item3]
Use case #2 is, I need to support horizontal "grouping"... ideally my xaml would look like:
<mycontrol>
<item1 />
<stackpanel orientation=horizontal>
<item2a />
<item2b />
<item2c />
</stackpanel>
<item3 />
</mycontrol>
and in this case, I would render as:
[item1]
[item2a] [item2b] [item2c]
[item3]
So, what I'm going for is 5 item containers being generated. I've already worked out a custom layout panel and that part works.
The issue is, if I use a stackpanel, I'll only get 3 item containers which makes sense, duh, but it breaks the keyboard interface of the control. I could do something hacky where I intercept all the keyboard and mouse stuff and "re-route" it in this case, but that seems hacky and difficult to get to work in a generic way.
Is there something obscure built into WPF to deal with this? The "sub items" getting generated as thier own containers?
The way I'm currently heading is to do something like:
<item1 />
<item2a isGrouped=true />
<item2b isGrouped=true />
<item2c isGrouped=true />
<item3 />
So, when I hit the first isGrouped=true, it'll start grouping until it hits a false, but I'm not crazy about that either because I'll have to make isGrouped a 3 state enum so I can have one group right below another group. Also, the hierarchy is not clear in the xaml.
Any suggestions?

I was able to get more or less the look you are going for using a HierarchicalDataTemplate within a TreeView that uses a custom TreeViewItem control template. For the control template, I simply copied the example template and made a few modifications. All of the containers are in the right place, but the keyboard navigation doesn't work on the nested items (because TreeView is not expecting that layout I guess). Here is what I came up with:
<TreeView
ItemsSource="{Binding Items}">
<TreeView.Resources>
<Color x:Key="SelectedBackgroundColor">#FFC5CBF9</Color>
<Color x:Key="SelectedUnfocusedColor">#FFDDDDDD</Color>
<Style
TargetType="{x:Type TreeViewItem}">
<Setter
Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter
Property="IsExpanded"
Value="True" />
<Setter
Property="Template">
<Setter.Value>
<!-- This template came from the example template and has just a few modifications.
Example is at: https://msdn.microsoft.com/en-us/library/ms752048.aspx -->
<ControlTemplate
TargetType="{x:Type TreeViewItem}">
<Grid>
<!-- Changed the grid configuration -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- The entire VisualStateGroups section is a direct copy+paste from the example template -->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="Selected">
<Storyboard>
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="Bd"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<EasingColorKeyFrame
KeyTime="0"
Value="{StaticResource SelectedBackgroundColor}" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Unselected" />
<VisualState x:Name="SelectedInactive">
<Storyboard>
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="Bd"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<EasingColorKeyFrame
KeyTime="0"
Value="{StaticResource SelectedUnfocusedColor}" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ExpansionStates">
<VisualState x:Name="Expanded">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.Visibility)"
Storyboard.TargetName="ItemsHost">
<DiscreteObjectKeyFrame
KeyTime="0"
Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Collapsed" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!-- Removed the ToggleButton -->
<!-- Tweaked the placement of items in the grid -->
<Border
x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter
x:Name="PART_Header"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
</Border>
<ItemsPresenter
x:Name="ItemsHost"
Grid.Column="1" />
</Grid>
<ControlTemplate.Triggers>
<!-- Removed the IsExpanded trigger -->
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="false" />
<Condition Property="Width" Value="Auto" />
</MultiTrigger.Conditions>
<Setter
TargetName="PART_Header"
Property="MinWidth"
Value="75" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="false" />
<Condition Property="Height" Value="Auto" />
</MultiTrigger.Conditions>
<Setter
TargetName="PART_Header"
Property="MinHeight"
Value="19" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
DataType="{x:Type local:MyItem}"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
For reference, my ItemsSource property is bound to a collection containing items that look like this:
internal class MyItem
{
public string Name { get; private set; }
public List<MyItem> Children { get; private set; }
public MyItem(string name = null)
{
Name = name;
Children = new List<MyItem>();
}
}
This is done without creating a custom control, but hopefully it gives you an idea of what you can do. The combination of a HeirarchicalDataTemplate and a specialized container type (in my case TreeViewItem with a custom control template) is key to the way I did it.
Here is some data that I tested with and the result:
public IEnumerable<MyItem> Items { get; private set; }
...
var items = new MyItem[]
{
new MyItem("[First]"),
new MyItem(),
new MyItem("[Third]")
};
items[1].Children.Add(new MyItem("[Second_0]"));
items[1].Children.Add(new MyItem("[Second_1]"));
items[1].Children.Add(new MyItem("[Second_2]"));
Items = items;
I am sure the visualization could be improved. I just threw this together in under 10 minutes.

Related

how to parametrize button style in WPF xaml

I'm trying to create a template button with a specific effect that can receive 2 background images as parameters (for button pushed and released). this template is implemented in a user control library :
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SwitchesLibrary">
<Style x:Key="PushButton" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Border x:Name="MyBorder" CornerRadius="5" Background="{TemplateBinding Background}" BorderThickness="1">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<Border x:Name="ButtonPressedOnClick" Opacity="0" CornerRadius="5" Background="{DynamicResource ButtonPressed}" BorderThickness="1">
</Border>
</Grid>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ButtonPressedOnClick" Storyboard.TargetProperty="(FrameworkElement.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.05" Value="1.0"/>
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="0.0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="MyBorder" Storyboard.TargetProperty="(FrameworkElement.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.05" Value="0.0"/>
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Background" Value="{DynamicResource ButtonReleased}"/>
</Style>
I use it in another user control library, but i don't know how to send the parameters {DynamicResource ButtonReleased} and {DynamicResource ButtonPushed}
:
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/SwitchesLibrary;component/PushButton.xaml"></ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Button Name="ButtonA" Style="{StaticResource PushButton, ButtonPressed="Images/PushButtons/A_Pushed.png", ButtonReleased="Images/PushButtons/A_Released.png"}" HorizontalAlignment="Left" Height="44" VerticalAlignment="Top" Width="42" Click="ButtonA_Click" Margin="69,112,0,0"/>
The closest solution to 'parameterizing' a Style would be to override the image resources in the Button itself, but this would be rather verbose and cumbersome to do more than a few times:
<Button Style="{StaticResource PushButton}">
<Button.Resources>
<ImageBrush x:Key="ButtonPressed"
ImageSource="Images/PushButtons/A_Pushed.png" />
<ImageBrush x:Key="ButtonReleased"
ImageSource="Images/PushButtons/A_Released.png" />
</Button.Resources>
</Button>
If this is something you're going to use more than a couple times, it's worth investing a little more time upfront to make your image button easier to use. Wouldn't it be more convenient to type this instead?
<l:ImageButton NormalImage="Images/PushButtons/A_Released.png"
PressedImage="Images/PushButtons/A_Pushed.png" />
If you like the look of that, then the first step would be to write a simple ImageButton class and define dependency properties for the images representing the various button states:
public class ImageButton : Button
{
public static readonly DependencyProperty NormalImageProperty =
DependencyProperty.Register(
"NormalImage",
typeof(ImageSource),
typeof(ImageButton),
new PropertyMetadata(default(ImageSource)));
public ImageSource NormalImage
{
get { return (ImageSource)GetValue(NormalImageProperty); }
set { SetValue(NormalImageProperty, value); }
}
public static readonly DependencyProperty PressedImageProperty =
DependencyProperty.Register(
"PressedImage",
typeof(ImageSource),
typeof(ImageButton),
new PropertyMetadata(default(ImageSource)));
public ImageSource PressedImage
{
get { return (ImageSource)GetValue(PressedImageProperty); }
set { SetValue(PressedImageProperty, value); }
}
static ImageButton()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(ImageButton),
new FrameworkPropertyMetadata(typeof(ImageButton)));
}
}
The second step is to define a default style for your ImageButton. To do this, create a Themes\Generic.xaml resource dictionary in the project where ImageButton is declared. Make sure the Build Action is set to 'Page' in the Properties pane. Implement your default style here, for example:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:TestApp">
<Style TargetType="l:ImageButton"
BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="l:ImageButton">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Image x:Name="image"
Source="{TemplateBinding NormalImage}"
Stretch="None"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="image"
Property="Source"
Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=PressedImage}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Check your project's AssemblyInfo.cs file and make sure you have an attribute like this (and add it if you don't):
[assembly: ThemeInfo(
// Where theme specific resource dictionaries are located
// (used if a resource is not found in the page, or application
// resource dictionaries)
ResourceDictionaryLocation.None,
// Where the generic resource dictionary is located
// (used if a resource is not found in the page, app, or
// any theme specific resource dictionaries)
ResourceDictionaryLocation.SourceAssembly
)]
And now you're ready to go!
<l:ImageButton NormalImage="Images/PushButtons/A_Released.png"
PressedImage="Images/PushButtons/A_Pushed.png" />

XAML Storyboarding - Setting a bool to "False" in a storyboard locks it as false

this is a somewhat odd one.
I'm looking to use XAML to get an expander (or anything with checked and unchecked states such as a Togglebutton and the like) to set itself back to being collapsed/unchecked after being 'ignored' (for lack of a better term) after a given period of time.
Currently the parameters I'm using to decide if it's being 'ignored' are that
MouseOver=False and IsExpanded=True, when this trigger is raised, it'll begin a storyboard that, after about 3 seconds will set "IsExpanded" to "False".
Which is working just fine.
The problem is when you then want to RE-expand the expander - you can't, because the storyboard appears to be holding it as False.
I'm probably just doing something dumb/not considering some angle of this, but it's incredibly frustrating.
If it should turn out that it can't be done with XAML alone then that's okay, but i would prefer a purely XAML solution if one exists.
To better illustrate the issue, i've made a sample project you can look at that can be downloaded Here.
Alternatively, here's the code to view:
<Window.Resources>
<Style x:Key="ExpanderStyle" TargetType="{x:Type Expander}">
<Style.Resources>
<Storyboard x:Key="ExpanderSelfCollapseStoryboard">
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="{x:Null}" Storyboard.TargetProperty="(Expander.IsExpanded)">
<DiscreteBooleanKeyFrame KeyTime="0:0:3" Value="False" />
</BooleanAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Tag)" Storyboard.TargetName="{x:Null}">
<DiscreteObjectKeyFrame KeyTime="0" Value="Not long..."/>
<DiscreteObjectKeyFrame KeyTime="0:0:3" Value="Notice you can't open the expander anymore :("/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</Style.Resources>
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.EnterActions>
<BeginStoryboard x:Name="ExpanderSelfCollapseStoryboard_BeginStoryboard" Storyboard="{StaticResource ExpanderSelfCollapseStoryboard}" />
</MultiTrigger.EnterActions>
<MultiTrigger.Conditions>
<Condition Property="IsExpanded" Value="True" />
<Condition Property="IsMouseOver" Value="False" />
</MultiTrigger.Conditions>
</MultiTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Expander x:Name="localExpander"
Background="DodgerBlue"
Header="Expand me!"
Style="{StaticResource ExpanderStyle}"
Tag="Up there!">
<Border Height="200"
Margin="5"
Background="Orange">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
FontWeight="Bold"
Foreground="White"
Text="Now move your mouse over the whitespace for a few seconds please."
TextAlignment="Center"
TextWrapping="Wrap" />
</Border>
</Expander>
<TextBlock Grid.Row="1"
VerticalAlignment="Center"
FontSize="22"
Foreground="LightGray"
Text="{Binding ElementName=localExpander,
Path=Tag}"
TextAlignment="Center" />
</Grid>
I'd be thankful for any help or advice anyone can render.
Thanks,
-- Logan

EventTrigger Not Working When Declared in Window.Resources in WPF

I am new to WPF, so I may be missing something essential, but I have experimented and tried to come up with an explanation for the following phenomenon, to no avail.
Basically, the following code works (displays animation):
<Window.Resources>
<Storyboard x:Key="LoadStoryBoard"
AutoReverse="True"
RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="button1"
Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
...
<Button x:Name="button1" Grid.Column="0" Grid.Row="1" Style="{StaticResource Load}">
<Button.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard Storyboard="{StaticResource LoadStoryBoard}" />
</EventTrigger>
</Button.Triggers>
</Button>
However, when I try to put the eventrigger in the Load Style in the following, the animation ceases to appear:
<Window.Resources>
<Storyboard x:Key="LoadStoryBoard"
AutoReverse="True"
RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="button1"
Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
...
<Style x:Key="Load" TargetType="Button">
...
<Style.Triggers>
...
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard Storyboard="{StaticResource LoadStoryBoard}" />
</EventTrigger>
</Style.Triggers>
</Style>
In the Style of triggers can not use objects with TargetName, such animation. To do this, they are placed in triggers template <ControlTemplate.Triggers>. Quote from link:
TargetName is not intended for use within the Triggers collection of a Style. A style does not have a namescope, so it does not make sense to refer to elements by name there. But a template (either DataTemplate or ControlTemplate) does have a namescope.
The following works:
<Window.Resources>
<Storyboard x:Key="LoadStoryBoard" AutoReverse="True" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="button1" Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Style x:Key="ButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Green" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="button1" CornerRadius="0" Background="{TemplateBinding Background}">
<Grid>
<ContentPresenter x:Name="MyContentPresenter" Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,0" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Orange" />
</Trigger>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard Storyboard="{StaticResource LoadStoryBoard}" />
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<Button Name="TestButton" Style="{StaticResource ButtonStyle}" Width="100" Height="30" Content="Test" Grid.Column="0" Grid.Row="1" />
</Grid>
Notice that now TargetName in the template specified in the Border: <Border x:Name="button1" .../>.
Note: Or, you can just remove the Storyboard.TargetName, since it triggers the style is not supported.
You are correct that the EventTrigger is not working, but it is not because it was declared in the Resources section. To see this, you can move your style directly into the Button declaration where it still does not work:
<Button x:Name="button1" Grid.Column="0" Grid.Row="1">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard Storyboard="{StaticResource LoadStoryBoard}" />
</EventTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
However, if we move the declaration of the Animation from the Resources section, it works again:
<Button x:Name="button1" Grid.Column="0" Grid.Row="1">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard AutoReverse="True" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
So it seems as though the problem has something to do with the Storyboard declared in the Resources section not being ready by the time the Loaded event fires. There is a similar problem noted in this post.
However, just to confuse things more, if we then put the full declaration for the Animation into the Style declared in the Resources section, then now the Style works:
<Window.Resources>
<Style x:Key="Load" TargetType="Button">
<Style.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard AutoReverse="True" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Button.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.7" Value="0.4" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Button x:Name="button1" Grid.Column="0" Grid.Row="1" Style="{StaticResource Load}" />
I could speculate as to why this happens, but I'm guessing that there are very few WPF developers that really know why everything is the way that it is... I've learnt that if a particular declaration method works, use it and if not, try a different one.
Background
In WPF, there are four places where we can define Triggers; Style.Triggers, ControlTemplate.Triggers, DataTemplate.Triggers and FrameworkElement.Triggers (eg. Button.Triggers).
Basically, there is a huge flaw in the FrameworkElement.Triggers TriggerCollection in that it only accepts triggers of type EventTrigger. This can be seen on the FrameworkElement.Triggers Property page at MSDN where the following definition is given as to what this property can accept:
One or more defined EventTrigger elements. Each such trigger is
expected to contain valid storyboard actions and references. Note that
this collection can only be established on the root element of a page.
The MSDN property pages for the other trigger properties each announce that they can accept either Zero or more TriggerBase objects, or One or more TriggerBase objects.
Furthermore, there are distinct rules that different triggers follow - a unified approach would have certainly helped newcomers to WPF. From the FrameworkElement.Triggers Property page:
This property does not enable you to examine triggers that exist as
part of styles in use on this element. It only reports the collection
of triggers that are literally added to the collection, either in
markup or code. Elements do not typically have such elements existing
by default (through a template for instance); it is more common for
triggers that come from control compositing to be established in
styles instead.
In terms of behavior (and trying to establish which effect came from
which element's declared Triggers collection), both the triggering
condition and the trigger effect might be on this element, or might be
on its child elements in the logical tree. Note that if you use
lifetime events such as Loaded to get this collection, the child
element's triggers might not yet be fully loaded, and the collection
will be smaller than it would truly be at run time.
Note that the collection of triggers established on an element only
supports EventTrigger, not property triggers (Trigger). If you require
property triggers, you must place these within a style or template and
then assign that style or template to the element either directly
through the Style property, or indirectly through an implicit style
reference.
From the DataTemplate.Triggers Property page at MSDN:
If you are creating triggers within a data template, the setters of
the triggers should be setting properties that are within the scope of
the data template. Otherwise, it may be more suitable to create
triggers using a style that targets the type that contains the data.
For example, if you are binding a ListBox control, the containers are
ListBoxItem objects. If you are using triggers to set properties that
are not within the scope of the DataTemplate, then it may be more
suitable to create a ListBoxItem style and create triggers within that
style.
Unfortunately, all this extra information doesn't actually answer your question as to why the animation resource does not work in the Style resource, but hopefully now, you can see that the whole Trigger area is a bit of a complicated, messy area. Not being an expert myself, I just tend to use whichever method of declaring Triggers that works.
I hope that helps in some way.

How to bind Opacity value based on opacity of the object from another ControlTemplate?

I’d like to find out how to bind the opacity of the object that is part of the ControlTemplate to the object that is part of another ControlTemplate.
I tried this but it is not doing anything.
Image x:Name="PART_IconHover" Source="{Binding IconHover}" Opacity="{Binding Opacity, ElementName=border, Mode=OneWay}" />
Below is the code of two ControlTemplates:
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="border" BorderBrush="#FF6E6E6E" BorderThickness="0.5" Opacity="0" Background="#00000000">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="border">
<EasingDoubleKeyFrame KeyTime="0" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed"/>
<VisualState x:Name="Disabled"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocused" Value="true"/>
<Trigger Property="ToggleButton.IsChecked" Value="true"/>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="#ADADAD"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
I want to bind the opacity to the image in the ControlTemplate below
<ControlTemplate x:Key="ThumbnailContainerTemplate" TargetType="{x:Type ContentControl}">
<Border x:Name="PART_Border" BorderThickness="1" BorderBrush="#FFd9d9d9" Opacity="0" />
<Grid Margin="10">
<Image x:Name="PART_IconHover" Source="{Binding IconHover}" Opacity="{Binding Opacity, ElementName=border, Mode=OneWay}" />
</Grid>
Any ideas are highly appreciated. Thank you in advance!
I don't think that you can bind to elements inside templates like that. The binding system isn't able to find them.
If you just need a numeric value somewhere in your xaml that you want everything to use, you can just add one like this:
<sys:Double x:Key="Opacity">.5</sys:Double>
Then just have everything bind to that. You'll need to add the sys namespace
xmlns:sys="clr-namespace:System;assembly=mscorlib"
As mdm20 said, you can't bind to elements inside templates from outside the template since a template is just used to build up a control. For instance, several Buttons could use the Template in your example so which Button would the ContentControl bind to?
I can't see a re-usable solution to this but one thing that comes to mind is to set the Binding in code behind once the Controls have finished loading like this
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Border border = myButton.Template.FindName("border", myButton) as Border;
Image PART_IconHover = contentControl.Template.FindName("PART_IconHover", contentControl) as Image;
Binding opacityBinding = new Binding("Opacity");
opacityBinding.Mode = BindingMode.OneWay;
opacityBinding.Source = border;
PART_IconHover.SetBinding(Image.OpacityProperty, opacityBinding);
}
Update
Two Controls binding to the border in a Button template. The binding is made in the Control_Loaded event handler.
<ContentControl ...
Loaded="Control_Loaded">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Setter Property="Template" Value="{StaticResource contentTemplate}"/>
</Style>
</ContentControl.Style>
</ContentControl>
<ContentControl ...
Loaded="Control_Loaded">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Setter Property="Template" Value="{StaticResource contentTemplate}"/>
</Style>
</ContentControl.Style>
</ContentControl>
private void Control_Loaded(object sender, RoutedEventArgs e)
{
Border border = myButton.Template.FindName("border", myButton) as Border;
Control control = sender as Control;
Image PART_IconHover = control.Template.FindName("PART_IconHover", control) as Image;
Binding opacityBinding = new Binding("Opacity");
opacityBinding.Mode = BindingMode.OneWay;
opacityBinding.Source = border;
PART_IconHover.SetBinding(Image.OpacityProperty, opacityBinding);
}

Silverlight TreeView ScrollViewer Issue

Hey SO, got a question about the TreeView control in Silverlight.
I have an application which dynamically adds elements to a treeview. Some of the elements are long enough to require horizontal scrolling. When they are added to the treeview, my treeview remains correctly all the way scrolled left so you have to scroll to see the end of the item. However, if I click on one of my items (which the hides the treeview), and then use the 'back to results' button I implemented (note this only deals with visibility changes) the treeview becomes visible and it is automatically scrolled to the center.
Does anyone know how I can get the treeview to scroll all the way left when I hit back to results?
I've tried messing with the treeview template:
<Style TargetType="controls:TreeView" x:Name="SCREW">
<Setter Property="Background" Value="#FFFFFFFF" />
<Setter Property="Foreground" Value="#FF000000" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="Cursor" Value="Arrow" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="1" />
<Setter Property="BorderBrush" Value="#FF000000" />
<Setter Property="IsTabStop" Value="True" />
<Setter Property="TabNavigation" Value="Once" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:TreeView" x:Name="SCREWTEMPLATE">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver" />
<VisualState x:Name="Pressed" />
<VisualState x:Name="Disabled" />
</VisualStateGroup>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Unfocused" />
<VisualState x:Name="Focused" />
</VisualStateGroup>
<VisualStateGroup x:Name="ValidationStates">
<VisualState x:Name="Valid" />
<VisualState x:Name="InvalidUnfocused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Validation" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="InvalidFocused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Validation" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ValidationToolTip" Storyboard.TargetProperty="IsOpen">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<System:Boolean>True</System:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2">
<Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" Margin="1">
<ScrollViewer x:Name="ScrollViewer" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Background="{x:Null}" BorderBrush="Transparent" BorderThickness="0" IsTabStop="False" TabNavigation="Once" Loaded="ScrollViewer_Loaded">
<ItemsPresenter Margin="5" />
</ScrollViewer>
</Border>
</Border>
<Border x:Name="Validation" Grid.Column="1" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="#FFDB000C" CornerRadius="2" Visibility="Collapsed">
<ToolTipService.ToolTip>
<ToolTip x:Name="ValidationToolTip" Placement="Right" PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}}" DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}" IsHitTestVisible="True" />
</ToolTipService.ToolTip>
<Grid Width="10" Height="10" HorizontalAlignment="Right" Margin="0,-4,-4,0" VerticalAlignment="Top" Background="Transparent">
<Path Margin="-1,3,0,0" Fill="#FFDC000C" Data="M 1,0 L6,0 A 2,2 90 0 1 8,2 L8,7 Z" />
<Path Margin="-1,3,0,0" Fill="#FFFFFFFF" Data="M 0,0 L2,0 L 8,6 L8,8" />
</Grid>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
But the problem with that is I don't know how to access the ScrollViewer from the code behind... so I can't call ScrollView.setScrollOffset(0d,0d) or anything like that.
Any ideas? Thanks a million.
One last thing, I'd like to try to avoid implementing a new control that extends treeview. I'm really hoping there is a way to access/modify and use functions associated with the control template from c# codebehind.
I'd just set up an attached property for this and create the logic you want in there. Then you would decorate your treeview with the attached property. We do something similar with other controls that contain scrollviewers:
public class ScrollResetService
{
public static DependencyProperty IsScrollResetProperty = DependencyProperty.RegisterAttached("IsScrollReset",
typeof(bool),
typeof(ScrollResetService),
new PropertyMetadata(false,
OnIsScrollResetChanged));
public static void SetIsScrollReset(DependencyObject d, bool value)
{
d.SetValue(IsScrollResetProperty, value);
}
public static bool GetIsScrollReset(DependencyObject d)
{
return d.GetValue(IsScrollResetProperty) == null ? false : (bool)d.GetValue(IsScrollResetProperty);
}
private static void OnIsScrollResetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var treeView = d as TreeView;
bool isScrollReset;
if (e.NewValue!= null && bool.TryParse(e.NewValue.ToString(), out isScrollReset) && treeView != null)
{
treeView.SelectedItemChanged += (sender, args) =>
{
var scrolls =
treeView.GetAllLogicalChildrenOfType<IScrollInfo>();
scrolls.ForEach(i => i.SetVerticalOffset(0));
};
}
}
}
public static class Extensions
{
public static IEnumerable<T> GetAllLogicalChildrenOfType<T>(this FrameworkElement parent)
{
Debug.Assert(parent != null, "The parent cannot be null.");
return parent.GetVisualChildren().Flatten(item => item.GetVisualChildren()).OfType<T>();
}
public static IEnumerable<T> Flatten<T>(this IEnumerable<T> items, Func<T, IEnumerable<T>> childSelector)
{
if (items == null) return Enumerable.Empty<T>();
return items.Concat(items.SelectMany(i => childSelector(i).Flatten(childSelector)));
}
internal static IEnumerable<DependencyObject> GetVisualChildren(this DependencyObject parent)
{
int childCount = VisualTreeHelper.GetChildrenCount(parent);
for (int counter = 0; counter < childCount; counter++)
{
yield return VisualTreeHelper.GetChild(parent, counter);
}
}
}
I put this code into a behavior (would also work in code behind) after handling the SelectedItemChanged event of the TreeView:
var offset = this.HorizontalScrollOffsetAfterSelect; // would be 0 if you don't want to make that adjustable
if (!Double.IsNaN(offset))
{
var scrollViewer = trv.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
if (scrollViewer != null)
{
scrollViewer.ScrollToHorizontalOffset(offset);
// and because that wasn't enough because of timing issues:
var scrollBar = scrollViewer.GetVisualDescendants().OfType<ScrollBar>().Where(cur => cur.Orientation == Orientation.Horizontal).FirstOrDefault();
if (scrollBar != null)
{
RoutedPropertyChangedEventHandler<double> handler = null;
handler = (sender, e) =>
{
scrollBar.ValueChanged -= handler;
scrollViewer.ScrollToHorizontalOffset(offset);
};
scrollBar.ValueChanged += handler;
}
}
}
Requires using System.Linq; and using System.Windows.Controls.Primitives; and the Toolkit ofc.
Works well enough for me so far.

Resources