web-dev-qa-db-ja.com

TabControlのタブ項目内のコントロールの状態を保持する方法

私はWPFの初心者で、Josh Smithの Model-View-ViewModel Design Pattern について説明した優れた記事の推奨に従うプロジェクトを構築しようとしています。

Joshのサンプルコードをベースにして、それぞれがTabControlのタブで表される多数の「ワークスペース」を含むシンプルなアプリケーションを作成しました。私のアプリケーションでは、ワークスペースは、階層ドキュメントをTreeViewコントロールを介して操作できるドキュメントエディターです。

複数のワークスペースを開いて、バインドされたTreeViewコントロールでドキュメントのコンテンツを表示することに成功しましたが、タブを切り替えると、TreeViewがその状態を「忘れて」しまうことがわかりました。たとえば、Tab1のTreeViewが部分的に展開されている場合、Tab2に切り替えてTab1に戻ると、完全に折りたたまれた状態で表示されます。この動作は、すべてのコントロールのコントロール状態のすべての側面に適用されるようです。

いくつかの実験を行った後、各コントロールの状態プロパティを基本となるViewModelの専用プロパティに明示的にバインドすることにより、TabItem内の状態を保持できることに気付きました。ただし、ワークスペースを切り替えるときにすべてのコントロールにその状態を記憶させたい場合、これは多くの追加作業のようです。

単純なものがないと思いますが、どこで答えを探すべきかわかりません。どんなガイダンスもいただければ幸いです。

ありがとう、ティム

更新:

要求に応じて、この問題を示すコードを投稿しようとします。ただし、TreeViewの基礎となるデータは複雑であるため、同じ症状を示す簡単な例を投稿します。メインウィンドウのXAMLは次のとおりです。

<TabControl IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Path=Docs}">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <ContentPresenter Content="{Binding Path=Name}" />
        </DataTemplate>
    </TabControl.ItemTemplate>

    <TabControl.ContentTemplate>
        <DataTemplate>
            <view:DocumentView />
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

上記のXAMLはDocumentViewModelのObservableCollectionに正しくバインドされ、各メンバーはDocumentViewを介して提示されます。

この例を簡単にするために、DocumentViewからTreeView(上記)を削除し、3つの固定タブを含むTabControlに置き換えました。

<TabControl>
    <TabItem Header="A" />
    <TabItem Header="B" />
    <TabItem Header="C" />
</TabControl>

このシナリオでは、DocumentViewとDocumentViewModelの間にバインディングはありません。コードが実行されると、外側のTabControlが切り替えられたときに、内側のTabControlはその選択を記憶できません。

ただし、内側のTabControlのSelectedIndexプロパティを明示的にバインドすると...

<TabControl SelectedIndex="{Binding Path=SelectedDocumentIndex}">
    <TabItem Header="A" />
    <TabItem Header="B" />
    <TabItem Header="C" />
</TabControl>

... DocumentViewModelの対応するダミープロパティに...

public int SelecteDocumentIndex { get; set; }

...内部タブはその選択を記憶することができます。

この手法をすべてのコントロールのすべての視覚的プロパティに適用することで問題を効果的に解決できることを理解していますが、よりエレガントな解決策があることを願っています。

39
Tim Coulter

WPFアプリケーションフレームワーク(WAF) のWriterサンプルアプリケーションは、問題を解決する方法を示しています。すべてのTabItemに新しいUserControlを作成します。したがって、ユーザーがアクティブなタブを変更しても状態は保持されます。

1
jbe

私は同じ問題を抱えており、 Nice solution を見つけました。テストした限り、通常のTabControlのように使用できます。ここであなたにとって重要な場合 現在のライセンス

ここで、リンクがダウンした場合のコード:

