在
Silverlight 下换肤的实现 中,我介绍了 Nikhil Kothari 实现的一种 Theme 方案。但是实践了一下,我很快发现有个小小的缺陷。作者的皮肤定义是仅针对系统自带的控件的,如 Button, TextBox, CheckBox 等,而对于我们自定义的控件的换肤问题没有很好的解决。从下列代码中可见一斑:
(/Framework/Applications/Theme.cs)
可以看出这里名称空间都是写死了的,不支持任何扩展。
而实际使用中,我们可能会对某个第三方控件定义样式。比如:
这里定义了 Liquid 这套组件中 Dialog 控件的样式,而其中 xmlns:liquid 这个名称空间的声明就是必须的。
要达到这个目标,我们可以修改 Theme.cs 的 parse 步骤,在其过程中补充一段收集 xml namespace 的代码,并在合并最终的样式 xaml 时加进去即可。
修改过的 Theme.cs 代码如下:
修改过的 Theme.cs
// Theme.cs
// Copyright (c) Nikhil Kothari, 2008. All Rights Reserved.
// http://www.nikhilk.net
//
// This product's copyrights are licensed under the Creative
// Commons Attribution-ShareAlike (version 2.5).B
// http://creativecommons.org/licenses/by-sa/2.5/
//
// You are free to:
// - use this framework as part of your app
// - make use of the framework in a commercial app
// as long as your app or product is itself not a framework, control pack or
// developer toolkit of any sort under the following conditions:
// Attribution. You must attribute the original work in your
// product or release.
// Share Alike. If you alter, transform, or build as-is upon this work,
// you may only distribute the resulting app source under
// a license identical to this one.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Resources;
using System.Xml;
namespace Silverlight.FX.Applications
{
/// <summary>
/// Represents a theme control, whose top-level resource items are to be included
/// when this theme is selected.
/// </summary>
public class Theme : UserControl
{
private string _includes;
/// <summary>
/// Any additional themes to include. Each of the additional themes must contain
/// a UserControl declaration. The list of themes are processed in order after
/// the current theme's resource items have been included.
/// </summary>
public string Includes
{
get
{
return _includes ?? String.Empty;
}
set
{
_includes = value;
}
}
private static void ExtractThemeContent(string xml, ThemeInfo theme, bool extractIncludes)
{
XmlReader xmlReader = XmlReader.Create(
new StringReader(xml),
new XmlReaderSettings
{
CheckCharacters = false,
DtdProcessing = DtdProcessing.Ignore,
IgnoreComments = true,
IgnoreProcessingInstructions = true,
IgnoreWhitespace = true,
});
while (xmlReader.Read())
{
if (xmlReader.NodeType == XmlNodeType.Element)
{
// Found the first element
// 保存元素节点名称,因为下面要跳到属性去读取
var elementName = xmlReader.Name;
if (extractIncludes)
{
theme.Includes = xmlReader.GetAttribute("Includes");
}
// 添加名称空间
var hasAttr = xmlReader.MoveToFirstAttribute();
if (hasAttr)
{
do
{
if (xmlReader.Name.StartsWith("xmlns:"))
{
var val = xmlReader.ReadContentAsString();
theme.AddNamespace(xmlReader.Name, val);
}
} while (xmlReader.MoveToNextAttribute());
}
string resourcesTag = elementName + ".Resources";
// 改为由父节点属性读到指定名称的子节点. 不能用 ReadToDescendant.
if (xmlReader.ReadToFollowing(resourcesTag))
{
//if (xmlReader.ReadToDescendant(resourcesTag)) {
xmlReader.MoveToContent();
// TODO: I would have expected MoveToContent to move to the first item
// within the current tag, but apparently the reader is still
// positioned at the current tag, unless a Read is performed.
xmlReader.Read();
while (xmlReader.EOF == false)
{
if (xmlReader.NodeType == XmlNodeType.Element)
{
string key = xmlReader.GetAttribute("x:Key");
if ((String.IsNullOrEmpty(key) == false) && (theme.ContainsKey(key) == false))
{
string markup = xmlReader.ReadOuterXml();
theme.AddItem(key, markup);
}
else
{
xmlReader.Skip();
}
}
else
{
// No more items
break;
}
}
}
// We only look at the resources of the root tag, and so we're done
break;
}
}
}
private static string GetThemeXaml(string themeName, string fileName, bool lookupShared)
{
StreamResourceInfo resourceInfo = null;
// First try /Themes/<ThemeName>/<FileName>.xaml
Uri resourceUri = new Uri("Themes/" + themeName + "/" + fileName + ".xaml", UriKind.Relative);
resourceInfo = Application.GetResourceStream(resourceUri);
if ((resourceInfo == null) && lookupShared)
{
// The fallback is one level up, i.e. /Themes/<FileName>.xaml
resourceUri = new Uri("Themes/" + fileName + ".xaml", UriKind.Relative);
resourceInfo = Application.GetResourceStream(resourceUri);
}
if (resourceInfo == null)
{
throw new InvalidOperationException("The file named '" + fileName + " in the theme named '" + themeName + "' could not be found.");
}
StreamReader resourceReader = new StreamReader(resourceInfo.Stream);
return resourceReader.ReadToEnd();
}
internal static void LoadTheme(ResourceDictionary targetResources, string name)
{
ThemeInfo theme = new ThemeInfo();
string themeXaml = GetThemeXaml(name, "Theme", /* lookupShared */ false);
try
{
ExtractThemeContent(themeXaml, theme, /* extractIncludes */ true);
if ((theme.Includes.Length == 0) && (theme.Keys.Count != 0))
{
UserControl userControl = (UserControl)XamlReader.Load(themeXaml);
ResourceDictionary themeResources = userControl.Resources;
foreach (string key in theme.Keys)
{
targetResources.Add(key, themeResources[key]);
}
return;
}
}
catch (Exception e)
{
throw new InvalidOperationException("The theme named '" + name + "' contained invalid XAML.", e);
}
if (theme.Includes.Length != 0)
{
string[] includes = theme.Includes.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < includes.Length; i++)
{
string includeXaml = GetThemeXaml(name, includes[i], /* lookupShared */ true);
try
{
ExtractThemeContent(includeXaml, theme, /* extractIncludes */ false);
}
catch (Exception e)
{
throw new InvalidOperationException("The include named '" + includes[i] + "' in the theme named '" + name + "' contained invalid XAML.", e);
}
}
}
try
{
string mergedXaml = theme.GetXml();
UserControl userControl = (UserControl)XamlReader.Load(mergedXaml);
ResourceDictionary themeResources = userControl.Resources;
foreach (string key in theme.Keys)
{
targetResources.Add(key, themeResources[key]);
}
}
catch (Exception e)
{
throw new InvalidOperationException("The theme named '" + name + "' contained invalid XAML.", e);
}
}
private sealed class ThemeInfo
{
private StringBuilder _content;
private List<string> _keys;
private Dictionary<string, string> _keyMap;
private string _includes;
public ThemeInfo()
{
_content = new StringBuilder();
_keys = new List<string>();
_keyMap = new Dictionary<string, string>();
InitXmlNamespaces();
}
public string Includes
{
get
{
return _includes ?? String.Empty;
}
set
{
_includes = value;
}
}
public ICollection<string> Keys
{
get
{
return _keys;
}
}
public void AddItem(string key, string xml)
{
_keyMap[key] = String.Empty;
_keys.Add(key);
_content.Append(xml);
}
public bool ContainsKey(string key)
{
return _keyMap.ContainsKey(key);
}
// 增加自定义名称空间的功能
private Dictionary<string, string> _xmlNamespaces;
// 初始化名称空间字典
private void InitXmlNamespaces()
{
_xmlNamespaces = new Dictionary<string, string>();
_xmlNamespaces["xmlns"] = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
_xmlNamespaces["xmlns:x"] = "http://schemas.microsoft.com/winfx/2006/xaml";
_xmlNamespaces["xmlns:vsm"] = "clr-namespace:System.Windows;assembly=System.Windows";
}
// 组合名称空间属性
private string GetNamespaceAttributes()
{
var sb = new StringBuilder();
foreach (var pair in _xmlNamespaces)
{
sb.AppendFormat("/n {0}=/"{1}/"", pair.Key, pair.Value);
}
return sb.ToString();
}
// 替换掉内容中的名称空间
private void ReplaceContent()
{
foreach (var pair in _xmlNamespaces)
{
_content.Replace(pair.Key + "=/"" + pair.Value + "/"", "");
}
}
/// <summary>
/// 添加名称空间
/// </summary>
/// <returns></returns>
public void AddNamespace(string key, string value)
{
// 不覆盖已有的名称
if (_xmlNamespaces.ContainsKey(key))
return;
_xmlNamespaces[key] = value;
}
public string GetXml()
{
string resourceDictionaryFormat =
@"<UserControl {0}>
<UserControl.Resources>
{1}
</UserControl.Resources>
</UserControl>";
ReplaceContent();
return String.Format(resourceDictionaryFormat,
GetNamespaceAttributes(),
_content.ToString());
}
}
}
}
其中 灰色部分是主要修改的代码。这样改过后,重新编译 Nikhil Kothari 的 Framework 项目,并把生成的 dll 引用到自己项目中即可支持第三方控件的换肤了。
修改过的 DLL [ 下载]
(/Framework/Applications/Theme.cs)
public
string
GetXml() {
string resourceDictionaryFormat =
@" <UserControl
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:vsm=""clr-namespace:System.Windows;assembly=System.Windows"">
<UserControl.Resources>
{0}
</UserControl.Resources>
</UserControl> " ;
_content.Replace( @" xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" " , "" );
_content.Replace( @" xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"" " , "" );
_content.Replace( @" xmlns:vsm=""clr-namespace:System.Windows;assembly=System.Windows"" " , "" );
return String.Format(resourceDictionaryFormat, _content.ToString());
}
string resourceDictionaryFormat =
@" <UserControl
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:vsm=""clr-namespace:System.Windows;assembly=System.Windows"">
<UserControl.Resources>
{0}
</UserControl.Resources>
</UserControl> " ;
_content.Replace( @" xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" " , "" );
_content.Replace( @" xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"" " , "" );
_content.Replace( @" xmlns:vsm=""clr-namespace:System.Windows;assembly=System.Windows"" " , "" );
return String.Format(resourceDictionaryFormat, _content.ToString());
}
可以看出这里名称空间都是写死了的,不支持任何扩展。
而实际使用中,我们可能会对某个第三方控件定义样式。比如:
<
UserControl
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:liquid ="clr-namespace:Liquid;assembly=Liquid"
mc:Ignorable ="d"
x:Class ="UserControl"
d:DesignWidth ="640" d:DesignHeight ="480" >
< UserControl.Resources >
< Style x:Key ="MyLiquidDialog" TargetType ="liquid:Dialog" >
<!-- 略 -->
</ Style >
</ UserControl.Resources >
< Grid x:Name ="LayoutRoot" Background ="White" />
</ UserControl >
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:liquid ="clr-namespace:Liquid;assembly=Liquid"
mc:Ignorable ="d"
x:Class ="UserControl"
d:DesignWidth ="640" d:DesignHeight ="480" >
< UserControl.Resources >
< Style x:Key ="MyLiquidDialog" TargetType ="liquid:Dialog" >
<!-- 略 -->
</ Style >
</ UserControl.Resources >
< Grid x:Name ="LayoutRoot" Background ="White" />
</ UserControl >
这里定义了 Liquid 这套组件中 Dialog 控件的样式,而其中 xmlns:liquid 这个名称空间的声明就是必须的。
要达到这个目标,我们可以修改 Theme.cs 的 parse 步骤,在其过程中补充一段收集 xml namespace 的代码,并在合并最终的样式 xaml 时加进去即可。
修改过的 Theme.cs 代码如下:
![](/Images/OutliningIndicators/ContractedBlock.gif)
![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
// Theme.cs
// Copyright (c) Nikhil Kothari, 2008. All Rights Reserved.
// http://www.nikhilk.net
//
// This product's copyrights are licensed under the Creative
// Commons Attribution-ShareAlike (version 2.5).B
// http://creativecommons.org/licenses/by-sa/2.5/
//
// You are free to:
// - use this framework as part of your app
// - make use of the framework in a commercial app
// as long as your app or product is itself not a framework, control pack or
// developer toolkit of any sort under the following conditions:
// Attribution. You must attribute the original work in your
// product or release.
// Share Alike. If you alter, transform, or build as-is upon this work,
// you may only distribute the resulting app source under
// a license identical to this one.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Resources;
using System.Xml;
namespace Silverlight.FX.Applications
{
/// <summary>
/// Represents a theme control, whose top-level resource items are to be included
/// when this theme is selected.
/// </summary>
public class Theme : UserControl
{
private string _includes;
/// <summary>
/// Any additional themes to include. Each of the additional themes must contain
/// a UserControl declaration. The list of themes are processed in order after
/// the current theme's resource items have been included.
/// </summary>
public string Includes
{
get
{
return _includes ?? String.Empty;
}
set
{
_includes = value;
}
}
private static void ExtractThemeContent(string xml, ThemeInfo theme, bool extractIncludes)
{
XmlReader xmlReader = XmlReader.Create(
new StringReader(xml),
new XmlReaderSettings
{
CheckCharacters = false,
DtdProcessing = DtdProcessing.Ignore,
IgnoreComments = true,
IgnoreProcessingInstructions = true,
IgnoreWhitespace = true,
});
while (xmlReader.Read())
{
if (xmlReader.NodeType == XmlNodeType.Element)
{
// Found the first element
// 保存元素节点名称,因为下面要跳到属性去读取
var elementName = xmlReader.Name;
if (extractIncludes)
{
theme.Includes = xmlReader.GetAttribute("Includes");
}
// 添加名称空间
var hasAttr = xmlReader.MoveToFirstAttribute();
if (hasAttr)
{
do
{
if (xmlReader.Name.StartsWith("xmlns:"))
{
var val = xmlReader.ReadContentAsString();
theme.AddNamespace(xmlReader.Name, val);
}
} while (xmlReader.MoveToNextAttribute());
}
string resourcesTag = elementName + ".Resources";
// 改为由父节点属性读到指定名称的子节点. 不能用 ReadToDescendant.
if (xmlReader.ReadToFollowing(resourcesTag))
{
//if (xmlReader.ReadToDescendant(resourcesTag)) {
xmlReader.MoveToContent();
// TODO: I would have expected MoveToContent to move to the first item
// within the current tag, but apparently the reader is still
// positioned at the current tag, unless a Read is performed.
xmlReader.Read();
while (xmlReader.EOF == false)
{
if (xmlReader.NodeType == XmlNodeType.Element)
{
string key = xmlReader.GetAttribute("x:Key");
if ((String.IsNullOrEmpty(key) == false) && (theme.ContainsKey(key) == false))
{
string markup = xmlReader.ReadOuterXml();
theme.AddItem(key, markup);
}
else
{
xmlReader.Skip();
}
}
else
{
// No more items
break;
}
}
}
// We only look at the resources of the root tag, and so we're done
break;
}
}
}
private static string GetThemeXaml(string themeName, string fileName, bool lookupShared)
{
StreamResourceInfo resourceInfo = null;
// First try /Themes/<ThemeName>/<FileName>.xaml
Uri resourceUri = new Uri("Themes/" + themeName + "/" + fileName + ".xaml", UriKind.Relative);
resourceInfo = Application.GetResourceStream(resourceUri);
if ((resourceInfo == null) && lookupShared)
{
// The fallback is one level up, i.e. /Themes/<FileName>.xaml
resourceUri = new Uri("Themes/" + fileName + ".xaml", UriKind.Relative);
resourceInfo = Application.GetResourceStream(resourceUri);
}
if (resourceInfo == null)
{
throw new InvalidOperationException("The file named '" + fileName + " in the theme named '" + themeName + "' could not be found.");
}
StreamReader resourceReader = new StreamReader(resourceInfo.Stream);
return resourceReader.ReadToEnd();
}
internal static void LoadTheme(ResourceDictionary targetResources, string name)
{
ThemeInfo theme = new ThemeInfo();
string themeXaml = GetThemeXaml(name, "Theme", /* lookupShared */ false);
try
{
ExtractThemeContent(themeXaml, theme, /* extractIncludes */ true);
if ((theme.Includes.Length == 0) && (theme.Keys.Count != 0))
{
UserControl userControl = (UserControl)XamlReader.Load(themeXaml);
ResourceDictionary themeResources = userControl.Resources;
foreach (string key in theme.Keys)
{
targetResources.Add(key, themeResources[key]);
}
return;
}
}
catch (Exception e)
{
throw new InvalidOperationException("The theme named '" + name + "' contained invalid XAML.", e);
}
if (theme.Includes.Length != 0)
{
string[] includes = theme.Includes.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < includes.Length; i++)
{
string includeXaml = GetThemeXaml(name, includes[i], /* lookupShared */ true);
try
{
ExtractThemeContent(includeXaml, theme, /* extractIncludes */ false);
}
catch (Exception e)
{
throw new InvalidOperationException("The include named '" + includes[i] + "' in the theme named '" + name + "' contained invalid XAML.", e);
}
}
}
try
{
string mergedXaml = theme.GetXml();
UserControl userControl = (UserControl)XamlReader.Load(mergedXaml);
ResourceDictionary themeResources = userControl.Resources;
foreach (string key in theme.Keys)
{
targetResources.Add(key, themeResources[key]);
}
}
catch (Exception e)
{
throw new InvalidOperationException("The theme named '" + name + "' contained invalid XAML.", e);
}
}
private sealed class ThemeInfo
{
private StringBuilder _content;
private List<string> _keys;
private Dictionary<string, string> _keyMap;
private string _includes;
public ThemeInfo()
{
_content = new StringBuilder();
_keys = new List<string>();
_keyMap = new Dictionary<string, string>();
InitXmlNamespaces();
}
public string Includes
{
get
{
return _includes ?? String.Empty;
}
set
{
_includes = value;
}
}
public ICollection<string> Keys
{
get
{
return _keys;
}
}
public void AddItem(string key, string xml)
{
_keyMap[key] = String.Empty;
_keys.Add(key);
_content.Append(xml);
}
public bool ContainsKey(string key)
{
return _keyMap.ContainsKey(key);
}
// 增加自定义名称空间的功能
private Dictionary<string, string> _xmlNamespaces;
// 初始化名称空间字典
private void InitXmlNamespaces()
{
_xmlNamespaces = new Dictionary<string, string>();
_xmlNamespaces["xmlns"] = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
_xmlNamespaces["xmlns:x"] = "http://schemas.microsoft.com/winfx/2006/xaml";
_xmlNamespaces["xmlns:vsm"] = "clr-namespace:System.Windows;assembly=System.Windows";
}
// 组合名称空间属性
private string GetNamespaceAttributes()
{
var sb = new StringBuilder();
foreach (var pair in _xmlNamespaces)
{
sb.AppendFormat("/n {0}=/"{1}/"", pair.Key, pair.Value);
}
return sb.ToString();
}
// 替换掉内容中的名称空间
private void ReplaceContent()
{
foreach (var pair in _xmlNamespaces)
{
_content.Replace(pair.Key + "=/"" + pair.Value + "/"", "");
}
}
/// <summary>
/// 添加名称空间
/// </summary>
/// <returns></returns>
public void AddNamespace(string key, string value)
{
// 不覆盖已有的名称
if (_xmlNamespaces.ContainsKey(key))
return;
_xmlNamespaces[key] = value;
}
public string GetXml()
{
string resourceDictionaryFormat =
@"<UserControl {0}>
<UserControl.Resources>
{1}
</UserControl.Resources>
</UserControl>";
ReplaceContent();
return String.Format(resourceDictionaryFormat,
GetNamespaceAttributes(),
_content.ToString());
}
}
}
}
其中 灰色部分是主要修改的代码。这样改过后,重新编译 Nikhil Kothari 的 Framework 项目,并把生成的 dll 引用到自己项目中即可支持第三方控件的换肤了。
修改过的 DLL [ 下载]