[WPF]如何使用代碼創建DataTemplate(或者ControlTemplate)


1. 前言

上一篇文章([UWP]如何使用代碼創建DataTemplate(或者ControlTemplate))介紹了在UWP上的情況,這篇文章再稍微介紹在WPF上如何實現。

2. 使用FrameworkElementFactory

FrameworkElementFactory用於以編程的方式創建模板,雖然文檔中說不推薦,但WPF中常常使用這個類,例如DisplayMemberTemplateSelector

FrameworkElementFactory text = new FrameworkElementFactory(typeof(TextBlock));
Binding binding = new Binding
{
    Path = new PropertyPath("Name")
};
text.SetBinding(TextBlock.TextProperty, binding);

var xmlNodeContentTemplate = new DataTemplate();
xmlNodeContentTemplate.VisualTree = text;
xmlNodeContentTemplate.Seal();

ListControl.ItemTemplate = xmlNodeContentTemplate;

使用方式如上,這種方式可以方便地使用代碼設置綁定或屬性值,並且提供了AppendChild方法用於創建復雜的樹結構。但是一旦這樣做將使代碼變得很復雜,建議還是不要這樣做。

3. 使用XamlReader和XamlWriter

和UWP一樣,WPF也支持使用XamlReader構建模板,只不過需要將

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

改為

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

和UWP不一樣的是WPF還有XamlWriter這個工具。

XamlWriter提供一個靜態 Save 方法,該方法可用於以受限的 XAML 序列化方式,將所提供的運行時對象序列化為 XAML 標記。如果使用這個類說不定可以用普通的方式創建一個UI元素並且最終創建它對應的DataTemplate,例如這樣:

TextBlock text = new TextBlock();
Binding binding = new Binding("Name");
text.SetBinding(TextBlock.TextProperty, binding);
string xaml = string.Empty;
using (var stream = new MemoryStream())
{
    XamlWriter.Save(text, stream);
    using (var streamReader = new StreamReader(stream))
    {
        stream.Seek(0, SeekOrigin.Begin);
        xaml = streamReader.ReadToEnd();
    }
}

var template = (DataTemplate)XamlReader.Parse(@"
        <DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                    xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
        		" + xaml + @"
</DataTemplate>");

但現實沒有這么簡單,在生成xaml的那步就出錯了,聲稱的xaml如下:

<TextBlock Text="" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />

可以看到這段XAML並沒有反映text.SetBinding(TextBlock.TextProperty, binding);這段設置的綁定。具體原因可見XamlWriter.Save 的序列化限制

值得慶幸的是WPF有足夠長的歷史,在這段歷史里經過了無數人上上下下的折騰,上面提到的問題在10年前已經有人給出了解決方案:XamlWriter and Bindings Serialization

首先,MarkupExtension及其派生類(如Binding)需要有一個TypeConverter以便可以序列化:

internal class BindingConvertor : ExpressionConverter
{
    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
        if (destinationType == typeof(MarkupExtension))
            return true;
        else return false;
    }
    public override object ConvertTo(ITypeDescriptorContext context,
                                    System.Globalization.CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType == typeof(MarkupExtension))
        {
            BindingExpression bindingExpression = value as BindingExpression;
            if (bindingExpression == null)
                throw new Exception();
            return bindingExpression.ParentBinding;
        }

        return base.ConvertTo(context, culture, value, destinationType);
    }
}

然后,需要由TypeDescriptor告訴大家要使用這個TypeConverter:

internal static class EditorHelper
{
    public static void Register<T, TC>()
    {
        Attribute[] attr = new Attribute[1];
        TypeConverterAttribute vConv = new TypeConverterAttribute(typeof(TC));
        attr[0] = vConv;
        TypeDescriptor.AddAttributes(typeof(T), attr);
    }
}

EditorHelper.Register<BindingExpression, BindingConvertor>();

然后就可以愉快地使用了:

Binding binding = new Binding("Name");
TextBlock text = new TextBlock();
text.SetBinding(TextBlock.TextProperty, binding);

StringBuilder outstr = new StringBuilder();
//this code need for right XML fomating 
XmlWriterSettings settings = new XmlWriterSettings
{
    Indent = true,
    OmitXmlDeclaration = true
};
var dsm = new XamlDesignerSerializationManager(XmlWriter.Create(outstr, settings))
{
    //this string need for turning on expression saving mode 
    XamlWriterMode = XamlWriterMode.Expression
};

XamlWriter.Save(text, dsm);

var xaml = outstr.ToString();

var template = (DataTemplate)XamlReader.Parse(@"
        <DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                    xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
        		" + xaml + @"
</DataTemplate>");

這樣就可以產生正確的XAML了:

<TextBlock Text="{Binding Path=Name}" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />

不過我沒遇到這么復雜的業務需求,所以這個方案我也沒實際使用過。從原文的評論來看果然還是有些問題,如ValidationRules不能正確地序列化。總之使用要謹慎。

4. 結語

有關TypeConverter和TypeDescriptor的更多信息可見我的另一篇文章了解TypeConverter。不過回顧了這篇文章后我發覺我更需要的是簡化文章的能力,所以以后盡可能還是寫簡短實用些。

5. 參考

FrameworkElementFactory
XamlWriter
XamlWriter and Bindings Serialization
TypeConverter
TypeDescriptor
了解TypeConverter


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM