WPF Double Listbox menu with tracking and selection problem - wpf

I'm trying to implement a double level menu with track in WPF.
I created my NavigationBar, I use Binding to fill the NavigationBar.
I've two problem:
I'm not able to remove the selection and mouse over from listbox used.
I tried to set the style in NavigationBar, base on some other link found:
How to disable highlighting on listbox but keep selection?
WPF: Remove highlight effect from ListViewItem
https://www.codeproject.com/Questions/838199/WPF-ListBox-not-showing-selection-highlight
But seems not working
Seletion and track doesn't work fine
ADD: For tracking I mean Selection is Bold and the element "SelectedMenuItemLine" is under the selected element. For the second NavigationBar, "SubNavigationBar", does not work fine, when I select an element of the first NavigationBar the second updates all element and I need to select the first element of the new selection.
Here my code
My ViewModel (ViewModelBase is a utility class that implement INotifyPropertyChanged)
public class MainMenuViewModel : ViewModelBase
{
public ObservableCollection<PluginItem> MainMenuTabs = new ObservableCollection<PluginItem>();
private PluginItem _selectedMainPluginItem;
private PluginItem _selectedSubPluginItem;
private ObservableCollection<PluginItem> _subMenuTabs;
public ObservableCollection<PluginItem> SubMenuTabs
{
get => _subMenuTabs;
set
{
_subMenuTabs = value;
OnPropertyChanged(nameof(SubMenuTabs));
}
}
public PluginItem SelectedMainPluginItem
{
get => _selectedMainPluginItem;
set
{
_selectedMainPluginItem = value;
OnPropertyChanged(nameof(SelectedMainPluginItem));
SubMenuTabs = value.PluginItems;
}
}
public PluginItem SelectedSubPluginItem
{
get => _selectedSubPluginItem;
set
{
_selectedSubPluginItem = value;
OnPropertyChanged(nameof(SelectedSubPluginItem));
}
}
}
public class PluginItem : ViewModelBase
{
private ObservableCollection<PluginItem> m_PluginItems = new ObservableCollection<PluginItem>();
public string Name { get; set; }
public string Description { get; set; }
public PluginItem Parent { get; set; }
public ObservableCollection<PluginItem> PluginItems
{
get => m_PluginItems;
set
{
m_PluginItems = value;
foreach (var pluginItem in m_PluginItems)
pluginItem.Parent = this;
OnPropertyChanged(nameof(PluginItems));
}
}
public bool HasChildren => PluginItems.Count > 0;
}
NavigationBar.xaml
<UserControl x:Class="DoubleMenuTest.NavigationBar"
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:DoubleMenuTest"
mc:Ignorable="d"
Height="32"
BorderThickness="0">
<UserControl.Resources>
<SolidColorBrush x:Key="ListBox.Static.Background" Color="#FFFFFFFF"/>
<SolidColorBrush x:Key="ListBox.Static.Border" Color="#FFABADB3"/>
<SolidColorBrush x:Key="ListBox.Disabled.Background" Color="#FFFFFFFF"/>
<SolidColorBrush x:Key="ListBox.Disabled.Border" Color="#FFD9D9D9"/>
</UserControl.Resources>
<Grid Height="32">
<ListBox
Name="MenuListBox"
BorderThickness="0"
Background="Transparent"
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType=local:NavigationBar,
AncestorLevel=1},
Path=ItemsSource}"
SelectionChanged="MenuListBox_OnSelectionChanged">
<ListBox.Style>
<Style>
<Setter Property="ListBox.Background" Value="{StaticResource ListBox.Static.Background}"/>
<Setter Property="ListBox.BorderBrush" Value="{StaticResource ListBox.Static.Border}"/>
<Setter Property="ListBox.BorderThickness" Value="1"/>
<Setter Property="ListBox.Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
<Setter Property="ScrollViewer.PanningMode" Value="Both"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="ListBox.VerticalContentAlignment" Value="Center"/>
<Setter Property="ListBox.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!--<RepeatButton x:Name="LeftButton" Tag="{Binding ElementName=sv}" Width="20" Click="Left_Click">
<Path x:Name="ArrowLeft" Data="M 3.18,7 C3.18,7 5,7 5,7 5,7 1.81,3.5 1.81,3.5 1.81,3.5 5,0 5,0 5,0 3.18,0 3.18,0 3.18,0 0,3.5 0,3.5 0,3.5 3.18,7 3.18,7 z" Fill="Black" Margin="3" Stretch="Uniform"/>
</RepeatButton>-->
<Border x:Name="Bd" Grid.Column="1" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="1" SnapsToDevicePixels="true">
<ScrollViewer x:Name="sv" Focusable="false" Padding="{TemplateBinding Padding}" HorizontalScrollBarVisibility="Hidden">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
<!--<RepeatButton x:Name="RightButton" Grid.Column="2" Tag="{Binding ElementName=sv}" Width="20" Click="Right_Click">
<Path x:Name="ArrowRight" Data="M 1.81,7 C1.81,7 0,7 0,7 0,7 3.18,3.5 3.18,3.5 3.18,3.5 0,0 0,0 0,0 1.81,0 1.81,0 1.81,0 5,3.5 5,3.5 5,3.5 1.81,7 1.81,7 z" Fill="Black" Margin="3" Stretch="Uniform"/>
</RepeatButton>-->
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="Bd" Value="{StaticResource ListBox.Disabled.Background}"/>
<Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource ListBox.Disabled.Border}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsGrouping" Value="true"/>
<Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Style>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Width="156" Height="26">
<TextBlock x:Name="MenuItem" Width="100" Text="{Binding Name}" TextAlignment="Center" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Line Stroke="Gray" StrokeThickness="4"
X1="0" Y1="{Binding ActualHeight, ElementName=MenuListBox}"
X2="{Binding ActualWidth, ElementName=MenuListBox}" Y2="{Binding ActualHeight, ElementName=MenuListBox}"
/>
<Line x:Name="SelectedMenuItemLine"
X1="0" Y1="{Binding ActualHeight, ElementName=MenuListBox}"
X2="156" Y2="{Binding ActualHeight, ElementName=MenuListBox}"
Stroke="Black" StrokeThickness="8"
>
<!--<Line.Style>
<Style TargetType="Line">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected, ElementName=_controlBoolField}" Value="True">
<Setter Property="Opacity" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Line.Style>-->
</Line>
</Grid>
</UserControl>
NavigationBar.xaml.cs
public partial class NavigationBar : UserControl
{
private bool m_FirstTime = true;
private bool m_LeftButtonInitialized = false;
private bool m_RightButtonInitialized = false;
private const double s_AnimationDuration = .2;
private double m_Offset;
[Bindable(true)]
public IEnumerable ItemsSource
{
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty ItemsSourceProperty = ListBox.ItemsSourceProperty.AddOwner(typeof(NavigationBar));
public NavigationBar()
{
InitializeComponent();
}
public int SelectedIndex
{
get => MenuListBox.SelectedIndex;
set => MenuListBox.SelectedIndex = value;
}
public PluginItem SelectedItem
{
get => (PluginItem) MenuListBox.SelectedItem;
set => MenuListBox.SelectedItem = value;
}
public event SelectionChangedEventHandler NavigationSelectionChanged
{
add => MenuListBox.SelectionChanged += value;
remove => MenuListBox.SelectionChanged -= value;
}
private void MenuListBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (m_FirstTime)
{
m_FirstTime = false;
return;
}
var listBoxItem = (ListBoxItem)MenuListBox.ItemContainerGenerator.ContainerFromItem(MenuListBox.SelectedItem);
if (listBoxItem == null)
return;
var position = listBoxItem.TransformToAncestor(MenuListBox).Transform(new Point());
var animation1 = new DoubleAnimation(position.X, new Duration(TimeSpan.FromSeconds(s_AnimationDuration)));
var animation2 = new DoubleAnimation(position.X + listBoxItem.ActualWidth, new Duration(TimeSpan.FromSeconds(s_AnimationDuration)));
SelectedMenuItemLine.BeginAnimation(Line.X1Property, animation1);
SelectedMenuItemLine.BeginAnimation(Line.X2Property, animation2);
}
//private void Left_Click(object sender, RoutedEventArgs e)
//{
// var btn = sender as System.Windows.Controls.Primitives.RepeatButton;
// var sv = btn.Tag as ScrollViewer;
// m_Offset -= 1;
// if (m_Offset < 0)
// m_Offset = 0;
// sv.ScrollToHorizontalOffset(m_Offset);
// if (m_LeftButtonInitialized)
// return;
// sv.ScrollChanged += (o, args) => MenuListBox_OnSelectionChanged(null, null);
// m_LeftButtonInitialized = true;
//}
//private void Right_Click(object sender, RoutedEventArgs e)
//{
// var btn = sender as System.Windows.Controls.Primitives.RepeatButton;
// var sv = btn.Tag as ScrollViewer;
// m_Offset += 1;
// if (m_Offset > sv.ScrollableWidth)
// m_Offset = sv.ScrollableWidth;
// sv.ScrollToHorizontalOffset(m_Offset);
// if (m_RightButtonInitialized)
// return;
// sv.ScrollChanged += (o, args) => MenuListBox_OnSelectionChanged(null, null);
// m_RightButtonInitialized = true;
//}
}
MainWindow.xaml
<Window x:Class="DoubleMenuTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DoubleMenuTest"
mc:Ignorable="d"
Title="MainWindow"
x:Name="TestMainWindow"
Height="768"
Width="1024">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<!--<ControlTemplate x:Key="ListViewControlTemplate1" TargetType="{x:Type ListView}">
<Grid Width="156" Height="26">
<TextBlock x:Name="MenuItem" Width="100" Text="{Binding Name}" TextAlignment="Center" />
</Grid>
</ControlTemplate>-->
</Window.Resources>
<StackPanel>
<local:NavigationBar
x:Name="MainNavigationBar"
NavigationSelectionChanged="NavigationBar_OnSelectionChanged"
ItemsSource="{Binding MainMenuTabs}"
/>
<!--DataContext="{Binding DataContext, ElementName=TestMainWindow}"-->
<local:NavigationBar
x:Name="SubNavigationBar"
NavigationSelectionChanged="SubNavigationBar_OnSelectionChanged"
DataContext="{Binding SelectedMainPluginItem}"
ItemsSource="{Binding PluginItems}"
DataContextChanged="SubNavigationBar_OnDataContextChanged"
/>
<!--ItemsSource="{Binding Path=SelectedMainPluginItem.PluginItems}"-->
<!--ItemsSource="{Binding SubMenuTabs}"-->
<!--ItemsSource="{Binding ElementName=MainNavigationBar, Path=SelectedItem.PluginItems}"-->
<ListView x:Name="PluginListView"
Width="1024"
Height="604"
Background="Transparent"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
SelectionMode="Single"
VerticalContentAlignment="Top"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
DataContext="{Binding SelectedSubPluginItem}"
ItemsSource="{Binding PluginItems}"
>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="5">
<TextBlock Text="{Binding Name}" />
<!--<local:MenuPluginItem MouseLeftButtonUp="MenuItem_OnMouseLeftButtonUp"
MouseRightButtonUp="MenuItem_OnMouseRightButtonUp"/>-->
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private readonly MainMenuViewModel m_ViewModel;
public MainWindow()
{
InitializeComponent();
m_ViewModel = CreateViewModel();
DataContext = m_ViewModel;
MainNavigationBar.ItemsSource = m_ViewModel.MainMenuTabs;
MainNavigationBar.SelectedIndex = 0;
DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(NavigationBar)).AddValueChanged(SubNavigationBar,
(sender, args) =>
{
SubNavigationBar.SelectedItem = m_ViewModel.SelectedMainPluginItem.PluginItems[0];
SubNavigationBar.SelectedIndex = 0;
});
}
private static MainMenuViewModel CreateViewModel()
{
var viewModel = new MainMenuViewModel
{
MainMenuTabs = new ObservableCollection<PluginItem>
{
CreateItem(1),
CreateItem(2),
CreateItem(3),
CreateItem(4)
}
};
return viewModel;
}
private static PluginItem CreateItem(int id)
{
var pluginItem = new PluginItem { Name = $"Test {id}", Description = $"Desc {id}" };
pluginItem.PluginItems = new ObservableCollection<PluginItem>
{
new PluginItem { Name = $"Test {id} 1", Description = $"Desc {id} 1", Parent = pluginItem, PluginItems = new ObservableCollection<PluginItem>
{
new PluginItem { Name = $"Test {id} 1 1" },
new PluginItem { Name = $"Test {id} 1 2" },
new PluginItem { Name = $"Test {id} 1 3" },
new PluginItem { Name = $"Test {id} 1 4" }
}},
new PluginItem { Name = $"Test {id} 2", Description = $"Desc {id} 2", Parent = pluginItem, PluginItems = new ObservableCollection<PluginItem>
{
new PluginItem { Name = $"Test {id} 2 1" },
new PluginItem { Name = $"Test {id} 2 2" },
new PluginItem { Name = $"Test {id} 2 3" },
new PluginItem { Name = $"Test {id} 2 4" }
}},
new PluginItem { Name = $"Test {id} 3", Description = $"Desc {id} 3", Parent = pluginItem, PluginItems = new ObservableCollection<PluginItem>
{
new PluginItem { Name = $"Test {id} 3 1" },
new PluginItem { Name = $"Test {id} 3 2" },
new PluginItem { Name = $"Test {id} 3 3" },
new PluginItem { Name = $"Test {id} 3 4" }
}},
new PluginItem { Name = $"Test {id} 4", Description = $"Desc {id} 4", Parent = pluginItem, PluginItems = new ObservableCollection<PluginItem>
{
new PluginItem { Name = $"Test {id} 4 1" },
new PluginItem { Name = $"Test {id} 4 2" },
new PluginItem { Name = $"Test {id} 4 3" },
new PluginItem { Name = $"Test {id} 4 4" }
}}
};
return pluginItem;
}
private void MenuItem_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
}
private void MenuItem_OnMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
}
private void NavigationBar_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
m_ViewModel.SelectedMainPluginItem = MainNavigationBar.SelectedItem;
}
private void SubNavigationBar_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (SubNavigationBar.SelectedItem != null)
m_ViewModel.SelectedSubPluginItem = SubNavigationBar.SelectedItem;
}
private void SubNavigationBar_OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
//SubNavigationBar.SelectedIndex = 0;
}
}
Thank in advance to all that read
ADD:
At this link you can download the example projects
https://drive.google.com/file/d/1Z32br2WWnA8OJ8JgPTnkUEUCSJpT7jtf/view?usp=sharing

