Search code examples
c#wpfdata-bindingdatatemplate

Data bind an ItemsControl in a DataTemplate


I seem to have a simple data-binding problem, but can't figure out the right way to do it. There is a TabControl which defines two DataTemplate's, one for the tab header and one for the tab content.

The content template contains an ItemsControl. The ItemsControl tries to bind to a dynamically created ViewModel (ConnectionInfoVM).

When I display the UI, the binding just fails, but there is no error-message in the output about it.

How do I have to set up the DataContext and the binding so the binding works and the DataBuffer is actually displayed? Any help greatly appreciated.

ConnectionsControl:

<UserControl x:Class="XXXViewer.Views.ConnectionsControl"
             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:viewModels="clr-namespace:XXXViewer.ViewModels"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid> 
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TabControl Grid.Row="0" Name="TabDynamic" SelectionChanged="tabDynamic_SelectionChanged">
            <TabControl.Resources>
                <DataTemplate x:Key="TabHeader" DataType="TabItem">
                    <DockPanel>
                        <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Header}" />
                        <Button Name="btnDelete" DockPanel.Dock="Right" Margin="5,0,0,0" Padding="0" Click="btnTabDelete_Click" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Name}">
                            <Image Source="{DynamicResource DeleteImg}" Height="11" Width="11"></Image>
                        </Button>
                    </DockPanel>
                </DataTemplate>

                <DataTemplate x:Key="TabContent" DataType="viewModels:ConnectionInfoVM">
                    <StackPanel>
                        <ScrollViewer Name="Scroller" Background="Black">
                            <StackPanel>
                                <TextBlock Text="This line gets printed" Foreground="White" FontFamily="Consolas"/>
                                <ItemsControl Name="ItemCtrl" ItemsSource="{Binding DataBuffer}">
                                    <ItemsControl.ItemTemplate>
                                        <DataTemplate>
                                            <TextBlock Text="{Binding Path=.}" Foreground="White" FontFamily="Consolas"/>
                                        </DataTemplate>
                                    </ItemsControl.ItemTemplate>
                                </ItemsControl>
                            </StackPanel>
                        </ScrollViewer>
                    </StackPanel>
                </DataTemplate>

            </TabControl.Resources>
        </TabControl>
    </Grid>
</UserControl>

ConnectionsControl code behind:

namespace XXXViewer.Views
{
    public partial class ConnectionsControl : UserControl
    {
        private readonly ObservableCollection<TabItem> _tabItems = new ObservableCollection<TabItem>();

        public ConnectionsControl()
        {
            InitializeComponent();

            // bindings
            TabDynamic.ItemsSource = _tabItems;
            TabDynamic.DataContext = this;
        }

        // assume this gets called
        private void AddTabItem(ConnectionInfoVM ci)
        {
            DataTemplate headerTemplate = TabDynamic.FindResource("TabHeader") as DataTemplate;
            DataTemplate contentTemplate = TabDynamic.FindResource("TabContent") as DataTemplate;

            // create new tab item
            TabItem tab = new TabItem
            {
                Header = $"Tab {ci.ConnectionID}",
                Name = $"T{ci.ConnectionID}",
                HeaderTemplate = headerTemplate,
                ContentTemplate = contentTemplate,
                DataContext = ci
            };

            _tabItems.Insert(0, tab);

            // set the new tab as active tab
            TabDynamic.SelectedItem = tab;
        }
    }
}

ConnectionInfoVM:

namespace XXXViewer.ViewModels
{
    public class ConnectionInfoVM : ViewModelBase
    {
        private readonly ObservableQueue<string> _dataBuffer = new ObservableQueue<string>();
        public ObservableQueue<string> DataBuffer => _dataBuffer;
    }
}

Screenshot of the tab that gets created: resulting tab


Solution

  • You set the ContentTemplate but never the Content, so the ContentTemplate is never applied because it's applied only when there's Content set. Instead of DataContext = ci write Content = ci.

    By the way the DataContext = ci was useless because the DataContext is already implicitely the object on which the DataTemplate is applied.

    Edit

    As you're using WPF, use and abuse of its core feature: bindings.

    How I would have written your code (if I didn't use full MVVM compliant code):

    Your XAML:

    <TabControl Grid.Row="0" Name="TabDynamic" 
                ItemsSource="{Binding TabItems, Mode=OneWay}" 
                SelectionChanged="tabDynamic_SelectionChanged">
        <TabControl.Resources>
            <DataTemplate x:Key="TabHeader" DataType="TabItem">
                <DockPanel>
                    <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Header}" />
                    <Button Name="btnDelete" DockPanel.Dock="Right" Margin="5,0,0,0" Padding="0" Click="btnTabDelete_Click" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=Name}">
                        <Image Source="{DynamicResource DeleteImg}" Height="11" Width="11"></Image>
                    </Button>
                </DockPanel>
            </DataTemplate>
        </TabControl.Resources>
        <TabControl.ItemTemplate>
            <DataTemplate DataType="viewModels:ConnectionInfoVM">
                <TabItem Header="{Binding ConnectionID, Mode=OneWay}"
                         Name="{Binding ConnectionID, Mode=OneWay}"
                         HeaderTemplate="{StaticResources TabHeader}">
                    <StackPanel>
                        <ScrollViewer Name="Scroller" Background="Black">
                            <StackPanel>
                                <TextBlock Text="This line gets printed" Foreground="White" FontFamily="Consolas"/>
                                <ItemsControl Name="ItemCtrl" ItemsSource="{Binding DataBuffer}">
                                    <ItemsControl.ItemTemplate>
                                        <DataTemplate>
                                            <TextBlock Text="{Binding Path=.}" Foreground="White" FontFamily="Consolas"/>
                                        </DataTemplate>
                                    </ItemsControl.ItemTemplate>
                                </ItemsControl>
                            </StackPanel>
                        </ScrollViewer>
                    </StackPanel>
                </TabItem>
    
            </DataTemplate>
        </TabControl.ItemTemplate>
    </TabControl>
    

    You cs code become much simpler:

    namespace XXXViewer.Views
    {
        public partial class ConnectionsControl : UserControl
        {
            private readonly ObservableCollection<ConnectionInfoVM> _tabItems = new ObservableCollection<ConnectionInfoVM>();
            public ObservableCollection<ConnectionInfoVM> TabItems {get {return _tabItems;}}
    
            public ConnectionsControl()
            {
                InitializeComponent();
    
                // bindings
                //TabDynamic.ItemsSource = _tabItems;
                TabDynamic.DataContext = this;
            }
    
            // assume this gets called
            private void AddTabItem(ConnectionInfoVM ci)
            {
                TabItems.Add(ci);
            }
        }
    }
    

    I noted while re-reading your code that you were probably confused about binding in code-behind.

    Your code TabDynamic.ItemsSource = _tabItems; is not a binding, it will only set it once.

    Anyway, I suggest you read a bit about MVVM. The TabItems should be in a ViewModel class instead of being in code-behind.