Vadim Moskvin
Vadim Moskvin

Reputation: 51

Wpf TabControl create only one view at all tabs

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>

Upvotes: 4

Views: 2261

Answers (2)

Rachel
Rachel

Reputation: 132648

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

Upvotes: 4

Ma&#235;l Pedretti
Ma&#235;l Pedretti

Reputation: 784

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>

Upvotes: 1

Related Questions