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

阻止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 回答

?
红颜莎娜

TA贡献1842条经验 获得超12个赞

默认情况下,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
?
qq_花开花谢_0

TA贡献1835条经验 获得超6个赞

有一个不是很明显但很优雅的解决方案。主要思想是通过自定义转换器手动生成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
  • 3 回答
  • 0 关注
  • 588 浏览

添加回答

举报

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