使用XamlReader.Load构建配置型自定义控件
我们知道,用Xaml来设计控件UI相比使用后台代码来说要容易得多,而且借助Blend或VS2010界面设计器也更容易维护,不必为了修改一个小小的背景前景色要投身茫茫码海中。但是Xaml相比代码构造来说,失去了动态配置的灵活性,而且也很难用于复制出若干相同配置的控件实例。
考虑下面这样的情景:
我们有一个图表控件,我们使用Blend为这个图表控件预先配置好了很多属性使其展示效果最佳,然后我们希望应用程序其他用到图表控件的地方也使用一样的配置,但是允许其他地方自由选择图表的类型,例如以饼状图、柱状图或是条形图展示。
如果我们在代码中使用工厂模式来构造这个图表控件的话,那么我们通过让使用者传入配置参数的方式的方式来生成使用者期望的图表,这倒是很容易,但如果这个控件的预配置过程是通过Xaml来完成的,那么要实现动态配置就麻烦很多了。而且由于Silverlight的UIElemnet没有Clone方法,即使使用Xaml构建出了一个配置好了的实例,也很难复制出若干个相同配置的实例来。
最近项目就遇到这样的问题,最终我用XamlReader.Load方法动态加载Xaml资源文件的方式解决了这个问题。
下面这个Xaml文件是一个包含DataGrid控件的Grid容器,这个DataGrid有两列组成,但是数据绑定以及列名均允许动态配置。
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sdk="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="336" d:DesignHeight="208"> <sdk:DataGrid x:Name="GridReport" ItemsSource="{Binding PagedItemsSource}" AutoGenerateColumns="False" IsReadOnly="True"> <sdk:DataGrid.Columns> <sdk:DataGridTextColumn Binding="{Binding Member}" Header="$(MemberHeader~BounceRate)"/> <sdk:DataGridTextColumn Binding="{Binding Metrics[$(Metric~Count)]}" Header="$(MetricHeader~Visitors)"/> </sdk:DataGrid.Columns> </sdk:DataGrid> <sdk:DataPager x:Name="GridPager" DisplayMode="FirstLastPreviousNext" PageSize="5" Source="{Binding PagedItemsSource}"/> </Grid>
在这里,我定义了一个简单的参数替换规则,就是"$(ConfigKey~DefaultValue)",一旦遇到这样的字符串,则认为此字符串为可配置字符串,如果使用者传入该ConfigKey的配置,则使用配置项,否则使用默认值。如果Xaml中只是写了"$(ConfigKey)”没有默认值的话,那么表示此配置项必须由使用者传入,否则抛出异常。
之所以用$(~)这几个特殊字符,是因为这几个字符不会造成Blend Xaml解析器异常。
那么怎么实例化这个Grid容器呢?我写了一个XamlLoader辅助类来实现。
/// <summary> /// 支持从某个Xaml资源中创建FrameworkElement。内建缓存。 /// </summary> public static class XamlLoader { public static readonly string ContentResourcesBaseUri = "/Assets/ContentResources/"; public static readonly string CurrentAssemblyName; private static Dictionary<string, string> _cache = new Dictionary<string, string>(); private static Regex _parameterPattern = new Regex(@"\$\((?<parameter>\w+)~?(?<defValue>.*?)\)", RegexOptions.ExplicitCapture); private static object _shareLock = new object(); static XamlLoader() { var assemblyName = new AssemblyName(Assembly.GetExecutingAssembly().FullName); CurrentAssemblyName = assemblyName.Name; } /// <summary> /// 从Xaml资源中实例化FrameworkElement对象。 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="parameters"></param> /// <returns></returns> public static T CreateTemplatedFrameworkElement<T>(string key, Dictionary<string, string> parameters) where T : FrameworkElement { string xamlString = LoadXaml(key); // 搜索Xaml资源文件中的所有参数,使用parameters参数字典进行替换 MatchCollection matches = _parameterPattern.Matches(xamlString); foreach (Match m in matches) { string param = m.Groups["parameter"].Value; string defValue = m.Groups["defValue"].Value; if (parameters.ContainsKey(param)) { xamlString = xamlString.Replace(m.Value, parameters[param]); } else if (!String.IsNullOrEmpty(defValue)) { xamlString = xamlString.Replace(m.Value, defValue); } else { throw new InvalidOperationException(String.Format("Parameter {0} is not provided.", param)); } } return XamlReader.Load(xamlString) as T; } public static FrameworkElement CreateTemplatedFrameworkElement(string key, Dictionary<string, string> parameters) { return CreateTemplatedFrameworkElement<FrameworkElement>(key, parameters); } /// <summary> /// 读取Xaml资源文件字符串。内建缓存。 /// </summary> /// <param name="key"></param> /// <returns></returns> public static string LoadXaml(string key) { string xamlString; if (_cache.ContainsKey(key)) { xamlString = _cache[key]; } else { lock (_shareLock) { if (_cache.ContainsKey(key)) { xamlString = _cache[key]; } else { string resourceName = string.Format("{0};component{1}{2}.xaml", CurrentAssemblyName, ContentResourcesBaseUri, key); Uri uri = new Uri(resourceName, UriKind.Relative); StreamResourceInfo streamResourceInfo = Application.GetResourceStream(uri); using (Stream resourceStream = streamResourceInfo.Stream) { using (StreamReader streamReader = new StreamReader(resourceStream)) { xamlString = streamReader.ReadToEnd(); if (!String.IsNullOrEmpty(xamlString)) { _cache[key] = xamlString; } } } } } } return xamlString; } }
实例化的过程就变得很容易了,直接传入模板控件所在资源文件的名称以及一个字典配置对象即可。
XamlLoader.CreateTemplatedFrameworkElement("DataGridTemplate", new Dictionary<string, string> { {"MemberHeader","视频名称"}, {"MetricHeader","访问人数"}, });
XamlReader.Load加载本程序集内自定义控件失败的问题
期间遇到一个很奇怪的问题,通过上述这种方法实例化一个定义在同一个程序集下的自定义控件却抛出AG_E_UNKNOWN_ERROR的XamlParseException。
<SlApp:MyControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:SlApp="clr-namespace:SlApp;" />
后来总算找到问题所在。平常当我们在UserControl中用到一个自定义控件的时候,需要添加命名空间,命令空间的格式为:
clr-namespace:CustomNamespace;assembly=CustomAssemblyName
如果控件是在当前程序集下定义的,那么后面的assembly部分就可以省略,但是使用XamlLoader.Load方法加载时这一部分却不能省去,否则就会抛出刚才提到的异常。
正确的写法应该是:
<SlApp:MyControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:SlApp="clr-namespace:SlApp;assembly=SlApp" />