WPF Binding MainWindow Control from UserControl, PropertyChanged not fired - wpf

I'm new to WPF and facing an issue regarding binding.
I have a MainWindow with a Rectangle that uses binding to change visual attributes. The XAML code is as follows:
<Rectangle Height="72" Canvas.Left="1011" RadiusY="6" RadiusX="6" StrokeThickness="2" Width="82" Canvas.Top="8">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Style.Triggers>
<DataTrigger Binding="{Binding BddState}" Value="True">
<Setter Property="Fill">
<Setter.Value>
<ImageBrush ImageSource="Pictures/BDD_on.png" Stretch="Uniform" />
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding BddState}" Value="False">
<Setter Property="Fill">
<Setter.Value>
<ImageBrush ImageSource="Pictures/Bdd_off.png" Stretch="Uniform" />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
I created a view model class to manage the binding:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
private Boolean _bddstate;
public Boolean BddState
{
get { return _bddstate; }
set
{
_bddstate = value;
OnPropertyChanged("BddState");
}
}
}
The binding works well from MainWindow using:
private ViewModel _viewModel = new ViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = _viewModel;
_viewModel.BddState = true;
}
Inside the MainWindow, I use a StackPanel to load different UserControl (the MainWindow acts as an header).
My concern is the binding from the UserControl; the BddState value changes accordingly but nothing is reflected on MainWindow UI.
At the same time, I can see that the handler of the view model is always null.
What am I doing wrong?
Thanks in advance for your support.
Update: Here is the code of the user control:
<Grid Background="#FFECECEC" Height="706" VerticalAlignment="Top">
<Label x:Name="label" Content="HOME" HorizontalAlignment="Left" Margin="525,8,0,0" VerticalAlignment="Top" FontWeight="Bold" Foreground="{DynamicResource gray_2}" FontSize="20"/>
<Label x:Name="label_Copy" Content="General Info" HorizontalAlignment="Left" Margin="441,34,0,0" VerticalAlignment="Top" Foreground="{DynamicResource gray_2}" FontSize="18"/>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="441,214,0,0" VerticalAlignment="Top" Width="107" Height="36" Click="button_Click"/>
</Grid>
for the code behind, it's just the following:
public partial class Home : UserControl
{
public Home()
{
InitializeComponent();
}
private void button_Click(object sender, RoutedEventArgs e)
{
ViewModel _vm = new ViewModel();
_vm.BddState = true;
}
}
When i click the button of Home UC, the handler of ViewModel is null and the binding is not effective on MainWindow rectangle
here some update following Andy's sample (it could help)
by casting mainwindow datacontext it works:
MainWindowViewModel viewModel = App.Current.MainWindow.DataContext as MainWindowViewModel;
if (viewModel != null)
{
viewModel.BddState = true;
}

