Binding TabControl.ItemsSource to ViewModels whose View's are a TabItem - wpf

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.

Related

How can I create a separate UserControl instance for each TabItem in a TabControl, using XAML?

I'm creating a WPF Application where you can create a soundtrack.
Each soundtrack has Albums, which contain Tracks, etc etc.
I've separated the Album creation interface into its own UserControl, and then created the TabControl in the main window (henceforth referred to as The Editor)
In the main Project class, there is an ObservableCollection of Albums, and Albums have an ObservableCollection of Tracks, along with other properties.
My plan of action is to bind the TabControl to the Album ObservableCollection, so that when a new album is made, it also makes a new Tab for it. This part works so far. I can see a new tab when a new album is made, I don't have to explicitly create a new TabItem and add it myself to the TabControl.
This is the TabControl:
<TabControl x:Name="AlbumTabs" SelectionChanged="TabControl_SelectionChanged" TabStripPlacement="Left" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding CurrentProject.AlbumCollection}">
<TabControl.Resources>
<DataTemplate x:Key="Album_UserControl" x:Shared="False">
<local:Soundtrack_Album/>
</DataTemplate>
</TabControl.Resources>
<TabControl.ContextMenu>
<ContextMenu>
<MenuItem Header="Add Album" Click="contextAddAlbum_Click" />
</ContextMenu>
</TabControl.ContextMenu>
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="LayoutTransform">
<Setter.Value>
<RotateTransform Angle="270"/>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="Album"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<local:Soundtrack_Album/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
When I create the tabs however, they all seem to be using the same instance of the UserControl. I select one tab, change some fields, select a different tab, and the fields are still the same. Shouldn't the content change when I select a different tab?
I'm also noticing that the Items of the TabControl are actually Albums, and not TabItems. I'm guessing this isn't correct, as my next problem will be trying to set the DataContext of the newly-created TabItem to the newly-created Album.
Thanks for your help.

How to retrieve VisualChild from ControlTemplate

I have a WPF application in .NET 3.5 SP1 which is using TabControl.
In that we have TabItems, which in turn have their Styles to determine currently displayed items.
Let's say we have a TabItem named Books, now Books will have three stages of display:
1. Loading results,
2. Displaying results,
3. Displaying no results - i.e. nothing found.
<TabControl>
<TabItem Header="Books"/>
<TabItem Header="DVD's"/>
...
</TavControl>
Now I have 5 TabItems which let's say represent "DVD's", "Blu-Rays", "CD's", "Books" and "Comics".
<TabItem Header="Books">
<Control>
<Control.Resources>
<Style TargetType="Control">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<ListView ItemsSource="{Binding Books}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<!-- Assign different Visuals depending on the current state of the app, i.e. Loading, No results, results found
<DataTrigger .../>
</Style.Triggers>
</Style>
</Control.Resources>
</Control>
</TabItem>
Underneath the TabItem I have a TextBlock to display number of currently found results:
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="Displaying {0} records for your Shop ({1})" Converter="{StaticResource tstMVC}">
<Binding ElementName="Tc" Path="SelectedValue"/>
<Binding Path="ShopId" FallbackValue="Liverpool"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Converter is there for me to check what values are passed in the MultiBinding.
ISSUE:
When user selects a tab item I would like to display current number of the items displayed but I can't locate the ListView in the Control, as that's the Current Content of the TabItem.
I have tried TabControl.SelectedItem, SelectedValue and still can't find Current ItemsSource.Count.
Thanks in advance
UPDATE:
I have tried both of the solutions big thanks to #Sheridan and #pushpraj!
Unfortunately I haven't used either of them, instead I used ListView inside of the TabItem and then accessed it with this code:
<TabControl Name="Tc">
<TabItem><ListView ItemsSource="{Binding Books}"/></TabItem>
...
</TabControl>
<TextBlock Text="{Binding ElementName=Tc, Path=SelectedItem.Content.Items.Count}"/>
This way Content of my TextBlock changes every time user selects different Tab.
P.S. Nevertheless I wouldn't have done it without evaluation of both answers.
if you have separate collection classes for all you entities you may use the following approach
define data templates for your collection classes
eg
<DataTemplate DataType="{x:Type l:BookCollection}">
<ListView ItemsSource="{Binding}" />
</DataTemplate>
xaml
<TabControl x:Name="Tc">
<TabItem Header="Books"
Content="{Binding Books}" />
<TabItem Header="DVD's"
Content="{Binding DVDs}" />
</TabControl>
or if you do not have separate collections then use DataTemplate as follows
<TabControl x:Name="Tc">
<TabControl.ItemTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabItem Header="Books"
Content="{Binding Books}" />
<TabItem Header="DVD's"
Content="{Binding DVDs}" />
</TabControl>
then the binding to get the selected tab's items count will be
<TextBlock Text="{Binding SelectedItem.Content.Count, ElementName=Tc}" />
In WPF, we generally work with data elements rather than UI elements. By that, I mean that it is customary to data bind data elements from our code behind or view models to the UI elements in the views, UserControls and Windows.
Therefore, if you have a data collection property named Books in your code behind or view model, then you can simply refer to that collection to find out how many items are in it, rather than trying to find it via the UI controls:
<ListView ItemsSource="{Binding Books}" />
You could even expose the count of items as a separate property and data bind to it directly:
public ObservableCollection<Book> Books
{
get { return books; }
set
{
books = value;
NotifyPropertyChanged("Books");
NotifyPropertyChanged("BookCount");
}
}
public int BookCount
{
get { return Books.Count; }
}
UPDATE >>>
In response to your latest comment, you can find out how to access UI elements from within a ControlTemplate from the How to: Find ControlTemplate-Generated Elements page on MSDN. In short though, you need to access the element that has the ControlTemplate applied (the relevant TabItem in your case) and then you can use the FrameworkTemplate.FindName Method to find the internally declared elements. Take this example from the linked page:
// Finding the grid that is generated by the ControlTemplate of the Button
Grid gridInTemplate = (Grid)myButton1.Template.FindName("grid", myButton1);
// Do something to the ControlTemplate-generated grid
MessageBox.Show("The actual width of the grid in the ControlTemplate: "
+ gridInTemplate.GetValue(Grid.ActualWidthProperty).ToString());

