2

Just when I thought I was getting better at this, TabControl is now giving me problems. I have read relevant posts here on StackOverflow, but have been unable to get my simple demo application to work the way I want it to.

To keep things focused, I'll start with a single question about something I don't understand.

I have a TabControl whose TabItems each host the same UserControl. When I set the TabControl.ContentTemplate's DataTemplate to my UserControl, a rendering of that control appears, but it looks like it's the same control for each tab. Or perhaps it's not tied to any of the tabs at all.

MainWindow.xaml

<Window x:Class="TabControlMvvm.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:localviews="clr-namespace:TabControlMvvm.Views"
    Title="MainWindow" Height="350" Width="525">
    <TabControl ItemsSource="{Binding Tabs}" SelectedIndex="{Binding Selected}">
        <TabControl.ContentTemplate>
            <DataTemplate>
               <localviews:PersonMainPanel />
            </DataTemplate>
        </TabControl.ContentTemplate>        
    </TabControl>
</Window>

Code-behind just sets the ViewModel as its DataContext:

namespace TabControlMvvm {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new TabControlMvvm.ViewModels.MainViewModel();
        }
    }
}

The TabItem's Content should be another UserControl, PersonMainPanel.xaml:

<UserControl x:Class="TabControlMvvm.Views.PersonMainPanel"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:localviews="clr-namespace:TabControlMvvm.Views"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Border BorderBrush="Red" BorderThickness="2">
        <TabControl TabStripPlacement="Bottom">
            <TabItem Header="Tab 1">
                <localviews:MyTabItem />
            </TabItem>
            <TabItem Header="Tab 2">
                <TextBlock Text="This was left blank intentionally" />
            </TabItem>
            <TabItem Header="Tab 3">
                <TextBlock Text="This was also left blank intentionally" />
            </TabItem>
        </TabControl>
    </Border>
</UserControl>    

Code-behind:

namespace TabControlMvvm.Views {
    /// <summary>
    /// Interaction logic for PersonMainPanel.xaml
    /// </summary>
    public partial class PersonMainPanel : UserControl {
        public PersonMainPanel()
        {
            InitializeComponent();
        }
    }
}

And the MainViewModel:

namespace TabControlMvvm.ViewModels {
    public class MainViewModel : ViewModelBase {
        public ICollectionView Tabs { get; set; }
        public int Selected { get; set; }

        public class Person
        {
            public string Name { get; set; }
        }

        public class DummyController {
            public List<Person> Persons { get; private set; }

            public DummyController()
            {
                Persons = new List<Person> {
                    new Person { Name = "Larry" },
                    new Person { Name = "Darryl" },
                    new Person { Name = "Other brother Darryl" }
                };
            }
        }

        public DummyController Controller { get; private set; }

        public RelayCommand HelloCommand { get; set; }

        public MainViewModel()
        {
            Controller = new DummyController();

            /*
            IEnumerable<TabItem> tabs = Enumerable.Range( 1, _controller.Persons.Count())
                                                  .Select( x => new TabItem { Header = String.Format( "Person {0}", x),
                                                                              Content = new PersonMainPanel() });
             */
            IEnumerable<TabItem> tabs = Enumerable.Range( 1, Controller.Persons.Count())
                                                  .Select( x => new TabItem { Header = String.Format( "Person {0}", x)});
            Tabs = CollectionViewSource.GetDefaultView( tabs.ToList());
            Tabs.MoveCurrentToFirst();

            InitializeCommands();
        }

        private void InitializeCommands()
        {
            HelloCommand = new RelayCommand( () => { MessageBox.Show( String.Format( "Hello, Person {0} named {1}!", 
                                                                      Selected, Controller.Persons[Selected].Name)); });
        }
    }
}

PersonMainPanel hosts another TabControl, where Tab 1's Content is MyTabItem.xaml:

<UserControl x:Class="TabControlMvvm.Views.MyTabItem"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <StackPanel Orientation="Vertical">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Name:" />
            <TextBox Text="{Binding Name}" Width="100" />            
        </StackPanel>
        <Button Command="{Binding HelloCommand}" Content="Say Hello" />
    </StackPanel>