I modified your markup to:
<Canvas Name="MyCanvas" >
<Rectangle Height="72" RadiusY="6" RadiusX="6" StrokeThickness="2" Width="82" Canvas.Top="8">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Style.Triggers>
<DataTrigger Binding="{Binding BddState}" Value="True">
<Setter Property="Fill" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding BddState}" Value="False">
<Setter Property="Fill" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
</Canvas>
Which has no reliance on any image and no Canvas.Left.
I get a green rectangle.
Possible reasons you see nothing include:
Your window is narrower than 1011px and Canvas.Left="1011" means your rectangle is not within your window.
Your image paths are not working.
Uniform stretch is creating a problem and the part of the rectangle which has any picture in it is off screen.
I then modified this further to set bddstate true initially via the private backing field in the viewmodel. Added a togglebutton with ischecked bound to bddstate.
That toggles between yellow and green successfully.
Here's a working version involves a usercontrol. I've inherited MainWindowViewModel from a BaseViewModel I had in my test sample.
MainWindow:
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Canvas Name="MyCanvas" >
<Rectangle Height="72" RadiusY="6" RadiusX="6" StrokeThickness="2" Width="82" Canvas.Top="8">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Fill" Value="Green"/>
<Style.Triggers>
<DataTrigger Binding="{Binding BddState}" Value="False">
<Setter Property="Fill" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
</Canvas>
<local:UserControl1 Height="30" Width="100"/>
</Grid>
The viewmodel:
public class MainWindowViewModel : BaseViewModel
{
private Boolean _bddstate = true;
public Boolean BddState
{
get { return _bddstate; }
set
{
_bddstate = value;
RaisePropertyChanged("BddState");
}
}
UserControl1
<ToggleButton Width="100" Height="30" Content="Toggle" IsChecked="{Binding BddState, Mode=TwoWay}"/>
BaseViewModel
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
There is no code behind.
And this still works.

Related

TabControl view doesn't update on template change

I need to change the view of a TabControl's content on-the-fly.
I am guessing the best way to accomplish this is to define the view as a DataTemplate, and then change said template using a trigger.
In my test app, the background color is tied to the same data trigger as the template. The background color updates immediately upon making the radio button selection.
Expected behavior: The Tab Item Content / DataTemplate also updates immediately.
Actual Behavior: Tab content view does not update until the tab selection is changed.
Here's my Minimal, Complete, and Verifiable example:
Window XAML
<Window x:Class="ChangeView.Window1"
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"
Title="Window1" Height="350" Width="400">
<Window.Resources>
<DataTemplate x:Key="ContentTemplate1">
<Grid>
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Content="{Binding MyBlurb}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="ContentTemplate2">
<Grid>
<Label HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Content="{Binding MyHeader}" Background="Black" Foreground="White" FontSize="72"/>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Style.Triggers>
<DataTrigger Binding="{Binding ViewType1}" Value="False">
<Setter Property="Background" Value="Chartreuse"/>
</DataTrigger>
<DataTrigger Binding="{Binding ViewType1}" Value="True">
<Setter Property="Background" Value="Bisque"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Margin="10,38,0,0" Text="Content Template:"/>
<RadioButton x:Name="radio1" Margin="120,40,0,0" Grid.ColumnSpan="2" Content="1" GroupName="ViewSelect" IsChecked="{Binding Path=ViewType1}"/>
<RadioButton Margin="170,40,0,0" Grid.ColumnSpan="2" Content="2" GroupName="ViewSelect"/>
<TabControl Grid.Row="1" ItemsSource="{Binding TabGroup}">
<TabControl.Style>
<Style TargetType="{x:Type TabControl}">
<Setter Property="Margin" Value="10"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ViewType1}" Value="True">
<Setter Property="ContentTemplate" Value="{DynamicResource ContentTemplate1}"/>
</DataTrigger>
<DataTrigger Binding="{Binding ViewType1}" Value="False">
<Setter Property="ContentTemplate" Value="{DynamicResource ContentTemplate2}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.Style>
<TabControl.ItemTemplate>
<DataTemplate>
<Border x:Name="headerBorder">
<Label Content="{Binding MyHeader}" FontSize="20"/>
</Border>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
</Window>
Code Behind
namespace ChangeView
{
using System.Windows;
using System.ComponentModel;
using System.Collections.ObjectModel;
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window, INotifyPropertyChanged
{
public ObservableCollection<TabData> TabGroup { get; set; } = new ObservableCollection<TabData>();
private bool _viewType1 = true;
public bool ViewType1
{
get { return _viewType1; }
set { _viewType1 = value; RaisePropertyChanged(nameof(ViewType1)); }
}
public Window1()
{
TabGroup.Add(new TabData("♻️", "Recycle"));
TabGroup.Add(new TabData("⚔", "Swords"));
TabGroup.Add(new TabData("⚗", "Chemistry"));
TabGroup.Add(new TabData("🌵", "Cactus"));
TabGroup.Add(new TabData("👺", "Tengu"));
TabGroup.Add(new TabData("🐙", "Octopus"));
DataContext = this;
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void RaisePropertyChanged(string propName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public class TabData : INotifyPropertyChanged
{
private string _myHeader, _myBlurb;
public TabData(string header, string blurb)
{
MyHeader = header;
MyBlurb = blurb;
}
public string MyHeader
{
get { return _myHeader; }
set { _myHeader = value; RaisePropertyChanged(nameof(MyHeader)); }
}
public string MyBlurb
{
get { return _myBlurb; }
set { _myBlurb = value; RaisePropertyChanged(nameof(MyBlurb)); }
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void RaisePropertyChanged(string propName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
After changing the radio button state, change the selected tab. You will then see the correct content template.
It looks as if, in a TabControl, changing the content template alone does not cause the content to be rendered. If you render new content by switching the selected tab, the current content template will then be used.
So let's write one ContentTemplate, which creates a ContentControl and switches the ContentControl's ContentTemplate. I've tested, and the ContentControl will re-render its content when its ContentTemplate changes. The bindings get a little bit verbose.
<TabControl ItemsSource="{Binding TabGroup}" Grid.Row="1">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl
x:Name="ContentCtl"
Content="{Binding}"
/>
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding DataContext.ViewType1, RelativeSource={RelativeSource AncestorType=TabControl}}"
Value="True">
<Setter
TargetName="ContentCtl"
Property="ContentTemplate"
Value="{DynamicResource ContentTemplate1}"
/>
</DataTrigger>
<DataTrigger
Binding="{Binding DataContext.ViewType1, RelativeSource={RelativeSource AncestorType=TabControl}}"
Value="False"
>
<Setter
TargetName="ContentCtl"
Property="ContentTemplate"
Value="{DynamicResource ContentTemplate2}"
/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</TabControl.ContentTemplate>
<TabControl.ItemTemplate>
<DataTemplate>
<Border x:Name="headerBorder">
<Label Content="{Binding MyHeader}" FontSize="20"/>
</Border>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
You could also do something ugly in your code behind to make the TabControl render itself again on command. Or maybe you can replace the metadata on TabControl.ContentTemplate.

WPF TextBox doesn`t update binded source

I've got a textbox with custom style and binded to it property. If i change value in textbox property doesn't changes but if property changed from code textbox updates right.
I guess it's Mode=TwoWay missing somewhere but I can't figure out where as if I clear style frm xaml it works fine.
Form xaml:
<TextBox x:Name="tbFrames" Height="27" Canvas.Left="53" TextWrapping="Wrap" Text="{Binding Path=frames, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Canvas.Top="185" Width="64" Style="{DynamicResource StyleTextBox}" PreviewTextInput="AnimationValidationTextBox"/>
Style xaml:
<Style x:Key="StyleTextBox" TargetType="{x:Type TextBox}">
<Setter Property="Height" Value="27" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Grid>
<Border
x:Name="PART_border"
Background="Transparent"
BorderBrush="#FF434346"
BorderThickness="1"
/>
<TextBox Foreground="#FFBABAC7" BorderThickness="0" Background="Transparent" Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Text, Mode=TwoWay}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_border" Property="Background" Value="#FF414146" />
<Setter TargetName="PART_border" Property="BorderBrush" Value="#FF5A5A5D" />
<Setter TargetName="PART_border" Property="Opacity" Value="0.7" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
INotifyPropertyChanged is implemented:
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
And looks like working as like I`ve said before if I remove my style from control source updetes.
At first in style textbox binding was:
<TextBox Foreground="#FFBABAC7" BorderThickness="0" Background="Transparent" Text="{TemplateBinding Text}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
And it doesn't worked so I've changed it to RelativeSource and TwoWay mode but there are still no result.
What can be wrong?
PreviewTextInputCode is :
private void AnimationValidationTextBox(object sender, TextCompositionEventArgs e)
{
if (!animationOpened)
{
e.Handled = true;
return;
}
NumberValidationTextBox(sender, e);
}
private void NumberValidationTextBox(object sender, TextCompositionEventArgs e)
{
Regex regex = new Regex("[^0-9]+");
e.Handled = regex.IsMatch(e.Text);
}
And property declared as:
private int _frames;
public int frames
{
get => _frames;
set
{
_frames = value;
OnPropertyChanged("frames");
}
}
And with default style frames updates both way with all this checking.

XAML change button background if databinding value is true

I'm trying to change the background of a button, if a property of the data binding is true.
To give a short overview - I have a ListBox. I also gave this ListBox a style for the items:
<ListBox Margin="0,5,0,0" Background="Transparent"
BorderThickness="0"
ItemsSource="{Binding PaymentsAndCollectionData.PaymentsToCreditors}"
SelectedItem="{Binding PaymentsAndCollectionData.SelectedPaymentToCreditor}">
<ListBox.ItemContainerStyle>
.......
</ListBox.ItemContainerStyle>
</ListBox
Now, I want to put a button in this style. So, I used a ControlTemplate like so (I know this looks weird, and I could propably have done this differently!):
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Button>
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Button Name="Button" HorizontalContentAlignment="Stretch" Background="Transparent" BorderThickness="0"
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl},
Path=DataContext.PaymentsAndCollectionData.SelectPaymentToCreditorCommand}"
CommandParameter="{Binding}">
<DockPanel Name="DP" LastChildFill="False" Background="{TemplateBinding Background}">
<TextBlock DockPanel.Dock="Left" Text="{Binding Name}" Margin="20,0,10,0" Padding="0,5,0,5" Foreground="{TemplateBinding Foreground}"/>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<TextBlock Text="R" VerticalAlignment="Center" Foreground="{TemplateBinding Foreground}"/>
<TextBlock Text="{Binding Amount, StringFormat=N2}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="{TemplateBinding Foreground}"/>
</StackPanel>
</DockPanel>
</Button>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="DP" Property="Background" Value="{StaticResource APPLICATION_GREEN_COLOR}" />
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
</Button>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
So, the above works properly. The Name and Amount is set from the DataBinding. I am just failing to set the background, if IsSelected is true.
My binding object looks like this:
public class PaymentItem
{
public string Name { get; set; }
public decimal Amount { get; set; }
public bool IsSelected { get; set; }
}
If you change the IsSelected property after the object is created, you need to implement INotifyPropertyChanged interface in this object to make the Binding work.
public class PaymentItem : INotifyPropertyChanged
{
private bool _isSelected;
public string Name { get; set; }
public decimal Amount { get; set; }
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
_isSelected = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

How to dynamically set style of an element in a User Control

In a WPF project, I have a user control (Valve.xaml) that defines a Polygon shape.
<Grid>
<Polygon Name="pValve" Points="0,50 0,20 50,50 50,20" Style="{StaticResource Valve_Open}"/>
</Grid>
I am displaying the Valve user control in a window xaml (FFG.xaml) file, like such:
<Window
<!-- removed other namespaces for brevity -->
xmlns:cl="clr-namespace:FFG.Controls;assembly=PID.Controls">
<Grid>
<cl:Valve x:Name="valve201A"></cl:Valve>
</Grid>
</Window>
I am setting the DataContext of FFG.xaml to class FFG_ViewModel.cs, and it contains an instance of the Valve_Model class. Valve_Model essentially represents the valve that is drawn on the window in FFG.xaml.
public class FFG_ViewModel : ViewModelBase {
public Valve_Model Valve201A { get; set; }
// There are other properties and methods, but I've removed them for brevity also
}
Here is the Valve_Model class:
public class Valve_Model INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private bool _isValveOpen { get; set; }
public bool IsValveOpen {
get {
return _isValveOpen;
}
set {
_isValveOpen = value;
OnPropertyChanged("IsValveOpen");
}
}
#region INotifyPropertyChanged
protected virtual void OnPropertyChanged(string propertyName) {
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null) {
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
#endregion
}
QUESTION:
What I want is for the Style property in the Valve.xaml to change when the IsValveOpen property changes.
So if the valve is open then it would be:
<Polygon Name="pValve" Points="0,50 0,20 50,50 50,20" Style="{StaticResource Valve_Open}"/>
and when the property is changed to false then I need the style of the polygon to be changed to:
<Polygon Name="pValve" Points="0,50 0,20 50,50 50,20" Style="{StaticResource Valve_Closed}"/>
How do I go about implementing this exactly?
You could use an IMultiValueConverter.
First, let's simplify the use case. Basicly you want to swap Styles based on a given state object, which I'll represent by a ToggleButton. The fact that you're wrapping everything in a UserControl also has no influence on the underlying concept.
Demo:
Starting a fresh project
Declaring our Resources
Feeding the Window and the state to the Converter.
MainWindow.xaml
<Window
...
>
<Window.Resources>
<local:ToStyleConverter x:Key="ToStyleConverter"/>
<Style x:Key="Valve_Open" TargetType="{x:Type Polygon}">
<Setter Property="Fill" Value="Red"/>
</Style>
<Style x:Key="Valve_Closed" TargetType="{x:Type Polygon}">
<Setter Property="Fill" Value="Green"/>
</Style>
</Window.Resources>
<DockPanel>
<ToggleButton x:Name="butt" DockPanel.Dock="Bottom">Switch</ToggleButton>
<Polygon Name="pValve" Points="0,50 0,20 50,50 50,20" Stretch="Uniform">
<Polygon.Style>
<MultiBinding Converter="{StaticResource ToStyleConverter}">
<Binding RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type Window}}"/>
<Binding ElementName="butt" Path="IsChecked"/>
</MultiBinding>
</Polygon.Style>
</Polygon>
</DockPanel>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
public class ToStyleConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if (values[0] is Window)
{
Window win = (Window)values[0];
if ((bool)values[1])
return win.FindResource("Valve_Open");
if (!(bool)values[1])
return win.FindResource("Valve_Closed");
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object values, Type[] targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Changing to any specific use case means:
Pointing the relativesource binding to the Control that holds the Resources (Styles)
Using the second binding to add the state to the Converter (DP/INPC)
Implementing Converter logic
You can (should as much as I know) use a DataTrigger, within a ControlTemplate. Assuming that these two are your Styles:
<Window.Resources>
<Style TargetType="Polygon" x:Key="Valve_Open">
<Setter Property="Fill" Value="Red"/>
</Style>
<Style TargetType="Polygon" x:Key="Valve_Close">
<Setter Property="Fill" Value="Green"/>
</Style>
</Window.Resources>
You should add this style to the resources:
<Style x:Key="changeStyle" TargetType="Control">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<Grid>
<Polygon Name="pValve" Points="0,50 0,20 50,50 50,20" Style="{StaticResource Valve_Open}" />
</Grid>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Valve201A.IsValveOpen}" Value="true">
<Setter TargetName="pValve" Property="Style" Value="{StaticResource Valve_Close}" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
and use them in your views:
<Control DataContext="{Binding}" Style="{StaticResource changeStyle}" />
Instead of setting the actual Style property to a new value, you could add a DataTrigger to the Style itself that changes the properties of the Polygon based on the value of the IsValveOpen source property.
Valve.xaml:
<Grid>
<Polygon Name="pValve" Points="0,50 0,20 50,50 50,20">
<Polygon.Style>
<Style TargetType="Polygon">
<!-- Copy the setters from the Valve_Closed style here -->
<Setter Property="Fill" Value="Red" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsValveOpen}" Value="True">
<!-- Copy the setters from the Valve_Open style here -->
<Setter Property="Fill" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</Polygon.Style>
</Polygon>
</Grid>
FFG.xaml:
<Grid>
<cl:Valve x:Name="valve201A" DataContext="{Binding Valve201A}" />
</Grid>

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