Veksi Veksi - 1 month ago 11
C# Question

A master-detail TabControl binding (1:n) using parent SelectedItem with ICommand

I have XAML related question I have tried to research an answer in vain. I have commented the relevant questions to the XAML. It looks to me this questions is a more complex because of the way I try to arrange things.

Basically I have a main view model used in the TabControl headers and then in the content area I would show a list of items from the main view model. I just don't know how to to the binding. This is the main question. However, I suspect the next and ultimate objectives I have might factor in how to think about this, so I added them too. The rest of the code is for the sake of completeness.



<StackPanel>
<TabControl x:Name="mainsTabControl"
IsSynchronizedWithCurrentItem="True"
IsEnabled="True"
Visibility="Visible"
ItemsSource="{Binding Path=Mains}">

<!-- How to select a different background for the selected header? Note that the background color is "selected tab" if MainContentViewModel.IsActive is not TRUE.
If it is, a different color is chosen. Here this fact is just emulated with IsEnabled property due to well, multi-binding to the rescue (and a converter)? -->

<!-- Is there a clean way to use ICommand binding (RelayCommand) to check if it is OK to change the tab and if necessary, present a dialogue asking for the change? -->
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="IsEnabled" Value="{Binding IsActive}"/>
</Style>
</TabControl.ItemContainerStyle>

<!-- This binds to every item in the MainViewModel.Mains collection. Note the question about background color. -->
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}" HorizontalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>

<TabControl.ContentTemplate>
<DataTemplate>
<!-- This binding gives reference to the selected MainContentViewModel. -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" Margin="10" DataContext="{Binding ElementName=mainsTabControl, Path=SelectedItem, Mode=OneWay}">
<ItemsControl ItemsSource="{Binding Path=CustomItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</StackPanel>




using GalaSoft.MvvmLight;
using System.Collections.ObjectModel;
using System.Linq;

namespace WpfDependencyInjection.ViewModel
{
public class MainContentViewModel: ViewModelBase
{
private ObservableCollection<CustomItemViewModel> customItems;

private mainContentDto MainContent { get; set; }

public string Name { get; }


public bool isActive;


public MainContentViewModel(Engine engine, mainContentDto mainContent)
{
MainContent = mainContent;
Name = MainContent.Name;
IsActive = true;

//The custom items belonging to this main content.
var customItems = engine.CustomItemContents.Where(i => i.MainContentId == MainContent.Id).Select(i => new CustomItemViewModel(engine, i));
CustomItems = new ObservableCollection<CustomItemViewModel>(customItems);
}


public ObservableCollection<CustomItemViewModel> CustomItems
{
get
{
return customItems;
}
set
{
customItems = value;
RaisePropertyChanged(nameof(CustomItems));
}
}


public bool IsActive
{
get
{
return isActive;
}
private set
{
isActive = value;
RaisePropertyChanged(nameof(IsActive));
}
}
}
}

public class CustomItemViewModel: ViewModelBase
{
private Engine Engine { get; }

private ItemTypeDto CustomItem { get; set; }

public string Name { get; }


public CustomItemViewModel(Engine engine, ItemTypeDto customItem)
{
Engine = engine;
CustomItem = customItem;
Name = customItem.Name;
}
}

namespace WpfDependencyInjection
{
public class Engine
{
public string Name { get; } = "EngineMan";

public List<mainContentDto> MainContents { get; set; } = new List<mainContentDto>(new[]
{
new mainContentDto { Name = "Main One", Id = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E1"), Version = 1 },
new mainContentDto { Name = "Main Two", Id = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E2"), Version = 1 }
});


public List<ItemTypeDto> CustomItemContents { get; set; } = new List<ItemTypeDto>(new ItemTypeDto[]
{
new ItemType1Dto { MainContentId = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E1"), Name = "ItemType1Dto I", Id = Guid.NewGuid(), Version = 1 },
new ItemType2Dto { MainContentId = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E1"), Name = "ItemType2Dto I", Id = Guid.NewGuid(), Version = 1 },

new ItemType2Dto { MainContentId = Guid.Parse("C51AC758-504B-4914-92DC-5EBE9A1F39E2"), Name = "ItemType2Dto 2", Id = Guid.NewGuid(), Version = 1 }
});


public Engine()
{
}
}
}


<edit: The binding solved partially, though not the ICommand part.

Answer

Try this:

<TabControl x:Name="mainsTabControl"
            IsEnabled="True"
            IsSynchronizedWithCurrentItem="True"
            ItemsSource="{Binding Path=Mains}"
            SelectedItem="0"
            Visibility="Visible">
    <TabControl.ItemContainerStyle>
        <Style TargetType="{x:Type TabItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected}" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TabItem}">
                        <Border Background="{TemplateBinding Background}">
                            <ContentPresenter ContentSource="Header" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="Background" Value="HotPink" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TabControl.ItemContainerStyle>

    <TabControl.ItemTemplate>
        <DataTemplate DataType="{x:Type local:MainContentViewModel}">
            <Button Background="{x:Null}"
                    Command="{Binding SomeCommand}"
                    Content="{Binding Name}"
                    FocusVisualStyle="{x:Null}" />
        </DataTemplate>
    </TabControl.ItemTemplate>

    <TabControl.ContentTemplate>
        <DataTemplate DataType="{x:Type local:MainContentViewModel}">
            <ItemsControl Margin="10"
                          VerticalAlignment="Top"
                          ItemsSource="{Binding Path=CustomItems}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Vertical" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate DataType="{x:Type local:CustomItem}">
                        <TextBlock Text="{Binding Name}" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

Command:

There may be cleaner ways but here we bind IsSelected for the TabItem to the IsSelected property of the viewmodel. This enables having a command that asks if it is ok to navigate and sets IsSelected to true if it is.

Background:

We also retemplate the tabitem so that background works as we want. If you check with Snoop WPF inserts an extra border when the item is selected.

Side note 1:

Don't put the TabControl into a StackPanel like that. A StackPanel sizes to content and will kill scrolling and draw outside the control. Also it comes with a cost, a deep visual tree is not cheap. Same in the ItemTemplate and the other places. In fact StackPanel is rarely right for anything :)

Side note 2:

If you specify DataType in your DataTemplate you get intellisense and some compiletime checking.