Listbox item trigger colour change - wpf

I am trying to change the colour of the items in a listbox based on a trigger using MVVM
<Border Grid.Row="1" Width="300" Margin="0,0,20,0" BorderThickness="1,2,1,1" CornerRadius="5" BorderBrush="#FF999393" Background="#FFE9EDF1" >
<ListBox ItemsSource="{Binding LogMessageList, UpdateSourceTrigger=PropertyChanged}" Background="{x:Null}" Margin="3" BorderBrush="{x:Null}" FontSize="13.333" >
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding FatalError, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Firebrick" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
I'm setting the property change correctly but nothing seems to change.
Thanks
EDIT:
Still stuck. Trying
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},Path=DataContext.FatalError, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Firebrick" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>

Tried your scenario. Please refer the below code example:
View Model
public class Vm : INotifyPropertyChanged
{
public ObservableCollection<VmUser> VmUsers { get; set; }
private string errorType;
public string ErrorType
{
get { return errorType; }
set
{
errorType = value;
Raise("ErrorType");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void Raise(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class VmUser
{
public string Name { get; set; }
public int Age { get; set; }
}
Set the DataContext of window in constructor :
public MainWindow()
{
Vm = new Vm
{
VmUsers = new ObservableCollection<VmUser>
{
new VmUser { Name = "Gil", Age = 1 },
new VmUser { Name = "Dan", Age = 2 },
new VmUser { Name = "John", Age = 3 },
},
ErrorType = "Fatal"
};
InitializeComponent();
DataContext = TheVm;
}
Defined ListBox in XAML as:
<ListBox Grid.Row="2" ItemsSource="{Binding VmUsers}" DisplayMemberPath="Name" SelectedValuePath="Age">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},Path=DataContext.ErrorType, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
To test the scenario I have wrote a small test to toggle the ErrorType on click on button
private void Button_Click(object sender, RoutedEventArgs e)
{
var vm = this.DataContext as Vm;
if (vm != null)
{
if (vm.ErrorType == "Fatal")
{
vm.ErrorType = "Non Fatal";
}
else
{
vm.ErrorType = "Fatal";
}
}
}
With above example foreground color changes based on ErrorType.

Related

How to switch Control by DataTrigger in the xaml?

There is a window consist of the two control.
one is TreeView, the other is ListBox.
The code is as shown below.
<Grid Grid.Row="1">
<TreeView x:Name="treeView" BorderThickness="0" Visibility="Visible"
ItemsSource="{Binding TotalCPUs}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<mvvm:EventToCommand Command="{Binding CPUSelectedCommand}"
PassEventArgsToCommand="True"
EventArgsConverter="{localConverters:SelectedItemConverter}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
<ListBox x:Name="listBox" BorderThickness="0" Visibility="Collapsed"/>
</Grid>
Now I would like to change the value of the Visibility property of the Control whenever changes property of the ViewModel. (FilterMode True = ListBox Visible, FilterMode False = TreeView = Visible)
To this, I modified my XAML code as below.
<Grid Grid.Row="1">
<TreeView x:Name="treeView" BorderThickness="0" Visibility="Visible"
ItemsSource="{Binding TotalCPUs}">
<TreeView.Style>
<Style TargetType="{x:Type TreeView}">
<Style.Triggers>
<DataTrigger Binding="{Binding FilterMode}" Value="true">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.Style>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<mvvm:EventToCommand Command="{Binding CPUSelectedCommand}"
PassEventArgsToCommand="True"
EventArgsConverter="{localConverters:SelectedItemConverter}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
<ListBox x:Name="listBox" BorderThickness="0" Visibility="Collapsed">
<ListBox.Style>
<Style TargetType="{x:Type ListBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding FilterMode}" Value="true">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Style>
</ListBox>
</Grid>
Below is the ViewModel code.
public class NewProjectViewModel : DialogViewModel
{
private Generator projectGenerator = new Generator();
public ObservableCollection<ClassHierarchyData> TotalCPUs { get; private set; } = new ObservableCollection<ClassHierarchyData>();
public ObservableCollection<DetailType> FilterCPUs { get; private set; } = new ObservableCollection<DetailType>();
private bool filterMode;
public bool FilterMode
{
get => filterMode;
set
{
if (this.filterMode == value) return;
this.filterMode = value;
this.RaisePropertyChanged("FilterMode");
}
}
private string cpuSearch;
public string CPUSearch
{
get => this.cpuSearch;
set
{
if (this.cpuSearch == value) return;
this.cpuSearch = value;
this.FilterCPUs.Add(new DetailType(typeof(Target), "abc"));
}
}
private Type selectedTerminalItem;
public Type SelectedTerminalItem
{
get => this.selectedTerminalItem;
private set
{
if (this.selectedTerminalItem == value) return;
this.selectedTerminalItem = value;
this.RaisePropertyChanged("SelectedTerminalItem");
}
}
private Type selectedItem;
public Type SelectedItem
{
get => selectedItem;
set
{
if (this.selectedItem == value) return;
this.selectedItem = value;
this.RaisePropertyChanged("SelectedItem");
CreateCommand.RaiseCanExecuteChanged();
}
}
private string solutionName = string.Empty;
public string SolutionName
{
get => this.solutionName;
set
{
if (this.solutionName == value) return;
this.solutionName = value;
this.RaisePropertyChanged("SolutionName");
this.RaisePropertyChanged("SolutionFullPath");
CreateCommand.RaiseCanExecuteChanged();
}
}
private string solutionPath = string.Empty;
public string SolutionPath
{
get => this.solutionPath;
set
{
if (this.solutionPath == value) return;
this.solutionPath = value;
if(this.SolutionPath.Length > 0)
{
if (this.solutionPath.Last() != '\\')
this.solutionPath += "\\";
}
this.RaisePropertyChanged("SolutionPath");
this.RaisePropertyChanged("SolutionFullPath");
CreateCommand.RaiseCanExecuteChanged();
}
}
public bool CreateSolutionFolder { get; set; }
public string SolutionFullPath { get => this.SolutionPath + this.solutionName; }
private RelayCommand searchCommand;
public RelayCommand SearchCommand
{
get
{
if (this.searchCommand == null) this.searchCommand = new RelayCommand(this.OnSearch);
return this.searchCommand;
}
}
private void OnSearch()
{
CommonOpenFileDialog selectFolderDialog = new CommonOpenFileDialog();
selectFolderDialog.InitialDirectory = "C:\\Users";
selectFolderDialog.IsFolderPicker = true;
if (selectFolderDialog.ShowDialog() == CommonFileDialogResult.Ok)
{
this.SolutionPath = selectFolderDialog.FileName + "\\";
}
}
private RelayCommand<Action> _createCommand;
public RelayCommand<Action> CreateCommand
{
get
{
if (this._createCommand == null)
this._createCommand = new RelayCommand<Action>(this.OnCreate, this.CanExecuteCreate);
return this._createCommand;
}
}
private void OnCreate(Action action)
{
projectGenerator.GenerateSolution(this.SolutionPath, this.SolutionName, this.CreateSolutionFolder);
action?.Invoke();
}
private bool CanExecuteCreate(Action action)
{
if (this.SelectedTerminalItem == null) return false;
if (string.IsNullOrEmpty(this.solutionPath)) return false;
if (string.IsNullOrEmpty(this.solutionName)) return false;
return true;
}
private RelayCommand<ClassHierarchyData> cpuSelectedCommand;
public RelayCommand<ClassHierarchyData> CPUSelectedCommand
{
get
{
if (this.cpuSelectedCommand == null)
this.cpuSelectedCommand = new RelayCommand<ClassHierarchyData>(OnCPUSelected);
return this.cpuSelectedCommand;
}
}
private void OnCPUSelected(ClassHierarchyData selected)
{
this.SelectedItem = selected.Data;
this.SelectedTerminalItem = (selected.Items.Count == 0) ? selected.Data : null;
}
private RelayCommand<string> navigateCommand;
public RelayCommand<string> NavigateCommand
{
get
{
if (this.navigateCommand == null)
this.navigateCommand = new RelayCommand<string>((uri) =>
{
Process.Start(new ProcessStartInfo(uri));
});
return navigateCommand;
}
}
public NewProjectViewModel()
{
ClassHierarchyGenerator classHierarchyGenerator = new ClassHierarchyGenerator();
this.TotalCPUs.Add(classHierarchyGenerator.ToHierarchyData(typeof(Target)));
this.FilterCPUs.CollectionChanged += FilterCPUs_CollectionChanged;
}
private void FilterCPUs_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
this.FilterMode = (this.FilterCPUs.Count > 0) ? true : false;
}
}
public class DetailType
{
public Type Type { get; }
public string Path { get; }
public DetailType(Type type, string path)
{
Type = type;
Path = path;
}
}
If user input data to the TextBox for filtering then the value of the CPUSearch is changed.
If the value of the CPUSearch is changed then be added test value into the FilterCPUs. (Note CPUSearch property)
when being added value into the FilterCPUs, FilterCPUs_CollectionChanged is called and the value of the FilterMode is changed.
But the above code does not work although be changed the value of the FilterMode. (works well except for functionality related to FilterMode)
Why doesn't the Control switch?
Thanks for reading.
I solved this problem by referring WPF Showing / Hiding a control with triggers
My XAML code updated like this and it works well.
Thanks for your interest.
<Grid Grid.Row="1">
<TreeView x:Name="treeView" BorderThickness="0"
ItemsSource="{Binding TotalCPUs}">
<TreeView.Style>
<Style TargetType="{x:Type TreeView}">
<Style.Triggers>
<DataTrigger Binding="{Binding FilterMode}" Value="true">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding FilterMode}" Value="false">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.Style>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<mvvm:EventToCommand Command="{Binding CPUSelectedCommand}"
PassEventArgsToCommand="True"
EventArgsConverter="{localConverters:SelectedItemConverter}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
<ListBox x:Name="listBox" BorderThickness="0"
ItemsSource="{Binding FilterCPUs}"
SelectedItem="{Binding SelectedItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock FontSize="8" Text="{Binding Path=Path}" Margin="0 0 0 3"/>
<TextBlock Text="{Binding Type.Name}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.Style>
<Style TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="{DynamicResource CommonEnableBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource CommonEnableBorderBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource CommonEnableTextBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding FilterMode}" Value="true">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding FilterMode}" Value="false">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Style>
</ListBox>
</Grid>

