Divide WPF TabControl Tabs by pairs based on condition

60 Views Asked by At

I need to make TabControl with tabs divided by pairs. I am already make template form that looks like I need and works as I need But I need certainly understand that tab button will be on right column after it dynamically created (from source). This is my code

MainWindow.xaml

<Window x:Class="HMI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:localVM="clr-namespace:HMI.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <localVM:HMIViewModel/>
    </Window.DataContext>
    <TabControl TabStripPlacement="Right" ItemsSource="{Binding ProcessVariablesGroups}">
        <TabControl.Resources>
            <Style TargetType="TabItem">
                <Setter Property="Height" Value="70"/>
                <Setter Property="Width" Value="70"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="TabItem">
                            <Border Name="Border" BorderThickness="1" BorderBrush="Black" CornerRadius="5" Margin="2">
                                <ContentPresenter x:Name="ContentSite" VerticalAlignment="Center" HorizontalAlignment="Center" ContentSource="Header" Margin="10,2"/>
                            </Border>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsSelected" Value="True">
                                    <Setter TargetName="Border" Property="Background" Value="LightGray" />
                                </Trigger>
                                <Trigger Property="IsSelected" Value="False">
                                    <Setter TargetName="Border" Property="Background" Value="White" />
                                </Trigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </TabControl.Resources>
        <TabControl.Style>
            <Style TargetType="TabControl" x:Name="myName">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="TabControl">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="150"/>
                                </Grid.ColumnDefinitions>
                                <ContentPresenter Grid.Column="0" ContentSource="SelectedContent"/>
                                <ItemsPresenter Grid.Column="1"/>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </TabControl.Style>
        <TabControl.ItemsPanel>
            <ItemsPanelTemplate>
                <UniformGrid Columns="2" VerticalAlignment="Top" HorizontalAlignment="Right"/>
            </ItemsPanelTemplate>
        </TabControl.ItemsPanel>
        <TabControl.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}"/>
            </DataTemplate>
        </TabControl.ItemTemplate>
        <TabControl.ContentTemplate>
            <DataTemplate>
                <StackPanel>
                    <Button Content="{Binding Name}" Margin="5" Width="120"/>
                    <ListView ItemsSource="{Binding ProcessVariables}">
                        <ListView.View>
                            <GridView>
                                <GridViewColumn Header="Some column"/>
                            </GridView>
                        </ListView.View>
                    </ListView>
                </StackPanel>
            </DataTemplate>
        </TabControl.ContentTemplate>
    </TabControl>
</Window>

ViewModelDTO.cs

using System.Collections.ObjectModel;

namespace HMI.ViewModel
{
    public class ProcessVariablesGroup
    {
        public string Name { get; set; }
        public string Value { get; set; }
        public bool IsActive { get; set; }
        public ObservableCollection<string> ProcessVariables { get; set; } = new ObservableCollection<string>();
    }
}

HMIViewModel.cs

using System.Collections.ObjectModel;

namespace HMI.ViewModel
{
    public class HMIViewModel
    {
        private readonly ObservableCollection<ProcessVariablesGroup> _processVariablesGroups;
        public ObservableCollection<ProcessVariablesGroup> ProcessVariablesGroups => _processVariablesGroups;

        public HMIViewModel()
        {
            _processVariablesGroups = new ObservableCollection<ProcessVariablesGroup>
            {
                new ProcessVariablesGroup { Name = "P1A", IsActive = true, ProcessVariables = new ObservableCollection<string>() { "P1A1", "P1A2", "P1A3" } },
                new ProcessVariablesGroup { Name = "P1P", IsActive = false, ProcessVariables = new ObservableCollection<string>() { "P1P1", "P1P2", "P1P3" } },
                new ProcessVariablesGroup { Name = "P2A", IsActive = true, ProcessVariables = new ObservableCollection<string>() { "P2A1", "P2A2", "P2A3" } },
                new ProcessVariablesGroup { Name = "P2P", IsActive = false, ProcessVariables = new ObservableCollection<string>() { "P2P1", "P2P2", "P2P3" } },
                new ProcessVariablesGroup { Name = "P3A", IsActive = true, ProcessVariables = new ObservableCollection<string>() { "P3A1", "P3A2", "P3A3" } },
                new ProcessVariablesGroup { Name = "P3P", IsActive = false, ProcessVariables = new ObservableCollection<string>() { "P3P1", "P3P2", "P3P3" } }
            };
        }
    }
}

