WPF Ribbon - Data templating in a RibbonTab - wpf

I've spent the better part of the last two days dinging around with this control and I'm stuck. Basically I don't how to data template it's RibbonTab. What I have would work for me if it would only not show it at the bottom of the RibbonTab! Grr!
What I have in my XAML is:
<r:Ribbon Grid.Row="0" Title="MSRibbon" x:Name="ribbon">
<r:Ribbon.Style>
<Style TargetType="{x:Type r:Ribbon}">
<Setter Property="TabHeaderTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding Header}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsSource" Value="{Binding AvailableRibbonTabs}"/>
<Setter Property="SelectedItem" Value="{Binding SelectedRibbonTab}"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type r:RibbonTab}">
<Setter Property="ItemsSource" Value="{Binding RibbonTabData}"/>
</Style>
</Setter.Value>
</Setter>
</Style>
</r:Ribbon.Style>
<r:Ribbon.Resources>
<DataTemplate DataType="{x:Type vmsrc:RecordingRibbonTabGroupData}">
<viewsrc:RecordingTabGroupControl/>
</DataTemplate>
</r:Ribbon.Resources>
</r:Ribbon>
The XAML of the control i would like to show in the ribbon tab group is (this, when displayed gets glued to the bottom of the ribbon tab):
<r:RibbonControl x:Class="Scanner.Views.RecordingRibbonTabGroupData">
<StackPanel Orientation="Horizontal">
<r:RibbonButton Label="foo" />
<r:RibbonButton Label="bar" />
<ListBox ItemsSource="{Binding Barcodes}" />
</StackPanel>
</r:RibbonControl>
Here I tried using different combinations of controls but to no effect. As the control base type I used the RibbonTab, the RibbonGroup, UserControl etc and I think I used every possible control as the main container, like StackPanel, Grid, ItemsControl, etc.. And also experimented with setting the Heights of every control and H/V alignment, etc. Nothing helped.
My view models are such (INPC is injected with INPCWeaver and it works):
public abstract class AbstractViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}
public abstract class AbstractRibbonTab : AbstractViewModel
{
public string Header { get; set; }
public bool IsSelected { get; set; }
public ObservableCollection<AbstractRibbonTabGroupData> RibbonTabData { get; set; }
}
public class RecordingRibbonTab : AbstractRibbonTab
{
public RecordingRibbonTab()
{
this.Header = "Recording";
this.RibbonTabData = new ObservableCollection<AbstractRibbonTabGroupData>() { new RecordingRibbonTabGroupData() };
}
}
public class SessionRibbonTab : AbstractRibbonTab
{
public SessionRibbonTab()
{
this.Header = "Session";
this.RibbonTabData = new ObservableCollection<AbstractRibbonTabGroupData>() { new AbstractRibbonTabGroupData() };
}
}
public class SettingsRibbonTab : AbstractRibbonTab
{
public SettingsRibbonTab()
{
this.Header = "Settings";
this.RibbonTabData = new ObservableCollection<AbstractRibbonTabGroupData>() { new AbstractRibbonTabGroupData() };
}
}
The XAML has it's data context set to an instance of:
public class MainWindowViewModel : AbstractViewModel, IMainWindowViewModel
{
...
public ObservableCollection<AbstractRibbonTab> AvailableRibbonTabs { get; private set; }
public AbstractRibbonTab SelectedRibbonTab { get; set; }
...
public MainWindowViewModel(PinChangeCommand pcc)
{
this.AvailableRibbonTabs = new ObservableCollection<AbstractRibbonTab>();
this.AvailableRibbonTabs.Add(new RecordingRibbonTab());
this.AvailableRibbonTabs.Add(new SessionRibbonTab());
this.AvailableRibbonTabs.Add(new SettingsRibbonTab());
}
}
The bindings work.
As a side note, below the ribbon there is a content control declared like so
<ContentControl Grid.Row="1" Content="{Binding SelectedRibbonTab}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type vmsr:RecordingRibbonTab}">
<views:RecordingView />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
that works perfectly fine as one would expect.
The 'recording' view that I did implement has the following XAML (it just shows the header, as one can see in the screenshot below):
<UserControl x:Class="Scanner.Views.RecordingView">
<Grid>
<TextBlock Text="{Binding Header}" />
</Grid>
</UserControl>
Wrapping up, a code listing that should explain some strange numbers:
public class RecordingRibbonTabGroupData : AbstractRibbonTabGroupData
{
public ObservableCollection<string> Barcodes { get; private set; }
public RecordingRibbonTabGroupData()
{
this.Barcodes = new ObservableCollection<string>();
this.Barcodes.Add("76765535642");
this.Barcodes.Add("43435356");
}
}
Without DataTemplate:
WITH DataTemplate:

