65.9K
CodeProject 正在变化。 阅读更多。
Home

通过代码创建 WPF 数据模板:正确的方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (31投票s)

2012年8月21日

CPOL

6分钟阅读

viewsIcon

117929

downloadIcon

1609

如何正确地通过代码创建 WPF 数据模板

背景

在编写大型复合 MVVM 应用程序时,我发现我经常创建只有一个可视元素的数据模板。这些数据模板建立视图模型类型和视图类型之间的连接,如下所示

<DataTemplate DataType="{x:Type vm:MyViewModelType}">
    <views:MyViewType />
</DataTemplate>

换句话说,这意味着“每当您看到 MyViewModel 类型的对象时,都使用 MyView 渲染它。”

在创建了三四个这样的数据模板后,我自然想自动化这项任务,但事实证明并非如此简单。

错误的方法 - 请勿在家尝试

似乎有一种简单的方法可以在代码中创建 DataTemplate:只需创建一个 DataTemplate 对象并分配一些属性

DataTemplate CreateTemplateObsolete(Type viewModelType, Type viewType)
{
    // WARNING: NOT WORKING CODE! DO NOT USE
    return new DataTemplate()
    {
        DataType = viewModelType,
        VisualTree = new FrameworkElementFactory(viewType)
    };
}

这段代码非常简单,而且在某种程度上是可行的。直到它不可行。MSDN 关于 FrameworkElementFactory 类的帮助文档警告

“此类别是一种已弃用的以编程方式创建模板的方法……使用此类别创建模板时,并非所有模板功能都可用。”

这种“不可用”的神秘功能是什么?我无法确定,但我确实发现了一个此方法创建模板无法正常工作的情况。当您的视图绑定到在绑定本身之后定义的 UI 元素时,就会出现这种情况。请考虑以下 XAML

<TextBlock Text="{Binding ActualWidth, ElementName=SomeControl}" />
<ListBox Name="SomeControl" />

此处 TextBlock 上的绑定引用了一个名为 SomeControlListBox,该 ListBox 在绑定之后的 XAML 中定义。

如果您将这种 XAML 放入通过 FrameworkElementFactory 创建的数据模板中,则绑定将失败。我通过艰难的方式发现了这一点,而且我确实有一个示例来证明这一点。

Small screen shot

但让我们首先看看创建数据模板的正确方法。

正确的方法

FrameworkElementFactory 的 MSDN 文章接着说

建议的以编程方式创建模板的方法是使用 XamlReader 类的 Load 方法从字符串或内存流加载 XAML。

问题是,.NET 框架中提供的 XAML 解析器与 Visual Studio 附带的 XAML 解析器不完全相同。特别是,您需要应用一些技巧来处理 C# 命名空间。结果代码如下所示

DataTemplate CreateTemplate(Type viewModelType, Type viewType)
{
    const string xamlTemplate = "<DataTemplate DataType=\"{{x:Type vm:{0}}}\"><v:{1} /></DataTemplate>";
    var xaml = String.Format(xamlTemplate, viewModelType.Name, viewType.Name, viewModelType.Namespace, viewType.Namespace);

    var context = new ParserContext();

    context.XamlTypeMapper = new XamlTypeMapper(new string[0]);
    context.XamlTypeMapper.AddMappingProcessingInstruction("vm", viewModelType.Namespace, viewModelType.Assembly.FullName);
    context.XamlTypeMapper.AddMappingProcessingInstruction("v", viewType.Namespace, viewType.Assembly.FullName);

    context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
    context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
    context.XmlnsDictionary.Add("vm", "vm");
    context.XmlnsDictionary.Add("v", "v");

    var template = (DataTemplate)XamlReader.Parse(xaml, context);
    return template;
}

坏消息是,这段代码比简单的代码冗长和笨拙得多。好消息是,这段代码效果更好。特别是,它对向前绑定没有问题。

关于创建模板的新方法的另一个缺点是,在 .NET 3.5 中,视图和视图模型类都必须是公共的。如果它们不是,您在解析 XAML 时会收到运行时异常,指出它们应该是公共的。.NET 4 没有这个限制:所有类都可以是内部的。

向应用程序注册数据模板

为了使用数据模板创建可视化对象,WPF 必须以某种方式了解它。您可以通过将其添加到应用程序资源中来使您的模板全局可用

var key = template.DataTemplateKey;
Application.Current.Resources.Add(key, template);

请注意,您需要一个从模板本身检索的特殊资源键。使用其数据类型作为模板的键似乎很自然,但此选项已被样式占用。因此,Microsoft 必须为数据模板提出不同类型的键。

DataTemplateManager 类

上述两个步骤:创建模板和将其注册到应用程序资源中,都被封装在 DataTemplateManager 类中。您注册模板如下

using IKriv.Wpf;

var manager = new DataTemplateManager();
manager.RegisterDataTemplate<ViewModelA, ViewA>();
manager.RegisterDataTemplate<ViewModelB, ViewB>();

