MaterialDesignInXAML / MaterialDesignInXamlToolkit

Google's Material Design in XAML & WPF, for C# & VB.Net.
http://materialdesigninxaml.net
MIT License
14.91k stars 3.4k forks source link

TabItem does not honor ContentTemplate #3271

Closed TWhidden closed 1 year ago

TWhidden commented 1 year ago

Bug explanation

The TabItem control does not allow you to set a ContentTemplate

For example, if added to the Demo app:

<smtx:XamlDisplay Margin="0,0,0,16"
                      UniqueKey="custom_tabs_2">
      <materialDesign:Card>
        <TabControl>

          <TabControl.Resources>
            <DataTemplate x:Key="TabCustomTemplate">
              <ItemsControl ItemsSource="{Binding Mode=OneWay}"
                            ItemTemplate="{DynamicResource MyCustomTemplate}"
                            ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                            Style="{DynamicResource VirtualizingItemsControl}">
                <ItemsControl.ItemsPanel>
                  <ItemsPanelTemplate>
                    <controls:VirtualizingWrapPanel Orientation="Horizontal"
                                                                 IsItemsHost="True"
                                                                 />
                  </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
              </ItemsControl>
            </DataTemplate>

            <DataTemplate x:Key="MyCustomTemplate">
              <TextBlock Text="{Binding Pro, Mode=OneWay}"/>
            </DataTemplate>

          </TabControl.Resources>

          <!-- Not working -->
          <TabItem Header="Test1"
                   ContentTemplate="{StaticResource TabCustomTemplate}"
                   Content="{Binding TabBinding}"/>

          <!-- Workaround -->
          <TabItem Header="Test2">
            <ContentControl ContentTemplate="{StaticResource TabCustomTemplate}"
                            Content="{Binding TabBinding}"></ContentControl>
          </TabItem>
        </TabControl>
      </materialDesign:Card>
</smtx:XamlDisplay>

with this small amount added to the TabsViewModel:

    public List<SomeTabData> TabBinding => new List<SomeTabData>()
    {
        new SomeTabData(),
        new SomeTabData(),
        new SomeTabData()
    };

    public class SomeTabData
    {
        public string Pro { get; set; } = "Some Data";
    }

In my case, I have a collection of data that shows up in each tab - results look like this:

image

Expected result is to use the ContentTemplate supplied on the TabItem

I have not tried using a ContentTemplateSelector as of yet - the forcing of the ContentTemplate should have handled the collection correctly.

The MaterialDesign source style looks correct as far as I can tell.

If I remove the ContentTemplate="{StaticResource TabCustomTemplate}" from the TabItem, the result is the same as if the ContentTemplate just is not being honored.

A work around I found is to inject the results into a ContentControl like this:

<TabItem Header="Test2">
            <ContentControl ContentTemplate="{StaticResource TabCustomTemplate}"
                            Content="{Binding TabBinding}"></ContentControl>
</TabItem>

which correctly rendered the results

image

For now Ill use my work around, but hoping if you can tell me if I am doing it wrong or this should work.

Thanks!

Version

4.9.0 (doing it also on previous versions so not new to 4.9 or anything)

nicolaihenriksen commented 1 year ago

@TWhidden Is there a particular reason you want to control the template on the TabItem rather than on the TabControl? Only use case I can see is if you want each tab content rendered differently, but then I would probably just handle that in a template selector at the TabControl level.

What I am hinting at, is that the control template for the TabControl uses the ContentStringFormat, ContentTemplate, and ContentTemplateSelector properties of the TabControl itself on the ContentPresenter displaying the selected tab content.

So I think something like this would solve your problem (unless each tab should be rendered with different templates)?

<TabControl ContentTemplate="{StaticResource TabCustomTemplate}">
  ... resources and items from your sample ...
</TabControl>
TWhidden commented 1 year ago

@nicolaihenriksen - Correct - just managing templates in a resource and applied directly to each TabItem (same datatype, different collections in my case). I believe but have not verified that the original TabItem style supported it. When we moved our TabControl over to MaterialDesign, I failed to notice the template wasn't being applied so I was researching a bug report which got me here. I'm ok with switching to a ContentTemplateSelector but figured I should report that the potentially breaking side of this. My alternate work around but placing it in a ContentControl worked for me also. Just a little muddied up.

Out of curiosity, I attempted to trace the theme to understand why the {TemplateBinding ContentTemplate} does not retrieve the template from the TabItem. While debugging in the Demo App, I ran out of time and couldn't find the exact reason. Could you provide me with a hint as to why this feature is not utilized? The TabItem control has always intrigued me in WPF.

nicolaihenriksen commented 1 year ago

@TWhidden So your comment got me curious and I had to go play around with it :-)

You are correct, the original default style for a TabControl does honor this, and therefore so should MDIX I believe. So this is a bug (see my findings at the end of my comment).

To answer your question: {TemplateBinding ContentTemplate} does not retrieve the template from the TabItem because when you boil it down, a TemplateBinding is just a Binding where the source is predefined to be the control for which you are currently defining a ControlTemplate. Thus it can only be used inside the <ControlTemplate/> element, and will always refer to the control being templated, as the source. If you want a different source, you can use a normal Binding and the various options available in the RelativeSource property. I this case however, the TabItem is actually a child of the TabControl and thus you cannot easily do it in this manner.

What I came to find, which was actually something I had not stumbled upon before, is that the TabControl actually exposes these properties: TabControl.SelectedContentStringFormat, TabControl.SelectedContentTemplate, and TabControl.SelectedContentTemplateSelector alongside the TabControl.SelectedContent property. And these actually do exactly what we desire. They first inspect the selected TabItem.ContentXyz property and if that is not set, it checks the corresponding TabControl.ContentXyz property. So the conclusion is that this is an actual bug and I will create a PR with a UI test that proves it (i.e. fails without the required changes) along with the simple fix of adding the "Selected" prefix to the bindings in the link from my previous comment.

TWhidden commented 1 year ago

@nicolaihenriksen - Sweet, well sorry for the extra work added. And also, thanks for the callout on the 4.9 release.