Привязка к ItemsSource для TabControl в WPF

Я пытаюсь создать пользовательский элемент управления, который представляет то, что я называю рабочей областью (ссылка из блога Джоша Смита). Рабочие области будут отображаться в виде вкладок. Я стремлюсь использовать интерфейс с вкладками для управления различными открытыми документами, как в браузере рабочей книги excal.

Каждый раз, когда пользователь открывает новую рабочую область, она должна отображаться в элементе управления вкладками. Каждая рабочая область принимает форму пользовательского элемента управления, и каждая рабочая область имеет свою собственную модель представления. Я хотел бы, чтобы в заголовке вкладки отображалось свойство из моей модели представления, которое, как мне кажется, должно быть представлено как свойство через мой пользовательский элемент управления.

До сих пор самым чистым решением, которое мне нравилось больше всего, пока я не столкнулся с многочисленными проблемами, было использование таблиц данных. В основном я делал следующее:

<DataTemplate x:Key="WorkspaceItem">
            <DockPanel Width="120">
                <ContentPresenter 
                    Content="{Binding Title}" 
                    VerticalAlignment="Center" 
                    />
            </DockPanel>
        </DataTemplate>     

<DataTemplate DataType="{x:Type CustomerViewModel}">
   <workspace:CustomerWorkspace />
</DataTemplate>

<TabControl ItemsSource="{Binding Workspaces}"
            ItemTemplate="{StaticResource WorkspaceItem}"/>

TabControl.ItemsSource привязан к наблюдаемой коллекции (объекта), которая содержит все мои рабочие области.

Это отлично работает, за исключением двух вещей:

  1. Если я открою несколько клиентов, у меня будет открыто несколько рабочих пространств. Из-за повторного использования DataTemplate я теряю состояние, когда переключаюсь с одной вкладки на другую. Так что все, что не связано, потеряет состояние.

  2. Производительность переключения между различными рабочими пространствами (которые используют разные таблицы данных) ужасно медленная.

Итак ... Я нашел предложение от другого пользователя SO добавить пользовательские элементы управления в ObservableCOllection и отказаться от шаблонов данных. что теперь решает одну из проблем потери состояния. однако теперь я столкнулся с двумя оставшимися проблемами:

  1. Как установить свойство TabItem.Header без использования DataTemplate
  2. Скорость переключения между вкладками по-прежнему низкая, если они не принадлежат одному и тому же DataTemplate.

Затем я приступил к фактическому добавлению TabItem в ObservableCollection в моем фоновом коде и установил для свойства TabItem.Content значение свойства пользовательского элемента управления. Проблема скорости была устранена, как и проблема потери состояния, поскольку я отказался от использования DataTemplates. Однако сейчас я застрял с проблемой привязки TabItem.header к свойству Custome «Title» моего пользовательского элемента управления, который должен отображаться в заголовке вкладки.

Итак, после этого ужасно длинного поста мои вопросы таковы:

  1. Есть ли способ использовать шаблоны данных и заставить их создавать новый экземпляр для каждого элемента в коллекции, чтобы предотвратить повторное использование и потерю состояния.

    1а. Есть ли лучшая альтернатива, чем то, что я упомянул в сообщении выше?

  2. есть ли способ сделать все это с помощью Xaml вместо создания внутреннего кода элементов вкладок?


person Chris Tremblay    schedule 14.09.2012    source источник


Ответы (1)


По умолчанию WPF выгружает невидимые элементы, включая выгрузку TabItems, которые не видны. Это означает, что когда вы вернетесь на вкладку, TabItem будет повторно загружен, и все, что не связано (например, положение прокрутки, состояния элементов управления и т. Д.), Будет сброшено.

Был хороший сайт здесь, который содержит код для расширения TabControl и предотвращения его разрушения TabItems при переключении вкладок, однако похоже, что сейчас его больше нет.

Вот копия кода, хотя я внес в него некоторые изменения. Он сохраняет ContentPresenter элементов вкладок при переключении вкладок и использует его для перерисовки TabItem, когда вы возвращаетесь на страницу. Он занимает немного больше памяти, но я считаю, что он лучше по производительности, поскольку TabItem больше не должен воссоздавать все элементы управления, которые были на нем.

// Extended TabControl which saves the displayed item so you don't get the performance hit of 
// unloading and reloading the VisualTree when switching tabs

// Obtained from http://eric.burke.name/dotnetmania/2009/04/26/22.09.28
// and made a some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations

