SelectAll checkbox inside Combobox items in Wpf - 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
}
}

Related

WPF Double Listbox menu with tracking and selection problem

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);
}

Why must I call Focus() after my button click event for seamless behavior?

I have a SearchTextBox custom control, pictured here:
When the mouse clicks on it, that label disappears. If the user clicks away and there is no text, the label reappears (if the user clicks away but leaves text there, the text stays and the label stays hidden). This button replaces the image when the user starts typing:
When the user clicks the button, the text is cleared.
That's all working correctly. The odd behavior I'm talking about is that when the clear button is clicked, the label flashes on the control for a split-second before disappearing (it should remain hidden the entire time). Because the Multitrigger in the XAML uses IsFocused, I thought maybe I could fix the problem simply by calling Focus() in the code-behind before the Click event finishes. This seems a little hacky, but it actually worked. My question is, why do I have to do that to make it work "right?"
Snippet from Generic.xaml:
<SolidColorBrush x:Key="TextBoxBorder" Color="#ababab"/>
<Style TargetType="{x:Type ui:SearchTextBox}">
<Setter Property="AllowDrop" Value="True" />
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" />
<Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="LabelText" Value="Search for..." />
<Setter Property="LabelTextColor" Value="Gray" />
<Setter Property="Padding" Value="1" />
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst" />
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ui:SearchTextBox}">
<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActualHeight}" />
</Grid.ColumnDefinitions>
<Label x:Name="LabelText"
Grid.Column="0"
Foreground="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=LabelTextColor}"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=LabelText}"
Padding="0"
Margin="5,0,0,0"
FontStyle="Italic"
VerticalAlignment="Center"
Visibility="Hidden" />
<ScrollViewer Grid.Column="0" Panel.ZIndex="1" x:Name="PART_ContentHost" Background="{TemplateBinding Background}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center" Margin="5,0,0,0" Padding="0"/>
<Image x:Name="Image" Grid.Column="1" Visibility="Hidden" Source="search2.png" Width="15" Height="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<Button x:Name="PART_Button" Grid.Column="1" Width="15" Height="15">
<Border HorizontalAlignment="Center" VerticalAlignment="Center">
<Image Source="searchstop.png" />
</Border>
</Button>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Text" Value="">
<Setter TargetName="Image" Property="Visibility" Value="Visible" />
<Setter TargetName="PART_Button" Property="Visibility" Value="Hidden" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Text" Value="" />
<Condition Property="IsFocused" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="LabelText" Property="Visibility" Value="Visible" />
<Setter TargetName="PART_ContentHost" Property="Visibility" Value="Hidden" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
SearchTextBox.cs:
public class SearchTextBox : TextBox
{
public static readonly DependencyProperty LabelTextProperty;
public static readonly DependencyProperty LabelTextColorProperty;
public static readonly DependencyProperty HasTextProperty;
private static readonly DependencyPropertyKey HasTextPropertyKey;
public static readonly DependencyProperty SourceProperty;
private static readonly DependencyProperty IsMouseLeftButtonDownProperty;
static SearchTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchTextBox), new FrameworkPropertyMetadata(typeof(SearchTextBox)));
LabelTextProperty = DependencyProperty.Register("LabelText", typeof(string), typeof(SearchTextBox));
LabelTextColorProperty = DependencyProperty.Register("LabelTextColor", typeof(Brush), typeof(SearchTextBox));
HasTextPropertyKey = DependencyProperty.RegisterReadOnly("HasText", typeof(bool), typeof(SearchTextBox), new PropertyMetadata());
HasTextProperty = HasTextPropertyKey.DependencyProperty;
SourceProperty = DependencyProperty.Register("Source", typeof(ImageSource), typeof(SearchTextBox));
IsMouseLeftButtonDownProperty = DependencyProperty.Register("IsMouseLeftButtonDown", typeof(bool), typeof(SearchTextBox), new PropertyMetadata());
}
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
HasText = Text.Length != 0;
}
public string LabelText
{
get { return (string)GetValue(LabelTextProperty); }
set { SetValue(LabelTextProperty, value); }
}
public Brush LabelTextColor
{
get { return (Brush)GetValue(LabelTextColorProperty); }
set { SetValue(LabelTextColorProperty, value); }
}
public ImageSource Source
{
get { return (ImageSource)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public bool HasText
{
get { return (bool)GetValue(HasTextProperty); }
private set { SetValue(HasTextPropertyKey, value); }
}
public bool IsMouseLeftButtonDown
{
get { return (bool)GetValue(IsMouseLeftButtonDownProperty); }
private set { SetValue(IsMouseLeftButtonDownProperty, value); }
}
public override void OnApplyTemplate()
{
Button b = GetTemplateChild("PART_Button") as Button;
if (b != null)
{
b.Click += OnClick;
}
base.OnApplyTemplate();
}
private void OnClick(object sender, RoutedEventArgs e)
{
Text = "";
Focus();
e.Handled = true;
}
}
The flashing most likely occurs because the focus is moving away from your control to the clear button if you press it.
You can fix that be making the clear button non-focusable, i.e.
<Button x:Name="PART_Button" Focusable="False" ... />
Then the focus does not move when you press the button.

Binding to Button Visibility

I have the following Style for a TabItem
<Style x:Key="SubStudioTabItem" TargetType="{x:Type TabItem}">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Height="20"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Margin="10,0,10,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ContentSource="Header" />
<Button Grid.Column="1"
x:Name="CloseButton"
Width="15"
Height="15"
HorizontalAlignment="Center"
VerticalAlignment="Center"
DockPanel.Dock="Right"
AttachedCommand:CommandBehavior.Event="Click"
AttachedCommand:CommandBehavior.Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},
Path=DataContext.CloseWorkspaceCommand}">
...
Now I want to make the visibility of the Button on the TabItem optional which can be used in a DataTemplate like
<DataTemplate x:Key="WorkspaceTemplate">
<TabControl x:Name="tabControl"
IsSynchronizedWithCurrentItem="true"
Style="{StaticResource StudioTabControl}"
ItemsSource="{Binding Workspaces}"
SelectedIndex="{Binding SelectedIndex, NotifyOnSourceUpdated=True, Mode=TwoWay}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem"
BasedOn="{StaticResource SubStudioTabItem}">
<Setter Property="??Button.Visibility??" Value="{Binding Path=Display, Converter={StaticResource BooleanToVisibiltyConverter}}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</DataTemplate>
How can I set the state of the visibility of the button on the TabItem from the DataTemplate?
Thanks for your time.
In that case, I would create an attached property, such as type of Visibility and would make a TemplateBinding in the Style, something like this:
<ControlTemplate TargetType="{x:Type TabItem}">
...
<Button Grid.Column="1"
x:Name="CloseButton"
Visibility="{TemplateBinding local:MyClass.ButtonVisibility}"
...
</Button>
And for TabItem in <ItemContainerStyle> would write this (or somewhere else):
<Style TargetType="TabItem" BasedOn="{StaticResource SubStudioTabItem}">
<Setter Property="local:MyClass.ButtonVisibility" Value="{Binding Path=Display, Converter={StaticResource BooleanToVisibiltyConverter}}"/>
</Style>
Edit:
I created a project that implements this method. In the project tried to follow the MVVM pattern. The structure of the project:
Start in order, in folder AttachedProperties there is an attached property, which is responsible for the appearance of the Button.
The code of ButtonVisibility.cs:
using System;
using System.Windows;
public class ButtonVisibilityPro : DependencyObject
{
public static readonly DependencyProperty ButtonVisibilityProperty;
public static void SetButtonVisibility(DependencyObject DepObject, Visibility value)
{
DepObject.SetValue(ButtonVisibilityProperty, value);
}
public static Visibility GetButtonVisibility(DependencyObject DepObject)
{
return (Visibility)DepObject.GetValue(ButtonVisibilityProperty);
}
static ButtonVisibilityPro()
{
PropertyMetadata MyPropertyMetadata = new PropertyMetadata(Visibility.Collapsed);
ButtonVisibilityProperty = DependencyProperty.RegisterAttached("ButtonVisibility",
typeof(Visibility),
typeof(ButtonVisibilityPro),
MyPropertyMetadata);
}
}
The data model is ButtonModel, wherein the boolean property is ButtonDisplay. This class inherits from the ViewModelBase, that implements INotifyPropertyChanged.
ButtonModel.cs
using System;
using ButtonVisibilityHelp.ViewModels;
namespace ButtonVisibilityHelp.Models
{
public class ButtonModel : ViewModelBase
{
private bool _buttonDisplay = false;
public bool ButtonDisplay
{
get
{
return _buttonDisplay;
}
set
{
_buttonDisplay = value;
NotifyPropertyChanged("ButtonDisplay");
}
}
}
}
ViewModelBase.cs
using System;
using System.ComponentModel;
namespace ButtonVisibilityHelp.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
ButtonViewModel.cs
using System;
using System.Windows.Input;
using System.Windows;
using ButtonVisibilityHelp.Models;
using ButtonVisibilityHelp.Workers;
namespace ButtonVisibilityHelp.ViewModels
{
public class ButtonViewModel : ViewModelBase
{
private ButtonModel _buttonModel;
private ICommand _hideButtonCommand = null;
private ICommand _showButtonCommand = null;
public ButtonModel ButtonModel
{
get
{
return _buttonModel;
}
set
{
_buttonModel = value;
NotifyPropertyChanged("ButtonModel");
}
}
public ICommand HideButtonCommand
{
get
{
if (_hideButtonCommand == null)
{
_hideButtonCommand = new RelayCommand(param => this.HideButton(), null);
}
return _hideButtonCommand;
}
}
public ICommand ShowButtonCommand
{
get
{
if (_showButtonCommand == null)
{
_showButtonCommand = new RelayCommand(param => this.ShowButton(), null);
}
return _showButtonCommand;
}
}
public ButtonViewModel()
{
ButtonModel = new ButtonModel();
}
private void HideButton()
{
ButtonModel.ButtonDisplay = false;
}
private void ShowButton()
{
ButtonModel.ButtonDisplay = true;
}
}
}
RelayCommand.cs
using System;
using System.Windows.Input;
namespace ButtonVisibilityHelp.Workers
{
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
public RelayCommand(Action<object> execute) : this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public void Execute(object parameter)
{
_execute(parameter);
}
}
}
Below is a piece of XAML code that is in the style of TabItem:
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" SnapsToDevicePixels="True" Name="Border" Margin="0,0,2,0" Padding="2" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="0">
<ContentPresenter Name="ContentSite"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="5,5,5,5"
VerticalAlignment="Center"
RecognizesAccessKey="True"
ContentSource="Header" />
</Border>
<!-- Here TemplateBinding for Visibility -->
<Button Name="CloseButton" Style="{StaticResource CloseButton}"
Visibility="{TemplateBinding AttachedProperties:ButtonVisibilityPro.ButtonVisibility}"
Grid.Column="1" Width="14" Height="14" HorizontalAlignment="Center" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="TabStripPlacement" Value="Bottom">
<Setter TargetName="Border" Property="CornerRadius" Value="0,0,0,0" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
XAML of MainWindow:
<Window x:Class="ButtonVisibilityHelp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModels="clr-namespace:ButtonVisibilityHelp.ViewModels"
xmlns:AttachedProperties="clr-namespace:ButtonVisibilityHelp.AttachedProperties"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ViewModels:ButtonViewModel x:Key="MyButtonViewModel" />
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource MyButtonViewModel}}">
<TabControl>
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}" BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="AttachedProperties:ButtonVisibilityPro.ButtonVisibility" Value="{Binding Path=ButtonModel.ButtonDisplay, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Style>
</TabControl.ItemContainerStyle>
<TabItem Header="Test1">
<StackPanel>
<TextBlock Text="{Binding ButtonModel.ButtonDisplay, StringFormat=ButtonDisplay: {0}, Mode=TwoWay}" HorizontalAlignment="Right" />
<Button Name="ShowInTest1" Command="{Binding ShowButtonCommand}" Width="100" Height="30" Content="Show me" HorizontalAlignment="Right" />
<Button Name="HideInTest1" Command="{Binding HideButtonCommand}" Width="100" Height="30" Content="Hide me" HorizontalAlignment="Right" />
</StackPanel>
</TabItem>
<TabItem Header="Test2">
<StackPanel>
<TextBlock Text="{Binding ButtonModel.ButtonDisplay, StringFormat=ButtonDisplay: {0}, Mode=TwoWay}" HorizontalAlignment="Right" />
<Button Name="ShowInTest2" Command="{Binding ShowButtonCommand}" Width="100" Height="30" Content="Show me" HorizontalAlignment="Right" />
<Button Name="HideInTest2" Command="{Binding HideButtonCommand}" Width="100" Height="30" Content="Hide me" HorizontalAlignment="Right" />
</StackPanel>
</TabItem>
</TabControl>
</Grid>
</Window>
Output
Show me:
Hide me:
The complete code example is available at this link.
Set the Tag property on your TabItem in style like below :
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem"
BasedOn="{StaticResource SubStudioTabItem}">
<Setter Property="Tag" Value="{Binding IsVisible, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
</Style>
</TabControl.ItemContainerStyle>
And then in ControlTemplate add DataTrigger to set the Visibility of CloseButton depending on Tag property
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Tag}", Value="false">
<Setter Property="Visibility" TargetName="CloseButton" Value="Collapsed"/>
</DataTrigger>
</ControlTemplate.Triggers>

