Change DataTemplate style in ListBoxItem if the item is selected - wpf

I have a listbox with an expander inside the ItemTemplate. I managed to bind the expander's IsExpanded property to the ListBoxItem's IsSelected property ok. Now I want to apply a style to the ListBoxItem's content also bound to the IsSelected property.
<ListBox.ItemTemplate>
<DataTemplate>
<Border Name="myBorder">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Description}" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Date:"/>
<TextBlock Text="{Binding Date}"/>
</StackPanel>
<dx:DXExpander Name="expanderDetails"
IsExpanded="{Binding Mode=TwoWay, Path=IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem, Mode=FindAncestor}}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Count:"/>
<TextBlock Text="{Binding Count}"/>
</StackPanel>
</dx:DXExpander>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
What I want to do is somehow set the style of the "myBorder" Border to "NotSelectedBorderStyle" for unselected ListBoxItems, and to "SelectedBorderStyle" for the SelectedItem (ListBox with single selection).
FYI, the styles define background, border and that kind of stuff, just to make the clear which item is selected, nothing fancy in these styles.
I tried the accepted answer here but if I completely switch styles I loose the nice expanding animation my DXExpander has.
I guess there must be some solution using triggers, but I can't just hit the right spot.

Finally I got it, I'm posting it here in the hope that this will save someone else time and pain :-P
This code does some additional things: the EventSetter and the corresponding Handler method are there to capture clicks to the elements inside the DataTemplate, in order to select the ListBoxItem which contains the element (if you don't you might type text inside an item, while a different one is selected).
The inner Border ("myBorder") is just a container for the stackpanels, I had to wrap everything inside another border ("backgroundBorder") which gets the style changed when the ListBoxItem gets selected.
<Style x:Key="FocusedContainer" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="LightGray"/>
<EventSetter Event="GotKeyboardFocus" Handler="OnListBoxItemContainerFocused" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="backgroundBorder" Width="Auto" Style="{StaticResource NotSelectedBorderStyle}">
<ContentPresenter Content="{TemplateBinding Content}">
<ContentPresenter.ContentTemplate>
<DataTemplate>
<Border Name="myBorder">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Description}" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Date:"/>
<TextBlock Text="{Binding Date}"/>
</StackPanel>
<dx:DXExpander Name="expanderDetails"
IsExpanded="{Binding Mode=TwoWay, Path=IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem, Mode=FindAncestor}}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Count:"/>
<TextBlock Text="{Binding Count}"/>
</StackPanel>
</dx:DXExpander>
</StackPanel>
</Border>
</DataTemplate>
</ContentPresenter.ContentTemplate>
</ContentPresenter>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="backgroundBorder" Property="Style" Value="{StaticResource SelectedBorderStyle}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Then I set the ItemContainerStyle in my ListBox to the above style:
<ListBox Background="#7FFFFFFF" HorizontalContentAlignment="Stretch"
ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True"
ItemContainerStyle="{StaticResource FocusedContainer}"/>
To finish, the code behind for the GotKeyBoardFocus handler:
private void OnListBoxItemContainerFocused(object sender, RoutedEventArgs e)
{
(sender as ListBoxItem).IsSelected = true;
}
A mess in code, but pretty neat in the UI. Hope this helps someone!

Related

ComboBoxItem Will Not Collapse

I wrote a basic visibility converter such that when the property "Active" is true the ComboBoxItem should be Visible, Collapsed otherwise. It currently displays the Active ones properly, the inactive ones Text are invisible but the item can still be seen.
http://snag.gy/Mh2Xq.jpg
May I ask how do I get the ComboBoxItem to collapse the comboboxitem that are inactive properly please.
<ComboBox Grid.Row="1" Grid.Column="2" SelectedItem="{Binding Product, Mode=TwoWay}" ItemsSource="{Binding Products}" VerticalContentAlignment="Center">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="{Binding Active, Converter={StaticResource VisibilityConverter }}"></Setter>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Apply the visibility converter to the parent stackpanel instead. Like so:
<StackPanel Orientation="Horizontal" Visibility="{Binding Active, Converter={StaticResource VisibilityConverter}}">
...
</StackPanel>
<ComboBox.Resources>
<Style TargetType="ComboBoxItem">
<Setter Property="Visibility" Value="{Binding Active, Converter={StaticResource VisibilityConverter}}" />
</Style>
</ComboBox.Resources>
You should filter the bound list on basis of IsActive
Try to Bind comboBox to
Products.Where(t => t.IsActive)

To know the hosting Control of a Control

I have a custom control, its control template will be looks below.
<Style TargetType="local:CustomButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:CustomButton">
<Grid>
<Border x:Name="CtrlBorder">
<StackPanel Orientation="Horizontal">
<TextBox Name="Tbox"
BorderThickness="1,1,0,1"
Text="{Binding TextBoxText,
RelativeSource={RelativeSource TemplatedParent},
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<Button Width="20"
Background="#FFF0F0F0"
BorderThickness="0,1,1,1"
IsTabStop="False">
</Button>
</StackPanel>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In an event, i got the Tbox and i need to get the CustomButton with this Tbox.
Any idea on this?
Can get throught TemplatedParent property of that control.
Custom control will lie in Visual Tree as parent so FindAncestor will work here:
Text="{Binding TextBoxText,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=local:CustomButton},
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />

Need multiple styles dependent on Listboxitem

i have a Listbox, which stores two different object types, based on the same baseclass. (e.g. BaseObject = baseclass and the children of it: CustomPath and CustomImage)
The Datasource:
ObservableCollection<BattlegroundBaseObject> _baseObjectCollection;
public ObservableCollection<BattlegroundBaseObject> BaseObjectCollection
{
get { return _baseObjectCollection?? (_baseObjectCollection= new ObservableCollection<BaseObject>()); }
}
The Listbox databinding: <ListBox ItemsSource="{Binding BaseObjectCollection}"
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem" x:Name="ListBoxPathLineStyle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem" x:Name="BattlegroundObjectControlTemplate">
<Path Stroke="{Binding ObjectColor}" StrokeThickness="{Binding StrokeThickness}" Data="{Binding PathGeometryData}" x:Name="PathLine" Opacity="{Binding Opacity}">
</Path>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Effect" TargetName="PathLine">
<Setter.Value>
<DropShadowEffect Color="CornflowerBlue" ShadowDepth="3" BlurRadius="10" />
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
I want to add to the ControlTemplate where the Path is, also a Image and to differ it by type or a property. doesnt matter.
anyone any ideas?
You can add to ListBox resources DataTemplate for each type.
In my example classes Car and Motorbike derived from Vehicle class.
<ListBox x:Name="listBox">
<ListBox.Resources>
<DataTemplate DataType="{x:Type local:Car}">
<StackPanel Background="Red">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Motorbike}">
<StackPanel Background="Orange">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ListBox.Resources>
</ListBox>
EDIT:
You can add style for ListBoxItem to resources:
<ListBox x:Name="listBox">
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="CornflowerBlue" ShadowDepth="3" BlurRadius="10" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type local:Car}">
<StackPanel Background="Red">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Motorbike}">
<StackPanel Background="Orange">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ListBox.Resources>
</ListBox>
You could define some DataTemplates for your different classes. They determine how the classes are displayed. I've been using them to display derived classes differently when working with a collection of the base class.
<DataTemplate DataType="{x:Type CustomPath}">
<TextBlock Text="This is a CustomPath"/>
</DataTemplate>
<DataTemplate DataType="{x:Type CustomImage}">
<TextBlock Text="This is a CustomImage"/>
</DataTemplate>
At the moment you are changing the style of the controls that WPF is using to render your bound data. A better way to do this is to provide WPF with a way of generating the correct controls. Ignore the ListBoxItem and use DataTemplates for your actual objects.
First you need to tell the Window or control how to find your types.
<Window or UserControl
...
xmlns:model="clr-namespace:yourNamespace"
>
Then you can provide WPF with a way to show your objects e.g.
<DataTemplate TargetType="{x:Type model:CustomPath}">
<Path Stroke="{Binding ObjectColor}" StrokeThickness="{Binding StrokeThickness}"
Data="{Binding PathGeometryData}" x:Name="PathLine" Opacity="{Binding Opacity}"/>
<!-- maybe use a binding from the Path.Effect back to the IsSelected and ValueConverters
to re-apply the selection effect-->
</DataTemplate>
<DataTemplate TargetType="{x:Type model:CustomImage}">
<Image Src="{Binding SomeProperty}" />
</DataTemplate>
Now all you need to do is to make these available to the ListBox in some way. Almost every element in WPF can have .Resources added to it, so you could choose to do these across the entire window
<Window ...>
<Window.Resources>
<DataTemplate .../>
<DataTemplate .../>
</Window.Resources>
...
<ListBox .../>
</Window>
or you can apply it more locally
<Window ...>
...
<ListBox>
<ListBox.Resources>
<DataTemplate .../>
<DataTemplate .../>
</ListBox.Resources>
</ListBox>
</Window>
And this way your listbox definition can become much neater too, e.g. if you are using Window.Resources
<ListBox ItemsSource="{Binding BaseObjectCollection}"/>

Binding a command defined in a datatemplate

