Empêcher TabControl de recréer ses enfants
j'ai un IList
de modèles de vue qui sont liés à un TabControl
. Ce IList
ne changera pas pendant la durée de vie du TabControl
.
<TabControl ItemsSource="{Binding Tabs}" SelectedIndex="0" >
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Content" Value="{Binding}" />
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
chaque modèle a un DataTemplate
qui est spécifié dans un ResourceDictionary
.
<DataTemplate TargetType={x:Type vm:MyViewModel}>
<v:MyView/>
</DataTemplate>
chacune des vues spécifiées dans DataTemplate sont assez intensives en ressources pour créer que je préférerais créer chaque vue juste une fois, mais quand je change les onglets, le constructeur de la fenêtre concernée est appelée. De ce que j'ai lu, c'est le comportement attendu pour le TabControl
, mais il n'est pas clair pour moi ce que le mécanisme qui appelle le constructeur.
j'ai jeté un coup d'oeil à une question similaire qui utilise UserControl
s mais la solution offerte là me demanderait de lier à des vues qui est indésirable.
4 réponses
par défaut, le TabControl
partage un panneau pour rendre son contenu. Pour faire ce que vous voulez (et beaucoup d'autres développeurs WPF), vous devez étendre TabControl
comme ceci:
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>
Note: Je n'ai pas trouvé cette solution. Il est partagé dans les forums de programmation depuis plusieurs années et je crois que c'est maintenant dans un de ces livres de recettes de WPF. La source la plus ancienne ou originale pour I believe était PluralSight .net blog post et ce réponse sur StackOverflow .
HTH,
La réponse par Dennis
est superbe, et il a travaillé très bien pour moi. Cependant, l'article original mentionné dans son post est maintenant manquant, de sorte que sa réponse a besoin d'un peu plus d'informations pour être utilisable directement de la boîte.
cette réponse est donnée d'un point de vue MVVM, et a été testée sous VS 2013.
D'abord, un peu de fond. La façon dont la première réponse de Dennis
fonctionne est qu'il se cache et montre le contenu de l'onglet, au lieu de détruire et recréer dit onglet contenu, chaque fois que l'utilisateur passe d'un onglet.
cela présente les avantages suivants:
- Le contenu des boîtes d'édition ne disparaissent pas lorsque l'onglet est activé.
- si vous utilisez une vue arborescente dans un onglet, elle ne s'effondre pas entre les changements d'onglet.
- la sélection courante pour toute grille est conservée entre les commutateurs tab.
- ce code est plus agréable avec un style de programmation MVVM.
- nous n'avons pas à écrire de code pour enregistrer et charger les paramètres sur un onglet entre les changements d'onglet.
- si vous utilisez un contrôle tiers (comme Telerik ou DevExpress), les paramètres comme la disposition de la grille sont conservés entre les commutateurs tab.
- grande amélioration de la performance - la commutation tab est pratiquement instantanée, car nous ne sommes pas en train de tout redessiner le temps d'un onglet changements.
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.
cela va dans la même classe que pointée par le DataContext.
XAML
// Copy XAML from @Dennis's answer.
C'est un style. Il va dans l'en-tête du fichier XAML. Ce style ne change jamais, et est référencé par tous les contrôles d'onglets.
d'Origine Tab
votre onglet original pourrait ressembler à quelque chose comme ça. Si vous changez d'onglet, vous remarquerez que le contenu des boîtes d'édition disparaîtra, car le contenu de l'onglet est abandonné et recréé à nouveau.
<TabControl
behaviours:TabControlBehaviour.DoSetSelectedTab="True"
IsSynchronizedWithCurrentItem="True">
<TabItem Header="Tab 1">
<TextBox>Hello</TextBox>
</TabItem>
<TabItem Header="Tab 2" >
<TextBox>Hello 2</TextBox>
</TabItem>
Onglet Personnalisé
modifier l'onglet pour utiliser notre nouvelle classe custom C#, et le point à notre nouveau style custom en utilisant le Style
tag:
<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>
Maintenant, lorsque vous changez les onglets, vous constaterez que le contenu des boîtes d'édition sont conservés, ce qui prouve que tout fonctionne bien.
mise à Jour
Cette solution fonctionne très bien. Cependant, il existe une façon plus modulaire et plus conviviale de le faire, qui utilise un comportement attaché pour obtenir le même résultat. Voir Le Code Projet: WPF TabControl: désactiver la virtualisation des onglets . J'ai ajouté cela comme une réponse supplémentaire.
mise à Jour
si vous utilisez DevExpress
, vous pouvez utiliser l'option CacheAllTabs
pour obtenir le même effet (ce qui éteint la virtualisation de l'onglet):
<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>
pour info, Je ne suis pas affilié à DevExpress, Je suis sûr que Telerik a l'équivalent.
cette solution existante de @Dennis (avec des notes supplémentaires de @Gravitas) fonctionne très bien.
cependant, il existe une autre solution plus modulaire et plus conviviale PUISQU'elle utilise un comportement attaché pour obtenir le même résultat.
Voir Code Projet: WPF TabControl: la désactivation de l'Onglet de la Virtualisation . Comme l'auteur est responsable technique chez Reuters, le code est probablement solide.
le code de démo est vraiment bien assemblé, il montre un TabControl régulier, à côté de celui avec le comportement attaché.