Caliburn micro and tabcontrol

I have looked at most resources but i can find a good solution. I have a tab control HARD coded.
<TabControl TabStripPlacement="Left" Padding="0" Style="{DynamicResource SettingsTab}" ItemContainerStyle="{DynamicResource SettingsTabItemStyle}" Background="WhiteSmoke" >
<TabItem Header="ΓΕΝΙΚΑ" Margin="0" IsEnabled="False" > <Grid /></TabItem>
<TabItem Header="Προσωπικό" Margin="0" IsSelected="True">
<Grid MinHeight="400">
<ContentControl HorizontalAlignment="Stretch" Margin="50,67,50,0" Name="ActiveItem" />
</Grid>
</TabItem>
<TabItem Header="Τραπέζια" Margin="0">
<Grid />
</TabItem>
UPDATE - Restate problem
Here is my customized tab control. The gray text is a disabled tab item it acts like a group. Like General settings, System settings etc. So it has a role as a navigation menu. Curently i have a content control at each tabitem (not the disabled ones) and bind the view model i want.
But i cant use Conductor.Collection.OneActive with CM.
Why?
I have seen helloscreens example from CM sample and other samples but the problem here is that if i do this via binding then there is no way to display the disabled tabitems other than creating a dummy view model that serves no purpose.So how can i achieve this ?
Update: source here
I have a tab control HARD coded.
Is this a requirement?
But i cant use Conductor.Collection.OneActive with CM. Why?
I think the reason this won't work is to use Conductor.Collection.OneActive you need to bind the ItemsSource to the Items collection. If you're going to bind ItemsSource you can't also describe the tab items in XAML.
I created a solution that doesn't involve describing the tab items in xaml.
The key parts:
In ShellView the TabControl ItemContainerStyle is described to the tab items IsEnabled property can be bound to the view model.
<TabControl x:Name="Items"
Grid.Row="1"
TabStripPlacement="Left">
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
ShellViewModel is similar but we're now activating the second view model because the first one is not enabled.
public class HeaderViewModel : BaseTabViewModel
{
public HeaderViewModel(string name)
{
DisplayName = name;
IsEnabled = false;
}
}
public ShellViewModel(Tab2ViewModel tab2ViewModel,
Tab3ViewModel tab3ViewModel
)
{
Items.Add (new HeaderViewModel ("ΓΕΝΙΚΑ"));
Items.Add(tab2ViewModel);
Items.Add(tab3ViewModel);
ActivateItem (tab2ViewModel);
}
Enable or disable the tab item in the constructor of the view model.
public Tab2ViewModel()
{
DisplayName = "Προσωπικό";
IsEnabled = true;
}
The un-styled result is that the first tab item is disabled and the next two are enabled.