using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace CefSharp.Wpf.Example.Controls
{
    /// <summary>
    /// Extended TabControl which saves the displayed item so you don't get the performance hit of
    /// unloading and reloading the VisualTree when switching tabs
    /// </summary>
    /// <remarks>
    /// Based on example from http://stackoverflow.com/a/9802346, which in turn is based on
    /// http://www.pluralsight-training.net/community/blogs/eburke/archive/2009/04/30/keeping-the-wpf-tab-control-from-destroying-its-children.aspx
    /// with some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations
    /// </remarks>
    [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
    public class NonReloadingTabControl : TabControl
    {
        private Panel itemsHolderPanel;

        public NonReloadingTabControl()
        {
            // This is necessary so that we get the initial databound selected item
            ItemContainerGenerator.StatusChanged += ItemContainerGeneratorStatusChanged;
        }

        /// <summary>
        /// If containers are done, generate the selected item
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
        private void ItemContainerGeneratorStatusChanged(object sender, EventArgs e)
        {
            if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                ItemContainerGenerator.StatusChanged -= ItemContainerGeneratorStatusChanged;
                UpdateSelectedItem();
            }
        }

        /// <summary>
        /// Get the ItemsHolder and generate any children
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            itemsHolderPanel = 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">The <see cref="NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
        protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
        {
            base.OnItemsChanged(e);

            if (itemsHolderPanel == null)
                return;

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

                case NotifyCollectionChangedAction.Add:
                case NotifyCollectionChangedAction.Remove:
                if (e.OldItems != null)
                {
                    foreach (var item in e.OldItems)
                    {
                        var cp = FindChildContentPresenter(item);
                        if (cp != null)
                            itemsHolderPanel.Children.Remove(cp);
                    }
                }

                // Don't do anything with new items because we don't want to
                // create visuals that aren't being shown

                UpdateSelectedItem();
                break;

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

        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            UpdateSelectedItem();
        }

        private void UpdateSelectedItem()
        {
            if (itemsHolderPanel == null)
                return;

            // Generate a ContentPresenter if necessary
            var item = GetSelectedTabItem();
            if (item != null)
                CreateChildContentPresenter(item);

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

        private ContentPresenter CreateChildContentPresenter(object item)
        {
            if (item == null)
                return null;

            var cp = FindChildContentPresenter(item);

            if (cp != null)
                return cp;

            var tabItem = item as TabItem;
            cp = new ContentPresenter
            {
                Content = (tabItem != null) ? tabItem.Content : item,
                ContentTemplate = this.SelectedContentTemplate,
                ContentTemplateSelector = this.SelectedContentTemplateSelector,
                ContentStringFormat = this.SelectedContentStringFormat,
                Visibility = Visibility.Collapsed,
                Tag = tabItem ?? (this.ItemContainerGenerator.ContainerFromItem(item))
            };
            itemsHolderPanel.Children.Add(cp);
            return cp;
        }

        private ContentPresenter FindChildContentPresenter(object data)
        {
            if (data is TabItem)
                data = (data as TabItem).Content;

            if (data == null)
                return null;

            if (itemsHolderPanel == null)
                return null;

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

            return null;
        }

        protected TabItem GetSelectedTabItem()
        {
            var selectedItem = SelectedItem;
            if (selectedItem == null)
                return null;

            var item = selectedItem as TabItem ?? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as TabItem;

            return item;
        }
    }
}

Copietimeでのライセンス

// Copyright © 2010-2016 The CefSharp Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//    * Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//
//    * Redistributions in binary form must reproduce the above
//      copyright notice, this list of conditions and the following disclaimer
//      in the documentation and/or other materials provided with the
//      distribution.
//
//    * Neither the name of Google Inc. nor the name Chromium Embedded
//      Framework nor the name CefSharp nor the names of its contributors
//      may be used to endorse or promote products derived from this software
//      without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3
WiiMaxx

このヒントで解決しましたWPF TabControl: Turning Off Tab Virtualization at http://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization これは、IsCachedプロパティを持つTabContentのクラスです。

3
Alexsandro

上記の@Arsenの回答に基づいて、以下に別の動作を示します。

  1. 追加の参照は必要ありません。 (コードを外部ライブラリに配置しない限り)
  2. 基本クラスは使用しません。
  3. これは、コレクションの変更のリセットと追加の両方を処理します。

使用するには

Xamlで名前空間を宣言します。

<ResourceDictionary
    ...
    xmlns:behaviors="clr-namespace:My.Behaviors;Assembly=My.Wpf.Assembly"
    ...
    >

スタイルを更新します。

<Style TargetType="TabControl" x:Key="TabControl">
    ...
    <Setter Property="behaviors:TabControlBehavior.DoNotCacheControls" Value="True" />
    ...
</Style>

Or TabControlを直接更新します。

<TabControl behaviors:TabControlBehavior.DoNotCacheControls="True" ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}">

そして、これが動作のコードです:

using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;