[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : System.Windows.Controls.TabControl
{
    // Holds all items, but only marks the current tab's item as visible
    private Panel _itemsHolder = null;

    // Temporaily holds deleted item in case this was a drag/drop operation
    private object _deletedObject = null;

    public TabControlEx()
        : base()
    {
        // this is necessary so that we get the initial databound selected item
        this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
    }

    /// <summary>
    /// if containers are done, generate the selected item
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
    {
        if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
        {
            this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
            UpdateSelectedItem();
        }
    }

    /// <summary>
    /// get the ItemsHolder and generate any children
    /// </summary>
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel;
        UpdateSelectedItem();
    }

    /// <summary>
    /// when the items change we remove any generated panel children and add any new ones as necessary
    /// </summary>
    /// <param name="e"></param>
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        if (_itemsHolder == null)
        {
            return;
        }

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Reset:
                _itemsHolder.Children.Clear();

                if (base.Items.Count > 0)
                {
                    base.SelectedItem = base.Items[0];
                    UpdateSelectedItem();
                }

                break;

            case NotifyCollectionChangedAction.Add:
            case NotifyCollectionChangedAction.Remove:

                // Search for recently deleted items caused by a Drag/Drop operation
                if (e.NewItems != null && _deletedObject != null)
                {
                    foreach (var item in e.NewItems)
                    {
                        if (_deletedObject == item)
                        {
                            // If the new item is the same as the recently deleted one (i.e. a drag/drop event)
                            // then cancel the deletion and reuse the ContentPresenter so it doesn't have to be 
                            // redrawn. We do need to link the presenter to the new item though (using the Tag)
                            ContentPresenter cp = FindChildContentPresenter(_deletedObject);
                            if (cp != null)
                            {
                                int index = _itemsHolder.Children.IndexOf(cp);

                                (_itemsHolder.Children[index] as ContentPresenter).Tag =
                                    (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
                            }
                            _deletedObject = null;
                        }
                    }
                }

                if (e.OldItems != null)
                {
                    foreach (var item in e.OldItems)
                    {

                        _deletedObject = item;

                        // We want to run this at a slightly later priority in case this
                        // is a drag/drop operation so that we can reuse the template
                        this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
                            new Action(delegate()
                        {
                            if (_deletedObject != null)
                            {
                                ContentPresenter cp = FindChildContentPresenter(_deletedObject);
                                if (cp != null)
                                {
                                    this._itemsHolder.Children.Remove(cp);
                                }
                            }
                        }
                        ));
                    }
                }

                UpdateSelectedItem();
                break;

            case NotifyCollectionChangedAction.Replace:
                throw new NotImplementedException("Replace not implemented yet");
        }
    }

    /// <summary>
    /// update the visible child in the ItemsHolder
    /// </summary>
    /// <param name="e"></param>
    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        UpdateSelectedItem();
    }

    /// <summary>
    /// generate a ContentPresenter for the selected item
    /// </summary>
    void UpdateSelectedItem()
    {
        if (_itemsHolder == null)
        {
            return;
        }

        // generate a ContentPresenter if necessary
        TabItem item = GetSelectedTabItem();
        if (item != null)
        {
            CreateChildContentPresenter(item);
        }

        // show the right child
        foreach (ContentPresenter child in _itemsHolder.Children)
        {
            child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
        }
    }

    /// <summary>
    /// create the child ContentPresenter for the given item (could be data or a TabItem)
    /// </summary>
    /// <param name="item"></param>
    /// <returns></returns>
    ContentPresenter CreateChildContentPresenter(object item)
    {
        if (item == null)
        {
            return null;
        }

        ContentPresenter cp = FindChildContentPresenter(item);

        if (cp != null)
        {
            return cp;
        }

        // the actual child to be added.  cp.Tag is a reference to the TabItem
        cp = new ContentPresenter();
        cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
        cp.ContentTemplate = this.SelectedContentTemplate;
        cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
        cp.ContentStringFormat = this.SelectedContentStringFormat;
        cp.Visibility = Visibility.Collapsed;
        cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
        _itemsHolder.Children.Add(cp);
        return cp;
    }

    /// <summary>
    /// Find the CP for the given object.  data could be a TabItem or a piece of data
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    ContentPresenter FindChildContentPresenter(object data)
    {
        if (data is TabItem)
        {
            data = (data as TabItem).Content;
        }

        if (data == null)
        {
            return null;
        }

        if (_itemsHolder == null)
        {
            return null;
        }

        foreach (ContentPresenter cp in _itemsHolder.Children)
        {
            if (cp.Content == data)
            {
                return cp;
            }
        }

        return null;
    }

    /// <summary>
    /// copied from TabControl; wish it were protected in that class instead of private
    /// </summary>
    /// <returns></returns>
    protected TabItem GetSelectedTabItem()
    {
        object selectedItem = base.SelectedItem;
        if (selectedItem == null)
        {
            return null;
        }

        if (_deletedObject == selectedItem)
        { 

        }

        TabItem item = selectedItem as TabItem;
        if (item == null)
        {
            item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
        }
        return item;
    }
}

Шаблон TabControl, который я обычно использую, выглядит примерно так:

<Style x:Key="TabControlEx_NoHeadersStyle" TargetType="{x:Type local:TabControlEx}">
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type localControls:TabControlEx}">
                <DockPanel>
                    <!-- This is needed to draw TabControls with Bound items -->
                    <StackPanel IsItemsHost="True" Height="0" Width="0" />
                    <Grid x:Name="PART_ItemsHolder" />
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Вы также можете упростить свой XAML, используя неявный DataTemplate вместо ItemTemplate, поскольку ваша ViewModel будет помещена в ваш TabItem.Content. Я также не совсем уверен, что вы спрашиваете о заголовке, но если я правильно вас понял, вы можете просто установить заголовок в другом неявном стиле для TabItem

<Window.Resources>
    <DataTemplate DataType="{x:Type CustomerViewModel}">
       <workspace:CustomerWorkspace />
    </DataTemplate>
</Window.Resources>

<TabControl ItemsSource="{Binding Workspaces}">
    <TabControl.Resources>
        <Style TargetType="{x:Type TabItem}">
            <Setter Property="Header" Value="{Binding HeaderProperty}" />
        </Style>
    </TabControl.Resources>
</TabControl>
person Rachel    schedule 14.09.2012