0

介绍:

我目前正在开发一个应用程序,其中数据应显示在 TabView 中。默认情况下,将打开一个“主页”选项卡,显示一个列表框。单击此框中的条目后,将在新选项卡中打开详细视图。

该应用程序是在 MVVM 架构中创建的。“MainViewModel”包含一个集合,其中 ViewModel 填充 TabView。我正在尝试使用 DataTemplateSelector 根据 ViewModel 类切换 DataTemplates。

该项目正在使用以下依赖项:

<PackageReference Include="CommunityToolkit.Mvvm" Version="7.0.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.0.2" />
<PackageReference Include="Microsoft.ProjectReunion" Version="0.5.7" />
<PackageReference Include="Microsoft.ProjectReunion.Foundation" Version="0.5.7" />
<PackageReference Include="Microsoft.ProjectReunion.WinUI" Version="0.5.7" />

示例代码

MainWindow.xaml.cs 包含 ViewModel 和模板选择器

using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;


namespace TestingApp
{
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
        }
    }

    public class TabItemDataTemplateSelector : DataTemplateSelector
    {
        public DataTemplate HomeTemplate { get; set; }
        public DataTemplate DetailTemplate { get; set; }

        protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
        {
            switch (item)
            {
                case HomeTabViewModel:
                    return HomeTemplate;
                case DetailTabViewModel:
                    return DetailTemplate;
                default:
                    throw new ArgumentException("Invalid ViewModel type supplied as item");
            }
        }
    }

    public class MainWindowViewModel : ObservableObject
    {
        private ObservableCollection<ObservableObject> tabItems;
        public ObservableCollection<ObservableObject> TabItems { get => tabItems; set => SetProperty(ref tabItems, value); }

        public MainWindowViewModel()
        {
            TabItems = new ObservableCollection<ObservableObject>();
            TabItems.Add(new HomeTabViewModel());
            TabItems.Add(new DetailTabViewModel());
            TabItems.Add(new DetailTabViewModel());
        }
    }

    public class HomeTabViewModel : ObservableObject
    {
        public string Header { get; set; } = "Home";
        public string Detail { get; set; } = "I'm the HomeView";
    }

    public class DetailTabViewModel : ObservableObject
    {
        public string Title { get; set; } = "Detail"; // Different Name to make the classes a bit different
        public string Detail { get; set; } = "Hello from the Details!";
    }
}

MainWindow.xaml 仅包含一个 TabView

<Window
    x:Class="TestingApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TestingApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid>
        <Grid.DataContext>
            <local:MainWindowViewModel />
        </Grid.DataContext>

        <Grid.Resources>
            <DataTemplate x:Key="HomeTemplate">
                <TabViewItem Header="{Binding Header}" />
            </DataTemplate>

            <DataTemplate x:Key="DetailTemplate">
                <TabViewItem Header="{Binding Title}" />
            </DataTemplate>
            
            <local:TabItemDataTemplateSelector x:Key="TemplateSelector" DetailTemplate="{StaticResource DetailTemplate}" HomeTemplate="{StaticResource HomeTemplate}" />
        </Grid.Resources>

        <TabView TabItemsSource="{Binding TabItems}" TabItemTemplateSelector="{StaticResource TemplateSelector}">
        </TabView>
    </Grid>
</Window>

结果

上面的代码创建了一个奇怪的嵌套结果,看起来模板多次应用于标题和内容,导致 TabViewItems 到处都是:

实际结果

期望的结果

对我来说奇怪的是,当我使用“内联”TabItemTemplate 时,结果很好,并且绑定有效。

    <Grid>
        <Grid.DataContext>
            <local:MainWindowViewModel />
        </Grid.DataContext>

        <TabView TabItemsSource="{Binding TabItems}">
            <TabView.TabItemTemplate>
                <DataTemplate>
                    <TabViewItem Header="Tab">
                        <TextBlock Text="{Binding Detail}" />
                    </TabViewItem>
                </DataTemplate>
            </TabView.TabItemTemplate>
        </TabView>
    </Grid

工作模板示例 #1 工作模板示例 #2

问题

如何使用 TemplateSelector 正确创建 TabViewItems,其中 Header 和 Content 基于 DataTemplate?在搜索类似问题时,我找到了旧项目的解决方案,当简单地将 TabViewItem 嵌套在 DataTemplate 中时,这似乎可以正常工作,这显然不是这里的情况。

4

1 回答 1

0

更新:明显不那么可怕

在 DataTemplate 中使用带有模板选择器的 ContentControl。

<DataTemplate x:Key="SelectorWrapper"
              x:DataType="t:Type">
    <TabViewItem>
        <TabViewItem.Header>
            <ContentControl Content="{x:Bind}"
                            ContentTemplateSelector="{StaticResource HeaderTemplateSelector}" />
        </TabViewItem.Header>
        <ContentControl Content="{x:Bind}"
                        ContentTemplateSelector="{StaticResource TemplateSelector}" />
    </TabViewItem>
</DataTemplate>

原始的可怕

以为我会把它放进去,以防有人只需要模板选择。相当肮脏的解决方法,但它的功能(至少对我来说)没有大量的变化。只要修复了错误,就应该能够进行一些小的复制/粘贴以退出。

摘要:使用包含子类 TabViewItem 的单个 DataTemplate,而不是实际的 DataTemplateSelector。The subclass acts as a selector, as well as a TabViewItem. 使 DataTemplates 可以在 XAML 中制作,具有相同的绑定等。