I know there are few answers on this topic. But none of them was working in my case.
I have a ListView with a style and an ItemContainerStyle. In the ItemContainer Style, I define some triggers in order to use a different DataTemplate depending if the item in the list is selected or not. Then, finally in the Datatemplate I have a context menu with a command. The problem is how to bind the command to the viewmodel.
This is the ListView:
<ListView
x:Name="lstPersons"
Grid.Row="1"
Style="{StaticResource ListViewStyle}"
ItemContainerStyle="{StaticResource ItemContainerStyle}"
DataContext="{Binding}"
ItemsSource="{Binding Path=Persons}"
Tag="{Binding}"
SelectedItem="{Binding Path=SelectedPerson, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</ListView>
and these are the styles, datatemplates and contextmenu (defined in a resource dictionary).
The commands in the context menu do not work....:
<ContextMenu x:Key="SelectedItemContextMenu">
<MenuItem
Header="Do Something"
Command="{Binding Path=DataContext.DoSomethingCmd, ElementName=LayoutRoot}">
</MenuItem>
<MenuItem
Header="Do Something"
Command="{Binding PlacementTarget.Tag.DoSomethingCmd, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
</MenuItem>
</ContextMenu>
<DataTemplate
x:Key="ItemTemplate">
<Canvas
Margin="4"
Width="60"
Height="60"
Background="LightGray">
<TextBlock
Foreground="Black"
Margin="2 0 0 0"
Opacity="0.5"
FontFamily="Segoe UI"
Text="{Binding Path=FirstName}" />
</Canvas>
</DataTemplate>
<DataTemplate
x:Key="ItemSelectedTemplate">
<Grid>
<Border
BorderBrush="Black"
BorderThickness="1"
Margin="3"
ContextMenu="{DynamicResource SelectedItemContextMenu}">
<Canvas
Width="60"
Height="60"
Background="LightBlue">
<TextBlock
Foreground="Black"
Margin="2 0 0 0"
Opacity="0.5"
FontFamily="Segoe UI"
Text="{Binding Path=FirstName}" />
</Canvas>
</Border>
</Grid>
</DataTemplate>
<!--style of the listviewitem-->
<Style
TargetType="{x:Type ListViewItem}"
x:Key="ItemContainerStyle">
<Setter
Property="ContentTemplate"
Value="{StaticResource ItemTemplate}" />
<Style.Triggers>
<Trigger
Property="IsSelected"
Value="True">
<Setter
Property="ContentTemplate"
Value="{StaticResource ItemSelectedTemplate}" />
</Trigger>
</Style.Triggers>
</Style>
<!--style of the listview-->
<Style
TargetType="{x:Type ListBox}"
x:Key="ListViewStyle">
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type ListBox}">
<Grid>
<Border>
<ScrollViewer
Focusable="false">
<WrapPanel
IsItemsHost="True"
Orientation="Horizontal"
Width="{Binding (FrameworkElement.ActualWidth), RelativeSource={RelativeSource AncestorType=ScrollContentPresenter}}"/>
</ScrollViewer>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Your ContextMenu is used inside a data template. I will be put in a different name scope of "LayoutRoot" and ElementName binding won't work. Also, the PlacementTarget of your context menu is the Border, and you've not setup any Tag on it. So the second command won't work either.
It looks like you are implement the commands on the ListBox level (or LayoutRoot?). It might be easier to put your context menu on the ListBox, and use ListBox.SelectedItem to find the current selection and apply your logic on it.
You can use RelativeSource:
<ContextMenu x:Key="SelectedItemContextMenu">
<MenuItem
Header="Do Something"
Command="{Binding Path=DataContext.DoSomethingCmd, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}">
</MenuItem>
</ContextMenu>
You should probably be using RoutedCommands instead of VM commands in this case. You would bind the RoutedCommand to the ContextMenu, and since you only need static object references for that, finding them shouldn't be a problem. Then you'd set up appropriate CommandBindings on the controls that should handle the commands (either ListView or ListViewItem, depending on whether you want the List-ViewModel or the Item-ViewModel to handle the command). These controls will know their ViewModels, so binding to them will not be a problem there. Through the process of Command Routing, which is built-in in WPF, the context menu will find the proper target for its command automatically.
For guidance on how to set up CommandBindings in a MVVM-friendly way, you might want to refer to http://wpfglue.wordpress.com/2012/05/07/commanding-binding-controls-to-methods/

How to make CollectionView grouping respect data template settings

I have ListBox with group style:
<GroupStyle HidesIfEmpty="True" x:Key="GroupStyle">
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Margin" Value="0,0,0,5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="True" BorderBrush="#FFA4B97F" BorderThickness="0,0,0,1">
<Expander.Header>
<DockPanel Background="LightSkyBlue"
Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Expander}},
Path=ActualWidth}">
<TextBlock FontWeight="Bold" Text="{Binding Path=Name}" Margin="5,0,0,0" Width="100"/>
<TextBlock FontWeight="Bold" Text="{Binding Path=ItemCount}"/>
</DockPanel>
</Expander.Header>
<Expander.Content>
<ItemsPresenter/>
</Expander.Content>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
Any change in bound collection causes expanders to reopen.
Is there any way to remember user preferences and leave expanders folded?
The question is what you are doing with the bound collection.
I agree with Nikolay that the best option is to have your viewmodel have a property that you will bind the expander to.
As for the question I've asked at the beginning: Are you somewhat refreshing the CollectionView?
Refresh causes the UI to be recreated. (Meaning, it is like Reset on an ObservableCollection. It tells the UI to create the user controls and load the XAML of your templates again which is bad for performance).
I can suggest a simple hack - just add IsExpanded property to your model class and bind Expander.IsExpanded to it.

Resources