I am not sure what do you mean by the tracking that doesn't work, regarding the first part of your question, just set the item container style of the ListBoxItem:
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<ContentPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
Your second problem is related to the listBoxItem.ActualWidth not being calculated on time in your MenuListBox_OnSelectionChanged event handler, the first execution is sometimes with a 0, either hardcode it to 156 like you are doing with the X2 or have a DP where the value is specified:
private void MenuListBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (m_FirstTime)
{
m_FirstTime = false;
return;
}
var listBoxItem = (ListBoxItem)MenuListBox.ItemContainerGenerator.ContainerFromItem(MenuListBox.SelectedItem);
if (listBoxItem == null)
return;
var position = listBoxItem.TransformToAncestor(MenuListBox).Transform(new Point());
var animation1 = new DoubleAnimation(position.X, new Duration(TimeSpan.FromSeconds(s_AnimationDuration)));
var animation2 = new DoubleAnimation(position.X + 156, new Duration(TimeSpan.FromSeconds(s_AnimationDuration)));
SelectedMenuItemLine.BeginAnimation(Line.X1Property, animation1);
SelectedMenuItemLine.BeginAnimation(Line.X2Property, animation2);
}

Related

SelectAll checkbox inside Combobox items in Wpf

I have a combobox with items from 0 to 63 integer values. I want to add select all option. How can i do that to work if i select all it should select all the items?
<DataTemplate x:Key="cmbIndex">
<CheckBox IsChecked="{Binding Path=IsSelected, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
Tag="{RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}"
Content="{Binding Name}" Name="chkbox"
Click="CheckBox_Click">
</CheckBox>
</DataTemplate>
<CollectionViewSource x:Key="coll" Source="{Binding Set2CmdList,UpdateSourceTrigger=PropertyChanged}"/>
<ComboBox Grid.Row="0" SelectedIndex="{Binding Set2SelectedIndex}"
HorizontalAlignment="Left" Margin="80,0,0,0"
Height="20" VerticalAlignment="Center" Width="60"
FontFamily="Calibri" FontSize="12" >
<ComboBox.ItemsSource>
<CompositeCollection>
<!--<ComboBoxItem>
<CheckBox x:Name="all">Select All</CheckBox>
</ComboBoxItem>-->
<CollectionContainer Collection="{Binding Source={StaticResource coll}}"/>
</CompositeCollection>
</ComboBox.ItemsSource>
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="ItemTemplate" Value="{StaticResource cmbIndex}"/>
</Style>
</ComboBox.Style>
</ComboBox>
View model:
private List<GenericDescription> _Set2CmdList;
public List<GenericDescription> Set2CmdList
{
get { return _Set2CmdList; }
set { _Set2CmdList = value; }
}
Constructor of viewmodel:
_CMDCollection = new ObservableCollection<int>();
_Set2CmdList = new List<GenericDescription>();
_Set2CmdList.Add(new GenericDescription() { Name="Select All",IsSelected=false });
for (int i = 0; i < 64; i++)
{
_CMDCollection.Add(i);
_Set2CmdList.Add(new GenericDescription()
{
Name = i.ToString(),
IsSelected = false
});
}
Class:
public class GenericDescription
{
private string _Name;
public string Name
{
get { return _Name; }
set { _Name = value;}
}
private bool _IsSelected;
public bool IsSelected
{
get { return _IsSelected; }
set { _IsSelected = value; }
}
}
you can try with below control,
Note, this is not a completed control, it will give you a headstart, (you need to check for NRE and other scenarios)
public class MultiComboBox : ComboBox
{
static MultiComboBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiComboBox), new FrameworkPropertyMetadata(typeof(MultiComboBox)));
EventManager.RegisterClassHandler(typeof(MultiComboBox), Selector.SelectedEvent, new RoutedEventHandler(OnSelected));
EventManager.RegisterClassHandler(typeof(MultiComboBox), Selector.UnselectedEvent, new RoutedEventHandler(OnUnselected));
}
CheckBox PART_SelectAll;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PART_SelectAll = base.GetTemplateChild("PART_SelectAll") as CheckBox;
PART_SelectAll.Checked += PART_SelectAll_Checked;
PART_SelectAll.Unchecked += PART_SelectAll_UnChecked;
}
private void PART_SelectAll_Checked(object sender, RoutedEventArgs e)
{
ProcessSelection(PART_SelectAll.IsChecked.Value);
}
private void PART_SelectAll_UnChecked(object sender, RoutedEventArgs e)
{
ProcessSelection(PART_SelectAll.IsChecked.Value);
}
internal void NotifySelectedItems(object item, bool isSelected)
{
if (SelectedItems == null)
SelectedItems = new List<Object>();
if (SelectedItems != null)
{
if (isSelected)
SelectedItems.Add((item as MultiComboBoxItem).DataContext);
else
SelectedItems.Remove((item as MultiComboBoxItem).DataContext);
}
}
internal void SetSelectedItem(object item)
{
SetValue(SelectedItemProperty, item);
}
private void ProcessSelection(bool select)
{
foreach (var item in this.Items)
{
if(this.ItemsSource != null)
{
var cItem = this.ItemContainerGenerator.ContainerFromItem(item) as MultiComboBoxItem;
if(cItem != null)
{
cItem.SetValue(ComboBoxItem.IsSelectedProperty, select);
}
}
}
}
private static void OnSelected(object sender, RoutedEventArgs e)
{
e.Handled = true;
}
private static void OnUnselected(object sender, RoutedEventArgs e)
{
e.Handled = true;
}
public MultiComboBox()
{
}
public IList SelectedItems
{
get { return (IList)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedItems. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(IList), typeof(MultiComboBox), new PropertyMetadata(null));
protected override DependencyObject GetContainerForItemOverride()
{
var multiComboItem = new MultiComboBoxItem();
multiComboItem.ParentComboBox = this;
return multiComboItem;
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
}
}
public class MultiComboBoxItem : ComboBoxItem
{
static MultiComboBoxItem()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiComboBoxItem), new FrameworkPropertyMetadata(typeof(MultiComboBoxItem)));
}
public MultiComboBox ParentComboBox { get; set; }
protected override void OnSelected(RoutedEventArgs e)
{
ParentComboBox.NotifySelectedItems(this, true);
base.OnSelected(e);
if (ParentComboBox.SelectedItem == null)
ParentComboBox.SetValue(ComboBox.SelectedItemProperty, this.DataContext);
}
protected override void OnUnselected(RoutedEventArgs e)
{
ParentComboBox.NotifySelectedItems(this, false);
base.OnUnselected(e);
if (ParentComboBox.SelectedItems.Count == 0 || this.DataContext == ParentComboBox.SelectedItem)
ParentComboBox.ClearValue(ComboBox.SelectedItemProperty);
}
}
and in generic file, add template for ComboBox and replace the below lines,
<ControlTemplate x:Key="ComboBoxTemplate" TargetType="{x:Type local:MultiComboBox}">
<Grid x:Name="templateRoot" SnapsToDevicePixels="true">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="0" MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" />
</Grid.ColumnDefinitions>
<Popup x:Name="PART_Popup"
Grid.ColumnSpan="2"
Margin="1"
AllowsTransparency="true"
IsOpen="{Binding IsDropDownOpen,
Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent}}"
Placement="Bottom"
PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}">
<Border x:Name="dropDownBorder"
Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"
BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}"
BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<CheckBox Grid.Row="0" Content="Select All" Name="PART_SelectAll"/>
<ScrollViewer Grid.Row="1" x:Name="DropDownScrollViewer">
<Grid x:Name="grid" RenderOptions.ClearTypeHint="Enabled">
<Canvas x:Name="canvas"
Width="0"
Height="0"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Rectangle x:Name="opaqueRect"
Width="{Binding ActualWidth,
ElementName=dropDownBorder}"
Height="{Binding ActualHeight,
ElementName=dropDownBorder}"
Fill="{Binding Background,
ElementName=dropDownBorder}" />
</Canvas>
<ItemsPresenter x:Name="ItemsPresenter"
KeyboardNavigation.DirectionalNavigation="Contained"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</ScrollViewer>
</Grid>
</Border>
</Popup>
<ToggleButton x:Name="toggleButton"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
IsChecked="{Binding IsDropDownOpen,
Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource ComboBoxToggleButton}" />
<ContentPresenter x:Name="contentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectionBoxItem}"
ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
IsHitTestVisible="false"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger SourceName="PART_Popup" Property="HasDropShadow" Value="true" />
<Trigger Property="HasItems" Value="false">
<Setter TargetName="dropDownBorder" Property="Height" Value="95" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsGrouping" Value="true" />
<Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false" />
</MultiTrigger.Conditions>
<Setter Property="ScrollViewer.CanContentScroll" Value="false" />
</MultiTrigger>
<Trigger SourceName="DropDownScrollViewer" Property="ScrollViewer.CanContentScroll" Value="false">
<Setter TargetName="opaqueRect" Property="Canvas.Top" Value="{Binding VerticalOffset, ElementName=DropDownScrollViewer}" />
<Setter TargetName="opaqueRect" Property="Canvas.Left" Value="{Binding HorizontalOffset, ElementName=DropDownScrollViewer}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
A method like this will assign all your item to Selected
Bind this check All method as Command to your checkbox.
public void CheckAll()
{
foreach(var item in Set2CmdList)
{
item.IsSelected = true;
}
}
Class
public bool IsSelected
{
get { return _IsSelected; }
set
{
_IsSelected = value;
OnPropertyChanged(); //See interface INotifyProeprtyChanged interface
}
}