WPF : give me a best way for icon button

We can easily make an icon button using a control template like the following code:
<Style x:Key="IconButton" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Image x:Name="Background" Source="/UOC;component/TOOLBAR_BUTTON_NORMAL.png"/>
<Image Source="/UOC;component/ICON_SLICER.gif" Width="20" Height="20" Margin="0,-10,0,0"/>
<TextBlock Foreground="White" FontSize="9" Text="{TemplateBinding Button.Content}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,15,0,0"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Button.IsMouseOver" Value="True">
<Setter Property="Source" TargetName="Background" Value="/UOC;component/TOOLBAR_BUTTON_OVER.png"/>
<Setter Property="Cursor" Value="Hand"/>
</Trigger>
<Trigger Property="Button.IsPressed" Value="True">
<Setter Property="Source" TargetName="Background" Value="/UOC;component/TOOLBAR_BUTTON_CLICK.png"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
But i think it's not a productive way in practice. because i can't make a number of styles for each one of icon buttons. (ex. let's assume three buttons in App:'open' button, 'close' button and 'navigate' button. these buttons have different icon sets. i can't make styles like 'IconButton_Close', 'IconButton_Open', 'IconButton_Nav'. it's too stupid.)
UserControl may be an answer. but i think it's not a smart way for that. because if i make UserControl, it'll be just a wrapper of the Button control. it's not a right way.
So, give me the best way for icon button.
thanks.
The correct way to do this would be to define a custom button class, like so:
public class MyButton : Button
{
static MyButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyButton), new FrameworkPropertyMetadata(typeof(MyButton)));
}
public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(ImageSource),
typeof(MyButton), new FrameworkPropertyMetadata(null));
public ImageSource ImageSource
{
get { return (ImageSource)GetValue(ImageSourceProperty); }
set { SetValue(ImageSourceProperty, value); }
}
public static readonly DependencyProperty ImageSourceHoverProperty = DependencyProperty.Register("ImageSourceHover", typeof(ImageSource),
typeof(MyButton), new FrameworkPropertyMetadata(null));
public ImageSource ImageSourceHover
{
get { return (ImageSource)GetValue(ImageSourceHoverProperty); }
set { SetValue(ImageSourceHoverProperty, value); }
}
public static readonly DependencyProperty ImageSourcePressedProperty = DependencyProperty.Register("ImageSourcePressed", typeof(ImageSource),
typeof(MyButton), new FrameworkPropertyMetadata(null));
public ImageSource ImageSourcePressed
{
get { return (ImageSource)GetValue(ImageSourcePressedProperty); }
set { SetValue(ImageSourcePressedProperty, value); }
}
}
Then define the default Style like so:
<Style x:Key="{x:Type local:MyButton}" TargetType="{x:Type local:MyButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyButton}">
<Grid>
<Image x:Name="Background" Source="{TemplateBinding ImageSource}" />
<Image Source="/UOC;component/ICON_SLICER.gif" Width="20" Height="20" Margin="0,-10,0,0"/>
<TextBlock Foreground="White" FontSize="9" Text="{TemplateBinding Button.Content}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,15,0,0"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Button.IsMouseOver" Value="True">
<Setter Property="Source" TargetName="Background" Value="{TemplateBinding ImageSourceHover}"/>
<Setter Property="Cursor" Value="Hand"/>
</Trigger>
<Trigger Property="Button.IsPressed" Value="True">
<Setter Property="Source" TargetName="Background" Value="{TemplateBinding ImageSourcePressed}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And you'd use it like so:
<local:MyButton ImageSource="/UOC;component/TOOLBAR_BUTTON_NORMAL.png"
ImageSourceHover="/UOC;component/TOOLBAR_BUTTON_OVER.png"
ImageSourcePressed="/UOC;component/TOOLBAR_BUTTON_CLICK.png" />
I did something similar to this for a custom control a while back using the TemplatePart attribute. This displays an icon and some text in a panel. If the icons is the error or fail icon, it turns the text red. There is a dependency property called "Type" which is really just the image file name without the extension. Here's the code, I bet you can adapt this for a custom Button where you can set the source and still have your customization to the template.
[TemplatePart(Name = "PART_Image", Type = typeof(Image))]
public class IconPanel : ContentControl
{
static IconPanel()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(IconPanel), new FrameworkPropertyMetadata(typeof(IconPanel)));
}
public string Type
{
get { return (string)GetValue(TypeProperty); }
set { SetValue(TypeProperty, value); }
}
public static readonly DependencyProperty TypeProperty =
DependencyProperty.Register("Type", typeof(string), typeof(IconPanel),
new UIPropertyMetadata("warning", TypeChangedCallback));
static void TypeChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
IconPanel panel = obj as IconPanel;
panel.UpdateImage();
}
void UpdateImage()
{
Image img = GetTemplateChild("PART_Image") as Image;
if (img == null) return;
string ImagePath = String.Format("pack://application:,,,/Resources/{0}.png", this.Type);
Uri uri = new Uri(ImagePath, UriKind.RelativeOrAbsolute);
BitmapImage bmp = new BitmapImage(uri);
img.Source = bmp;
if ( String.Compare(Type, "error", true) == 0 ||
String.Compare(Type, "fail", true) == 0 )
{
this.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0x00, 0x00));
}
}
public override void OnApplyTemplate()
{
UpdateImage();
base.OnApplyTemplate();
}
}
XAML:
<Style TargetType="{x:Type local:IconPanel}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:IconPanel}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="7">
<Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image
x:Name="PART_Image"
Margin="0,0,5,5"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Width="16"
Height="16" />
<ContentPresenter Grid.Column="1"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

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;
}
}

Resources