Остановить TabControl от воссоздания своих детей
У меня есть IList
моделей, которые связаны с TabControl
, это IList
не изменится в течение срока службы TabControl
,
<TabControl ItemsSource="{Binding Tabs}" SelectedIndex="0" >
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Content" Value="{Binding}" />
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
Каждая модель имеет DataTemplate
который указан в ResourceDictionary
,
<DataTemplate TargetType={x:Type vm:MyViewModel}>
<v:MyView/>
</DataTemplate>
Каждое из представлений, указанных в DataTemplate, достаточно ресурсоемкое, чтобы создать его. Я бы предпочел создать каждое представление только один раз, но когда я переключаю вкладки, вызывается конструктор для соответствующего представления. Из того, что я прочитал, это ожидаемое поведение для TabControl
, но мне не ясно, что это за механизм, который вызывает конструктор.
Я взглянул на похожий вопрос, который используетUserControl
Но решение, предложенное там, потребовало бы от меня привязки к взглядам, что нежелательно.
6 ответов
По умолчанию TabControl
делится панелью для отображения ее содержимого. Чтобы делать то, что вы хотите (и многие другие разработчики WPF), вам нужно расширить TabControl
вот так:
TabControlEx.cs
[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : TabControl
{
private Panel ItemsHolderPanel = null;
public TabControlEx()
: base()
{
// This is necessary so that we get the initial databound selected item
ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
/// <summary>
/// If containers are done, generate the selected item
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private 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();
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"></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)
{
ContentPresenter 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
TabItem 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;
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));
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()
{
object selectedItem = base.SelectedItem;
if (selectedItem == null)
return null;
TabItem item = selectedItem as TabItem;
if (item == null)
item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
return item;
}
}
XAML
<Style TargetType="{x:Type controls:TabControlEx}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid Background="{TemplateBinding Background}" ClipToBounds="True" KeyboardNavigation.TabNavigation="Local" SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnDefinition0" />
<ColumnDefinition x:Name="ColumnDefinition1" Width="0" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="RowDefinition0" Height="Auto" />
<RowDefinition x:Name="RowDefinition1" Height="*" />
</Grid.RowDefinitions>
<DockPanel Margin="2,2,0,0" LastChildFill="False">
<TabPanel x:Name="HeaderPanel" Margin="0,0,0,-1" VerticalAlignment="Bottom" Panel.ZIndex="1" DockPanel.Dock="Right"
IsItemsHost="True" KeyboardNavigation.TabIndex="1" />
</DockPanel>
<Border x:Name="ContentPanel" Grid.Row="1" Grid.Column="0"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
KeyboardNavigation.DirectionalNavigation="Contained" KeyboardNavigation.TabIndex="2" KeyboardNavigation.TabNavigation="Local">
<Grid x:Name="PART_ItemsHolder" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Примечание: я не придумал это решение. Он был опубликован на форумах по программированию в течение нескольких лет, и я считаю, что сейчас он входит в число тех, кто пишет о рецептах WPF Я полагаю, что самым старым или оригинальным источником был пост в блоге PluralSight .NET и этот ответ на Stackru.
НТН,
Ответ от Dennis
превосходно, и работал очень хорошо для меня. Тем не менее, оригинальная статья, о которой говорится в его посте, теперь отсутствует, поэтому для его ответа нужно немного больше информации, чтобы его можно было использовать прямо из коробки.
Этот ответ дан с точки зрения MVVM и был протестирован в рамках VS 2013.
Сначала немного предыстории. Способ первого ответа от Dennis
работает то, что он скрывает и показывает содержимое вкладки, вместо того, чтобы уничтожать и воссоздавать указанное содержимое вкладки, каждый раз, когда пользователь переключает вкладку.
Это имеет следующие преимущества:
- Содержимое полей редактирования не исчезает при переключении вкладки.
- Если вы используете древовидное представление на вкладке, оно не сворачивается между изменениями вкладок.
- Текущий выбор для любых сеток сохраняется между вкладками.
- Этот код более соответствует стилю программирования MVVM.
- Нам не нужно писать код для сохранения и загрузки настроек на вкладке между изменениями вкладок.
- Если вы используете сторонний элемент управления (например, Telerik или DevExpress), такие параметры, как расположение сетки, сохраняются между переключателями вкладок.
- Значительные улучшения производительности - переключение вкладок происходит практически мгновенно, поскольку мы не перерисовываем все при каждом изменении вкладки.
TabControlEx.cs
// Copy C# code from @Dennis's answer, and add the following property after the
// opening "<Style" tag (this sets the key for the style):
// x:Key="TabControlExStyle"
// Ensure that the namespace for this class is the same as your DataContext.
Это входит в тот же класс, на который указывает DataContext.
XAML
// Copy XAML from @Dennis's answer.
Это стиль. Он входит в заголовок файла XAML. Этот стиль никогда не меняется, и на него ссылаются все элементы управления вкладками.
Оригинальная вкладка
Ваша оригинальная вкладка может выглядеть примерно так. Если вы переключите вкладки, вы заметите, что содержимое полей редактирования исчезнет, так как содержимое вкладки будет удалено и заново создано.
<TabControl
behaviours:TabControlBehaviour.DoSetSelectedTab="True"
IsSynchronizedWithCurrentItem="True">
<TabItem Header="Tab 1">
<TextBox>Hello</TextBox>
</TabItem>
<TabItem Header="Tab 2" >
<TextBox>Hello 2</TextBox>
</TabItem>
Пользовательская вкладка
Измените вкладку, чтобы использовать наш новый пользовательский класс C#, и укажите его на наш новый пользовательский стиль, используя Style
тег:
<sdm:TabControlEx
behaviours:TabControlBehaviour.DoSetSelectedTab="True"
IsSynchronizedWithCurrentItem="True"
Style="{StaticResource TabControlExStyle}">
<TabItem Header="Tab 1">
<TextBox>Hello</TextBox>
</TabItem>
<TabItem Header="Tab 2" >
<TextBox>Hello 2</TextBox>
</TabItem>
Теперь, когда вы переключаете вкладки, вы обнаружите, что содержимое полей редактирования сохраняется, что доказывает, что все работает хорошо.
Обновить
Это решение работает очень хорошо. Тем не менее, существует более модульный и дружественный MVVM способ сделать это, который использует прикрепленное поведение для достижения того же результата. См. Проект кода: WPF TabControl: Отключение виртуализации вкладок. Я добавил это как дополнительный ответ.
Обновить
Если вы используете DevExpress
, вы можете использовать CacheAllTabs
возможность получить тот же эффект (это отключает виртуализацию вкладок):
<dx:DXTabControl TabContentCacheMode="CacheAllTabs">
<dx:DXTabItem Header="Tab 1" >
<TextBox>Hello</TextBox>
</dx:DXTabItem>
<dx:DXTabItem Header="Tab 2">
<TextBox>Hello 2</TextBox>
</dx:DXTabItem>
</dx:DXTabControl>
Для справки, я не связан с DevExpress, я уверен, что у Telerik есть аналог.
Это существующее решение @Dennis (с дополнительными примечаниями @Gravitas) работает очень хорошо.
Однако есть другое решение, которое является более модульным и дружественным к MVVM, так как для достижения того же результата используется прикрепленное поведение.
См. Проект кода: WPF TabControl: Отключение виртуализации вкладок. Поскольку автор является техническим лидером в Reuters, код, вероятно, надежен.
Демонстрационный код действительно хорошо составлен, он показывает обычный TabControl, а также код с прикрепленным поведением.
Пожалуйста, проверьте мой ответ из этого поста в SO. Надеюсь, что это решит проблему, но это немного от дороги MVVM. Ссылка на сайт
Существует не очень очевидное, но элегантное решение. Основная идея состоит в том, чтобы вручную создать свойство VisualTree for Content для TabItem через пользовательский конвертер.
Определите некоторые ресурсы
<Window.Resources>
<converters:ContentGeneratorConverter x:Key="ContentGeneratorConverter"/>
<DataTemplate x:Key="ItemDataTemplate">
<StackPanel>
<TextBox Text="Try to change this text and choose another tab"/>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
<markup:Set x:Key="Items">
<system:String>Red</system:String>
<system:String>Green</system:String>
<system:String>Blue</system:String>
</markup:Set>
</Window.Resources>
где
public class ContentGeneratorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var control = new ContentControl {ContentTemplate = (DataTemplate) parameter};
control.SetBinding(ContentControl.ContentProperty, new Binding());
return control;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new NotImplementedException();
}
и Set это что-то вроде этого
public class Set : List<object> { }
Тогда вместо классического использования свойства ContentTemplate
<TabControl
ItemsSource="{StaticResource Items}"
ContentTemplate="{StaticResource ItemDataTemplate}">
</TabControl>
мы должны указать ItemContainerStyle следующим образом
<TabControl
ItemsSource="{StaticResource Items}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="Content" Value="{Binding Converter={StaticResource ContentGeneratorConverter}, ConverterParameter={StaticResource ItemDataTemplate}}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
Теперь попробуйте сравнить оба варианта, чтобы увидеть разницу в поведении TextBox в ItemDataTemplate при переключении вкладок.
@Dennis
ответил хорошо работает для меня.
Единственная небольшая «проблема» заключается в том, что автоматизация Windows не работает с TabControlEx при реализации автоматизированных тестов пользовательского интерфейса. Симптомом будет метод AutomationElement.FindFirst(TreeScope, Condition), всегда возвращающий значение null
Чтобы исправить это, я бы добавил
public class TabControlEx : TabControl
{
// Dennis' version here
...
public Panel ItemsHolderPanel => _itemsHolderPanel;
protected override AutomationPeer OnCreateAutomationPeer()
{
return new TabControlExAutomationPeer(this);
}
}
С этим добавлены новые типы:
public class TabControlExAutomationPeer : TabControlAutomationPeer
{
public TabControlExAutomationPeer(TabControlEx owner) : base(owner)
{
}
protected override ItemAutomationPeer CreateItemAutomationPeer(object item)
{
return new TabItemExAutomationPeer(item, this);
}
}
public class TabItemExAutomationPeer : TabItemAutomationPeer
{
public TabItemExAutomationPeer(object owner, TabControlExAutomationPeer tabControlExAutomationPeer)
: base(owner, tabControlExAutomationPeer)
{
}
protected override List<AutomationPeer> GetChildrenCore()
{
var headerChildren = base.GetChildrenCore();
if (ItemsControlAutomationPeer.Owner is TabControlEx parentTabControl)
{
var contentHost = parentTabControl.ItemsHolderPanel;
if (contentHost != null)
{
AutomationPeer contentHostPeer = new FrameworkElementAutomationPeer(contentHost);
var contentChildren = contentHostPeer.GetChildren();
if (contentChildren != null)
{
if (headerChildren == null)
headerChildren = contentChildren;
else
headerChildren.AddRange(contentChildren);
}
}
}
return headerChildren;
}
}