Listbox item trigger colour change

I am trying to change the colour of the items in a listbox based on a trigger using MVVM
<Border Grid.Row="1" Width="300" Margin="0,0,20,0" BorderThickness="1,2,1,1" CornerRadius="5" BorderBrush="#FF999393" Background="#FFE9EDF1" >
<ListBox ItemsSource="{Binding LogMessageList, UpdateSourceTrigger=PropertyChanged}" Background="{x:Null}" Margin="3" BorderBrush="{x:Null}" FontSize="13.333" >
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding FatalError, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Firebrick" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
I'm setting the property change correctly but nothing seems to change.
Thanks
EDIT:
Still stuck. Trying
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},Path=DataContext.FatalError, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Firebrick" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
Tried your scenario. Please refer the below code example:
View Model
public class Vm : INotifyPropertyChanged
{
public ObservableCollection<VmUser> VmUsers { get; set; }
private string errorType;
public string ErrorType
{
get { return errorType; }
set
{
errorType = value;
Raise("ErrorType");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void Raise(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class VmUser
{
public string Name { get; set; }
public int Age { get; set; }
}
Set the DataContext of window in constructor :
public MainWindow()
{
Vm = new Vm
{
VmUsers = new ObservableCollection<VmUser>
{
new VmUser { Name = "Gil", Age = 1 },
new VmUser { Name = "Dan", Age = 2 },
new VmUser { Name = "John", Age = 3 },
},
ErrorType = "Fatal"
};
InitializeComponent();
DataContext = TheVm;
}
Defined ListBox in XAML as:
<ListBox Grid.Row="2" ItemsSource="{Binding VmUsers}" DisplayMemberPath="Name" SelectedValuePath="Age">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},Path=DataContext.ErrorType, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
To test the scenario I have wrote a small test to toggle the ErrorType on click on button
private void Button_Click(object sender, RoutedEventArgs e)
{
var vm = this.DataContext as Vm;
if (vm != null)
{
if (vm.ErrorType == "Fatal")
{
vm.ErrorType = "Non Fatal";
}
else
{
vm.ErrorType = "Fatal";
}
}
}
With above example foreground color changes based on ErrorType.

Using a DataTrigger to change Just the left or right margin (or both)

So I've got a grid that needs to change it's margin based on flag in the VM.
Seems like datatriggers is the right way to handle this.
So I set this up:
<Grid x:Name="myGrid" Grid.Row="1" Margin="30,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding UI_Preferences.RightPanelPinned}" Value="true" >
<Setter Property="Margin" value="200" />
</DataTrigger>
<DataTrigger Binding="{Binding UI_Preferences.LeftPanelPinned}" Value="true" >
<Setter Property="Margin" value="200" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
Which works, but I can't figure out how to modify just the left or right margins individually.
A margin is actually a Thickness element, so you can do it like this:
EDIT - have added condition where bot are set:
<Grid x:Name="myGrid" Grid.Row="1" Margin="30,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding UI_Preferences.RightPanelPinned}" Value="true" >
<Setter Property="Margin">
<Setter.Value>
<Thickness Left="200"/>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding UI_Preferences.LeftPanelPinned}" Value="true" >
<Setter Property="Margin">
<Setter.Value>
<Thickness Right="200"/>
</Setter.Value>
</Setter>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding UI_Preferences.LeftPanelPinned}" Value="true" />
<Condition Binding="{Binding UI_Preferences.RightPanelPinned}" Value="true" />
</MultiDataTrigger.Conditions>
<Setter Property="Margin">
<Setter.Value>
<Thickness Right="200" Left="200"/>
</Setter.Value>
</Setter>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
Well, this doesn't use DataTriggers, but it works quite well, and I think it does what you're looking for:
MainWindow.xaml
<Window x:Class="Wpf1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="200" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid x:Name="myGrid" Grid.Row="0" Grid.ColumnSpan="2" Background="AliceBlue">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Margin" Value="{Binding Margin}" />
</Style>
</Grid.Style>
</Grid>
<Button Content="Left Toggle" Name="LeftButton" Grid.Row="1" Grid.Column="0" />
<Button Content="Right Toggle" Name="RightButton" Grid.Row="1" Grid.Column="1" />
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace Wpf1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainWindowVM mainWindowVM = new MainWindowVM(this);
this.LeftButton.Click += mainWindowVM.LeftButton_Click;
this.RightButton.Click += mainWindowVM.RightButton_Click;
DataContext = mainWindowVM;
}
}
}
And the view model: MainWindowVM.cs
using System.Windows;
using System.ComponentModel;
namespace Wpf1
{
class MainWindowVM : INotifyPropertyChanged
{
public MainWindow MainWindow { get; set; }
private Thickness _margin;
public Thickness Margin
{
get { return _margin; }
set {
if (_margin != value)
{
_margin = value;
OnPropertyChanged("Margin");
}
}
}
private bool _rightPanelPinned;
public bool RightPanelPinned
{
get { return _rightPanelPinned; }
set
{
if (_rightPanelPinned != value)
{
_rightPanelPinned = value;
if (_rightPanelPinned == true)
{
Thickness thickness = Margin;
thickness.Right = 30.0;
Margin = thickness;
}
else
{
Thickness thickness = Margin;
thickness.Right = 0.0;
Margin = thickness;
}
}
}
}
private bool _leftPanelPinned;
public bool LeftPanelPinned
{
get { return _leftPanelPinned; }
set
{
if (_leftPanelPinned != value)
{
_leftPanelPinned = value;
if (_leftPanelPinned == true)
{
Thickness thickness = Margin;
thickness.Left = 30.0;
Margin = thickness;
}
else
{
Thickness thickness = Margin;
thickness.Left = 0.0;
Margin = thickness;
}
}
}
}
public MainWindowVM(MainWindow mainWindow)
{
MainWindow = mainWindow;
LeftPanelPinned = false;
RightPanelPinned = false;
}
public void LeftButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.BeginInit();
LeftPanelPinned = (!LeftPanelPinned);
MainWindow.EndInit();
MainWindow.UpdateLayout();
}
public void RightButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.BeginInit();
RightPanelPinned = (!RightPanelPinned);
MainWindow.EndInit();
MainWindow.UpdateLayout();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I found it easiest to just bind the Margin of the grid to a property in the view model. Hope this helps!
Cheers,
Andrew

TabControl with Add New Tab Button (+)

What is the proper way of adding a '+' button tab at the end of all the tab items in the tab strip of a tab control in WPF?
It should work correctly with multiple tab header rows.
It should be at the end of all tab items
Tab cycling should work correctly (Alt + Tab), that is, the + tab should be skipped.
I shouldn't have to modify the source collection I am binding to. That is, the control should be reusable.
The solution should work with MVVM
To be more precise, the button should appear exactly as an additional last tab and not as a separate button somewhere on the right of all tab strip rows.
I am just looking for the general approach to doing this.
Google throws many examples, but if you dig a little deep none of them satisfy all the above five points.
An almost complete solution using IEditableCollectionView:
ObservableCollection<ItemVM> _items;
public ObservableCollection<ItemVM> Items
{
get
{
if (_items == null)
{
_items = new ObservableCollection<ItemVM>();
var itemsView = (IEditableCollectionView)CollectionViewSource.GetDefaultView(_items);
itemsView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
}
return _items;
}
}
private DelegateCommand<object> _newCommand;
public DelegateCommand<object> NewCommand
{
get
{
if (_newCommand == null)
{
_newCommand = new DelegateCommand<object>(New_Execute);
}
return _newCommand;
}
}
private void New_Execute(object parameter)
{
Items.Add(new ItemVM());
}
<DataTemplate x:Key="newTabButtonContentTemplate">
<Grid/>
</DataTemplate>
<DataTemplate x:Key="newTabButtonHeaderTemplate">
<Button Content="+"
Command="{Binding ElementName=parentUserControl, Path=DataContext.NewCommand}"/>
</DataTemplate>
<DataTemplate x:Key="itemContentTemplate">
<Grid/>
</DataTemplate>
<DataTemplate x:Key="itemHeaderTemplate">
<TextBlock Text="TabItem_test"/>
</DataTemplate>
<vw:TemplateSelector x:Key="headerTemplateSelector"
NewButtonTemplate="{StaticResource newTabButtonHeaderTemplate}"
ItemTemplate="{StaticResource itemHeaderTemplate}"/>
<vw:TemplateSelector x:Key="contentTemplateSelector"
NewButtonTemplate="{StaticResource newTabButtonContentTemplate}"
ItemTemplate="{StaticResource itemContentTemplate}"/>
<TabControl ItemsSource="{Binding Items}"
ItemTemplateSelector="{StaticResource headerTemplateSelector}"
ContentTemplateSelector="{StaticResource contentTemplateSelector}"/>
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate ItemTemplate { get; set; }
public DataTemplate NewButtonTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item == CollectionView.NewItemPlaceholder)
{
return NewButtonTemplate;
}
else
{
return ItemTemplate;
}
}
}
Enter code here
It's almost complete, because the tab cycle doesn't skip the '+' tab, and will show empty content (which is not exactly great, but I can live with it until a better solution come around...).
Existing answers were too complex for me and I am lazy. So, I tried to implement a very simple idea.
Always add [+] tab to the last.
When the last tab is selected, make it as a new tab, and add another last tab.
The idea was simple, but the damn WPF is verbose, so the code became a little bit long. But it probably is very simple to understand... because even I did.
Code behind.
public partial class MainWindow : Window
{
int TabIndex = 1;
ObservableCollection<TabVM> Tabs = new ObservableCollection<TabVM>();
public MainWindow()
{
InitializeComponent();
var tab1 = new TabVM()
{
Header = $"Tab {TabIndex}",
Content = new ContentVM("First tab", 1)
};
Tabs.Add(tab1);
AddNewPlusButton();
MyTabControl.ItemsSource = Tabs;
MyTabControl.SelectionChanged += MyTabControl_SelectionChanged;
}
private void MyTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(e.Source is TabControl)
{
var pos = MyTabControl.SelectedIndex;
if (pos!=0 && pos == Tabs.Count-1) //last tab
{
var tab = Tabs.Last();
ConvertPlusToNewTab(tab);
AddNewPlusButton();
}
}
}
void ConvertPlusToNewTab(TabVM tab)
{
//Do things to make it a new tab.
TabIndex++;
tab.Header = $"Tab {TabIndex}";
tab.IsPlaceholder = false;
tab.Content = new ContentVM("Tab content", TabIndex);
}
void AddNewPlusButton()
{
var plusTab = new TabVM()
{
Header = "+",
IsPlaceholder = true
};
Tabs.Add(plusTab);
}
class TabVM:INotifyPropertyChanged
{
string _Header;
public string Header
{
get => _Header;
set
{
_Header = value;
OnPropertyChanged();
}
}
bool _IsPlaceholder = false;
public bool IsPlaceholder
{
get => _IsPlaceholder;
set
{
_IsPlaceholder = value;
OnPropertyChanged();
}
}
ContentVM _Content = null;
public ContentVM Content
{
get => _Content;
set
{
_Content = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string property = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
class ContentVM
{
public ContentVM(string name, int index)
{
Name = name;
Index = index;
}
public string Name { get; set; }
public int Index { get; set; }
}
private void OnTabCloseClick(object sender, RoutedEventArgs e)
{
var tab = (sender as Button).DataContext as TabVM;
if (Tabs.Count>2)
{
var index = Tabs.IndexOf(tab);
if(index==Tabs.Count-2)//last tab before [+]
{
MyTabControl.SelectedIndex--;
}
Tabs.RemoveAt(index);
}
}
}
XAML
<TabControl Name="MyTabControl">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Header, Mode=OneWay}" />
<Button Click="OnTabCloseClick" Width="20" Padding="0" Margin="8 0 0 0" Content="X">
<Button.Style>
<Style TargetType="Button" x:Name="CloseButtonStyle">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsPlaceholder}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl>
<ContentControl.Resources>
<ContentControl x:Key="TabContentTemplate">
<StackPanel DataContext="{Binding Content}" Orientation="Vertical">
<TextBlock Text="{Binding Path=Name}"/>
<TextBlock Text="{Binding Path=Index}"/>
</StackPanel>
</ContentControl>
</ContentControl.Resources>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding IsPlaceholder}" Value="True">
<Setter Property="Content"
Value="{x:Null}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsPlaceholder}" Value="False">
<Setter Property="Content"
Value="{StaticResource TabContentTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
I used a modification of the tab control template and binding to the AddNewItemCommand command in my view model.
XAML:
<TabControl x:Class="MyNamespace.MyTabView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
ItemsSource="{Binding MyItemSource}"
SelectedIndex="{Binding LastSelectedIndex}"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Control.Template>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid ClipToBounds="true"
SnapsToDevicePixels="true"
KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnDefinition0" />
<ColumnDefinition x:Name="ColumnDefinition1"
Width="0" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="RowDefinition0"
Height="Auto" />
<RowDefinition x:Name="RowDefinition1"
Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Column="0"
Grid.Row="0"
Orientation="Horizontal"
x:Name="HeaderPanel">
<TabPanel x:Name="_HeaderPanel"
IsItemsHost="true"
Margin="2,2,2,0"
KeyboardNavigation.TabIndex="1"
Panel.ZIndex="1" />
<Button Content="+"
Command="{Binding AddNewItemCommand}" />
</StackPanel>
<Border x:Name="ContentPanel"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Grid.Column="0"
KeyboardNavigation.DirectionalNavigation="Contained"
Grid.Row="1"
KeyboardNavigation.TabIndex="2"
KeyboardNavigation.TabNavigation="Local">
<ContentPresenter x:Name="PART_SelectedContentHost"
ContentSource="SelectedContent"
Margin="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="TabStripPlacement"
Value="Bottom">
<Setter Property="Grid.Row"
TargetName="HeaderPanel"
Value="1" />
<Setter Property="Grid.Row"
TargetName="ContentPanel"
Value="0" />
<Setter Property="Height"
TargetName="RowDefinition0"
Value="*" />
<Setter Property="Height"
TargetName="RowDefinition1"
Value="Auto" />
<Setter Property="Margin"
TargetName="HeaderPanel"
Value="2,0,2,2" />
</Trigger>
<Trigger Property="TabStripPlacement"
Value="Left">
<Setter Property="Orientation"
TargetName="HeaderPanel"
Value="Vertical" />
<Setter Property="Grid.Row"
TargetName="HeaderPanel"
Value="0" />
<Setter Property="Grid.Row"
TargetName="ContentPanel"
Value="0" />
<Setter Property="Grid.Column"
TargetName="HeaderPanel"
Value="0" />
<Setter Property="Grid.Column"
TargetName="ContentPanel"
Value="1" />
<Setter Property="Width"
TargetName="ColumnDefinition0"
Value="Auto" />
<Setter Property="Width"
TargetName="ColumnDefinition1"
Value="*" />
<Setter Property="Height"
TargetName="RowDefinition0"
Value="*" />
<Setter Property="Height"
TargetName="RowDefinition1"
Value="0" />
<Setter Property="Margin"
TargetName="HeaderPanel"
Value="2,2,0,2" />
</Trigger>
<Trigger Property="TabStripPlacement"
Value="Right">
<Setter Property="Orientation"
TargetName="HeaderPanel"
Value="Vertical" />
<Setter Property="Grid.Row"
TargetName="HeaderPanel"
Value="0" />
<Setter Property="Grid.Row"
TargetName="ContentPanel"
Value="0" />
<Setter Property="Grid.Column"
TargetName="HeaderPanel"
Value="1" />
<Setter Property="Grid.Column"
TargetName="ContentPanel"
Value="0" />
<Setter Property="Width"
TargetName="ColumnDefinition0"
Value="*" />
<Setter Property="Width"
TargetName="ColumnDefinition1"
Value="Auto" />
<Setter Property="Height"
TargetName="RowDefinition0"
Value="*" />
<Setter Property="Height"
TargetName="RowDefinition1"
Value="0" />
<Setter Property="Margin"
TargetName="HeaderPanel"
Value="0,2,2,2" />
</Trigger>
<Trigger Property="IsEnabled"
Value="false">
<Setter Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Control.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Caption}" />
<Button Content="x"
Grid.Column="2"
VerticalAlignment="Top"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</TabControl>
Code in the relevant view model looks like this:
public ICommand AddNewItemCommand
{
get
{
return new DelegateCommand((param) =>
{
MyItemSource.Add(CreateMyValueViewModel());
},
(param) => MyItemSource != null);
}
}
Pay attention: I wrapped TabPanel by StackPanel to flip the "+" button together with TabPanel regarding to value of property "TabStripPlacement". Without inheritance and without code-behind in your view.
I believe I have come up with a complete solution, I started with NVM's solution to create my template. And then referenced the DataGrid source code to come up with an extended TabControl capable of adding and removing items.
ExtendedTabControl.cs
public class ExtendedTabControl : TabControl
{
public static readonly DependencyProperty CanUserAddTabsProperty = DependencyProperty.Register("CanUserAddTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(false, OnCanUserAddTabsChanged, OnCoerceCanUserAddTabs));
public bool CanUserAddTabs
{
get { return (bool)GetValue(CanUserAddTabsProperty); }
set { SetValue(CanUserAddTabsProperty, value); }
}
public static readonly DependencyProperty CanUserDeleteTabsProperty = DependencyProperty.Register("CanUserDeleteTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(true, OnCanUserDeleteTabsChanged, OnCoerceCanUserDeleteTabs));
public bool CanUserDeleteTabs
{
get { return (bool)GetValue(CanUserDeleteTabsProperty); }
set { SetValue(CanUserDeleteTabsProperty, value); }
}
public static RoutedUICommand DeleteCommand
{
get { return ApplicationCommands.Delete; }
}
public static readonly DependencyProperty NewTabCommandProperty = DependencyProperty.Register("NewTabCommand", typeof(ICommand), typeof(ExtendedTabControl));
public ICommand NewTabCommand
{
get { return (ICommand)GetValue(NewTabCommandProperty); }
set { SetValue(NewTabCommandProperty, value); }
}
private IEditableCollectionView EditableItems
{
get { return (IEditableCollectionView)Items; }
}
private bool ItemIsSelected
{
get
{
if (this.SelectedItem != CollectionView.NewItemPlaceholder)
return true;
return false;
}
}
private static void OnCanExecuteDelete(object sender, CanExecuteRoutedEventArgs e)
{
((ExtendedTabControl)sender).OnCanExecuteDelete(e);
}
private static void OnCanUserAddTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ExtendedTabControl)d).UpdateNewItemPlaceholder();
}
private static void OnCanUserDeleteTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// The Delete command needs to have CanExecute run.
CommandManager.InvalidateRequerySuggested();
}
private static object OnCoerceCanUserAddTabs(DependencyObject d, object baseValue)
{
return ((ExtendedTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, true);
}
private static object OnCoerceCanUserDeleteTabs(DependencyObject d, object baseValue)
{
return ((ExtendedTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, false);
}
private static void OnExecutedDelete(object sender, ExecutedRoutedEventArgs e)
{
((ExtendedTabControl)sender).OnExecutedDelete(e);
}
private static void OnSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == CollectionView.NewItemPlaceholder)
{
var tc = (ExtendedTabControl)d;
tc.Items.MoveCurrentTo(e.OldValue);
tc.Items.Refresh();
}
}
static ExtendedTabControl()
{
Type ownerType = typeof(ExtendedTabControl);
DefaultStyleKeyProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(typeof(ExtendedTabControl)));
SelectedItemProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(OnSelectionChanged));
CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(DeleteCommand, new ExecutedRoutedEventHandler(OnExecutedDelete), new CanExecuteRoutedEventHandler(OnCanExecuteDelete)));
}
protected virtual void OnCanExecuteDelete(CanExecuteRoutedEventArgs e)
{
// User is allowed to delete and there is a selection.
e.CanExecute = CanUserDeleteTabs && ItemIsSelected;
e.Handled = true;
}
protected virtual void OnExecutedDelete(ExecutedRoutedEventArgs e)
{
if (ItemIsSelected)
{
int indexToSelect = -1;
object currentItem = e.Parameter ?? this.SelectedItem;
if (currentItem == this.SelectedItem)
indexToSelect = Math.Max(this.Items.IndexOf(currentItem) - 1, 0);
if (currentItem != CollectionView.NewItemPlaceholder)
EditableItems.Remove(currentItem);
if (indexToSelect != -1)
{
// This should focus the row and bring it into view.
SetCurrentValue(SelectedItemProperty, this.Items[indexToSelect]);
}
}
e.Handled = true;
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
CoerceValue(CanUserAddTabsProperty);
CoerceValue(CanUserDeleteTabsProperty);
UpdateNewItemPlaceholder();
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (Keyboard.FocusedElement is TextBox)
Keyboard.FocusedElement.RaiseEvent(new RoutedEventArgs(LostFocusEvent));
base.OnSelectionChanged(e);
}
private bool OnCoerceCanUserAddOrDeleteTabs(bool baseValue, bool canUserAddTabsProperty)
{
// Only when the base value is true do we need to validate
// that the user can actually add or delete rows.
if (baseValue)
{
if (!this.IsEnabled)
{
// Disabled TabControls cannot be modified.
return false;
}
else
{
if ((canUserAddTabsProperty && !this.EditableItems.CanAddNew) || (!canUserAddTabsProperty && !this.EditableItems.CanRemove))
{
// The collection view does not allow the add or delete action.
return false;
}
}
}
return baseValue;
}
private void UpdateNewItemPlaceholder()
{
var editableItems = EditableItems;
if (CanUserAddTabs)
{
// NewItemPlaceholderPosition isn't a DP but we want to default to AtEnd instead of None
// (can only be done when canUserAddRows becomes true). This may override the users intent
// to make it None, however they can work around this by resetting it to None after making
// a change which results in canUserAddRows becoming true.
if (editableItems.NewItemPlaceholderPosition == NewItemPlaceholderPosition.None)
editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
}
else
{
if (editableItems.NewItemPlaceholderPosition != NewItemPlaceholderPosition.None)
editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.None;
}
// Make sure the newItemPlaceholderRow reflects the correct visiblity
TabItem newItemPlaceholderTab = (TabItem)ItemContainerGenerator.ContainerFromItem(CollectionView.NewItemPlaceholder);
if (newItemPlaceholderTab != null)
newItemPlaceholderTab.CoerceValue(VisibilityProperty);
}
}
CustomStyleSelector.cs
internal class CustomStyleSelector : StyleSelector
{
public Style NewItemStyle { get; set; }
public override Style SelectStyle(object item, DependencyObject container)
{
if (item == CollectionView.NewItemPlaceholder)
return NewItemStyle;
else
return Application.Current.FindResource(typeof(TabItem)) as Style;
}
}
TemplateSelector.cs
internal class TemplateSelector : DataTemplateSelector
{
public DataTemplate ItemTemplate { get; set; }
public DataTemplate NewItemTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item == CollectionView.NewItemPlaceholder)
return NewItemTemplate;
else
return ItemTemplate;
}
}
Generic.xaml
<!-- This style explains how to style a NewItemPlaceholder. -->
<Style x:Key="NewTabItemStyle" TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<ContentPresenter ContentSource="Header" HorizontalAlignment="Left" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- This template explains how to render a tab item with a close button. -->
<DataTemplate x:Key="ClosableTabItemHeader">
<DockPanel MinWidth="120">
<Button DockPanel.Dock="Right" Command="ApplicationCommands.Delete" CommandParameter="{Binding}" Content="X" Cursor="Hand" Focusable="False" FontSize="10" FontWeight="Bold" Height="16" Width="16" />
<TextBlock Padding="0,0,10,0" Text="{Binding DisplayName}" VerticalAlignment="Center" />
</DockPanel>
</DataTemplate>
<!-- This template explains how to render a tab item with a new button. -->
<DataTemplate x:Key="NewTabItemHeader">
<Button Command="{Binding NewTabCommand, RelativeSource={RelativeSource AncestorType={x:Type local:ExtendedTabControl}}}" Content="+" Cursor="Hand" Focusable="False" FontWeight="Bold"
Width="{Binding ActualHeight, RelativeSource={RelativeSource Self}}"/>
</DataTemplate>
<local:CustomStyleSelector x:Key="StyleSelector" NewItemStyle="{StaticResource NewTabItemStyle}" />
<local:TemplateSelector x:Key="HeaderTemplateSelector" ItemTemplate="{StaticResource ClosableTabItemHeader}" NewItemTemplate="{StaticResource NewTabItemHeader}" />
<Style x:Key="{x:Type local:ExtendedTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" TargetType="{x:Type local:ExtendedTabControl}">
<Setter Property="ItemContainerStyleSelector" Value="{StaticResource StyleSelector}" />
<Setter Property="ItemTemplateSelector" Value="{StaticResource HeaderTemplateSelector}" />
</Style>
Define the ControlTemplate of the TabControl like this:
<!-- Sets the look of the Tabcontrol. -->
<Style x:Key="TabControlStyle" TargetType="{x:Type TabControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid>
<!-- Upperrow holds the tabs themselves and lower the content of the tab -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
The upper row in the grid would be the TabPanel, but you would put that into a StackPanel with a button following the TabPanel, and style the button to look like a tab.
Now the button would create a new TabItem (your custom-created one perhaps) and add it to the ObservableCollection of Tabs you have as the Itemssource for your TabControl.
2 & 3) It should always appear at the end, and it's not a tab so hopefully not part of tab cycling
4) Well, your TabControl should use a ObservableCollection of TabItems as Itemssource to be notified when a new one is added/removed
Some code:
The NewTabButton usercontrol .cs file
public partial class NewTabButton : TabItem
{
public NewTabButton()
{
InitializeComponent();
Header = "+";
}
}
And the main window:
public partial class Window1 : Window
{
public ObservableCollection<TabItem> Tabs { get; set; }
public Window1()
{
InitializeComponent();
Tabs = new ObservableCollection<TabItem>();
for (int i = 0; i < 20; i++)
{
TabItem tab = new TabItem();
tab.Header = "TabNumber" + i.ToString();
Tabs.Add(tab);
}
Tabs.Add(new NewTabButton());
theTabs.ItemsSource = Tabs;
}
}
Now we would need to find a way to let it always appear bottom right and also add the event and style for it (the plus sign is there as a placeholder).
This would likely be better as a comment on #NVM's own solution; but I don't have the rep to comment yet so...
If you are trying to use the accepted solution and not getting the add command to trigger then you probably don't have a usercontrol named "parentUserControl".
You can alter #NVM's TabControl declaration as follows to make it work:
<TabControl x:Name="parentUserControl"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{StaticResource headerTemplateSelector}"
ContentTemplateSelector="{StaticResource contentTemplateSelector}"/>
Obviously not a good name to give a tab control :); but I guess #NVM had the data context hooked further up his visual tree to an element to match the name.
Note that personally I preferred to use a relative binding by changing the following:
<Button Content="+"
Command="{Binding ElementName=parentUserControl,
Path=DataContext.NewCommand}"/>
To this:
<Button Content="+"
Command="{Binding DataContext.NewCommand,
RelativeSource={RelativeSource AncestorType={x:Type TabControl}}}"/>
In addition to NVM's answer.
I don't use so many templates and selector's for NewItemPlaceholder. Easier solution with no empty content:
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Static CollectionView.NewItemPlaceholder}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Button Command="{Binding DataContext.AddPageCommand, RelativeSource={RelativeSource AncestorType={x:Type TabControl}}}"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center" ToolTip="Add page" >
+
</Button>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.ItemContainerStyle>
Ctrl+Tab I desided to disable. It's not SO easy, you should subscribe on KeyDown on parent element, i.e. Window (Ctrl+Shift+Tab also handled correctly):
public View()
{
InitializeComponent();
AddHandler(Keyboard.PreviewKeyDownEvent, (KeyEventHandler)controlKeyDownEvent);
}
private void controlKeyDownEvent(object sender, KeyEventArgs e)
{
e.Handled = e.Key == Key.Tab && Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
}
To complete the answer given by #NVM what you have to add is the PreviewMouseDown event:
<TabControl PreviewMouseDown="ActionTabs_PreviewMouseDown"
</TabControl>
And then:
private void ActionTabs_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
ouseButtonEventArgs args = e as MouseButtonEventArgs;
FrameworkElement source = (FrameworkElement)args.OriginalSource;
if (source.DataContext.ToString() == "{NewItemPlaceholder}")
{
e.Handled = true;
}
}

