How can I bind WPF TabItem to specific C# class using MVVM?

71 Views Asked by At

I want to bind each TabItem of TabControl to specific C# class to use state of class properties for reactions on View. I try to use MVVM and I know that to better to bind collection of objects to TabControl's ItemsSource but in my case classes which I want to bind so different and only few properties of it must be identical. I'm not used collection because customers want to see tab items by groups and I had to change standard style of TabControl and had to create each of Items in XAML and bound to specific element

<TabControl>
    <TabControl.Resources>
        <Style x:Key="TabItemDefaultStyle" TargetType="TabItem">
            <Setter Property="Height" Value="{StaticResource SquareButtonSize}"/>
            <Setter Property="Width" Value="{StaticResource SquareButtonSize}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="TabItem">
                        <ControlTemplate.Resources>
                            <Storyboard x:Key="TabItemBorderBackgroundBlinking" >
                                <ColorAnimation Storyboard.TargetName="TabItemBorder" Storyboard.TargetProperty="Background.Color" From="LightBlue" To="Red" AutoReverse="True" Duration="0:0:0.5" RepeatBehavior="Forever" />
                            </Storyboard>
                        </ControlTemplate.Resources>
                        <Border Name="TabItemBorder" BorderThickness="1" BorderBrush="Black" CornerRadius="5" Margin="1">
                            <ContentPresenter x:Name="ContentSite" VerticalAlignment="Center" HorizontalAlignment="Center" ContentSource="Header" Margin="5"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter TargetName="TabItemBorder" Property="Background" Value="{StaticResource GeneralColor}" />
                            </Trigger>
                            <Trigger Property="IsSelected" Value="False">
                                <Setter TargetName="TabItemBorder" Property="Background" Value="Transparent" />
                            </Trigger>
                            <DataTrigger Binding="{Binding IsAlarm}" Value="True">
                                <DataTrigger.EnterActions>
                                    <BeginStoryboard Name="BeginTabItemBorderBackgroundBlinking" Storyboard="{StaticResource TabItemBorderBackgroundBlinking}" />
                                </DataTrigger.EnterActions>
                                <DataTrigger.ExitActions>
                                    <StopStoryboard BeginStoryboardName="BeginTabItemBorderBackgroundBlinking"/>
                                </DataTrigger.ExitActions>
                            </DataTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </TabControl.Resources>
    <TabControl.Template>
        <ControlTemplate TargetType="TabControl">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="{StaticResource SettingsPanelWidth}"/>
                </Grid.ColumnDefinitions>
                <Border Grid.Column="0" Margin="5" Padding="5" BorderBrush="Black" BorderThickness="1,1,1,1" CornerRadius="5">
                    <Grid IsItemsHost="True" >
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="*" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                    </Grid>
                </Border>
                <ContentPresenter Grid.Column="1" ContentSource="SelectedContent" />
            </Grid>
        </ControlTemplate>
    </TabControl.Template>
    <TabItem Grid.Row="0" Style="{StaticResource TabItemDefaultStyle}">
        <vc:MainIndicators />
    </TabItem>
    <TabItem Grid.Row="1" Style="{StaticResource TabItemDefaultStyle}">
        <vc:Dueses />
    </TabItem>
    <TabItem Grid.Row="3" Style="{StaticResource TabItemDefaultStyle}">
        <vc:ChartSettings />
    </TabItem>
    <TabItem Grid.Row="4" Style="{StaticResource TabItemDefaultStyle}">
        <vc:GeneralSettings />
    </TabItem>
</TabControl>

My UI

But I want to bound each of TabItem to object inherited of base class to use its IsAlarm property (you can see it in code above in DataTrigger tag) to make item blink if IsAlarm=true. Looks like I can make it via DataContext but I can't understand how.

UPDATE

As I see I can do something like this (add DataContext to item; all code samples below are parts of code above, except TabItemContext declaration)

<TabItem Grid.Row="0" Style="{StaticResource TabItemDefaultStyle}" DataContext="{Binding TabContext}">
    <vc:MainIndicators />
</TabItem>

And let

public class TabItemContext : BindableBase
{
    public bool IsAlarm { get; set; }
}

public TabItemContext TabContext { get; set; }

But in this case DataTrigger not worked

<DataTrigger Binding="{Binding IsAlarm}" Value="True">
    <DataTrigger.EnterActions>
        <BeginStoryboard Name="BeginTabItemBorderBackgroundBlinking" Storyboard="{StaticResource TabItemBorderBackgroundBlinking}" />
    </DataTrigger.EnterActions>
    <DataTrigger.ExitActions>
        <StopStoryboard BeginStoryboardName="BeginTabItemBorderBackgroundBlinking"/>
    </DataTrigger.ExitActions>
