WPF Dynamic TabControl with view as content (MVVM)

Advertisements

I generate dynamic tabs in a TabControl and the content for each should be a view/UserControl that fills a DataGrid.

The data seems to be loaded but there is a problem with the bindings as the view does not show anything. I think there is a mismatch with the binding of the ViewModel to the View itself.

If I add <views:MachineView DataContext="{Binding}"/> in MainWindow.xaml I get the updated data, but I need to remove & add it for it to "work".

MainWindow.xaml

<TabControl x:Name="machineTabs" Grid.Row="1" ItemsSource="{Binding Tabs}" SelectedIndex="{Binding SelectedIndex}"  >
    <TabControl.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Header}"/>
        </DataTemplate>
    </TabControl.ItemTemplate>
    <TabControl.ContentTemplate>
        <DataTemplate DataType="{x:Type viewModels:MachineViewModel}">
            <views:MachineView/>
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

The tab header (<TabControl.ItemTemplate>) successfully binds to the Header field but when I try to output the same value inside the view itself it doesn’t show.

MainWindow.xaml.cs

public partial class MainWindow : Window
{

    private readonly MainWindowModel mainWindowModel;

    public MainWindow()
    {
        mainWindowModel = new MainWindowModel();
        DataContext = mainWindowModel;
        InitializeComponent();
    }
}

MainWindowModel.cs

public class MainWindowModel : ObservableObject
{
    private SqlConnection connection;
    public ObservableCollection<MachineViewModel> Tabs { get; } = new ObservableCollection<MachineViewModel>();
    public MainWindowModel()
    {
        connection = new(GetConnectionString());

        AddTab("Free");
        AddTab("Night Shift");
        AddTab("Day Shift");
        AddTab("Afternoon Shift");
    }

    private void AddTab(string header)
    {
        var mvm = new MachineViewModel();
        mvm.Initialize(header, connection);
        Tabs.Add(mvm);
    }

    public ICommand SearchCommand => new RelayCommand(Search);

    public int SelectedIndex { get; set; }

    /* ... */
}

MachineView.xaml

<UserControl x:Class="Namespace.View.MachineView"
             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:local="clr-namespace:Namespace.View"
             xmlns:vm="clr-namespace:Namespace.ViewModel"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid Background="LightBlue">
        <Grid.RowDefinitions>
            <RowDefinition Height="20"/>
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Text="{Binding Header}" HorizontalAlignment="Center" VerticalAlignment="Center" />
        <DataGrid x:Name="DataGrid" Grid.Row="1" ItemsSource="{Binding DataTable}" AutoGenerateColumns="True" IsReadOnly="True"/>
    </Grid>
</UserControl>

MachineView.xaml.cs

public partial class MachineView : UserControl
{

    private readonly MachineViewModel machineViewModel;

    public MachineView()
    {
        machineViewModel = new MachineViewModel();
        DataContext = machineViewModel;
        InitializeComponent();
    }

}

MachineViewModel.cs

public sealed class MachineViewModel : ObservableObject
{
    public string Header { get; private set; } = "";
    private SqlConnection? connection;

    private SqlDataAdapter? adapter;
    private DataTable? dataTable;
    private DataView? dataTableView;

    public void Initialize(string header, SqlConnection connection)
    {
        this.connection = connection;
        Header = header;
        OnPropertyChanged(nameof(Header));
    }

    public void Search()
    {
        if (connection == null) return;
        try
        {
            connection.Open();

            using SqlCommandBuilder sqlcommandbuilder = new();
            string dmc = sqlcommandbuilder.QuoteIdentifier(Header);

            string query = "select xyz";
            using SqlCommand command = new(query, connection);

            // Init the command and the builder
            adapter = new SqlDataAdapter(command);
            SqlCommandBuilder commandBuilder = new(adapter);

            // Fill the table with the values from the adapter and set it to the public field to notify about changes
            dataTable = new DataTable();
            adapter.Fill(dataTable);

            DataTable = dataTable.DefaultView;
            connection.Close();
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }


    public DataView? DataTable
    {
        get => dataTableView;
        set
        {
            if(dataTableView != value)
            {
                dataTableView = value;
                OnPropertyChanged(nameof(DataTable));
            }
        }
    }

}

Note that the Header must be used to populate the data table. I would love to use a constructor but the xaml needs a parameterless constructor for the view (?)

Why is the content not updating even though I’ve bound the view to the DataTable and I call OnPropertyChanged(nameof(DataTable)); when updating it?

>Solution :

MachineView must not create its own, private view model instance, but instead operate on the view model that it gets by the inherited value of its DataContext property.

So just remove the DataContext assignment from its constructor:

public partial class MachineView : UserControl
{
    public MachineView()
    {
        InitializeComponent();
    }
}

Since a TabControl does not create a new TabItem and hence a new MachineView for each data item, you could "reset" the visual state of the single MachineView in a DataContextChanged event handler:

public partial class MachineView : UserControl
{
    public MachineView()
    {
        InitializeComponent();

        DataContextChanged += (s, e) =>
        {
            DataGrid.ScrollIntoView(...); // or similar
        };
    }
}

Leave a ReplyCancel reply