WPF DataGridColumnHeader MouseOver - Apply Triggers to the DataGridCell Element

I would like to change background color of that whole datagrid column when I do mouse over of that datagrid column header. Here is code for style I am using.
<Style x:Key="RhinoDataGridBaseStyle" TargetType="{x:Type ctrls:RhinoDataGrid}">
<Style.Resources>
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="White"></Setter>
<Setter Property="BorderThickness" Value="3"></Setter>
<Setter Property="BorderBrush" Value="#4CB7FF"></Setter>
<Setter Property="Foreground" Value="Black"></Setter>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="Height" Value="26"></Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
?????????????????????? What Should I write here ??????????????????????????????
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type DataGridRowHeader}">
<Setter Property="Width" Value="36"></Setter>
</Style>
</Style.Resources>
Please help.
Thanks
Its a little tricky, but you could use an attached property to link the cells to their header.
Here is some code I used a while back (excuse it if its ver long)
Here is the attached property file
public static class GroupMessaging
{
private static readonly Dictionary<string, List<DependencyObject>> messageDictionary = new Dictionary<string, List<DependencyObject>>();
public static readonly DependencyProperty MessageKeyProperty = DependencyProperty.RegisterAttached("MessageKey", typeof(string), typeof(GroupMessaging), new PropertyMetadata(null, OnMessageKeyChanged));
public static void SetMessageKey(UIElement element, string value)
{
element.SetValue(MessageKeyProperty, value);
}
public static string GetMessageKey(UIElement element)
{
return (string)element.GetValue(MessageKeyProperty);
}
public static readonly DependencyProperty MessageProperty = DependencyProperty.RegisterAttached("Message", typeof(string), typeof(GroupMessaging), new PropertyMetadata(null, OnMessageChanged));
public static void SetMessage(UIElement element, string value)
{
element.SetValue(MessageProperty, value);
}
public static string GetMessage(UIElement element)
{
return (string)element.GetValue(MessageProperty);
}
private static void OnMessageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var key = d.GetValue(MessageKeyProperty);
if (key == null || !messageDictionary.ContainsKey((string)key))
{
return;
}
messageDictionary[(string)key].ForEach(o =>
{
var old = o.GetValue(MessageProperty);
if (o != d && e.NewValue != old)
{
o.SetValue(MessageProperty, e.NewValue);
}
});
}
private static void OnMessageKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue != null && messageDictionary.ContainsKey((string)e.OldValue))
{
messageDictionary[(string)e.OldValue].Remove(d);
}
if (e.NewValue != null)
{
if (!messageDictionary.ContainsKey((string)e.NewValue))
{
messageDictionary.Add((string)e.NewValue, new List<DependencyObject>());
}
messageDictionary[(string)e.NewValue].Add(d);
}
}
}
and here is my XAML
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="core:GroupMessaging.MessageKey" Value="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Content}" />
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid x:Name="headergrid">
<TextBlock Text="{TemplateBinding Content}" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="core:GroupMessaging.Message" Value="Active" />
<Setter Property="Background" Value="Aqua" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Prop1">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="templategrid" core:GroupMessaging.MessageKey="Prop1">
<TextBlock Text="{Binding Prop1}" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=templategrid, Path=(core:GroupMessaging.Message)}" Value="Active">
<Setter TargetName="templategrid" Property="Background" Value="Aqua" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Prop2">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="templategrid" core:GroupMessaging.MessageKey="Prop2">
<TextBlock Text="{Binding Prop2}" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=templategrid, Path=(core:GroupMessaging.Message)}" Value="Active">
<Setter TargetName="templategrid" Property="Background" Value="Aqua" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Prop3">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="templategrid" core:GroupMessaging.MessageKey="Prop3">
<TextBlock Text="{Binding Prop3}" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=templategrid, Path=(core:GroupMessaging.Message)}" Value="Active">
<Setter TargetName="templategrid" Property="Background" Value="Aqua" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Prop4">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid x:Name="templategrid" core:GroupMessaging.MessageKey="Prop4">
<TextBlock Text="{Binding Prop4}" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=templategrid, Path=(core:GroupMessaging.Message)}" Value="Active">
<Setter TargetName="templategrid" Property="Background" Value="Aqua" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
and here is my viewmodel
public class MainViewModel
{
public List<Item> Items { get; private set; }
public MainViewModel()
{
Items = new List<Item>();
Items.Add(new Item() { Prop1 = "item1_1",
Prop2 = "item1_2", Prop3 = "item1_3", Prop4 = "item1_4"});
Items.Add(new Item() { Prop1 = "item2_1",
Prop2 = "item2_2", Prop3 = "item2_3", Prop4 = "item2_4"});
Items.Add(new Item() { Prop1 = "item3_1",
Prop2 = "item3_2", Prop3 = "item3_3", Prop4 = "item3_4"});
Items.Add(new Item() { Prop1 = "item4_1",
Prop2 = "item4_2", Prop3 = "item4_3", Prop4 = "item4_4"});
Items.Add(new Item() { Prop1 = "item5_1",
Prop2 = "item5_2", Prop3 = "item5_3", Prop4 = "item5_4"});
}
}
public class Item
{
public string Prop1 { get; set; }
public string Prop2 { get; set; }
public string Prop3 { get; set; }
public string Prop4 { get; set; }
}