</DataTrigger>

As I understand it is wrong context for trigger. How to change to right? Maybe need to define datacontext type for template?

2

There are 2 best solutions below

0
Fedor Yudin On BEST ANSWER

I was near the right answer. In the UPDATE of original question if change TabItemContext to

public class TabItemContext : BindableBase
{
    private bool _isAlarm;
    public bool IsAlarm
    {
        get => _isAlarm;
        set
        {
            SetProperty(ref _isAlarm, value, nameof(IsAlarm));
        }
    }
}

all be good. Thanks @BionicCode for help

5
BionicCode On

The following basic example shows how you can design your data models to be used with the same TabControl.

TabControl.ItemsContainerStyle is used to define the property on the data model which should be used to present the tab content (TabItem.Content).

The TabControl.ItemTemplate allows you to define how the tab headers will look like.

The TabControl.ContentTemplate allows you to define the layout of the currently selected tab. I recommend defining an implicit DataTemplate for each content object type. This example defines a single content object of type FirstTabContentDataItem to give an example.

TabDataItem.cs
Each tab item has a title, an icon and data (content) that it will present.

class TabDataItem : INotifyPropertyChanged
{
  public TabDataItem(TabContentModel tabContentModel)
    => this.TabContentModel = tabContentModel;

  public TabContentDataItem ContentDataItem { get; }

  public string Title { get; set; }

  public string IconPath { get; set; }
}

TabContentDataItem.cs
The base class for the content data.

class TabContentDataItem : INotifyPropertyChanged
{
  public string Title { get; set; }
}

FirstTabContentDataItem.cs

class FirstTabContentDataItem : TabContentDataItem, INotifyPropertyChanged
{
  public string SomeContentTextValue { get; set; }
}

SecondTabContentDataItem.cs

class SecondTabContentDataItem : TabContentDataItem, INotifyPropertyChanged
{
  public int SomeContentNumericValue { get; set; }
}

MainViewModel.cs

class MainViewModel : INotifyPropertyChanged
{
  public ObservableCollection<TabDataItem> TabDataItems { get; }

  public MainViewModel()
  {
    this.TabDataItems = new ObservableCollection<TabDataItem>();
    
    var tabContent = new FirstTabContentDataItem() 
    { 
      Title = "MainIndicators",
      SomeContentTextValue = "Abc" 
    };
    var tabHeader = new TabDataItem(tabContent) 
    { 
      Title = "First tab",
      IconPath = "SomeImage.pmg"
    };
    this.TabDataItems.Add(tabHeader);
    
    tabContent = new SecondTabContentDataItem() 
    { 
      Title = "Numbers",
      SomeContentNumericValue = 123 
    };
    tabHeader = new TabDataItem(tabContent) 
    { 
      Title = "Second tab",
      IconPath = "SomeImage.pmg"
    };
    this.TabDataItems.Add(tabHeader);
  }
}

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();

    this.DataContext = new MainViewModel();
  }
}

MainWindow.xaml

<Window>
  <Window.Resources>

    <!-- 
         Define a DataTemplate for the TabControl.ContentTemplate for each 
         content data type e.g. FirstTabContentDataItem. 
         Because the DataTemplate is implicit (keyless) 
         it will be automatically implied 
    -->
    <DataTemplate DataType="FirstTabContentDataItem">
      <StackPanel Background="Yellow">
        <TextBlock Text="{Binding Title}" />
        <TextBlock Text="{Binding SomeContentTextValue}" />
      </StackPanel>
    </DataTemplate>

    <DataTemplate DataType="SecondTabContentDataItem">
      <StackPanel Background="Red">
        <TextBlock Text="{Binding Title}" />
        <TextBlock Text="{Binding SomeContentNumericValue}" />
      </StackPanel>
    </DataTemplate>
  </Window.Resources>

  <TabControl ItemsSource="{Binding TabDataItems}">

    <!-- ItemContainerStyle to bind the content data object of the TabDataItem to the TabItem.Content property -->
    <TabControl.ItemContainerStyle>
      <Style TargetType="TabItem">
        <Setter Property="Content"
                Value="{Binding ContentDataItem}" />
      </Style>
    </TabControl.ItemContainerStyle>

    <!-- ItemTemplate defines how the tab headers look like -->
    <TabControl.ItemTemplate>
      <DataTemplate DataType="local:TabDataItem">
        <Image Source="{Binding IconPath}" />
      </DataTemplate>
    </TabControl.ItemTemplate>
  <TabControl>
</Window>