Enable a TabItem via Binding

I want to use MVVM in an application where the different pages are TabItems.
For this I use an observable collection of my view models (Items) and bind it to the tabcontrols ItemSource.
For each view model, I created an individual data template to render the correct view like this:
<DataTemplate DataType="{x:Type baseVm:AViewModel}">
<baseVw:AView />
</DataTemplate>
To display the correct name in the tab's header I created another data template to be applied to each of the tab control's elements:
<DataTemplate x:Key="ViewModelTabTemplate">
<DockPanel>
<ContentPresenter Content="{Binding Path=Name}"/>
</DockPanel>
</DataTemplate>
The tab control looks like this:
<TabControl x:Name="myTabControl"
ItemsSource="{Binding Items}"
ItemTemplate="{DynamicResource ViewModelTabTemplate}">
</TabControl>
What I want to do now is to enable/disable tabs from within the view model that contains the collection. The view model's base class contains a dependency property IsEnabled and I would like to bind this to the views. If I do this directly in the view like this:
IsEnabled="{Binding IsEnabled, FallbackValue=true}"
the tab page's content gets disabled when I turn the IsEnabled property to false. But what I really want is to also disable the tabpage's tab and not just the content.
Thanks for any help!
Maybe you could try something like this -
<TabControl>
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>

How to bind to Tabcontrol.Items

I have a WPF application that I'm trying to dynamically add items to a tabcontrol. I have a list of menu items that should be databound to the tabcontrol's items. The only problem is that TabControl.Items does not notify others that items have been added. I've tested this by binding instead to TabControl.Items.Count and get calls to the converter (but the value passed in is the count and not something useful). Here's the relevent code that doesn't get databound properly because Items doesn't call out updates:
<MenuItem ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}, Path=Items, Converter={StaticResource TabControlItemConverter}}">
This MenuItem XAML is inside a ControlTemplate for a TabControl. With static items, i.e., items that are already defined in a TabControl, this code works perfectly. But I have a TabControl that gets items added at runtime and can't seem to update this binding. Has anyone added some sort of attached property to a TabControl that can bind to the Items collection?
Edit for background info
The TabControl that has items added to it is a region (this is a Prism application). Here is the relevent XAML
<TabControl cal:RegionManager.RegionName="{x:Static local:LocalRegionNames.SelectedItemRegion}" >
<TabControl.Resources>
<Style TargetType="TabItem" BasedOn="{StaticResource TabItemStyle}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Style="{StaticResource tabItemImage}" Height="20" />
<TextBlock Text="{Binding Content.DataContext.TabHeader, RelativeSource={RelativeSource AncestorType=TabItem}}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>
</TabControl>
The relevent code for adding a view to the region is here:
ProjectDetailView view = new ProjectDetailView();
ProjectDetailViewModel viewModel = new ProjectDetailViewModel();
viewModel.CurrentProject = project;
view.DataContext = viewModel;
IRegionManager retManager = RegionManager.Regions[LocalRegionNames.SelectedItemRegion].Add(view, null, true);
RegionManager.Regions[LocalRegionNames.SelectedItemRegion].Activate(view);
All this works fine...views get added, the tab control adds items, and views appear. But the Items property on the tabcontrol never broadcasts the changes to its collection.
You do the same thing for TabControls, you bind the ItemsSource, the only thing you need to take into account is that the source collection should implement INotifyCollectionChanged if you want it updated if items are added. ObservableCollection<T> already implements the interface and is often used as source for such bindings.

Resources