使用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" />
浙公網安備 33010602011771號