I have created TreeView user control that allows multiselection. In order to visually show the selected items I use triggers in a Style for TreeViewItem. This Style is defined in the user control resources as follows:
<TreeView x:Class="SchoolsExample.MultiSelectionTreeView"
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"
xmlns:local="clr-namespace:SchoolsExample"
d:DesignHeight="300" d:DesignWidth="300" SelectedItemChanged="TreeView_SelectedItemChanged">
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="TreeViewItem_MouseLeftButtonDown"/>
<EventSetter Event="MouseLeftButtonUp" Handler="TreeViewItem_MouseLeftButtonUp"/>
<Style.Triggers>
<Trigger Property="local:MultiSelectionTreeViewItemBehaviour.IsMultiSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Trigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(local:MultiSelectionTreeViewItemBehaviour.IsMultiSelected), RelativeSource={RelativeSource Self}}" Value="True"/>
<Condition Binding="{Binding IsSelectionActive, RelativeSource={RelativeSource Self}}" Value="False"/>
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</TreeView.Resources>
</TreeView>
But when I want to use this control I want to style my TreeViewItems something like this:
<local:MultiSelectionTreeView x:Name="MultiSelectionTreeView" ItemsSource="{Binding Schools}">
<local:MultiSelectionTreeView.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="local:MultiSelectionTreeViewItemBehaviour.IsMultiSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</local:MultiSelectionTreeView.Resources>
<local:MultiSelectionTreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Pupils}">
<TextBlock Text="{Binding Name}"/>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FullName}"/>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</local:MultiSelectionTreeView.ItemTemplate>
</local:MultiSelectionTreeView>
Then I get an exception saying that I can't set the Resource twice. Even if I manage to set the style for the TreeViewItems somehow, then the visual behaviour that I want for the TreeViewItems is overwriten.
What I want to know is if there is some way to set a default visual behaviour for the TreeViewItems I use in my TreeView user control and yet allow me to set an Style for those TreeViewItems when I use the user control.
Thanks in advance.
Update:
Exception message:
System.Windows.Markup.XamlParseException: ''Set property 'System.Windows.ResourceDictionary.DeferrableContent' threw an exception.' Line number '22' and line position '14'.'
Inner exception:
InvalidOperationException: Cannot re-initialize ResourceDictionary instance.
The initial problem is because you cannot have two resources sections for a control. They collide.
Since the idea is to expose the treeview itself as the control then you need some other way to pass in extra styling.
One way to do that is with a dependency property.
Code in the control itself can then grab whatever you pass in and add it to the styling it already has. Not a very nice solution but it has the benefits of being reasonably straight forward without forcing you to redo the way the control works.
Related
I have below WPF UserControl:
<UserControl x:Class="myComponents.UI.TextBoxWithPlaceholder"
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:local="clr-namespace:pEp.UI"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Loaded="UserControl_Loaded">
<Grid DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TextBoxWithPlaceholder}}"
Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox Name="myCustomTextBox"
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
Padding="5"
IsReadOnly="{Binding IsReadOnly}"
HorizontalAlignment="Stretch"
TextChanged="CustomTextBox_TextChanged"
GotFocus="CustomTextBox_GotFocus"
LostFocus="CustomTextBox_LostFocus"
Margin="5"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto" />
<TextBlock Name="myPlaceholderTextBlock"
IsHitTestVisible="False"
Padding="5"
Text="{Binding Placeholder}"
HorizontalAlignment="Left"
Foreground="DarkGray"
Margin="5">
</TextBlock>
</Grid>
</UserControl>
Basically it is a TextBox with a placeholder.
Now from a WPF view I reuse this component by doing:
xmlns:ui="clr-namespace:myComponents.UI"
and then place it as a normal control:
<ui:TextBoxWithPlaceholder Name="myNewTextBox" IsReadOnly="{Binding IsReadOnly}"
Style="{StaticResource myTextBoxStyle}"
Placeholder="please, enter something here"/>
Now as you see above I set a custom style for it:
<Style x:Key="myTextBoxStyle" TargetType="{x:Type ui:TextBoxWithPlaceholder}">
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="False">
<Setter Property="Background" Value="{x:Null}"/>
<Setter Property="Foreground" Value="{x:Null}"/>
<Setter Property="BorderBrush" Value="{x:Null}"/>
</Trigger>
</Style.Triggers>
</Style>
Now in my "myNewTextBox" control I am trying to override some inherited properties for the controls named myCustomTextBox and myPlaceholderTextBlock such as Margin, Padding, Background, Foreground, BorderBrush, etc. but I have tried above style and it is not working. Also I have tried:
<Style x:Key="myTextBoxStyle" TargetType="{x:Type ui:TextBoxWithPlaceholder}">
<Setter Property="{Binding Path=Margin, ElementName=myCustomTextBox}" Value="0" />
<Setter Property="{Binding Path=Padding, ElementName=myCustomTextBox}" Value="0" />
<Setter Property="{Binding Path=Margin, ElementName=myPlaceholderTextBlock }" Value="0" />
<Setter Property="{Binding Path=Padding, ElementName=myPlaceholderTextBlock }" Value="0" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="False">
<Setter Property="{Binding Path=Background, ElementName=myCustomTextBox}" Value="{x:Null}"/>
<Setter Property="{Binding Path=Foreground, ElementName=myCustomTextBox}" Value="{x:Null}"/>
<Setter Property="{Binding Path=BorderBrush, ElementName=myCustomTextBox}" Value="{x:Null}"/>
</Trigger>
</Style.Triggers>
</Style>
If you want to be able to set properties of the myCustomTextBox from view that consumes your TextBoxWithPlaceholder control, you should add dependency properties to the latter and bind to them in TextBoxWithPlaceholder.xaml and set them in the consuming view, e.g.:
<ui:TextBoxWithPlaceholder ....PlaceHolderMargin="10" />
TextBoxWithPlaceholder.xaml:
<TextBlock Name="myPlaceholderTextBlock"
...
Margin="{Binding PlaceHolderMargin,RelativeSource={RelativeSource AncestorType=UserControl}}">
I am afraid you cannot refer to ElementName=myPlaceholderTextBlock from a namescope outside the TextBoxWithPlaceholder control so trying to do this in a Style that's defined in a consuming view won't work.
This is a task that cries out for a Custom control and Visual States instead of a UserControl with Triggers. But if you must do this as a UserControl (and I don't blame you because that's a lot to learn at this stage) then here goes:
First of all, when you use ElementName it is supposed to refer to elements that the XAML processor has already seen, previously in the current UI being laid out. Not elements inside the control being styled. I don't see that approach working.
If you want the TextBox and TextBlock inside a TextBoxWithPlaceholder to use the properties of that outer control, you could bind them to it, inside your control's XAML. For example, to rewrite a small part of that binding the background.
<TextBox Name="myCustomTextBox"
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
Background={Binding RelativeSource={RelativeSource.FindAncestor, AncestorType={x:Type ui:TextBoxWithPlaceholder}, Path=Background}}"
But if you truly want that nested TextBox ("myCustomTextBox") to use a style with triggers and its own dedicated property values, then what you might try is creating a Resources section inside your style that itself contains implicit styles for the TextBox and TextBlock Something like this
<Style x:Key="myTextBoxStyle" TargetType="{x:Type ui:TextBoxWithPlaceholder}">
<Style.Resources>
<!-- Implicit style for TextBox should only apply to TextBoxes inside a TextBoxWithPlaceholder -->
<Style TargetType="{x:Type TextBox}">
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="False">
<Setter Property="Background Value="{x:Null}"/>
<Setter Property="Foreground" Value="{x:Null}"/>
<Setter Property="BorderBrush" Value="{x:Null}"/>
</Trigger>
<Style.Triggers>
</Style>
</Style.Resources>
</Style>
I want to assign a new DataTrigger programmatically (in code/not xaml) to every "TreeViewItem" style like I do in the following xaml.
I did some code (under xaml) where I defined my trigger but "ItemContainerStyle" is null. The function is called on Window Initialize event.
Anybody has an idea what I'm doing wrong ?
EDIT
I found some part of my problem: I moved my style from the "Resource" section to the "ItemContainerStyle" section as defined below in code sample. This way the style is still applied and I can access the style from the TReeView.ItemContainerStyle property in code. But I still don't know how to get the TreeViewItem selected style like the color of the background by code ??
I have xaml:
<TreeView Name="TreeViewSelectScopeStudy" MinHeight="24" Margin="7"
ItemsSource="{Binding Path=TvItemRootPssTreeViewRoot.ChildsView}" Height="Auto"
VerticalAlignment="Stretch"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsHilighted}" Value="true">
<!--<Setter Property="Background" Value="SlateBlue"></Setter>-->
<Setter Property="Background" Value="{StaticResource {x:Static SystemColors.HighlightBrushKey}}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
I have that code:
public static void EnableMultipleSelection(TreeView treeView)
{
if (!_isMultiSelectTreeViewLeftButtonHandlerRegistered)
{
EventManager.RegisterClassHandler(typeof(TreeViewItem), UIElement.MouseDownEvent,
new MouseButtonEventHandler(TreeViewMouseDownGlobal));
_isMultiSelectTreeViewLeftButtonHandlerRegistered = true;
}
DataTrigger dataTrigger = new DataTrigger();
dataTrigger.Binding = new Binding("IsHilighted");
dataTrigger.Value = true;
dataTrigger.Setters.Add(new Setter(TreeViewItem.BackgroundProperty, new SolidColorBrush(Colors.Brown)));
treeView.ItemContainerStyle.Triggers.Add(dataTrigger);
Edited code:
<TreeView Name="TreeViewSelectScopeStudy" MinHeight="24" Margin="7"
ItemsSource="{Binding Path=TvItemRootPssTreeViewRoot.ChildsView}" Height="Auto"
VerticalAlignment="Stretch"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsHilighted}" Value="true">
<Setter Property="Background" Value="{StaticResource {x:Static SystemColors.HighlightBrushKey}}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<!--<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsHilighted}" Value="true">
<Setter Property="Background" Value="{StaticResource {x:Static SystemColors.HighlightBrushKey}}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>-->
It sounds like you are trying to set the Style.Trigger too soon, before the template has been applied. If you apply the Trigger after the FrameWorkElement you are targeting has fully loaded you'll probably find that the ItemStyleContainer is no longer null.
http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.loaded.aspx
Implementing OnApplyTemplate for your FrameworkElement should hook you in after the ItemStyleContainer has been set as it is called after the Visual Tree has been rendered.
http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.onapplytemplate.aspx
I want to create a Trigger to be applied to all TextBox on Validation.HasError to show the Validation.Error in a custom ToolTip.
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="ToolTip">
<Setter.Value>
<StackPanel>
<TextBlock Text="{Binding RelativeSource={RelativeSource XXX}, Path=(Validation.Error)[0].ErrorContent}"/>
</StackPanel>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
What should I put in the XXX?
My problem is that I don't really understand how RelativeSource works in this context and I can't get the correct code for binding to the TextBox.
I am guessing Self would refer to the TextBlock and FindAncestor x:Type TextBox will fail becuase it will traverse from TextBlock > StackPanel > Setter.Value > Setter > etc.. instead.
How can I refer to the Style Target instead?
Since ToolTip is not part of the visual tree, it's a bit cumbersome to get the behavior you want.
You can use its PlacementTarget property to find the element it is attached to, and set its DataContext to that element. In your case that will be a TextBox.
Now you can bind directly to the Validation.Errors property, and it will find the validation errors on a given TextBox.
You can use the following code to get it working:
<Window.Resources>
<ToolTip x:Key="errorTooltip"
DataContext="{Binding PlacementTarget,
RelativeSource={RelativeSource Self}}">
<StackPanel>
<TextBlock Text="{Binding (Validation.Errors)[0].ErrorContent}" />
</StackPanel>
</ToolTip>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="ToolTip" Value="{StaticResource errorTooltip}" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
This seems like a pretty common scenario but I can't figure out how to bind the menu items to disable when there is nothing in the clipboard.
I've decided against using the Windows clipboard and instead store the actual object in a reference variable on the UserControl called NodeClipboard. Since it is strongly typed and implements INotifyProperty it is a lot more useful to me than the Windows clipboard.
Binding to the individual item works fine though it is extremely verbose because you can't set EventHandlers within resources without using the Style Event Setters.
It sort of looks like this...
<UserControl x:Name="PART_Root">
<TreeView x:Name="PART_Tree" ItemsSource="{Binding ElementName=PART_Root, Path=RootItemContainer}">
<TreeView.Resources>
<ContextMenu x:Key="ContextMenu">
<ContextMenu.Style>
<Style TargetType="ContextMenu">
<!-- I use this event to select the tree view item otherwise it is actually pretty difficult to know what item you right clicked on -->
<EventSetter Event="Opened" Handler="ContextMenu_Opened"/>
</Style>
</ContextMenu.Style>
<MenuItem Header="Cut">
<MenuItem.Style>
<Style TargetType="MenuItem">
<EventSetter Event="Click" Handler="CutNode_Click"/>
<Style.Triggers>
<!-- This binding is fine because it binds to the item that was right clicked on -->
<DataTrigger Binding="{Binding Path=IsRoot}" Value="True">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<MenuItem Header="Paste">
<MenuItem.Style>
<Style TargetType="MenuItem">
<EventSetter Event="Click" Handler="PasteNode_Click"/>
<!-- This binding always fails because ContextMenu lives outside of the logical tree -->
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=PART_Root, Path=NodeClipboard" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
</ContextMenu>
<Style TargetType="TreeViewItem">
<Setter Property="ContextMenu" Value="{StaticResource ContextMenu}"/>
</Style>
<HierarchicalDataTemplate DataType="{x:Type local:Node}" ItemsSource="{Binding Path=Children}">
<TextBlock Text="{Binding Path=Id}"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</UserControl>
The key part that doesn't work is this here:
<!-- This binding always fails because ContextMenu lives outside of the logical tree -->
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=PART_Root, Path=NodeClipboard" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
I've tried using relative source which results in the same problem. The only solution I've thought of so far is making two context menus, one with Paste enabled and one without, and switching the context menu on the style on the TreeViewItem style's ContextMenu setter like so...
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="ContextMenu" Value="{StaticResource ContextMenu_PasteEnabled}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=PART_Root, Path=NodeClipboard" Value="{x:Null}">
<Setter Property="ContextMenu" Value="{StaticResource ContextMenu_PasteDisabled}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.Resources>
Use the Clipboard class. Then you can use the ContainsText method to determine if there is any data on the clipboard.
http://msdn.microsoft.com/en-us/library/system.windows.forms.clipboard.aspx
First, let me say I've been working with WPF for about a week. I want to style a TextBox so that when it is disable, it is cleared. This article explained how to do it, however I'm confused on how to set the generic style as a resource so that every TextBox can bind to a different property without repeating the style for each TextBox.
<Window.Resources>
<Style TargetType="{x:Type TextBox}" x:Key="style1">
<Setter Property="Text" Value="{What do I really put here?}" />
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Text" Value="{x:Null}" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
....
<TextBox Style="{StaticResource style1}" Text="{Binding SomeProperty}"/>
Thanks!
You won't be able to use the Text property like that. Setting the Text property explicitly on any TextBox that has that style will override the Text setter in the trigger (like you noticed).
If you only need the TextBox to be cleared and not the property it is binding to, then a workaround is to use an attached property (or Tag) for the text which you bind Text to in the Style.
Example..
<Style TargetType="{x:Type TextBox}" x:Key="style1">
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self},
Path=Tag}"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Text" Value="{x:Null}" />
</Trigger>
</Style.Triggers>
</Style>
Then a TextBox can use this Style like
<TextBox Style="{StaticResource style1}" Tag="{Binding SomeProperty}" />