上一篇文章([UWP]如何使用代码创建DataTemplate(或者ControlTemplate))介绍了在UWP上的情况,这篇文章再稍微介绍在WPF上如何实现。
2. 使用FrameworkElementFactoryFrameworkElementFactory用于以编程的方式创建模板,虽然文档中说不推荐,但WPF中常常使用这个类,例如。
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;使用方式如上,这种方式可以方便地使用代码设置绑定或属性值,并且提供了方法用于创建复杂的树结构。但是一旦这样做将使代码变得很复杂,建议还是不要这样做。
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. 结语