usar pestañas como c# wpf mvvm datatemplate tabcontrol

c# - pestañas - Evite que TabControl recree sus hijos



como usar tabcontrol c# (4)

Tengo un IList de viewmodels que están vinculados a un TabControl . Este IList no cambiará durante la vida útil de TabControl .

<TabControl ItemsSource="{Binding Tabs}" SelectedIndex="0" > <TabControl.ItemContainerStyle> <Style TargetType="TabItem"> <Setter Property="Content" Value="{Binding}" /> </Style> </TabControl.ItemContainerStyle> </TabControl>

Cada modelo de vista tiene un DataTemplate que se especifica en un ResourceDictionary .

<DataTemplate TargetType={x:Type vm:MyViewModel}> <v:MyView/> </DataTemplate>

Cada una de las vistas especificadas en DataTemplate requiere un uso intensivo de recursos para crear que prefiero crear cada vista solo una vez, pero cuando cambio las pestañas, se llama al constructor para la vista relevante. Según lo que he leído, este es el comportamiento esperado de TabControl , pero no me queda claro cuál es el mecanismo que llama al constructor.

He echado un vistazo a una pregunta similar que utiliza UserControl s, pero la solución ofrecida allí me obligaría a enlazar las vistas, lo cual es indeseable.


Esta solución existente de @Dennis (con notas adicionales de @Gravitas) funciona muy bien.

Sin embargo, hay otra solución que es más modular y amigable con MVVM ya que usa un comportamiento adjunto para lograr el mismo resultado.

Ver Proyecto de Código: WPF TabControl: desactivar la virtualización de pestañas . Como el autor es líder técnico en Reuters, el código probablemente sea sólido.

El código de demostración está muy bien organizado, muestra un TabControl regular, junto con el que tiene el comportamiento adjunto.


La respuesta de Dennis es excelente y funcionó muy bien para mí. Sin embargo, el artículo original al que se hace referencia en su publicación ahora no se encuentra, por lo que su respuesta necesita un poco más de información para poder utilizarla de inmediato.

Esta respuesta se da desde un punto de vista MVVM, y se probó en VS 2013.

Primero, un poco de fondo. La primera respuesta de Dennis es que oculta y muestra los contenidos de las pestañas, en lugar de destruir y recrear dichos contenidos de pestañas, cada vez que el usuario cambia de pestaña.

Esto tiene las siguientes ventajas:

  • El contenido de los cuadros de edición no desaparece cuando se cambia la pestaña.
  • Si está utilizando una vista en árbol en una pestaña, no colapsará entre cambios de pestaña.
  • La selección actual para cualquier cuadrícula se conserva entre los interruptores de tabulación.
  • Este código es más aceptable con un estilo de programación MVVM.
  • No tenemos que escribir el código para guardar y cargar la configuración en una pestaña entre los cambios de pestaña.
  • Si está utilizando un control de terceros (como Telerik o DevExpress), las configuraciones como el diseño de la cuadrícula se conservan entre los modificadores de pestañas.
  • Grandes mejoras de rendimiento: el cambio de pestañas es prácticamente instantáneo, ya que no estamos redibujando todo cada vez que cambia una pestaña.

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.

Esto entra en la misma clase que apunta el DataContext.

XAML

// Copy XAML from @Dennis''s answer.

Este es un estilo. Va al encabezado del archivo XAML. Este estilo nunca cambia, y todos los controles de pestañas lo mencionan.

Pestaña original

Tu pestaña original podría verse más o menos así. Si cambia de pestaña, notará que el contenido de los cuadros de edición desaparecerá, ya que los contenidos de la pestaña se eliminarán y volverán a crear.

<TabControl behaviours:TabControlBehaviour.DoSetSelectedTab="True" IsSynchronizedWithCurrentItem="True"> <TabItem Header="Tab 1"> <TextBox>Hello</TextBox> </TabItem> <TabItem Header="Tab 2" > <TextBox>Hello 2</TextBox> </TabItem>

Pestaña personalizada

Altere la pestaña para usar nuestra nueva clase C # personalizada, y apúntela a nuestro nuevo estilo personalizado con la etiqueta 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>

Ahora, cuando cambias de pestaña, encontrarás que el contenido de los cuadros de edición se conserva, lo que demuestra que todo está funcionando bien.

Actualizar

Esta solución funciona muy bien. Sin embargo, hay una forma más modular y amigable de MVVM para hacer esto, que usa un comportamiento adjunto para lograr el mismo resultado. Ver Proyecto de Código: WPF TabControl: desactivar la virtualización de pestañas . He agregado esto como una respuesta adicional.

Actualizar

Si usa DevExpress , puede usar la opción CacheAllTabs para obtener el mismo efecto (esto desactiva la virtualización de pestañas):

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

Para el registro, no estoy afiliado a DevExpress, estoy seguro de que Telerik tiene el equivalente.


Por defecto, TabControl comparte un panel para representar su contenido. Para hacer lo que quiera (y muchos otros desarrolladores de WPF), necesita extender TabControl manera:

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>

Nota: No se me ocurrió esta solución. Se ha compartido en foros de programación durante varios años y creo que ahora es uno de esos libros de recetas de WPF. La fuente más antigua u original de lo que creo fue la publicación de blog PluralSight .NET y esta respuesta en .

HTH,


Por favor revisa mi respuesta de esta publicación en SO. Espero que eso solucione el problema, pero está un poco alejado de la carretera de MVVM. Link