programing

TabControl의 탭 항목 내 제어 상태를 유지하는 방법

showcode 2023. 4. 15. 09:41
반응형

TabControl의 탭 항목 내 제어 상태를 유지하는 방법

저는 WPF에 입사한 지 얼마 안 된 사람입니다.조쉬 스미스의 훌륭한 기사에서 모델-뷰-뷰-모델 디자인 패턴을 설명하는 추천을 따르는 프로젝트를 시도하고 있습니다.

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의 Selected를 명시적으로 바인드한 경우인덱스 속성...

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

...DocumentViewModel의 대응하는 더미 속성에...

public int SelecteDocumentIndex { get; set; }

...inner 탭은 선택을 기억할 수 있습니다.

이 기술을 모든 컨트롤의 모든 시각적 특성에 적용함으로써 문제를 효과적으로 해결할 수 있다는 것은 이해하지만, 보다 우아한 해결책이 있기를 바랍니다.

했습니다.WPF TabControl: Turning Off Tab Virtualizationhttp://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization에서 IsCached 속성을 가진 TabContent 클래스입니다.

저도 같은 문제가 있어서 평소처럼 사용할 수 있는 좋은 해결책을 찾았습니다.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.

위의 @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>

또는 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 };
                }
            }
        }
    }
}

WPF Application Framework(WAF; WPF 애플리케이션 프레임워크)의 Writer 샘플애플리케이션에는, 문제를 해결하는 방법이 표시됩니다.모든 TabItem에 대해 새로운 UserControl을 만듭니다.따라서 사용자가 활성 탭을 변경할 때 상태가 유지됩니다.

저는 WAF의 아이디어를 이용하여 이 문제를 해결할 수 있을 것 같은 간단한 해결책에 도달했습니다.

Interactivity Behavior를 사용하지만 Interactivity 라이브러리가 참조되지 않으면 연결된 속성에서도 동일한 작업을 수행할 수 있습니다.

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

비슷한 질문에 대한 답변을 올렸습니다.내 경우 수동으로 TabItems를 생성하면 보기를 여러 번 생성하는 문제가 해결되었습니다.여기를 체크해 주세요

언급URL : https://stackoverflow.com/questions/2080764/how-to-preserve-control-state-within-tab-items-in-a-tabcontrol

반응형