4

TabControl's ItemsSource property binded to collection in the ViewModel. ContentTemplate is ListView - UserControl. All the tabs use only one ListView control (the constructor of ListView is called only once). The problem is that all tabs have a common visual state - for example, if you change the size of any item in the one tab, this change will be on all tabs. How to create a separate ListView for each tab, but at the same time use ItemsSource property?

<TabControl Grid.Row="1" Grid.Column="2" TabStripPlacement="Bottom" >    

    <TabControl.ContentTemplate>
        <DataTemplate DataType="viewModel:ListViewModel" >
            <view:ListView />
        </DataTemplate>
    </TabControl.ContentTemplate>

    <TabControl.ItemsSource>
        <Binding Path="Lists"/>
    </TabControl.ItemsSource>
</TabControl>
Walt Ritscher
  • 6,977
  • 1
  • 28
  • 35
  • @mm8 I think that is an incorrect duplicate link. This question is about creating a separate ContentTemplate for displaying each of the tab items, rather than the default of creating a single item and just swapping the DataContext behind them. I think there might be a template property to enforce this, but I am not sure. Personally I'd be careful of such a design... you'd be creating/storing multiple copies of a control, even when it isn't visible. If the size matters that much, you could create a property for it on your DataContext and bind it so it changes with each tab change. – Rachel Apr 11 '17 at 15:01
  • The property I was thinking of is `x:Shared="False"` (example [here](http://stackoverflow.com/a/3488396/302677)). This isn't really ideal though, as it creates a new copy of the UserControl each time you select the tab, so things like size changes won't be kept regardless. If you're building your TabControl items using a template, I'd recommend just storing/binding all properties you care about so when the user switches tabs, the same template it used but the DataContext is different so all bindings are updated – Rachel Apr 11 '17 at 15:20
  • Maybe look into a custom TabControl DependencyProperty that builds out the `.TabItems` for each item, rather than using `ItemsSource`? – Rachel Apr 11 '17 at 15:25
  • The workaround that I use is to bind the visual properties (column widths in your example) to properties in the ViewModel. Then it doesn't matter that they're sharing a template. – Robin Bennett Dec 07 '18 at 10:33

2 Answers2

4

There's no easy way of doing this.

The problem is you have a WPF Template, which is meant to be the same regardless of what data you put behind it. So one copy of the template is created, and anytime WPF encounters a ListViewModel in your UI tree it draws it using that template. Properties of that control which are not bound to the DataContext will retain their state between changing DataSources.

You could use x:Shared="False" (example here), however this creates a new copy of your template anytime WPF requests it, which includes when you switch tabs.

When [x:Shared is] set to false, modifies Windows Presentation Foundation (WPF) resource retrieval behavior such that requests for a resource will create a new instance for each request, rather than sharing the same instance for all requests.

What you really need is for the TabControl.Items to each generate a new copy of your control for each item, but that doesn't happen when you use the ItemsSource property (this is by design).

One possible alternative which might work would be to create a custom DependencyProperty that binds to your collection of items, and generates the TabItem and UserControl objects for each item in the collection. This custom DP would also need to handle the collection change events to make sure the TabItems stay in sync with your collection.

Here's one I was playing around with. It was working for simple cases, such as binding to an ObservableCollection, and adding/removing items.

    public class TabControlHelpers
    {
        // Custom DependencyProperty for a CachedItemsSource
        public static readonly DependencyProperty CachedItemsSourceProperty =
            DependencyProperty.RegisterAttached("CachedItemsSource", typeof(IList), typeof(TabControlHelpers), new PropertyMetadata(null, CachedItemsSource_Changed));

        // Get
        public static IList GetCachedItemsSource(DependencyObject obj)
        {
            if (obj == null)
                return null;

            return obj.GetValue(CachedItemsSourceProperty) as IList;
        }

        // Set
        public static void SetCachedItemsSource(DependencyObject obj, IEnumerable value)
        {
            if (obj != null)
                obj.SetValue(CachedItemsSourceProperty, value);
        }

        // Change Event
        public static void CachedItemsSource_Changed(
            DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (!(obj is TabControl))
                return;

            var changeAction = new NotifyCollectionChangedEventHandler(
                (o, args) =>
                {
                    var tabControl = obj as TabControl;

                    if (tabControl != null)
                        UpdateTabItems(tabControl);
                });


            // if the bound property is an ObservableCollection, attach change events
            INotifyCollectionChanged newValue = e.NewValue as INotifyCollectionChanged;
            INotifyCollectionChanged oldValue = e.OldValue as INotifyCollectionChanged;

            if (oldValue != null)
                newValue.CollectionChanged -= changeAction;

            if (newValue != null)
                newValue.CollectionChanged += changeAction;

            UpdateTabItems(obj as TabControl);
        }

        static void UpdateTabItems(TabControl tc)
        {
            if (tc == null)
                return;

            IList itemsSource = GetCachedItemsSource(tc);

            if (itemsSource == null || itemsSource.Count == null)
            {
                if (tc.Items.Count > 0)
                    tc.Items.Clear();

                return;
            }

            // loop through items source and make sure datacontext is correct for each one
            for(int i = 0; i < itemsSource.Count; i++)
            {
                if (tc.Items.Count <= i)
                {
                    TabItem t = new TabItem();
                    t.DataContext = itemsSource[i];
                    t.Content = new UserControl1(); // Should be Dynamic...
                    tc.Items.Add(t);
                    continue;
                }

                TabItem current = tc.Items[i] as TabItem;
                if (current == null)
                    continue;

                if (current.DataContext == itemsSource[i])
                    continue;

                current.DataContext = itemsSource[i];
            }

            // loop backwards and cleanup extra tabs
            for (int i = tc.Items.Count; i > itemsSource.Count; i--)
            {
                tc.Items.RemoveAt(i - 1);
            }
        }
    }

Its used from the XAML like this :

<TabControl local:TabControlHelpers.CachedItemsSource="{Binding Values}">
    <TabControl.Resources>
        <Style TargetType="{x:Type TabItem}">
            <Setter Property="Header" Value="{Binding SomeString}" />
        </Style>
    </TabControl.Resources>
</TabControl>

A few things to note :

  • TabItem.Header is not set, so you'll have to setup a binding for it in TabControl.Resources
  • DependencyProperty implementation currently hardcodes the creation of the new UserControl. May want to do that some other way, such as trying to use a template property or perhaps a different DP to tell it what UserControl to create
  • Would probably need more testing... not sure if there's any issues with memory leaks due to change handler, etc
Community
  • 1
  • 1
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • Hello Rachel, thank you for your answer, you saved my day. I completed your answer below, feel free to correct me if needed. Cheers – Maël Pedretti Dec 14 '18 at 12:59
1

Based on @Rachel answer I made a few modifications.

First of all, you now have to specify a user control type as content template which is dynamically created.

I have also corrected a mistake in collectionChanged handler removal.

The code is the following:

public static class TabControlExtension
{
    // Custom DependencyProperty for a CachedItemsSource
    public static readonly DependencyProperty CachedItemsSourceProperty =
        DependencyProperty.RegisterAttached("CachedItemsSource", typeof(IList), typeof(TabControlExtension), new PropertyMetadata(null, CachedItemsSource_Changed));

    // Custom DependencyProperty for a ItemsContentTemplate
    public static readonly DependencyProperty ItemsContentTemplateProperty =
        DependencyProperty.RegisterAttached("ItemsContentTemplate", typeof(Type), typeof(TabControlExtension), new PropertyMetadata(null, CachedItemsSource_Changed));

    // Get items
    public static IList GetCachedItemsSource(DependencyObject dependencyObject)
    {
        if (dependencyObject == null)
            return null;

        return dependencyObject.GetValue(CachedItemsSourceProperty) as IList;
    }

    // Set items
    public static void SetCachedItemsSource(DependencyObject dependencyObject, IEnumerable value)
    {
        if (dependencyObject != null)
            dependencyObject.SetValue(CachedItemsSourceProperty, value);
    }

    // Get ItemsContentTemplate
    public static Type GetItemsContentTemplate(DependencyObject dependencyObject)
    {
        if (dependencyObject == null)
            return null;

        return dependencyObject.GetValue(ItemsContentTemplateProperty) as Type;
    }

    // Set ItemsContentTemplate
    public static void SetItemsContentTemplate(DependencyObject dependencyObject, IEnumerable value)
    {
        if (dependencyObject != null)
            dependencyObject.SetValue(ItemsContentTemplateProperty, value);
    }

    // Change Event
    public static void CachedItemsSource_Changed(
        DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        if (!(dependencyObject is TabControl))
            return;

        var changeAction = new NotifyCollectionChangedEventHandler(
            (o, args) =>
            {

                if (dependencyObject is TabControl tabControl && GetItemsContentTemplate(tabControl) != null && GetCachedItemsSource(tabControl) != null)
                    UpdateTabItems(tabControl);
            });

        // if the bound property is an ObservableCollection, attach change events
        if (e.OldValue is INotifyCollectionChanged oldValue)
            oldValue.CollectionChanged -= changeAction;

        if (e.NewValue is INotifyCollectionChanged newValue)
            newValue.CollectionChanged += changeAction;

        if (GetItemsContentTemplate(dependencyObject) != null && GetCachedItemsSource(dependencyObject) != null)
            UpdateTabItems(dependencyObject as TabControl);
    }

    private static void UpdateTabItems(TabControl tabControl)
    {
        if (tabControl == null)
            return;

        IList itemsSource = GetCachedItemsSource(tabControl);

        if (itemsSource == null || itemsSource.Count == 0)
        {
            if (tabControl.Items.Count > 0)
                tabControl.Items.Clear();

            return;
        }

        // loop through items source and make sure datacontext is correct for each one
        for (int i = 0; i < itemsSource.Count; i++)
        {
            if (tabControl.Items.Count <= i)
            {
                TabItem tabItem = new TabItem
                {
                    DataContext = itemsSource[i],
                    Content = Activator.CreateInstance(GetItemsContentTemplate(tabControl))
                };
                tabControl.Items.Add(tabItem);
                continue;
            }

            TabItem current = tabControl.Items[i] as TabItem;
            if (!(tabControl.Items[i] is TabItem))
                continue;

            if (current.DataContext == itemsSource[i])
                continue;

            current.DataContext = itemsSource[i];
        }

        // loop backwards and cleanup extra tabs
        for (int i = tabControl.Items.Count; i > itemsSource.Count; i--)
        {
            tabControl.Items.RemoveAt(i - 1);
        }
    }
}

This one is used the following way:

<TabControl main:TabControlExtension.CachedItemsSource="{Binding Channels}" main:TabControlExtension.ItemsContentTemplate="{x:Type YOURUSERCONTROLTYPE}">
    <TabControl.Resources>
        <Style BasedOn="{StaticResource {x:Type TabItem}}" TargetType="{x:Type TabItem}">
            <Setter Property="Header" Value="{Binding Name}" />
        </Style>
    </TabControl.Resources>
</TabControl>
Maël Pedretti
  • 718
  • 7
  • 22