WPF/XAML - DataTriggers to set ValidatesOnDataErrors = false/true when a radio button is checked

I am working on an application that implements the MVVM design pattern with DataAnnotations. The application is a dynamically generated list of pages. On one of those pages, I have 10 required fields with 2 yes/no radio buttons. Those 10 fields are divided into two groups and each group is wwapped with a border tag. Each border's visibility is bound with the radio buttons for hidden/visible.
My question is if yes was selected and the related 5 required text boxes are displayed how can i set the ValidatesOnDataErrors to false/true and clear the text boxes values of the other hidden required TextBoxes?
Here is a code Snippet.
thanks
<Border>
<Border.Style>
<Style>
<Setter Property="Border.Visibility" Value="Hidden"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=PresentlyEmployed_yes, Path=IsChecked}"
Value="True">
<Setter Property="Border.Visibility" Value="Visible"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid Height="Auto" Width="Auto">
<Label Name="JobTitle"
Content="{x:Static properties:Resources.JobTitlelbl}" />
<TextBox Name="JobTitle" Text="{Binding JobTitle, Mode=TwoWay,
ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Text" Value="{Binding PrimaryInsuredBusinessDuties, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged, IsAsync=True}" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=PresentlyEmployed_yes, Path=IsChecked}"
Value="True">
<Setter Property="Text" Value="{Binding JobTitle, Mode=TwoWay,
ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=PresentlyEmployed_yes, Path=IsChecked}"
Value="False">
<Setter Property="Text" Value="{Binding JobTitle, Mode=TwoWay,
ValidatesOnDataErrors=False, UpdateSourceTrigger=PropertyChanged}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</Grid>
</Border>
Try setting the Validation.Template to {x:Null} if it shouldn't show the Validation Error
<StackPanel>
<ListBox x:Name="MyListBox" SelectedIndex="0">
<ListBoxItem>Validate Value 1</ListBoxItem>
<ListBoxItem>Validate Value 2</ListBoxItem>
</ListBox>
<TextBox Text="{Binding Value1, ValidatesOnDataErrors=True}">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedIndex, ElementName=MyListBox}" Value="1" >
<Setter Property="Validation.ErrorTemplate" Value="{x:Null}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<TextBox Text="{Binding Value2, ValidatesOnDataErrors=True}">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedIndex, ElementName=MyListBox}" Value="0" >
<Setter Property="Validation.ErrorTemplate" Value="{x:Null}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</StackPanel>
Sure, here is how my validationbase class looks like (Simplified)
public class ValidationViewModelBase : ViewModelBase, IDataErrorInfo, IValidationExceptionHandler
{
private Dictionary<string, Func<ValidationViewModelBase, object>> _propertyGetters;
private Dictionary<string, ValidationAttribute[]> _validators;
/// <summary>
/// Gets the error message for the property with the given name.
/// </summary>
/// <param name="propertyName">Name of the property</param>
public string this[string propertyName]
{
IList<string> fieldsNames = new List<string>();
{
if (propertyName == "PresentlyEmployed")
{
//if its true then
fieldsNames.Add("JobTitle");
AddFieldsValidation(fieldsNames);
}else{
fieldsNames.Add("EmploymentAddress");
RemoveValidation(fieldsNames);
}
if (this.propertyGetters.ContainsKey(propertyName))
{
var propertyValue = this.propertyGetters[propertyName](this);
var errorMessages = this.validators[propertyName]
.Where(v => !v.IsValid(propertyValue))
.Select(v => v.ErrorMessage).ToArray();
return string.Join(Environment.NewLine, errorMessages);
}
return string.Empty;
}
/// <summary>
/// Gets an error message indicating what is wrong with this object.
/// </summary>
public string Error
{
get
{
var errors = from validator in this.validators
from attribute in validator.Value
where !attribute.IsValid(this.propertyGetters[validator.Key](this))
select attribute.ErrorMessage;
return string.Join(Environment.NewLine, errors.ToArray());
}
}
}
/// <summary>
/// Gets the number of properties which have a validation attribute and are currently valid
/// </summary>
public int ValidPropertiesCount
{
get
{
var query = from validator in this.validators
where validator.Value.All(attribute => attribute.IsValid(this.propertyGetters[validator.Key](this)))
select validator;
var count = query.Count() - this.validationExceptionCount;
return count;
}
}
}
/// <summary>
/// Gets the number of properties which have a validation attribute
/// </summary>
public int TotalPropertiesWithValidationCount
{
get
{
return this.validators.Count();
}
}
public ValidationViewModelBase()
{
this.validators = this.GetType()
.GetProperties()
.Where(p => this.GetValidations(p).Length != 0)
.ToDictionary(p => p.Name, p => this.GetValidations(p));
this.propertyGetters = this.GetType()
.GetProperties()
.Where(p => this.GetValidations(p).Length != 0)
.ToDictionary(p => p.Name, p => this.GetValueGetter(p));
}
private ValidationAttribute[] GetValidations(PropertyInfo property)
{
return (ValidationAttribute[])property.GetCustomAttributes(typeof(ValidationAttribute), true);
}
private Func<ValidationViewModelBase, object> GetValueGetter(PropertyInfo property)
{
return new Func<ValidationViewModelBase, object>(viewmodel => property.GetValue(viewmodel, null));
}
private int validationExceptionCount;
public void ValidationExceptionsChanged(int count)
{
this.validationExceptionCount = count;
this.OnPropertyChanged("ValidPropertiesCount");
}

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

