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 Virtualization
http://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의 답변을 바탕으로 다음 동작을 나타냅니다.
- 추가 참조가 필요 없습니다.(외부 라이브러리에 코드를 입력하지 않는 한)
- 기본 클래스는 사용하지 않습니다.
- 수집의 리셋과 추가의 변경을 모두 처리합니다.
사용하기 위해서
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
'programing' 카테고리의 다른 글
데이터베이스가 현재 사용 중이므로 삭제할 수 없습니다. (0) | 2023.04.15 |
---|---|
응용 프로그램에서 stdout을 파이프가 아닌 터미널로 생각하도록 속이는 무엇입니까? (0) | 2023.04.15 |
AVFundation AVPlayer를 사용한 비디오 루프 (0) | 2023.04.15 |
WPF 사용자 제어에서 Import된 리소스와 로컬 리소스를 결합하는 방법 (0) | 2023.04.15 |
WPF 페이지 또는 UserControl 개체에서 KeyDown 이벤트를 캡처하려면 어떻게 해야 합니까? (0) | 2023.04.15 |