Items in ProcessVariablesGroup now created "manually" for testing. In general it will be create based on any data.

And question: how can I place items with property IsActive = true in the first column and with IsActive = false in second. I am understand that now is the same but because I add data in constructor on right order. But if it will be created automatically I cant guarantee the right order. I check stackoverflow.com and see that changing UniformGrid to WrapPanel can help me, but I don't understand how.

1

There are 1 best solutions below

0
mm8 On

A WrapPanel positions the child elements in sequential position from left to right which means that the order in which you add the items to the source collection is as important as when using a UniformGrid.

If you want to be able to put an item in a specific column based on a property value, you could use a Grid with two ColumnDefinitions as the ItemsPanelTemplate for the TabControl. The problem with this approach is to dynamically being able to add a RowDefinition for each pair of active and inactive items in the source collection.

You could use the GridHelpers class from this blog post to create a Grid with a dynamic number of rows or columns:

<TabControl.ItemsPanel>
    <ItemsPanelTemplate>
        <Grid VerticalAlignment="Top" HorizontalAlignment="Right"
              local:GridHelpers.RowCount="{Binding DataContext.ProcessVariablesGroups.Count, 
                RelativeSource={RelativeSource AncestorType=TabControl}}"
              local:GridHelpers.ColumnCount="2" />
    </ItemsPanelTemplate>
</TabControl.ItemsPanel>

Then use an ItemContainerStyle to put the TabItem in the right cell based on the values of the source properties:

<TabControl.ItemContainerStyle>
    <Style TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
        <Style.Triggers>
            <DataTrigger Binding="{Binding IsActive}" Value="False">
                <Setter Property="Grid.Column" Value="1" />
            </DataTrigger>
        </Style.Triggers>
        <Setter Property="Grid.Row" Value="{Binding N}" />
    </Style>
</TabControl.ItemContainerStyle>

The view model will be responsible for tracking the order of the items. You could handle the CollectionChanged event for the ObservableCollection<T>. Here is an example that should get you started:

public class HMIViewModel
{
    private readonly ObservableCollection<ProcessVariablesGroup> _processVariablesGroups;

    public ObservableCollection<ProcessVariablesGroup> ProcessVariablesGroups => _processVariablesGroups;

    public HMIViewModel()
    {
        _processVariablesGroups = new ObservableCollection<ProcessVariablesGroup>
            {
                new ProcessVariablesGroup { Name = "P1A", IsActive = true, N = 0, ProcessVariables = new ObservableCollection<string>() { "P1A1", "P1A2", "P1A3" } },
                new ProcessVariablesGroup { Name = "P1P", IsActive = false, N = 0, ProcessVariables = new ObservableCollection<string>() { "P1P1", "P1P2", "P1P3" } },
                new ProcessVariablesGroup { Name = "P2A", IsActive = true, N = 1, ProcessVariables = new ObservableCollection<string>() { "P2A1", "P2A2", "P2A3" } },
                new ProcessVariablesGroup { Name = "P2P", IsActive = false, N = 1, ProcessVariables = new ObservableCollection<string>() { "P2P1", "P2P2", "P2P3" } },
                new ProcessVariablesGroup { Name = "P3A", IsActive = true, N = 2, ProcessVariables = new ObservableCollection<string>() { "P3A1", "P3A2", "P3A3" } },
                new ProcessVariablesGroup { Name = "P3P", IsActive = false, N = 2, ProcessVariables = new ObservableCollection<string>() { "P3P1", "P3P2", "P3P3" } }
            };

        _processVariablesGroups.CollectionChanged += OnCollectionChanged;
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        int numberOfActiveProcessVariablesGroups = 0;
        int numberOfInactiveProcessVariablesGroups = 0;
        foreach (var item in _processVariablesGroups)
            item.N = item.IsActive ? numberOfActiveProcessVariablesGroups++ : numberOfInactiveProcessVariablesGroups++;
    }
}

The ProcessVariablesGroup class should implement INotifyPropertyChanged to provide change notifications to the view:

public class ProcessVariablesGroup : INotifyPropertyChanged
{
    public string Name { get; set; }
    public string Value { get; set; }
    public bool IsActive { get; set; }
    public ObservableCollection<string> ProcessVariables { get; set; } = new ObservableCollection<string>();

    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; NotifyPropertyChanged(); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}