I have created a small application as a prototype of a larger application I need to build to test some of the functionality of nesting one TabControl within another. For the most part, things seems to work very well. However, I am encountering some strange behavior that I have spent days trying to resolve unsuccessfully. Here's a description of my app:
The outer TabControl is bound to an ObservableCollection of product groups, so I have a tab for each group. Selecting any one of these tabs is supposed to display as its content another TabControl which is bound to an ObservableCollection of products that belong to that group. Selecting any one of these tabs is supposed to display product-specific information and allow the user to select from various configuration options that apply to that product using a ComboBox. Up to this point, things seem to work just fine - all bindings seems to be doing what they are supposed to do.
Now, I would like each group tab to keep track of the last selected product on that tab, and each product tab to keep track of the last selected configuration on that tab, so if I change to another group or product tab, then come back to the original tab, the same product is still selected and the selected configuration is also still selected. Each group tab should function this way. So I added a SelectedProduct property to my Group class and bound it to the SelectedItem property of the product TabControl, and added a SelectedConfiguration property to my Product class and bound it to the SelectedItem property of the ComboBox on the product tab.
This is where the unexpected behavior begins. When I run the app, the first group tab is selected, and the first product tab in that group is also selected as expected and desired. However, when I click another group tab, none of the products are selected on that tab (i.e. none of the tabs have the appearance of being selected) although the content of the product tab indicates the first tab is actually selected. If I select another product tab, the correct product information is displayed, but if I select another group tab, then come back to the previous group tab again, the product selection has changed and the selected configuration has been lost.
I'm pretty sure this is all related to the order in which bindings occur and events are raised, but I have not been able to succeed in getting it to work as desired. Maybe what I am trying to accomplish is not possible using only XAML and I need to add more code behind for the SelectionChanged events of the TabControls? Suggestions?
Here is my complete project code combined into a single file and simplified as much as I can. I've wired the SelectionChanged event of each TabControl just to try to see what is happening.
Interfaces and Classes:
Window XAML:
And the Code-Behind:
I've attached the images also. They should be placed in the \Resources\Images folder of the project.
The outer TabControl is bound to an ObservableCollection of product groups, so I have a tab for each group. Selecting any one of these tabs is supposed to display as its content another TabControl which is bound to an ObservableCollection of products that belong to that group. Selecting any one of these tabs is supposed to display product-specific information and allow the user to select from various configuration options that apply to that product using a ComboBox. Up to this point, things seem to work just fine - all bindings seems to be doing what they are supposed to do.
Now, I would like each group tab to keep track of the last selected product on that tab, and each product tab to keep track of the last selected configuration on that tab, so if I change to another group or product tab, then come back to the original tab, the same product is still selected and the selected configuration is also still selected. Each group tab should function this way. So I added a SelectedProduct property to my Group class and bound it to the SelectedItem property of the product TabControl, and added a SelectedConfiguration property to my Product class and bound it to the SelectedItem property of the ComboBox on the product tab.
This is where the unexpected behavior begins. When I run the app, the first group tab is selected, and the first product tab in that group is also selected as expected and desired. However, when I click another group tab, none of the products are selected on that tab (i.e. none of the tabs have the appearance of being selected) although the content of the product tab indicates the first tab is actually selected. If I select another product tab, the correct product information is displayed, but if I select another group tab, then come back to the previous group tab again, the product selection has changed and the selected configuration has been lost.
I'm pretty sure this is all related to the order in which bindings occur and events are raised, but I have not been able to succeed in getting it to work as desired. Maybe what I am trying to accomplish is not possible using only XAML and I need to add more code behind for the SelectionChanged events of the TabControls? Suggestions?
Here is my complete project code combined into a single file and simplified as much as I can. I've wired the SelectionChanged event of each TabControl just to try to see what is happening.
Interfaces and Classes:
using System; |
using System.Collections.ObjectModel; |
using System.Linq; |
using System.Text; |
using System.Windows.Media.Imaging; |
namespace NestedTabControlBinding |
{ |
public interface IGroup |
{ |
string Name { get; set; } |
BitmapImage Image { get; set; } |
ObservableCollection<IProduct> Products { get; } |
IProduct SelectedProduct { get; set; } |
} |
public interface IProduct |
{ |
string BaseModel { get; set; } |
BitmapImage Image { get; set; } |
ObservableCollection<IConfiguration> Configurations { get; } |
IConfiguration SelectedConfiguration { get; set; } |
} |
public interface IConfiguration |
{ |
string Name { get; set; } |
string Voltage { get; set; } |
string Frequency { get; set; } |
} |
public class Group : IGroup |
{ |
private const string _imagePath = "pack://application:,,,/NestedTabControlBinding;Component/Resources/Images/{0}"; |
private string _name; |
BitmapImage _image; |
private ObservableCollection<IProduct> _products = new ObservableCollection<IProduct>(); |
private IProduct _selectedProduct; |
public Group(string name, string path) |
{ |
Name = name; |
Image = new BitmapImage(new Uri(path)); |
Products.Add(new CMyProduct(Name + " Model 1", string.Format(_imagePath, "Model1.jpg"))); |
Products.Add(new CMyProduct(Name + " Model 2", string.Format(_imagePath, "Model2.jpg"))); |
Products.Add(new CMyProduct(Name + " Model 3", string.Format(_imagePath, "Model3.jpg"))); |
} |
#region IGroup Members |
public string Name |
{ |
get { return _name; } |
set { _name = value; } |
} |
public BitmapImage Image |
{ |
get { return _image; } |
set { _image = value; } |
} |
public ObservableCollection<IProduct> Products |
{ |
get { return _products; } |
} |
public IProduct SelectedProduct |
{ |
get { return _selectedProduct; } |
set { _selectedProduct = value; } |
} |
#endregion |
} |
public abstract class ProductBase : IProduct |
{ |
private string _baseModel; |
BitmapImage _image; |
private ObservableCollection<IConfiguration> _configurations = new ObservableCollection<IConfiguration>(); |
private IConfiguration _selectedConfiguration; |
public ProductBase(string baseModel, string path) |
{ |
BaseModel = baseModel; |
Image = new BitmapImage(new Uri(path)); |
} |
#region IProduct Members |
public string BaseModel |
{ |
get { return _baseModel; } |
set { _baseModel = value; } |
} |
public BitmapImage Image |
{ |
get { return _image; } |
set { _image = value; } |
} |
public abstract string Description { get; } |
public System.Collections.ObjectModel.ObservableCollection<IConfiguration> Configurations |
{ |
get { return _configurations; } |
} |
public IConfiguration SelectedConfiguration |
{ |
get { return _selectedConfiguration; } |
set { _selectedConfiguration = value; } |
} |
#endregion |
} |
public class Configuration : IConfiguration |
{ |
private string _Name; |
private string _Voltage; |
private string _Frequency; |
public Configuration(string name, string voltage, string frequency) |
{ |
Name = name; |
Voltage = voltage; |
Frequency = frequency; |
} |
#region IConfiguration Members |
public string Name |
{ |
get { return _Name; } |
set { _Name = value; } |
} |
public string Voltage |
{ |
get { return _Voltage; } |
set { _Voltage = value; } |
} |
public string Frequency |
{ |
get { return _Frequency; } |
set { _Frequency = value; } |
} |
#endregion |
} |
public class CMyProduct : ProductBase |
{ |
private const string _description = "This product has a base model of "; |
public CMyProduct(string baseModel, string imagePath) |
: base(baseModel, imagePath) |
{ |
Configurations.Add(new Configuration(BaseModel + "-1", "110VAC", "50Hz")); |
Configurations.Add(new Configuration(BaseModel + "-2", "220VAC", "60Hz")); |
Configurations.Add(new Configuration(BaseModel + "-3", "110VAC", "50Hz")); |
Configurations.Add(new Configuration(BaseModel + "-4", "220VAC", "50Hz")); |
Configurations.Add(new Configuration(BaseModel + "-5", "110VAC", "60Hz")); |
} |
public override string Description |
{ |
get { return _description + BaseModel + "."; } |
} |
} |
public class GroupsViewModel |
{ |
private const string _imagePath = "pack://application:,,,/NestedTabControlBinding;Component/Resources/Images/{0}"; |
private ObservableCollection<IGroup> _groups = new ObservableCollection<IGroup>(); |
private IGroup _selectedGroup; |
public GroupsViewModel() |
{ |
Groups.Add(new Group("MetrologyWells", string.Format(_imagePath,"MetWell.jpg"))); |
Groups.Add(new Group("DryWells", string.Format(_imagePath, "DryWell.jpg"))); |
Groups.Add(new Group("Baths", string.Format(_imagePath, "Bath.jpg"))); |
} |
public ObservableCollection<IGroup> Groups |
{ |
get { return _groups; } |
} |
public IGroup SelectedGroup |
{ |
get { return _selectedGroup; } |
set { _selectedGroup = value; } |
} |
} |
} |
Window XAML:
<Window x:Class="NestedTabControlBinding.Window1" |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
xmlns:diagnostics="clr-namespace:System.Diagnostics;assembly=WindowsBase" |
xmlns:vm="clr-namespace:NestedTabControlBinding" |
xmlns:telerikNavigation="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.Navigation" |
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" |
Title="Nested TabControl Binding Example" Height="480" Width="800"> |
<Window.Resources> |
<vm:GroupsViewModel x:Key="GroupsDataSource"/> |
</Window.Resources> |
<Grid x:Name="grid" DataContext="{StaticResource GroupsDataSource}"> |
<Grid.RowDefinitions> |
<RowDefinition Height="40" /> |
<RowDefinition Height="385*" /> |
<RowDefinition Height="100*" /> |
</Grid.RowDefinitions> |
<StackPanel Grid.Row="1" Orientation="Vertical" > |
<telerikNavigation:RadTabControl x:Name="tabGroups" MinWidth="630" MinHeight="200" MaxWidth="1024" MaxHeight="600" Margin="5" TabStripPlacement="Top" |
VerticalAlignment="Stretch" HorizontalAlignment="Stretch" telerik:StyleManager.Theme="Vista" |
SelectedItem="{Binding Path=SelectedGroup, Mode=TwoWay}" |
SelectedIndex="{Binding Path=SelectedIndex, Mode=TwoWay}" |
ItemsSource="{Binding Path=Groups}" |
SelectionChanged="tabGroups_SelectionChanged"> |
<telerikNavigation:RadTabControl.ItemContainerStyle> |
<Style TargetType="telerikNavigation:RadTabItem"> |
<Setter Property="HeaderTemplate"> |
<Setter.Value> |
<DataTemplate> |
<StackPanel Orientation="Horizontal"> |
<Image Source="{Binding Path=Image}" Margin="3" Height="32" Width="32"/> |
<TextBlock Text="{Binding Path=Name}" Foreground="Black" Margin="3" VerticalAlignment="Center"/> |
</StackPanel> |
</DataTemplate> |
</Setter.Value> |
</Setter> |
<Setter Property="ContentTemplate"> |
<Setter.Value> |
<DataTemplate> |
<StackPanel Orientation="Vertical"> |
<TextBlock Margin="10" FontSize="14" FontWeight="Bold">Choose a product from the tabs below:</TextBlock> |
<telerikNavigation:RadTabControl x:Name="tabProducts" MinWidth="620" MinHeight="200" Margin="5" TabStripPlacement="Left" |
VerticalAlignment="Stretch" HorizontalAlignment="Stretch" telerik:StyleManager.Theme="Vista" |
SelectedItem="{Binding Path=SelectedProduct, Mode=TwoWay}" |
SelectedIndex="{Binding Path=SelectedIndex, Mode=TwoWay}" |
ItemsSource="{Binding Path=Products}" |
SelectionChanged="tabProducts_SelectionChanged"> |
<telerikNavigation:RadTabControl.ItemContainerStyle> |
<Style TargetType="telerikNavigation:RadTabItem"> |
<Setter Property="HeaderTemplate"> |
<Setter.Value> |
<DataTemplate> |
<StackPanel Orientation="Vertical" > |
<TextBlock Text="{Binding Path=BaseModel}" Margin="3" VerticalAlignment="Center"/> |
<Image Source="{Binding Path=Image}" Margin="3" Height="32" Width="32"/> |
</StackPanel> |
</DataTemplate> |
</Setter.Value> |
</Setter> |
<Setter Property="ContentTemplate"> |
<Setter.Value> |
<DataTemplate> |
<StackPanel Margin="5" Orientation="Vertical"> |
<StackPanel Orientation="Horizontal" > |
<TextBlock Margin="2" FontWeight="Bold" FontSize="14" Text="Model"/> |
<TextBlock Margin="2" FontWeight="Bold" FontSize="14" Text="{Binding Path=BaseModel}"/> |
<TextBlock Margin="2" FontWeight="Bold" FontSize="14" Text="configurations: "/> |
<ComboBox Margin="5" MinWidth="150" ItemsSource="{Binding Path=Configurations}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedConfiguration}"></ComboBox> |
</StackPanel> |
<StackPanel Orientation="Horizontal"> |
<TextBlock Margin="4" Text="Voltage: "/> |
<TextBlock Margin="4" FontWeight="Bold" Text="{Binding Path=SelectedConfiguration.Voltage}" MinWidth="70"/> |
<TextBlock Margin="4" Text="Frequency: "/> |
<TextBlock Margin="4" FontWeight="Bold" Text="{Binding Path=SelectedConfiguration.Frequency}" MinWidth="70"/> |
</StackPanel> |
<TextBlock Margin="10" Text="{Binding Path=Description}"/> |
</StackPanel> |
</DataTemplate> |
</Setter.Value> |
</Setter> |
</Style> |
</telerikNavigation:RadTabControl.ItemContainerStyle> |
</telerikNavigation:RadTabControl> |
</StackPanel> |
</DataTemplate> |
</Setter.Value> |
</Setter> |
</Style> |
</telerikNavigation:RadTabControl.ItemContainerStyle> |
</telerikNavigation:RadTabControl> |
</StackPanel> |
</Grid> |
</Window> |
And the Code-Behind:
using System; |
using System.Collections.Generic; |
using System.Linq; |
using System.Text; |
using System.Windows; |
using System.Windows.Controls; |
using System.Windows.Data; |
using System.Windows.Documents; |
using System.Windows.Input; |
using System.Windows.Media; |
using System.Windows.Media.Imaging; |
using System.Windows.Navigation; |
using System.Windows.Shapes; |
using Telerik.Windows.Controls; |
namespace NestedTabControlBinding |
{ |
/// <summary> |
/// Interaction logic for Window1.xaml |
/// </summary> |
public partial class Window1 : Window |
{ |
public Window1() |
{ |
InitializeComponent(); |
} |
private void tabGroups_SelectionChanged(object sender, RoutedEventArgs e) |
{ |
RadTabControl Tab = (RadTabControl)sender; |
if (Tab.SelectedItem != null) |
{ |
IGroup Group = (IGroup)Tab.SelectedItem; |
Console.WriteLine("Selected group: " + Group.Name); |
} |
else |
{ |
Console.WriteLine("Selected group: NONE"); |
} |
} |
private void tabProducts_SelectionChanged(object sender, RoutedEventArgs e) |
{ |
RadTabControl Tab = (RadTabControl)sender; |
if (Tab.SelectedItem != null) |
{ |
IProduct Product = (IProduct)Tab.SelectedItem; |
Console.WriteLine("Selected product: " + Product.BaseModel); |
} |
else |
{ |
Console.WriteLine("Selected product: NONE"); |
} |
} |
} |
} |
I've attached the images also. They should be placed in the \Resources\Images folder of the project.