What you need is two ItemContainerStyle
<Ribbon:Ribbon ItemContainerStyle="{StaticResource RibbonTabStyle}" ItemsSource="{Binding DummyRibbonTabContent}">
first:
<Style TargetType="{x:Type Ribbon:RibbonTab}" x:Key="RibbonTabStyle">
<Setter Property="ItemsSource" Value="{Binding DummyRibbonGroups}" />
<Setter Property="ItemContainerStyle" Value="{DynamicResource RibbonGroupStyle}" />
<Setter Property="Header" Value="{Binding DummyRibbonHeader"} />
</Style>
second:
<Style TargetType="{x:Type Ribbon:RibbonGroup}" x:Key="RibbonGroupStyle">
<Setter Property="Header" Value="{Binding RibbonGroupHeader}" />
<Setter Property="ItemsSource" Value="{Binding DummyRibbonButtons}" />
<Setter Property="ItemTemplate" Value="{DynamicResource RibbonButtonTemplate}" />
</Style>
Obviously you have to create a ribbon button datatemplate. You could also use item templateselector for the ribbongroupstyle and then you can add not just ribbonbuttons but whatever you wish. Its not the exact solution you need, but I hope you get the idea.

Related

Template binding inside a ResourceDictionary Control Template

I have a resource dictionary as follows,
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uwpControls="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:controls="using:Presentation.Common.Controls">
<Style x:Key="ExpanderStyleSection" TargetType="uwpControls:Expander">
<Setter Property="HeaderStyle" Value="{StaticResource LightExpanderHeaderToggleButtonStyle}"/>
<Setter Property="Margin" Value="4"/>
</Style>
<Style x:Key="LightExpanderHeaderToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid x:Name="RootGrid"
Background="{StaticResource BrushBeckmanAquaA1}"> <!-- I want to change the background from an other View based on some condition -->
</ControlTemplate>
</Style>
</ResourceDictionary>
Now I am using the style inside this Dictionary as follows in an other view.
<uwpControls:Expander x:Name="ExpanderLisSharedSettings" Grid.Row="0" Style="{StaticResource ExpanderStyleSection}">
<!--Some Content here-->
</uwpControls:Expander
Whenever a property in the ViewModel changes, I want to update the RootGrid background in the ControlTemplate to some other color. How can I do it? In other words is there a way to create a dependency property of type Brush color and bind to the RootGrid Background in the ResourceDictionary. Please help.
You can create a ViewModel which contains a SolidColorBrush property and bind it with the Background of RootGrid and you need to declare the DataContext in the page which you use this style. In that case, it applies for Binding.
If you want to use x:bind in ResourceDictionary, you need to create a code behind class for it. Since the x:Bind depends on code generation, so it needs a code-behind file containing a constructor that calls InitializeComponent (to initialize the generated code).
Here we take binding as an example.
Page.xaml:
<ToggleButton Style="{StaticResource LightExpanderHeaderToggleButtonStyle}"></ToggleButton>
Page.xaml.cs:
public BlankPage1()
{
this.InitializeComponent();
VM = new MyViewModel();
VM.Fname = "fleegu";
VM.MyColor = new SolidColorBrush(Colors.Green);
this.DataContext = VM;
}
public MyViewModel VM { get; set; }
ResourceDictionary.xaml:
<Style x:Key="LightExpanderHeaderToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid x:Name="RootGrid" Background="{Binding MyColor}">
<TextBlock Text="{Binding Fname}"></TextBlock>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
MyViewModel:
public class MyViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string _fname { get; set; }
private SolidColorBrush myColor { get; set; }
public string Fname
{
get { return _fname; }
set
{
_fname = value;
this.OnPropertyChanged();
}
}
public SolidColorBrush MyColor
{
get { return myColor; }
set
{
myColor = value;
this.OnPropertyChanged();
}
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Use a TemplateBinding in the template. You may use a <Setter> to define the default value:
<Style x:Key="LightExpanderHeaderToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="Background" Value="{StaticResource BrushBeckmanAquaA1}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
...
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You can then set or bind the Background of each instance of the control:
<uwpControls:Expander x:Name="ExpanderLisSharedSettings" Grid.Row="0" Background="Red" />
For example, if the view model defines a Brush property called "TheBackground", you could bind to it as usual (only UWP supports {x:Bind}):
<uwpControls:Expander x:Name="ExpanderLisSharedSettings" Grid.Row="0"
Background="{Binding TheBackground}" />

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

Content control, ContentTemplateSelector, how to choose a staticresource based on value comming through binding, without datatriggers

In a resource dictionary, I have few Viewboxes, and a datatemplate which has content control in it. I’m using style to switch between the viewboxes like this
<DataTemplate x:Key="foo">
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Viewbox}" Value="SageCashYes">
<Setter Property="Content" Value="{StaticResource SageCashYesViewbox}" />
</DataTrigger>
<DataTrigger Binding="{Binding Viewbox}" Value="SageCashNo">
<Setter Property="Content" Value="{StaticResource SageCashNoViewbox}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
Is there anyway to do something like this
<DataTemplate x:Key="foo">
<ContentControl ContentTemplateSelector="{Binding Viewbox}" >
</DataTemplate>
So I’d like to be able to tell it to choose appropriate Viewbox based on this binding but without these data triggers?
I’m asking it because the view boxes have vectors inside to draw images, and these data trigger will grow huge in no time. So if there is something out there which will make my life easier I’d like to know about it.
EDIT
<ListBox ItemTemplate="{StaticResource Foo}
ItemsSource="{Binding MyList}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
public class MyViewModel
{
public MyViewModel()
{
MyList = new List<MyList>()
{
new MyItem() {BoolValue = false, Heading = "Bank Payment", Viewbox = "SageCashNo"},
new MyItem() {BoolValue = true, Heading = "Cash Payment", Viewbox = "SageCashYes"}
};
}
public List<MyItem> MyList { get; set; }
}
public class MyItem
{
public bool {BoolValue { get; set; }
public string Heading { get; set; }
public string Viewbox { get; set; }
}
Kind Regards
Daniel

DatagridTemplateColumn with triggers failed to bind

I have a datagrid that contain of the 2 columns and I would like to show some of cells in column#2 to be:
ComboBox
TextBox
based on property
code :
Solution #1 :
<Window.Resources>
<DataTemplate x:Key="DropDownTemplate">
<StackPanel>
<ComboBox SelectedValuePath="Id" DisplayMemberPath="Name" ItemsSource="{Binding MarketConfigurationLOVs, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="TextBoxTemplate">
<StackPanel>
<TextBox Text="{Binding ConfigurationValue, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
Here the datagrid tags :
<DataGrid ItemsSource="{Binding Path= MarketConfigurationValues,Mode=TwoWay}" HeadersVisibility="None" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Width="120" Binding="{Binding Path= ConfigurationName}" />
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl Content="{Binding}" >
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding HasLOV}" Value="true">
<Setter Property="ContentTemplate" Value="{StaticResource DropDownTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding HasLOV}" Value="false">
<Setter Property="ContentTemplate" Value="{StaticResource TextBoxTemplate}" />
</DataTrigger>
<!-- and so on -->
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
this failed to show actual value as it show the name of property and always show cell in dropdown and when I swapped the TextBoxContentTemplate with ComboBoxContentTemplate it show all cells as textBox so it seem it ignore the trigger however when I debug I found the HasLOV some items contain true and some contain false
Solution #2 : (also failed) Reference : Jon solution from the following post WPF MVVM Creating Dynamic controls
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl x:Name="MyContentControl" Content="{Binding}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding HasLOV}" Value="false">
<Setter TargetName="MyContentControl" Property="ContentTemplate" Value="{StaticResource TextBoxTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding HasLOV}" Value="true">
<Setter TargetName="MyContentControl" Property="ContentTemplate" Value="{StaticResource DropDownTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
ViewModel is big class but I pulled here the main parts related to my problem . In constructor I created list of MarketConfigurationValue and I filled values inside MarketConfigurationLOVs in case if I set HasLov=true and I filled value in configurationValue in case if I set HasLov=false and I run the application and in debug mode I found the main object List contain what I said correctly but it failed to show comboBox in case HasLov=true and textBox in case HasLov=false :
public class MarketConfigurationValue
{
public string ConfigurationName { get; set; }
public string ConfigurationValue { get; set; }
public int ConfigurationValueId { get; set; }
public List<Market_Config_Lov> MarketConfigurationLOVs {get;set;}
public bool HasLov { get; set; }
}
public class Market_Config_Lov
{
public virtual string Name { get; set; }
public virtual int Sequence { get; set; }
}
Can you please help ?
Here's how you implement the INotifyPropertyChanged Interface. With this the binding updates itself when the setter of HasLOV is called.
public class MarketConfigurationValue:INotifyPropertyChanged
{
private bool _hasLOV;
public bool HasLOV
{
get {return _hasLOV;}
set {_hasLOV = value; OnPropertyChanged("HasLOV");}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
Hope this helps.
====>The property name is typo as the property in class called : HasLov and in XAML called HasLOV .

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