Customizing the TreeView to allow multi select

The built-in WPF TreeView control does not allow for multi selection, like a ListBox does. How can I customize the TreeView to allow for multi selection without rewriting it.
I have a variation on SoMoS implementation that uses an attached property declared on a derivation of the base TreeView control to track the selection state of the TreeViewItems. This keeps the selection tracking on the TreeViewItem element itself, and off of the model object being presented by the tree-view.
This is the new TreeView class derivation.
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Controls;
using System.Collections;
using System.Collections.Generic;
namespace MultiSelectTreeViewDemo
{
public sealed class MultiSelectTreeView : TreeView
{
#region Fields
// Used in shift selections
private TreeViewItem _lastItemSelected;
#endregion Fields
#region Dependency Properties
public static readonly DependencyProperty IsItemSelectedProperty =
DependencyProperty.RegisterAttached("IsItemSelected", typeof(bool), typeof(MultiSelectTreeView));
public static void SetIsItemSelected(UIElement element, bool value)
{
element.SetValue(IsItemSelectedProperty, value);
}
public static bool GetIsItemSelected(UIElement element)
{
return (bool)element.GetValue(IsItemSelectedProperty);
}
#endregion Dependency Properties
#region Properties
private static bool IsCtrlPressed
{
get { return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl); }
}
private static bool IsShiftPressed
{
get { return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); }
}
public IList SelectedItems
{
get
{
var selectedTreeViewItems = GetTreeViewItems(this, true).Where(GetIsItemSelected);
var selectedModelItems = selectedTreeViewItems.Select(treeViewItem => treeViewItem.Header);
return selectedModelItems.ToList();
}
}
#endregion Properties
#region Event Handlers
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
base.OnPreviewMouseDown(e);
// If clicking on a tree branch expander...
if (e.OriginalSource is Shape || e.OriginalSource is Grid || e.OriginalSource is Border)
return;
var item = GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
if (item != null) SelectedItemChangedInternal(item);
}
#endregion Event Handlers
#region Utility Methods
private void SelectedItemChangedInternal(TreeViewItem tvItem)
{
// Clear all previous selected item states if ctrl is NOT being held down
if (!IsCtrlPressed)
{
var items = GetTreeViewItems(this, true);
foreach (var treeViewItem in items)
SetIsItemSelected(treeViewItem, false);
}
// Is this an item range selection?
if (IsShiftPressed && _lastItemSelected != null)
{
var items = GetTreeViewItemRange(_lastItemSelected, tvItem);
if (items.Count > 0)
{
foreach (var treeViewItem in items)
SetIsItemSelected(treeViewItem, true);
_lastItemSelected = items.Last();
}
}
// Otherwise, individual selection
else
{
SetIsItemSelected(tvItem, true);
_lastItemSelected = tvItem;
}
}
private static TreeViewItem GetTreeViewItemClicked(DependencyObject sender)
{
while (sender != null && !(sender is TreeViewItem))
sender = VisualTreeHelper.GetParent(sender);
return sender as TreeViewItem;
}
private static List<TreeViewItem> GetTreeViewItems(ItemsControl parentItem, bool includeCollapsedItems, List<TreeViewItem> itemList = null)
{
if (itemList == null)
itemList = new List<TreeViewItem>();
for (var index = 0; index < parentItem.Items.Count; index++)
{
var tvItem = parentItem.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
if (tvItem == null) continue;
itemList.Add(tvItem);
if (includeCollapsedItems || tvItem.IsExpanded)
GetTreeViewItems(tvItem, includeCollapsedItems, itemList);
}
return itemList;
}
private List<TreeViewItem> GetTreeViewItemRange(TreeViewItem start, TreeViewItem end)
{
var items = GetTreeViewItems(this, false);
var startIndex = items.IndexOf(start);
var endIndex = items.IndexOf(end);
var rangeStart = startIndex > endIndex || startIndex == -1 ? endIndex : startIndex;
var rangeCount = startIndex > endIndex ? startIndex - endIndex + 1 : endIndex - startIndex + 1;
if (startIndex == -1 && endIndex == -1)
rangeCount = 0;
else if (startIndex == -1 || endIndex == -1)
rangeCount = 1;
return rangeCount > 0 ? items.GetRange(rangeStart, rangeCount) : new List<TreeViewItem>();
}
#endregion Utility Methods
}
}
And here's the XAML. Make note that the salient part is the replacement of the two triggers that use the singular 'IsSelected' property with the new 'IsItemSelected' attached property in the MultiSelectTreeViewItemStyle to achieve the visual state.
Also note I'm not aggregating the new TreeView control into a UserControl.
<Window
x:Class="MultiSelectTreeViewDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MultiSelectTreeViewDemo"
Title="MultiSelect TreeView Demo" Height="350" Width="525">
<Window.Resources>
<local:DemoViewModel x:Key="ViewModel"/>
<Style x:Key="TreeViewItemFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Fill" Color="#FF595959"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Stroke" Color="#FF262626"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Stroke" Color="#FF1BBBFA"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Fill" Color="Transparent"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Stroke" Color="#FF262626"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Fill" Color="#FF595959"/>
<PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Fill" Color="Transparent"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Stroke" Color="#FF989898"/>
<Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16">
<Path x:Name="ExpandPath" Data="{StaticResource TreeArrow}" Fill="{StaticResource TreeViewItem.TreeArrow.Static.Fill}" Stroke="{StaticResource TreeViewItem.TreeArrow.Static.Stroke}">
<Path.RenderTransform>
<RotateTransform Angle="135" CenterY="3" CenterX="3"/>
</Path.RenderTransform>
</Path>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="RenderTransform" TargetName="ExpandPath">
<Setter.Value>
<RotateTransform Angle="180" CenterY="3" CenterX="3"/>
</Setter.Value>
</Setter>
<Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Fill}"/>
<Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Stroke}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Stroke}"/>
<Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Fill}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Stroke}"/>
<Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Fill}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="MultiSelectTreeViewItemStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Padding" Value="1,0,0,0"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="19" Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ToggleButton
x:Name="Expander"
ClickMode="Press"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource ExpandCollapseToggleStyle}"/>
<Border
x:Name="Bd"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Grid.Column="1"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="true">
<ContentPresenter
x:Name="PART_Header"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<ItemsPresenter
x:Name="ItemsHost"
Grid.ColumnSpan="2"
Grid.Column="1"
Grid.Row="1"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="false">
<Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="false">
<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
</Trigger>
<!--Trigger Property="IsSelected" Value="true"-->
<Trigger Property="local:MultiSelectTreeView.IsItemSelected" Value="true">
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<!--Condition Property="IsSelected" Value="true"/-->
<Condition Property="local:MultiSelectTreeView.IsItemSelected" Value="true"/>
<Condition Property="IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="VirtualizingPanel.IsVirtualizing" Value="true">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid
Background="WhiteSmoke"
DataContext="{DynamicResource ViewModel}">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<local:MultiSelectTreeView
x:Name="multiSelectTreeView"
ItemContainerStyle="{StaticResource MultiSelectTreeViewItemStyle}"
ItemsSource="{Binding FoodGroups}">
<local:MultiSelectTreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid>
<TextBlock FontSize="14" Text="{Binding Name}"/>
</Grid>
</HierarchicalDataTemplate>
</local:MultiSelectTreeView.ItemTemplate>
</local:MultiSelectTreeView>
<Button
Grid.Row="1"
Margin="0,10"
Padding="20,2"
HorizontalAlignment="Center"
Content="Get Selections"
Click="GetSelectionsButton_OnClick"/>
</Grid>
</Window>
And here's a cheesy view-model to drive it (for demo purposes).
using System.Collections.ObjectModel;
namespace MultiSelectTreeViewDemo
{
public sealed class DemoViewModel
{
public ObservableCollection<FoodItem> FoodGroups { get; set; }
public DemoViewModel()
{
var redMeat = new FoodItem { Name = "Reds" };
redMeat.Add(new FoodItem { Name = "Beef" });
redMeat.Add(new FoodItem { Name = "Buffalo" });
redMeat.Add(new FoodItem { Name = "Lamb" });
var whiteMeat = new FoodItem { Name = "Whites" };
whiteMeat.Add(new FoodItem { Name = "Chicken" });
whiteMeat.Add(new FoodItem { Name = "Duck" });
whiteMeat.Add(new FoodItem { Name = "Pork" });
var meats = new FoodItem { Name = "Meats", Children = { redMeat, whiteMeat } };
var veggies = new FoodItem { Name = "Vegetables" };
veggies.Add(new FoodItem { Name = "Potato" });
veggies.Add(new FoodItem { Name = "Corn" });
veggies.Add(new FoodItem { Name = "Spinach" });
var fruits = new FoodItem { Name = "Fruits" };
fruits.Add(new FoodItem { Name = "Apple" });
fruits.Add(new FoodItem { Name = "Orange" });
fruits.Add(new FoodItem { Name = "Pear" });
FoodGroups = new ObservableCollection<FoodItem> { meats, veggies, fruits };
}
}
public sealed class FoodItem
{
public string Name { get; set; }
public ObservableCollection<FoodItem> Children { get; set; }
public FoodItem()
{
Children = new ObservableCollection<FoodItem>();
}
public void Add(FoodItem item)
{
Children.Add(item);
}
}
}
And here's the button click-handler on the MainWindow code-behind that shows the selections in a MessageBox.
private void GetSelectionsButton_OnClick(object sender, RoutedEventArgs e)
{
var selectedMesg = "";
var selectedItems = multiSelectTreeView.SelectedItems;
if (selectedItems.Count > 0)
{
selectedMesg = selectedItems.Cast<FoodItem>()
.Where(modelItem => modelItem != null)
.Aggregate(selectedMesg, (current, modelItem) => current + modelItem.Name + Environment.NewLine);
}
else
selectedMesg = "No selected items!";
MessageBox.Show(selectedMesg, "MultiSelect TreeView Demo", MessageBoxButton.OK);
}
Hope this helps.
When I consider overriding the fundamental behavior of a control, like a treeview, I always like to consider the usability and effort associated with my decision.
In the specific case of a treeview I find that switching to a listview in combination with zero, one, or more controls makes for a more usable solution that often is easier to implement.
As an example, consider the common Open dialog, or Windows Explorer application.
I've simplified this task adding a checkbox before the text for each treeviewitem.
So, I've created a dockpanel with 2 items inside: checkbox + textblock.
So...
XAML
<TreeView x:Name="treeViewProcesso" Margin="1,30.351,1,5" BorderBrush="{x:Null}" MinHeight="250" VerticalContentAlignment="Top" BorderThickness="0" >
<TreeViewItem Header="Documents" x:Name="treeView" IsExpanded="True" DisplayMemberPath="DocumentsId" >
</TreeViewItem>
</TreeView>
CS
TreeViewItem treeViewItem = new TreeViewItem();
DockPanel dp = new DockPanel();
CheckBox cb = new CheckBox();
TextBlock tb = new TextBlock();
tb.Text = "Item";
dp.Children.Add(cb);
dp.Children.Add(tb);
treeViewItem.Header = dp;
treeViewItem.Selected += new RoutedEventHandler(item_Selected);
treeView.Items.Add(treeViewItem);
And then you can access checkbox value:
void item_Selected(object sender, RoutedEventArgs e)
{
selectedTVI = ((TreeViewItem)sender);
CheckBox cb = (Checkbox)((DockPanel)selectedTVI.Header).Children[0];
}
This is a simple way to do if you don't need anything complex.
I finally ended coding my own CustomControl containing a TreeView inside. Based on the work of others the key of the functionality resides on making all the items of the Model of the TreeView inherit the interface ISelectable:
public interface ISelectable
{
public bool IsSelected {get; set}
}
This way we will have a new 'IsSelected' property that has nothing to do with the TreeViewItem IsSelected. We just need to style our tree so it handles the model IsSelected property. Here the code (it's using the Drag & drop libraries available at http://code.google.com/p/gong-wpf-dragdrop/):
XAML
<UserControl x:Class="Picis.Wpf.Framework.ExtendedControls.TreeViewEx.TreeViewEx"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:DragAndDrop="clr-namespace:Picis.Wpf.Framework.DragAndDrop">
<TreeView ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=UserControl}}"
ItemTemplate="{Binding ItemTemplate, RelativeSource={RelativeSource AncestorType=UserControl}}"
ItemContainerStyle="{Binding ItemContainerStyle, RelativeSource={RelativeSource AncestorType=UserControl}}"
DragAndDrop:DragDrop.DropHandler ="{Binding DropHandler, RelativeSource={RelativeSource AncestorType=UserControl}}"
PreviewMouseDown="TreeViewOnPreviewMouseDown"
PreviewMouseUp="TreeViewOnPreviewMouseUp"
x:FieldModifier="private" x:Name="InnerTreeView" >
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="White" />
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" />
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="White" />
</Style.Resources>
</Style>
</TreeView.Resources>
</TreeView>
C#:
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using GongSolutions.Wpf.DragDrop;
using DragDrop = GongSolutions.Wpf.DragDrop;
namespace <yournamespace>.TreeViewEx
{
public partial class TreeViewEx : UserControl
{
#region Attributes
private TreeViewItem _lastItemSelected; // Used in shift selections
private TreeViewItem _itemToCheck; // Used when clicking on a selected item to check if we want to deselect it or to drag the current selection
private bool _isDragEnabled;
private bool _isDropEnabled;
#endregion
#region Dependency Properties
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<ISelectable>), typeof(TreeViewEx));
public IEnumerable<ISelectable> ItemsSource
{
get
{
return (IEnumerable<ISelectable>)this.GetValue(TreeViewEx.ItemsSourceProperty);
}
set
{
this.SetValue(TreeViewEx.ItemsSourceProperty, value);
}
}
public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(TreeViewEx));
public DataTemplate ItemTemplate
{
get
{
return (DataTemplate)GetValue(TreeViewEx.ItemTemplateProperty);
}
set
{
SetValue(TreeViewEx.ItemTemplateProperty, value);
}
}
public static readonly DependencyProperty ItemContainerStyleProperty = DependencyProperty.Register("ItemContainerStyle", typeof(Style), typeof(TreeViewEx));
public Style ItemContainerStyle
{
get
{
return (Style)GetValue(TreeViewEx.ItemContainerStyleProperty);
}
set
{
SetValue(TreeViewEx.ItemContainerStyleProperty, value);
}
}
public static readonly DependencyProperty DropHandlerProperty = DependencyProperty.Register("DropHandler", typeof(IDropTarget), typeof(TreeViewEx));
public IDropTarget DropHandler
{
get
{
return (IDropTarget)GetValue(TreeViewEx.DropHandlerProperty);
}
set
{
SetValue(TreeViewEx.DropHandlerProperty, value);
}
}
#endregion
#region Properties
public bool IsDragEnabled
{
get
{
return _isDragEnabled;
}
set
{
if (_isDragEnabled != value)
{
_isDragEnabled = value;
DragDrop.SetIsDragSource(this.InnerTreeView, _isDragEnabled);
}
}
}
public bool IsDropEnabled
{
get
{
return _isDropEnabled;
}
set
{
if (_isDropEnabled != value)
{
_isDropEnabled = value;
DragDrop.SetIsDropTarget(this.InnerTreeView, _isDropEnabled);
}
}
}
#endregion
#region Public Methods
public TreeViewEx()
{
InitializeComponent();
}
#endregion
#region Event Handlers
private void TreeViewOnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is Shape || e.OriginalSource is Grid || e.OriginalSource is Border) // If clicking on the + of the tree
return;
TreeViewItem item = this.GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
if (item != null && item.Header != null)
{
this.SelectedItemChangedHandler(item);
}
}
// Check done to avoid deselecting everything when clicking to drag
private void TreeViewOnPreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (_itemToCheck != null)
{
TreeViewItem item = this.GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
if (item != null && item.Header != null)
{
if (!TreeViewEx.IsCtrlPressed)
{
GetTreeViewItems(true).Select(t => t.Header).Cast<ISelectable>().ToList().ForEach(f => f.IsSelected = false);
((ISelectable)_itemToCheck.Header).IsSelected = true;
_lastItemSelected = _itemToCheck;
}
else
{
((ISelectable)_itemToCheck.Header).IsSelected = false;
_lastItemSelected = null;
}
}
}
}
#endregion
#region Private Methods
private void SelectedItemChangedHandler(TreeViewItem item)
{
ISelectable content = (ISelectable)item.Header;
_itemToCheck = null;
if (content.IsSelected)
{
// Check it at the mouse up event to avoid deselecting everything when clicking to drag
_itemToCheck = item;
}
else
{
if (!TreeViewEx.IsCtrlPressed)
{
GetTreeViewItems(true).Select(t => t.Header).Cast<ISelectable>().ToList().ForEach(f => f.IsSelected = false);
}
if (TreeViewEx.IsShiftPressed && _lastItemSelected != null)
{
foreach (TreeViewItem tempItem in GetTreeViewItemsBetween(_lastItemSelected, item))
{
((ISelectable)tempItem.Header).IsSelected = true;
_lastItemSelected = tempItem;
}
}
else
{
content.IsSelected = true;
_lastItemSelected = item;
}
}
}
private static bool IsCtrlPressed
{
get
{
return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
}
}
private static bool IsShiftPressed
{
get
{
return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
}
}
private TreeViewItem GetTreeViewItemClicked(UIElement sender)
{
Point point = sender.TranslatePoint(new Point(0, 0), this.InnerTreeView);
DependencyObject visualItem = this.InnerTreeView.InputHitTest(point) as DependencyObject;
while (visualItem != null && !(visualItem is TreeViewItem))
{
visualItem = VisualTreeHelper.GetParent(visualItem);
}
return visualItem as TreeViewItem;
}
private IEnumerable<TreeViewItem> GetTreeViewItemsBetween(TreeViewItem start, TreeViewItem end)
{
List<TreeViewItem> items = this.GetTreeViewItems(false);
int startIndex = items.IndexOf(start);
int endIndex = items.IndexOf(end);
// It's possible that the start element has been removed after it was selected,
// I don't find a way to happen on the end but I add the code to handle the situation just in case
if (startIndex == -1 && endIndex == -1)
{
return new List<TreeViewItem>();
}
else if (startIndex == -1)
{
return new List<TreeViewItem>() {end};
}
else if (endIndex == -1)
{
return new List<TreeViewItem>() { start };
}
else
{
return startIndex > endIndex ? items.GetRange(endIndex, startIndex - endIndex + 1) : items.GetRange(startIndex, endIndex - startIndex + 1);
}
}
private List<TreeViewItem> GetTreeViewItems(bool includeCollapsedItems)
{
List<TreeViewItem> returnItems = new List<TreeViewItem>();
for (int index = 0; index < this.InnerTreeView.Items.Count; index++)
{
TreeViewItem item = (TreeViewItem)this.InnerTreeView.ItemContainerGenerator.ContainerFromIndex(index);
returnItems.Add(item);
if (includeCollapsedItems || item.IsExpanded)
{
returnItems.AddRange(GetTreeViewItemItems(item, includeCollapsedItems));
}
}
return returnItems;
}
private static IEnumerable<TreeViewItem> GetTreeViewItemItems(TreeViewItem treeViewItem, bool includeCollapsedItems)
{
List<TreeViewItem> returnItems = new List<TreeViewItem>();
for (int index = 0; index < treeViewItem.Items.Count; index++)
{
TreeViewItem item = (TreeViewItem)treeViewItem.ItemContainerGenerator.ContainerFromIndex(index);
if (item != null)
{
returnItems.Add(item);
if (includeCollapsedItems || item.IsExpanded)
{
returnItems.AddRange(GetTreeViewItemItems(item, includeCollapsedItems));
}
}
}
return returnItems;
}
#endregion
}
}

Resources