</UserControl>

Code-behind:

namespace TabControlMvvm.Views {
    /// <summary>
    /// Interaction logic for MyTabItem.xaml
    /// </summary>
    public partial class MyTabItem : UserControl {
        public MyTabItem()
        {
            InitializeComponent();
        }
    }
}

Which looks like this at runtime:

enter image description here

Issues I have so far:

  1. When I enter Person 1's Name and then click the Person 2 tab, Person 1's Name is still visible, hence my assumption that the controls are not databound properly. I understand that ItemsControls do not pass their DataContext down to their children, but I am not sure how to fix this without associating the View in code-behind.
  2. I would have expected to get databinding errors in the Output window because of the missing DataContext, but I don't get any errors. I assume the DataContext is null, but wouldn't this still result in a binding error?
  3. How can I use Snoop effectively to debug problems like this?

Here's the sample solution: http://www.filedropper.com/tabcontrolmvvm

Dave
  • 14,618
  • 13
  • 91
  • 145
  • Maybe helpful: https://stackoverflow.com/a/1870658/1136211 – Clemens Sep 14 '17 at 18:48
  • How is your MainViewModel look? – sTrenat Sep 14 '17 at 19:01
  • Oops, forgot the obvious! Edited. Also, @Clemens one takeaway from your linked post is that the backing collection shouldn't of TabItems, it should probably be a ViewModel for MyTabItem. And the XAML which specifies the DataTemplate should be good as-is. I'll try that now in case that was my problem all along! – Dave Sep 14 '17 at 19:03
  • Please add your solution in an answer not in the question (remove it from question) See: [Ask questions, get answers, no distractions](//stackoverflow.com/tour), I will suggest Clements post as duplicate if you think the solution is in that post, just accept it. – Petter Friberg Sep 14 '17 at 19:23
  • Will do once I confirm that everything is solved 100% – Dave Sep 14 '17 at 19:31
  • Possible duplicate of [TabControl.ItemTemplate: set TabItem.Header.Text to a MultiBinding with StringFormat](https://stackoverflow.com/questions/1870564/tabcontrol-itemtemplate-set-tabitem-header-text-to-a-multibinding-with-stringfo) – Hussein El Feky Sep 14 '17 at 22:11

1 Answers1

2

Here is solution:

In MainWindow modify your TabControl template, to bind Header from your Model:

<TabControl ItemsSource="{Binding Tabs}" SelectedIndex="{Binding Selected}">
    <TabControl.ContentTemplate>
        <DataTemplate>
            <localviews:PersonMainPanel />
        </DataTemplate>
    </TabControl.ContentTemplate>
    <TabControl.ItemContainerStyle>
        <Style TargetType="TabItem">
            <Setter Property="Header" Value="{Binding Header}"/>
        </Style>
    </TabControl.ItemContainerStyle>
</TabControl>

In MyTabItem.xaml, set UpdateTrigger, because default one 'OnLostFocus' can sometimes not save your data:

 <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" Width="100" />       

In MainViewModel modify creating of your tabs, so it will have Name property too:

IEnumerable<TabItem> tabs = Enumerable.Range( 1, Controller.Persons.Count())
                                        .Select( x => new TabItem { Header = String.Format("Person {0}", x), Name = Controller.Persons[x-1].Name });

Also, the most important, create own TabItem class to contain some bounded data:

public class TabItem
{
    public string Name { set; get; }
    public string Header { set; get; }    
}
sTrenat
  • 1,029
  • 9
  • 13
  • 1
    I'll mark this as the correct solution, since it is close enough. My implementation has a collection of TabItemViewModel, and my XAML is slightly different. I'll post my actual answer later. But this is basically the same thing, and the biggest thing is that the ItemsSource for the TabControl should NOT be of TabItem, but of the ViewModel. – Dave Sep 14 '17 at 19:34
  • @Dave Waiting for your definitive solution ;) – Soleil May 01 '23 at 18:59