相关 GitHub 问题:https ://github.com/microsoft/microsoft-ui-xaml/issues/5852

选项卡的基本 ViewModel

TabItem.cs

添加您喜欢的 INPC 口味

public abstract class TabItem {
    public object Header { get; set; }
    public object Icon { get; set; }
    public bool IsClosable { get; set; }
}
子类 TabViewItem

TabViewItemSelector.xaml

<TabViewItem x:Class="TabViewTest.TabViewItemSelector"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:l="using:TabViewTest">
    <TabViewItem.Resources>
        <!--Header Templates-->
        <DataTemplate x:Name="Tab1HeaderTemplate">
            <TextBlock Text="Win32 Apps" />
        </DataTemplate>
        <DataTemplate x:Name="Tab2HeaderTemplate"
                      x:DataType="l:Tab2ViewModel">
            <TextBlock Text="{x:Bind DisplayName, Mode=OneWay}" />
        </DataTemplate>

        <!--Tab Content Templates-->
        <DataTemplate x:Name="Tab1Template"
                      x:DataType="l:Tab1ViewModel">
            <l:APageOrSomething ViewModel="{x:Bind}" />
        </DataTemplate>
        <DataTemplate x:Name="Tab2Template"
                      x:DataType="l:Tab2ViewModel">
            <l:RepeatPage ViewModel="{x:Bind}" />
        </DataTemplate>
    </TabViewItem.Resources>
</TabViewItem>
子类 TabViewItem 的代码

TabViewItemSelector.xaml.cs

public sealed partial class TabViewItemSelector : TabViewItem {
    private DataTemplate _tabTemplate;
    private DataTemplate _headerTemplate;

    public TabViewItemSelector() {
        InitializeComponent();
    }

    // DependencyProperty update callback handles template load.  Never cleans up after itself.
    private static void SelectOrClearTemplate(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        if (d is not TabViewItemSelector selector || selector == null) return;

        // Create the content
        if (e.NewValue != null && e.NewValue is TabItem tab && tab != null) {
            // Generate header from template, if not set previously
            if (tab.Header == null) {
                selector._headerTemplate = tab switch {
                    Tab1ViewModel => selector.Tab1HeaderTemplate,
                    Tab2ViewModel => selector.Tab2HeaderTemplate,
                    _ => throw new NotImplementedException()
                };
                var headerArgs = new ElementFactoryGetArgs { Data = selector._headerTemplate, Parent = selector };
                tab.Header = selector._headerTemplate.GetElement(headerArgs);
            }

            // Generate tab content
            selector._tabTemplate = tab switch {
                Tab1ViewModel => selector.Tab1Template,
                Tab2ViewModel => selector.Tab2Template,
                _ => throw new NotImplementedException()
            };
            var tabArgs = new ElementFactoryGetArgs { Data = selector._tabTemplate, Parent = selector };
            selector.Content = selector._tabTemplate.GetElement(tabArgs);
        }
    }

    public object TabContentSource {
        get => GetValue(TabContentSourceProperty);
        set => SetValue(TabContentSourceProperty, value);
    }
    public static readonly DependencyProperty TabContentSourceProperty = DependencyProperty
        .Register(nameof(TabContentSource), typeof(object), typeof(TabViewItemSelector), new PropertyMetadata(null, SelectOrClearTemplate));
}
带有 TabView 的页面 - TabPage.xaml
<Page x:Class="TabViewTest.TabPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:v="using:TabViewTest">
    <Page.Resources>
        <DataTemplate x:Key="TabViewItemSelectorTemplate"
                      x:DataType="v:TabItem">
            <v:TabViewItemSelector Header="{x:Bind Header, Mode=OneWay}"
                                   IconSource="{x:Bind (IconSource)Icon, Mode=OneWay}"
                                   IsClosable="{x:Bind IsClosable}"
                                   TabContentSource="{x:Bind}" />
        </DataTemplate>
    </Page.Resources>

    <TabView x:Name="TabView"
             TabItemsSource="{x:Bind Tabs, Mode=OneWay}"
             TabItemTemplate="{StaticResource TabViewItemSelectorTemplate}" />
</Page>
带有 TabView 的页面代码 - TabPage.xaml.cs
public sealed partial class TabPage : Page {
    public ObservableCollection<TabItem> Tabs { get; set; } = new();

    public TabPage() {
        InitializeComponent();

        // First tab - quick and easy
        var tab1 = new Tab1ViewModel() {
            Header = "Basic header",
            Icon = new SymbolIconSource { Symbol = Symbol.ProtectedDocument },
        };

        // Second tab header - Tab2ViewModel.DisplayName
        var tab2 = new Tab2ViewModel {
            DisplayName = "Second tab",
            Icon = new SymbolIconSource { Symbol = Symbol.AddFriend },
        };

        // Third tab - this workaround even supports terribleness
        var tab3 = new Tab2ViewModel() {
            Header = new TextBox { Text = "Textboxes for everyone!" },
            Icon = new SymbolIconSource { Symbol = Symbol.People },
        };

        // And awaaay we go.
        Tabs.Add(tab1);
        Tabs.Add(tab2);
        Tabs.Add(tab3);
    }
}

public class Tab1ViewModel : TabItem { }
public class Tab2ViewModel : TabItem {
    public string DisplayName { get; set; }
}
于 2022-01-23T11:53:33.407 回答