示例代码

在示例中,我有一个名为 TextView 的视图和一个名为 TextViewModel 的视图模型。视图模型仅定义一个名为 Text 的属性。视图显示文本字符串及其实际宽度,这是一个向前绑定。

<UserControl x:Class="DataTemplateCreation.TextView">
    <DockPanel>
        <TextBlock 
            DockPanel.Dock="Top"
            Margin="5"
            Text="{Binding ActualWidth, ElementName=TextControl,
                           StringFormat='Text width is \{0\}', FallbackValue='Binding failed!'}" />
        <Grid>
            <TextBlock Name="TextControl" HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Text}" />
        </Grid>
    </DockPanel>
</UserControl>

然后我以三种方式实例化此视图

  • 作为主窗口的直接子项,不涉及数据模板。
  • 通过使用正确技术创建的数据模板。
  • 通过使用简单但实际不起作用的技术创建的数据模板。

然后我在 App.xaml.cs 中创建了两个数据模板。

var manager = new DataTemplateManager();
manager.RegisterDataTemplate<TextViewModel, TextView>();
manager.RegisterObsoleteDataTemplate<TextViewModelObsolete, TextView>();

上面代码的第二行表示 TextViewModel 类型的内容将显示为 TextView。第三行表示 TextViewModelObsolete 类型的内容也将显示为 TextView,但此数据模板是使用 MSDN 不推荐的简单但实际不起作用的过时技术创建的。主窗口 XAML 如下所示

<UniformGrid Rows="3" Columns="1">
    
    <local:TextView Background="Red" Foreground="White">
        <local:TextView.DataContext>
            <local:TextViewModel Text="Direct child" />
        </local:TextView.DataContext>
    </local:TextView>
   
    <Border Background="Yellow">
        <ContentPresenter>
            <ContentPresenter.Content>
                <local:TextViewModel Text="New Data Template" />
            </ContentPresenter.Content>
        </ContentPresenter>
    </Border>

    <Border Background="LightGray">
        <ContentPresenter>
            <ContentPresenter.Content>
                <local:TextViewModelObsolete Text="Obsolete Data Template" />
            </ContentPresenter.Content>
        </ContentPresenter>
    </Border>

</UniformGrid>

它有三个水平带

  • 一个在 XAML 中显式创建的 TextView,不涉及数据模板。
  • 一个 ContentControl,其内容类型为 TextViewModel,这会触发新的数据模板。
  • 一个 ContentControl,其内容类型为 TextViewModelObsolete,这会触发过时的数据模板。
Sample screen shot

正如您在上面的屏幕截图中清晰可见,第三个条带看起来不太好,因为向前绑定失败了。

IKriv.Windows 库

DataTemplateManager 类现在作为我发布在 NuGet 上的 IKriv.Windows 库的一部分提供。在 Visual Studio 中使用工具->库包管理器->管理解决方案的 NuGet 包对话框,轻松地将 IKriv.Windows 添加到您的解决方案中。

结论

尽管 MSDN 关于以编程方式创建 DataTemplate 的文档模糊且难以找到,但他们确实知道自己在说什么。在代码中实例化 DataTemplate 类将无法正常工作,您需要使用如上所示的 XAML 解析器。

然而,我确实觉得这太复杂了。应该可以手动构建模板的可视化树,就像没有模板参与一样。从元素类型制作 XAML 字符串,只是为了将它们反馈给 XAML 解析器,这是笨拙且低效的。此外,您在代码中获得的 XAML 解析器与 Visual Studio 使用的 XAML 解析器略有不同,这加剧了烦恼。

幸运的是,对于典型情况,问题应该只解决一次,然后您就可以调用 RegisterDataTemplate 方法了。封装万岁!

PS。支持泛型的可能性

许多人抱怨提供的解决方案不支持泛型视图模型。不幸的是,为泛型视图模型支持数据模板是不可能的。WPF 使用 XAML 2006,不支持泛型。XAML 2009 版本引入了 x:TypeArguments 属性,如此 MSDN 主题所述,但 WPF XAML 编译器不完全支持它,即使在 .NET 4.5 中也是如此,并且可能永远不会支持,因为微软已经放弃了 WPF 并且不会对其进行任何重大更改。

另一种将泛型挤入的方法是创建一个类似于 x:Type 的标记扩展,并像这样使用它

<DataTemplate DataType={x:GenericType vm:MyViewModel(coll:List(sys:String))}">

不幸的是,这条路也被堵死了。我尝试创建这样的扩展,并将此 DataTemplate 放入资源字典时收到了以下错误

A key for a dictionary cannot be of type 'GenericInXaml.GenericType'. 
Only String, TypeExtension, and StaticExtension are supported.  

换句话说,资源键只能是字符串、{x:Type}{x:Static},并且这些都不支持泛型。

所以,底线是:抱歉,不支持泛型。

© . All rights reserved.