为了账号安全,请及时绑定邮箱和手机立即绑定

阻止TabControl重新创建其子级

/ 猿问

阻止TabControl重新创建其子级

米脂 2019-12-25 10:49:08

我有一个IList绑定到的viewmodel 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,但是对我来说,尚不清楚调用构造函数的机制是什么。


我看过一个类似的问题,它使用了UserControls,但是那里提供的解决方案将需要我绑定到视图,这是不希望的。


查看完整描述

3 回答

?
红颜莎娜

默认情况下,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博客文章,以及StackOverflow上的答案。


查看完整回答
反对 回复 2019-12-25
?
蛊毒传说

答案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具有同等的身份。


更新资料


Telerik确实具有以下等效功能:IsContentPreserved。感谢@Luishg在下面的评论中。


查看完整回答
反对 回复 2019-12-25
?
qq_花开花谢_0

有一个不是很明显但很优雅的解决方案。主要思想是通过自定义转换器手动生成TabItem的ContentTree的VisualTree属性。


定义一些资源


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

现在,尝试比较两种变体,以查看选项卡切换期间ItemDataTemplate处TextBox行为的不同。


查看完整回答
反对 回复 2019-12-25

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信