WPF - Any ideas why datatrigger not firing?

My listbox is defined below. "Properties" is a BindingList which I am changing one of the items but the Image style is not being updated. Any ideas what I might be missing?
<ListBox x:Name="lstProperties"
Margin="0,0,5,0"
ItemsSource="{Binding Properties}"
SelectedItem="{Binding CurrentProperty}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image>
<Image.Style>
<Style TargetType="{x:Type Image}">
<Setter Property="Source" Value="Images/HouseRed_16.png"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SuitableApplicationCount, Converter={StaticResource greaterThanConverter}, ConverterParameter=0}" Value="True">
<Setter Property="Source" Value="Images/HouseYellow_16.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding InterestedApplicationCount, Converter={StaticResource greaterThanConverter}, ConverterParameter=0}" Value="True">
<Setter Property="Source" Value="Images/HouseAmber_16.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding MatchedApplicationId, Converter={StaticResource isNullOrEmptyConverter}}" Value="False">
<Setter Property="Source" Value="Images/HouseGreen_16.png"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<TextBlock Grid.Column="1" VerticalAlignment="Center">
<TextBlock.Text>
<MultiBinding StringFormat="{}Id: {0}, Plot: {1}">
<Binding Path="Id" FallbackValue="" />
<Binding Path="Plot" FallbackValue=""/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Make your model implement INotifyPropertyChanged.
I included some sample code:
public class MyModel : ViewModelBase
{
private int _suitableApplicationCount;
public int SuitableApplicationCount
{
get { return _suitableApplicationCount; }
set
{
_suitableApplicationCount = value;
OnPropertyChanged("SuitableApplicationCount");
}
}
public int _interestedApplicationCount;
public int InterestedApplicationCount
{
get { return _interestedApplicationCount; }
set
{
_interestedApplicationCount = value;
OnPropertyChanged("InterestedApplicationCount");
}
}
public int? _matchedApplicationId;
public int? MatchedApplicationId
{
get { return _matchedApplicationId; }
set
{
_matchedApplicationId = value;
OnPropertyChanged("MatchedApplicationId");
}
}
}
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Resources