I need some TabItems to have a customized Header.
For example, given the following (working) XAML:
<TabControl>
<TabItem>
<TabItem.Header>
<Button>Header 1</Button>
</TabItem.Header>
<Label>Content 1</Label>
</TabItem>
<TabItem>
<TabItem.Header>
<Label>Header 2</Label>
</TabItem.Header>
<Grid>
<TextBlock>Content 2</TextBlock>
</Grid>
</TabItem>
</TabControl>
I would like to extract the tab items into their own Views + ViewModels. The TabItem's View should still be a TabItem, so that I can configure the Header per tab item instead of setting TabControl.ItemTemplate and using a DataTemplateSelector to achieve different headers per tab item.
At the same time I'd need to be able to bind the selected tab item view model to a property ActiveItem. => The underlying view-model for the TabControl is a Conductor.Collection.OneActive<T> (only the selected tab should be activated).
If there's an alternative to using TabItem as view-type, but still achieving the Header and Content to be specified in the same view, it would be acceptable, too.
You should be able to achieve this by binding a TabControlViewModel to the TabControl, and that VM should have an ObservableCollection of TabViewModels (maybe a base class or interface). You would bind your collection of TabViewModels to the TabControl's ItemsSource. Here is my implementation, but using the Telerik TabControl (should be same for MS):
<telerik:RadTabControl x:Name="RadTabControl"
Grid.Row="0"
Align="Justify"
ContentTemplateSelector="{StaticResource LoggerDataTemplateSelector}"
IsContentPreserved="True"
IsDefaultItemSelected="True"
ItemsSource="{Binding LogHistory}"
SupressSelectedContentTemplateReapplying="False">
<telerik:RadTabControl.ItemContainerStyle>
<!-- Allow IsSelected to be bound to view models-->
<Style BasedOn="{StaticResource RadTabItemStyle}"
TargetType="{x:Type telerik:RadTabItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</telerik:RadTabControl.ItemContainerStyle>
<telerik:RadTabControl.ItemTemplate>
<!-- Define what is shown in the header -->
<DataTemplate>
<Grid Height="30">
<TextBlock VerticalAlignment="Center"
Text="{Binding Title}" />
</Grid>
</DataTemplate>
</telerik:RadTabControl.ItemTemplate>
</telerik:RadTabControl>
Be aware, MS doesn't have something like IsContentPreserved, so switching tabs that have a lot of data to show will be rather timely. There are also a couple other properties not in MS TabControl, however the important properties should all be there. In this case, your TabViewModel should have a IsSelected property and Title property.
I have the following Tabcontrol definition:
<TabControl Grid.Row="0" SelectedItem="{Binding SelectedTab}" >
<TabItem Header="Foo" IsSelected="True" >
One
</TabItem>
<TabItem Header="Bar">
Two
</TabItem>
<TabItem Header="Smeh">
Three
</TabItem>
</TabControl>
I would expect the "Foo" tab to be selected and the 'One' text to be visible. This is not the behaviour I'm seeing. It appears all tabs are unselected and until I click on a different tab (clicking on the one that is supposedly already selected does nothing), then the tab looks selected and shows content.
Does anyone know why the TabControl works like this and how to fix it to work correctly?
By default there is no selected item in the TabControl. If you want to synchronize some model with the selected item, try
<TabControl Grid.Row="0" SelectedItem="{Binding SelectedTab, Mode=OneWayToSource}" >
How to set the background of TabItem? I tried the following code:
<TabControl>
<TabItem Header="Test" Background="Blue" Foreground="Red" />
</TabControl>
Foreground works, but Background does not work.
Any ideas? Thanks
What is happening is that in the case of a single tab, it is always selected, and so you are only seeing the selection style of the tab item.
For example, take a look at the following TabControl:
<TabControl>
<TabItem Header="Tab A" Background="Blue" Foreground="Red">
<Grid />
</TabItem>
<TabItem Header="Tab B" Background="Green" Foreground="Navy" >
<Grid />
</TabItem>
<TabItem Header="Tab C" Background="LightBlue">
<Grid />
</TabItem>
</TabControl>
Tab A will not display its Blue background until you select a different tab. If you truly want the Background to remain the same regardless of whether it is selected or not, you will need to override the control template of the TabItem.
See the question TabItem Background color changes when tabitem selected or hover over for an example of how to do this.
I have a WPF window with a tab control, and I'm defining the TabItems in the XAML file, like:
<TabControl>
<TabItem Name="tab1" Tag="Transactions"/>
<TabItem Name="tab2" Tag="Promotions" />
...
</TabControl>
Elsewhere on the screen I have a textblock which I want to use to display the Tag value of the selected tab. It works when the screen is initially loaded, and whenever the "transactions" tab is selected, but when a different tab is selected, it's blank. Why is that, and how can I make it display the tag of any selected tab? Here is the TextBlock:
<TextBlock Text="{Binding ElementName=tabControl1, Path=SelectedItem.Tag}"/>
This works as expected for me. (You did set the name of the TabControl, right?)
Note that if the TabControl is populated via ItemsSource unlike your example code the SelectedItem will not contain the TabItem but the data-object from which the TabItem is created, so the binding path SelectedItem.Tag does not work.
Code used:
<!-- Both controls enclosed in a Stackpanel -->
<TabControl Name="tabControl1">
<TabItem Name="tab1" Tag="Transactions"/>
<TabItem Name="tab2" Tag="Promotions" />
</TabControl>
<TextBlock Text="{Binding ElementName=tabControl1, Path=SelectedItem.Tag}"/>
The tab headers are obviously going to be empty but they are selectable.
What i am really trying to achieve is to full control the active TabItem by using the combobox as the navigation control.
Here is what ive got so far:
<TabControl Canvas.Left="26" Canvas.Top="27" Height="100" Name="TabControl1" Width="220">
<TabItem Header="TabItem1" x:Name="TabItem1">
<Grid />
</TabItem>
<TabItem Header="TabItem2" x:Name="TabItem2">
<Grid />
</TabItem>
</TabControl>
<ComboBox Canvas.Left="126" Canvas.Top="134" Height="23" Name="CmbTabs" Width="120"
ItemsSource="{Binding ElementName=TabControl1, Path=Items}"
SelectedValue="{Binding ElementName=TabControl1, Path=SelectedIndex}"
SelectedValuePath="TabIndex"
DisplayMemberPath="Header"/>
Still the only thing that actual works is the list that shows up when i press the togglebutton of the combobox.
Even selecting a tabitem name through the list does not do anything, it does not even update the selected value textbox of the combobox.
Any help ?
Edit:
Ok the answer of Steve Robbins worked fine for the "controling" issue.
What about the fact that selecting an item in the combobox drop down list does not update the value of the combobox? (the comboboxes textbox is still blank!!)
If you're trying to control the TabControl from the Combo then it looks a bit backwards to me.. if you change the SelectedIndex on the tab control to bind it to the combo it should work:
<TabControl Canvas.Left="26" Canvas.Top="27" Height="100" Name="TabControl1" Width="220" SelectedIndex="{Binding ElementName=CmbTabs, Path=SelectedIndex}">
<TabItem Header="TabItem1" x:Name="TabItem1">
<Grid />
</TabItem>
<TabItem Header="TabItem2" x:Name="TabItem2">
<Grid />
</TabItem>
</TabControl>
I started looking at this because of some problems I'm having with the combobox. Unfortunately, I haven't solved my problem but I can provide some additional insight and a workaround to this problem. First off, lets start with my changes to the original xaml.
<TabControl Height="100" Name="TabControl1" Width="220">
<TabItem Header="TabItem1" x:Name="TabItem1">
<TextBlock Text="TabItem1 Content" />
</TabItem>
<TabItem Header="TabItem2" x:Name="TabItem2">
<TextBlock Text="TabItem2 Content" />
</TabItem>
</TabControl>
<ComboBox Height="23" Name="CmbTabs" Width="120"
ItemsSource="{Binding ElementName=TabControl1, Path=Items}"
SelectedIndex="{Binding ElementName=TabControl1, Path=SelectedIndex}"
DisplayMemberPath="Name"
>
</ComboBox>
Notice that instead of creating a binding from the tab control to the ComboBox and vice versa we can create a TwoWay binding (the default in this case) between the SelectedIndex of the tab and combo controls. Next, let's add some content to the TabItems. At this point, similar to Steve's suggestion, we've fixed the "controling" problem. That is, changing the selected TabItem changes the selected ComboBox item (you'll have to trust me on this one or keep reading!) and changing the ComboBox changes the selected TabItem. Great!
The above xaml also changes DiplayMemberPath property to "Name". I think you will find that this eliminates hughdbrown's "weird result". Recall that the Header property (an object) is wrapped by a ContentPresenter. I believe that if no template is supplied the default behavior is to display the Header object as a string in a TextBlock. Thus, the "weird result" correctly reports that the TextBlock control does not contain a Header property.
Now let's make some changes to the previous ComboBox xaml.
<ComboBox Height="23" Name="CmbTabs" Width="120"
ItemsSource="{Binding ElementName=TabControl1, Path=Items}"
SelectedIndex="{Binding ElementName=TabControl1, Path=SelectedIndex}"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
This actually produces an interesting result and makes use of the content we added to each TabItem. When this code runs, you'll notice that the ComboBox now displays the selected TabItem's content. Furthermore, the list now displays "System.Windows.Controls.TabItem..." for each TabItem. We can change the TextBlock binding to {Binding Header} and display the Header object but the ComboBox still displays the selected TabItem's content. As it is late on a Friday evening and there just is not enough beer in the world, I didn't look into possible reasons for this. However, I do have a workaround!
First, let's create a ValueConverter to convert the TabControl's Items collection to something we can use. Here's the code.
public class TabItemCollectionConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
ItemCollection collection = value as ItemCollection;
IList<string> names = new List<string>();
foreach (TabItem ti in collection.SourceCollection)
{
names.Add(ti.Header.ToString());
}
return names;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
#endregion
}
The converter simply creates a new collection from the TabControl's Items collection that contains the string-ized Header object from each TabItem. This works fine for simple Header objects but obviously has limitations. Now let's consider how we use this in the xaml.
<ComboBox Height="23" Name="CmbTabs" Width="120"
ItemsSource="{Binding ElementName=TabControl1, Path=Items, Converter={StaticResource ItemConverter}}"
SelectedIndex="{Binding ElementName=TabControl1, Path=SelectedIndex}"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Remember that a ValueConverter used in an ItemsSource binding returns a new collection. In this case we are converting the TabControl's Items collection to a string collection. Don't forget to create the converter StaticResource! It looks something like this.
<local:TabItemCollectionConverter x:Key="ItemConverter"/>
Now, using the converter, the whole ball of wax works as expected.
What still puzzles me is why the ComboBox displays the TabItem Headers in the list and the TabItem content as the selection. No doubt there is some obvious explanation but as I said, it's Friday...
Hope this helps!
I've been playing with this a lot. There is something about the source of the ComboBox's databinding changing after the selection is made. Or something.
I added the Diagnostics namespace:
xmlns:debug="clr-namespace:System.Diagnostics;assembly=WindowsBase"
And I changed your <Grid /> into TextBoxes with big honking numbers so that I could see that things really were changing:
<TabItem Header="TabItem1" x:Name="TabItem1">
<TextBlock Name="tb1" FontSize="24" Text="1" Width="100" Height="26" />
</TabItem>
And when I ran the app, I found that Diagnostics reports a weird result:
System.Windows.Data Error: 39 :
BindingExpression path error: 'Header'
property not found on 'object'
''TextBlock' (Name='tb1')'.
BindingExpression:Path=Header;
DataItem='TextBlock' (Name='tb2');
target element is 'TextBlock'
(Name=''); target property is 'Text'
(type 'String')
I tried to set the databinding once, in code, at startup:
public Window1()
{
InitializeComponent();
Binding positionBinding = new Binding("Items");
positionBinding.ElementName = "TabControl1";
positionBinding.Path = new PropertyPath("Items");
positionBinding.Mode = BindingMode.OneTime;
CmbTabs.SetBinding(ComboBox.ItemsSourceProperty, positionBinding);
CmbTabs.DisplayMemberPath = "Header";
}
And it still switches the CombobBox to show no selected item after the selection and change of TabItem is made. It's as if the DataContext is switched to the TabControl.TabItem.TextBlock after the TabControl changes selection.
So I don't exactly have an answer for you but I have some results for you to work on.
Bea Stollnitz has a good article on using this diagnostic technique. "How can I debug WPF bindings?"
Building off of hughdbrown's answer, I found this post on MSDN that describes your problem as a bug. You can reproduce it with this XAML (which has the opposite problem as your XAML):
<Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TabControl Name="TabControl1" Width="220" Height="100" Canvas.Left="26" Canvas.Top="27">
<TabItem x:Name="TabItem1" Header="TabItem1">
foobar
</TabItem>
<TabItem x:Name="TabItem2" Header="TabItem2">
fizzbuzz
</TabItem>
</TabControl>
<ComboBox Name="CmbTabs" Width="120" Height="23" Canvas.Left="126" Canvas.Top="134"
ItemsSource="{Binding ElementName=TabControl1, Path=Items}"
DisplayMemberPath="Length"
SelectedIndex="{Binding ElementName=TabControl1, Path=SelectedIndex}"/>
</Canvas>
As you can see the Length binding works fine except for in the dropdown, where it is going off the TabItem instead of the string inside.
I'm not sure it's ideal for your purposes, but you can get around it by being a little less elegant and reproducing your headers in ComboBoxItems:
<Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TabControl Name="TabControl1" Width="220" Height="100" Canvas.Left="26" Canvas.Top="27">
<TabItem x:Name="TabItem1" Header="TabItem1">
<Grid/>
</TabItem>
<TabItem x:Name="TabItem2" Header="TabItem2">
<Grid/>
</TabItem>
</TabControl>
<ComboBox Name="CmbTabs" Width="120" Height="23" Canvas.Left="126" Canvas.Top="134"
SelectedIndex="{Binding ElementName=TabControl1, Path=SelectedIndex}">
<ComboBoxItem>TabItem1</ComboBoxItem>
<ComboBoxItem>TabItem2</ComboBoxItem>
</ComboBox>
</Canvas>