namespace My.Behaviors
{
    /// <summary>
    /// Wraps tab item contents in UserControl to prevent TabControl from re-using its content
    /// </summary>
    public class TabControlBehavior
    {
        private static readonly HashSet<TabControl> _tabControls = new HashSet<TabControl>();
        private static readonly Dictionary<ItemCollection, TabControl> _tabControlItemCollections = new Dictionary<ItemCollection, TabControl>();

        public static bool GetDoNotCacheControls(TabControl tabControl)
        {
            return (bool)tabControl.GetValue(DoNotCacheControlsProperty);
        }

        public static void SetDoNotCacheControls(TabControl tabControl, bool value)
        {
            tabControl.SetValue(DoNotCacheControlsProperty, value);
        }

        public static readonly DependencyProperty DoNotCacheControlsProperty = DependencyProperty.RegisterAttached(
            "DoNotCacheControls",
            typeof(bool),
            typeof(TabControlBehavior),
            new UIPropertyMetadata(false, OnDoNotCacheControlsChanged));

        private static void OnDoNotCacheControlsChanged(
            DependencyObject depObj,
            DependencyPropertyChangedEventArgs e)
        {
            var tabControl = depObj as TabControl;
            if (null == tabControl)
                return;
            if (e.NewValue is bool == false)
                return;

            if ((bool)e.NewValue)
                Attach(tabControl);
            else
                Detach(tabControl);
        }

        private static void Attach(TabControl tabControl)
        {
            if (!_tabControls.Add(tabControl))
                return;
            _tabControlItemCollections.Add(tabControl.Items, tabControl);
            ((INotifyCollectionChanged)tabControl.Items).CollectionChanged += TabControlUcWrapperBehavior_CollectionChanged;
        }

        private static void Detach(TabControl tabControl)
        {
            if (!_tabControls.Remove(tabControl))
                return;
            _tabControlItemCollections.Remove(tabControl.Items);
            ((INotifyCollectionChanged)tabControl.Items).CollectionChanged -= TabControlUcWrapperBehavior_CollectionChanged;
        }

        private static void TabControlUcWrapperBehavior_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            var itemCollection = (ItemCollection)sender;
            var tabControl = _tabControlItemCollections[itemCollection];
            IList items;
            if (e.Action == NotifyCollectionChangedAction.Reset)
            {   /* our ObservableArray<T> swops out the whole collection */
                items = (ItemCollection)sender;
            }
            else
            {
                if (e.Action != NotifyCollectionChangedAction.Add)
                    return;

                items = e.NewItems;
            }

            foreach (var newItem in items)
            {
                var ti = tabControl.ItemContainerGenerator.ContainerFromItem(newItem) as TabItem;
                if (ti != null)
                {
                    var userControl = ti.Content as UserControl;
                    if (null == userControl)
                        ti.Content = new UserControl { Content = ti.Content };
                }
            }
        }
    }
}
2

WAFのアイデアを使用して、私は問題を解決しているように見えるこの単純なソリューションにたどり着きました。

私はインタラクティビティ動作を使用していますが、インタラクティビティライブラリが参照されていない場合は、添付プロパティでも同じことができます

/// <summary>
/// Wraps tab item contents in UserControl to prevent TabControl from re-using its content
/// </summary>
public class TabControlUcWrapperBehavior 
    : Behavior<UIElement>
{
    private TabControl AssociatedTabControl { get { return (TabControl) AssociatedObject; } }

    protected override void OnAttached()
    {
        ((INotifyCollectionChanged)AssociatedTabControl.Items).CollectionChanged += TabControlUcWrapperBehavior_CollectionChanged;
        base.OnAttached();
    }

    protected override void OnDetaching()
    {
        ((INotifyCollectionChanged)AssociatedTabControl.Items).CollectionChanged -= TabControlUcWrapperBehavior_CollectionChanged;
        base.OnDetaching();
    }

    void TabControlUcWrapperBehavior_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action != NotifyCollectionChangedAction.Add) 
            return;

        foreach (var newItem in e.NewItems)
        {
            var ti = AssociatedTabControl.ItemContainerGenerator.ContainerFromItem(newItem) as TabItem;

            if (ti != null && !(ti.Content is UserControl)) 
                ti.Content = new UserControl { Content = ti.Content };
        }
    }
}

そして使い方

<TabControl ItemsSource="...">
    <i:Interaction.Behaviors>
        <controls:TabControlUcWrapperBehavior/>
    </i:Interaction.Behaviors>
</TabControl>
1
Arsen Mkrtchyan