绑定到 WPF 中 TabControl 的 ItemsSource [英] Binding to ItemsSource of TabControl in WPF

查看:67
本文介绍了绑定到 WPF 中 TabControl 的 ItemsSource的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试创建一个代表我所说的工作区的用户控件(来自 Josh Smith 的博客的参考).工作区将显示在选项卡控件中.我的目标是使用选项卡式界面来管理我打开的各种文档,就像在 excal 工作簿的浏览器中一样.

I am trying to create a User Control that represents what I am calling a workspace (a reference from a blog by Josh Smith). The workspaces will be displayed in a tab control. I am aiming to use a tabbed interface to manage various documents that I have open much like in a browser of an excal work book.

每次用户打开一个新工作区时,该工作区都应显示在选项卡控件中.每个工作区都采用用户控件的形式,每个工作区都有自己的视图模型.我希望 Tab Header 显示我的视图模型中的一个属性,我认为该属性可能必须通过我的用户控件作为属性公开.

Each time a user opens a new workspace, that workspace should be displayed in the tab control. Each Workspace takes the form of a user control, and each workspace has its own view model. I would like for the Tab Header to display a property from my view model which I think will likely have to be exposed as a property through my user control.

到目前为止,在遇到许多问题之前,我最喜欢的最干净的解决方案是使用数据模板.基本上我做了以下事情:

So far, the cleanest solution that I liked the best until I ran into numerous issues was by using datatemplates. Basically I Did the following:

<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 绑定到包含我所有工作区的 observablecollection(of Object).

The TabControl.ItemsSource is bound to an observablecollection(of Object) which contains all of my workspaces.

这很好用,除了两件事:

This works great except for 2 things:

  1. 如果我打开多个客户,那么我打开了多个工作区.由于 DataTemplate Recycling,当我从一个选项卡切换到另一个选项卡时,我会丢失状态.所以所有没有绑定的东西都会失去状态.

  1. If I open multiple customers, then I have multiple workspaces open. Because of DataTemplate Recycling, I lose state when i swap from one tab to another. So everything that is not bound will lose state.

在不同工作区(使用不同数据模板)之间交换的性能非常慢.

The performance of swapping between Different workspaces (that use different datatemplates) is terribly slow.

所以...我在 SO 上找到了另一个用户的建议,将用户控件添加到 ObservableCOllection 并丢弃数据模板.现在解决了失去状态的问题之一.但是,现在我面临着 2 个剩余的问题:

So... I found a suggestion from another user on SO to add the user controls to the ObservableCOllection and ditch the data templates. that now solves one of the problems of losing state. however, now I am faced with 2 remaining problems:

  1. 如何在不使用 DataTemplate 的情况下设置 TabItem.Header 属性
  2. 标签之间来回交换的速度仍然很慢,除非它们属于相同的 DataTemplate.

然后我继续在我的代码隐藏中将 TabItem 添加到 ObservableCollection 并将 TabItem.Content 属性设置为用户控件的属性.速度问题和丢失状态问题现在都被消除了,因为我已经删除了 DataTemplates 的使用.但是,我现在遇到了将 TabItem.header 绑定到应显示在选项卡标题中的用户控件的自定义标题"属性的问题.

I then proceeded to Actually add a TabItem to the ObservableCollection in my codebehind and Set the TabItem.Content Property to that of the user control. The speed issue was now eliminated as is the losing state issue since I have removed the use of the DataTemplates. However, I am now stuck with the issue of binding a TabItem.header to the Custome "Title" Property of my usercontrol that should be displayed in the Tab Header.

所以在这篇非常长的帖子之后,我的问题是:

So after this terribly long post, my questions are:

  1. 有什么方法可以使用数据模板并强制它们为集合中的每个项目创建一个新实例,以防止回收和状态丢失.

  1. Is there any way to use datatemplates and force them to create a new Instance for each item in the collection to prevent recycling and state loss.

1a.有没有比我在上面的帖子中提到的更好的选择?

1a. Is there a better alternative than what I mentioned in the post above?

有没有办法通过 Xaml 而不是通过标签项的后端代码构建来完成所有这些工作?

is there a way to do all of this through the Xaml instead of through back end code construction of Tab Items?

推荐答案

WPF 的默认行为是卸载不可见的项目,包括卸载不可见的 TabItems.这意味着当您返回选项卡时,TabItem 会重新加载,并且任何未绑定的内容(例如滚动位置、控件状态等)都将重置.

The default behavior of WPF is to unload items which are not visible, which includes unloading TabItems which are not visible. This means when you go back to the tab, the TabItem gets re-loaded, and anything not bound (such as a scroll position, control states, etc) will get reset.

有一个很好的网站 此处 包含扩展 TabControl 并阻止其销毁其 的代码TabItems 切换标签时,但现在似乎不再存在.

There was a good site here which contains code to extend the TabControl and stop it from destroying its TabItems when switching tabs, however it no longer seems to exist now.

这是代码的副本,尽管我对其进行了一些更改.它在切换选项卡时保留 TabItems 的 ContentPresenter,并在您返回页面时使用它重新绘制 TabItem.它占用更多内存,但我发现它的性能更好,因为 TabItem 不再需要重新创建其上的所有控件.

Here's a copy of the code, although I've made some changes to it. It preserves the ContentPresenter of TabItems when switching tabs, and uses it to redraw the TabItem when you go back to the page. It takes up a bit more memory, however I find it better on performance since the TabItem no longer has to re-create all the controls that were on it.

// 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 模板如下所示:

The TabControl template I usually use looks something like this:

<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>

您还可以通过使用隐式 DataTemplate 而不是 ItemTemplate 来简化您的 XAML,因为您的 ViewModel 将放置在您的 TabItem.Content 中.我也不太确定你问的是什么标题,但如果我理解正确,你可以为 TabItem

You can also simplify your XAML by using an implicit DataTemplate instead of an ItemTemplate since your ViewModel will be placed in your TabItem.Content. I'm also not too sure what you're asking about the header, but if I understand you correctly you can just set the header in another implicit style for the 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>

这篇关于绑定到 WPF 中 TabControl 的 ItemsSource的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