Вопрос по binding, datatemplate, wpf, tabitem, tabcontrol – Привязка к ItemsSource TabControl в WPF

1

Я в безвыходном положении (без каламбура), когда дело касается моих текущих потребностей в связывании в WPF Я провел большую часть дня, пытаясь исследовать свою проблему, и я не могу найти надежного решения своей проблемы. вот:

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

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

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

<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 связан с наблюдаемой коллекцией (объекта), которая содержит все мои рабочие пространства.

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

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 оставшимися проблемами:

How do i set the TabItem.Header property without using a DataTemplate The speed of swapping back and forth between tabs is still slow unless they are of the same DataTemplate.

Затем я приступил к фактическому добавлению TabItem к ObservableCollection в моем коде и установил свойство TabItem.Content для пользовательского элемента управления. Проблема скорости теперь устранена, так же как и проблема потери состояния, поскольку я прекратил использование шаблонов данных. Однако теперь я застрял с проблемой привязки TabItem.header к Custome & quot; Заголовок & quot; Свойство моего usercontrol, которое должно отображаться в заголовке вкладки.

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

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. Is there a better alternative than what I mentioned in the post above?

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

Ваш Ответ

1   ответ
5

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

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

Вот копия кода, хотя я внес в нее некоторые изменения. Это сохраняетContentPresenter TabItems при переключении вкладок, и использует его для перерисовки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>

Похожие вопросы