Подтвердить что ты не робот

Остановить 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 s, но предлагаемое там решение потребует от меня привязки к представлениям, которые нежелательны.

4b9b3361

Ответ 1

По умолчанию 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 и этот fooobar.com/questions/141608/....

НТН,

Ответ 2

Ответ от Dennis превосходен и очень хорошо сработал для меня. Тем не менее, оригинальная статья, о которой говорится в его посте, теперь отсутствует, поэтому его ответу нужно немного больше информации, чтобы его можно было использовать прямо из коробки.

Этот ответ дан с точки зрения MVVM и был протестирован в рамках VS 2013.

Сначала немного предыстории. Первый ответ Dennis заключается в том, что он скрывает и показывает содержимое вкладки, а не уничтожает и воссоздает указанное содержимое вкладки каждый раз, когда пользователь переключает вкладку.

Это имеет следующие преимущества:

  • Содержимое полей редактирования не исчезает при переключении вкладки.
  • Если вы используете древовидное представление на вкладке, оно не сворачивается между изменениями вкладок.
  • Текущий выбор для любых сеток сохраняется между вкладками.
  • Этот код более соответствует стилю программирования MVVM.
  • Нам не нужно писать код для сохранения и загрузки настроек на вкладке между изменениями вкладок.
  • Если вы используете сторонний элемент управления (например, Telerik или DevExpress), такие параметры, как расположение сетки, сохраняются между переключателями вкладок.
  • Значительные улучшения производительности - переключение вкладок происходит практически мгновенно, поскольку мы не перерисовываем все при каждом изменении вкладки.

TabControlEx.cs

// Copy C# code from @Dennis 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 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>

Пользовательская вкладка

Измените вкладку, чтобы использовать наш новый пользовательский класс С#, и укажите его на наш новый пользовательский стиль, используя тег 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 есть аналог.

Обновить

У Telerik есть эквивалент: IsContentPreserved. Спасибо @Luishg в комментариях ниже.

Ответ 3

Это существующее решение от @Dennis (с дополнительными примечаниями от @Gravitas) работает очень хорошо.

Однако есть еще одно решение, более модульное и MVVM, так как оно использует приложенное поведение для достижения того же результата.

См. Проект кода: WPF TabControl: Включение виртуализации вкладок. Поскольку автор является техническим лидером в Reuters, код, вероятно, прочный.

Демо-код действительно хорошо сочетается, он показывает регулярный TabControl, наряду с тем, который связан с приложением.

enter image description here

Ответ 4

Пожалуйста, проверьте мой ответ из этого сообщения в формате SO. Надеюсь, что это решит проблему, но это немного от дороги MVVM. Ссылка

Ответ 5

Существует не очень очевидное, но элегантное решение. Основная идея состоит в том, чтобы вручную создать свойство 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 при переключении вкладок.