Silverlight开发中的疑难杂症-控件设计篇-如何自动合并控件的默认样式
在WPF中开发自定义控件时,可以将控件的默认样式放在以“<控件类型>.Generic.xaml”的形式命名的资源文件中,从而分离各个自定义控件的默认样式的定义,减少单个Generic.xaml文件的复杂度。
但是在Silverlight控件开发时,却发现无法采用上面的方法来实现这一效果,尝试了许久都没有找到其他的办法实现这一效果。郁闷之中,突然想起看一下Silverlight Toolkit中是如何解决这一问题的,结果惊讶的发现它也是将所有的默认样式都堆积在了Generic.xaml一个文件当中,感觉相当的不可思议。但是,仔细一看,发现在Generic.xaml文件的开头有如下的一段话:
// WARNING:
//
// This XAML was automatically generated by merging the individual default
// styles. Changes to this file may cause incorrect behavior and will be lost
// if the XAML is regenerated.
-->
这让我又感觉到了希望,于是祭出神器“谷歌”,以上面的话中的一部分为关键字,进行了搜索,果然让我找到了相关的文章,原文地址如下:http://www.jeff.wilcox.name/2009/01/default-style-task/ ,在这里面介绍了如何通过MSBuild的自定义任务来实现将项目中的独立的控件样式在编译阶段合并到一起,文章里面包含了详细的解说和代码,只要按照提示一步步来做就可以了。
如果您对MSBuild比较熟悉,那么按照他的提示,应该就能顺利的完成这一个功能。但是很不幸,我对于MSBuild一窍不通,于是第一遍下来,编译后,什么事情都没有发生。如果你跟我一样对MSBuild不甚了解,但是又想要能够得到这个非常有用的特性,那么希望下面的介绍能够对你有所帮助。关于MSBuild的介绍及相关知识,有兴趣的朋友可以参见MSDN中的相关文章,链接如下:http://msdn.microsoft.com/zh-cn/library/ms171452.aspx 我在这里只介绍如何简单的实现这一功能。
首先,新建一个类库项目,在里面添加文章中提到的两个类,代码如下:
2 // This source is subject to the Microsoft Public License (Ms-PL).
3 // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
4 // All other rights reserved.
5
6 using System;
7 using System.Collections.Generic;
8 using System.Diagnostics.CodeAnalysis;
9 using System.IO;
10 using System.Text;
11 using Microsoft.Build.Framework;
12 using Microsoft.Build.Utilities;
13
14 namespace Engineering.Build.Tasks
15 {
16 /// <summary>
17 /// Build task to automatically merge the default styles for controls into
18 /// a single generic.xaml file.
19 /// </summary>
20 public class MergeDefaultStylesTask : Task
21 {
22 /// <summary>
23 /// Gets or sets the root directory of the project where the
24 /// generic.xaml file resides.
25 /// </summary>
26 [Required]
27 public string ProjectDirectory { get; set; }
28
29 /// <summary>
30 /// Gets or sets the project items marked with the "DefaultStyle" build
31 /// action.
32 /// </summary>
33 [Required]
34 public ITaskItem[] DefaultStyles { get; set; }
35
36 /// <summary>
37 /// Initializes a new instance of the MergeDefaultStylesTask class.
38 /// </summary>
39 public MergeDefaultStylesTask()
40 {
41 }
42
43 /// <summary>
44 /// Merge the project items marked with the "DefaultStyle" build action
45 /// into a single generic.xaml file.
46 /// </summary>
47 /// <returns>
48 /// A value indicating whether or not the task succeeded.
49 /// </returns>
50 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Task should not throw exceptions.")]
51 public override bool Execute()
52 {
53 Log.LogMessage(MessageImportance.Low, "Merging default styles into generic.xaml.");
54
55 // Get the original generic.xaml
56 string originalPath = Path.Combine(ProjectDirectory, Path.Combine("themes", "generic.xaml"));
57 if (!File.Exists(originalPath))
58 {
59 Log.LogError("{0} does not exist!", originalPath);
60 return false;
61 }
62 Log.LogMessage(MessageImportance.Low, "Found original generic.xaml at {0}.", originalPath);
63 string original = null;
64 Encoding encoding = Encoding.Default;
65 try
66 {
67 using (StreamReader reader = new StreamReader(File.Open(originalPath, FileMode.Open, FileAccess.Read)))
68 {
69 original = reader.ReadToEnd();
70 encoding = reader.CurrentEncoding;
71 }
72 }
73 catch (Exception ex)
74 {
75 Log.LogErrorFromException(ex);
76 return false;
77 }
78
79 // Create the merged generic.xaml
80 List<DefaultStyle> styles = new List<DefaultStyle>();
81 foreach (ITaskItem item in DefaultStyles)
82 {
83 string path = Path.Combine(ProjectDirectory, item.ItemSpec);
84 if (!File.Exists(path))
85 {
86 Log.LogWarning("Ignoring missing DefaultStyle {0}.", path);
87 continue;
88 }
89
90 try
91 {
92 Log.LogMessage(MessageImportance.Low, "Processing file {0}.", item.ItemSpec);
93 styles.Add(DefaultStyle.Load(path));
94 }
95 catch (Exception ex)
96 {
97 Log.LogErrorFromException(ex);
98 }
99 }
100 string merged = null;
101 try
102 {
103 merged = DefaultStyle.Merge(styles).GenerateXaml();
104 }
105 catch (InvalidOperationException ex)
106 {
107 Log.LogErrorFromException(ex);
108 return false;
109 }
110
111 // Write the new generic.xaml
112 if (original != merged)
113 {
114 Log.LogMessage(MessageImportance.Low, "Writing merged generic.xaml.");
115
116 try
117 {
118 // Could interact with the source control system / TFS here
119 File.SetAttributes(originalPath, FileAttributes.Normal);
120 Log.LogMessage("Removed any read-only flag for generic.xaml.");
121
122 File.WriteAllText(originalPath, merged, encoding);
123 Log.LogMessage("Successfully merged generic.xaml.");
124 }
125 catch (Exception ex)
126 {
127 Log.LogErrorFromException(ex);
128 return false;
129 }
130 }
131 else
132 {
133 Log.LogMessage("Existing generic.xaml was up to date.");
134 }
135
136 return true;
137 }
138 }
139 }
140
2 // This source is subject to the Microsoft Public License (Ms-PL).
3 // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
4 // All other rights reserved.
5
6 using System;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.IO;
10 using System.Linq;
11 using System.Xml.Linq;
12
13 namespace Engineering.Build
14 {
15 /// <summary>
16 /// DefaultStyle represents the XAML of an individual Control's default
17 /// style (in particular its ControlTemplate) which can be merged with other
18 /// default styles). The XAML must have a ResourceDictionary as its root
19 /// element and be marked with a DefaultStyle build action in Visual Studio.
20 /// </summary>
21 public partial class DefaultStyle
22 {
23 /// <summary>
24 /// Root element of both the default styles and the merged generic.xaml.
25 /// </summary>
26 private const string RootElement = "ResourceDictionary";
27
28 /// <summary>
29 /// Gets or sets the file path of the default style.
30 /// </summary>
31 public string DefaultStylePath { get; set; }
32
33 /// <summary>
34 /// Gets the namespaces imposed on the root element of a default style
35 /// (including explicitly declared namespaces as well as those inherited
36 /// from the root ResourceDictionary element).
37 /// </summary>
38 public SortedDictionary<string, string> Namespaces { get; private set; }
39
40 /// <summary>
41 /// Gets the elements in the XAML that include both styles and shared
42 /// resources.
43 /// </summary>
44 public SortedDictionary<string, XElement> Resources { get; private set; }
45
46 /// <summary>
47 /// Gets or sets the history tracking which resources originated from
48 /// which files.
49 /// </summary>
50 private Dictionary<string, string> MergeHistory { get; set; }
51
52 /// <summary>
53 /// Initializes a new instance of the DefaultStyle class.
54 /// </summary>
55 protected DefaultStyle()
56 {
57 Namespaces = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
58 Resources = new SortedDictionary<string, XElement>(StringComparer.OrdinalIgnoreCase);
59 MergeHistory = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
60 }
61
62 /// <summary>
63 /// Load a DefaultStyle from the a project item.
64 /// </summary>
65 /// <param name="path">
66 /// Path of the default style which is used for reporting errors.
67 /// </param>
68 /// <returns>The DefaultStyle.</returns>
69 public static DefaultStyle Load(string path)
70 {
71 DefaultStyle style = new DefaultStyle();
72 style.DefaultStylePath = path;
73
74 string xaml = File.ReadAllText(path);
75 XElement root = XElement.Parse(xaml, LoadOptions.PreserveWhitespace);
76 if (root.Name.LocalName == RootElement)
77 {
78 // Get the namespaces
79 foreach (XAttribute attribute in root.Attributes())
80 {
81 if (attribute.Name.LocalName == "xmlns")
82 {
83 style.Namespaces.Add("", attribute.Value);
84 }
85 else if (attribute.Name.NamespaceName == XNamespace.Xmlns.NamespaceName)
86 {
87 style.Namespaces.Add(attribute.Name.LocalName, attribute.Value);
88 }
89 }
90
91 // Get the styles and shared resources
92 foreach (XElement element in root.Elements())
93 {
94 string name = (element.Name.LocalName == "Style") ?
95 GetAttribute(element, "TargetType", "Key", "Name") :
96 GetAttribute(element, "Key", "Name");
97 if (style.Resources.ContainsKey(name))
98 {
99 throw new InvalidOperationException(string.Format(
100 CultureInfo.InvariantCulture,
101 "Resource \"{0}\" is used multiple times in {1} (possibly as a Key, Name, or TargetType)!",
102 name,
103 path));
104 }
105 style.Resources.Add(name, element);
106 style.MergeHistory[name] = path;
107 }
108 }
109
110 return style;
111 }
112
113 /// <summary>
114 /// Get the value of the first attribute that is defined.
115 /// </summary>
116 /// <param name="element">Element with the attributes defined.</param>
117 /// <param name="attributes">
118 /// Local names of the attributes to find.
119 /// </param>
120 /// <returns>Value of the first attribute found.</returns>
121 private static string GetAttribute(XElement element, params string[] attributes)
122 {
123 foreach (string name in attributes)
124 {
125 string value =
126 (from a in element.Attributes()
127 where a.Name.LocalName == name
128 select a.Value)
129 .FirstOrDefault();
130 if (name != null)
131 {
132 return value;
133 }
134 }
135 return "";
136 }
137
138 /// <summary>
139 /// Merge a sequence of DefaultStyles into a single style.
140 /// </summary>
141 /// <param name="styles">Sequence of DefaultStyles.</param>
142 /// <returns>Merged DefaultStyle.</returns>
143 public static DefaultStyle Merge(IEnumerable<DefaultStyle> styles)
144 {
145 DefaultStyle combined = new DefaultStyle();
146 if (styles != null)
147 {
148 foreach (DefaultStyle style in styles)
149 {
150 combined.Merge(style);
151 }
152 }
153 return combined;
154 }
155
156 /// <summary>
157 /// Merge with another DefaultStyle.
158 /// </summary>
159 /// <param name="other">Other DefaultStyle to merge.</param>
160 private void Merge(DefaultStyle other)
161 {
162 // Merge or lower namespaces
163 foreach (KeyValuePair<string, string> ns in other.Namespaces)
164 {
165 string value = null;
166 if (!Namespaces.TryGetValue(ns.Key, out value))
167 {
168 Namespaces.Add(ns.Key, ns.Value);
169 }
170 else if (value != ns.Value)
171 {
172 other.LowerNamespace(ns.Key);
173 }
174 }
175
176 // Merge the resources
177 foreach (KeyValuePair<string, XElement> resource in other.Resources)
178 {
179 if (Resources.ContainsKey(resource.Key))
180 {
181 throw new InvalidOperationException(string.Format(
182 CultureInfo.InvariantCulture,
183 "Resource \"{0}\" is used by both {1} and {2}!",
184 resource.Key,
185 MergeHistory[resource.Key],
186 other.DefaultStylePath));
187 }
188 Resources[resource.Key] = resource.Value;
189 MergeHistory[resource.Key] = other.DefaultStylePath;
190 }
191 }
192
193 /// <summary>
194 /// Lower a namespace from the root ResourceDictionary to its child
195 /// resources.
196 /// </summary>
197 /// <param name="prefix">Prefix of the namespace to lower.</param>
198 private void LowerNamespace(string prefix)
199 {
200 // Get the value of the namespace
201 string @namespace;
202 if (!Namespaces.TryGetValue(prefix, out @namespace))
203 {
204 return;
205 }
206
207 // Push the value into each resource
208 foreach (KeyValuePair<string, XElement> resource in Resources)
209 {
210 // Don't push the value down if it was overridden locally or if
211 // it's the default namespace (as it will be lowered
212 // automatically)
213 if (((from e in resource.Value.Attributes()
214 where e.Name.LocalName == prefix
215 select e).Count() == 0) &&
216 !string.IsNullOrEmpty(prefix))
217 {
218 resource.Value.Add(new XAttribute(XName.Get(prefix, XNamespace.Xmlns.NamespaceName), @namespace));
219 }
220 }
221 }
222
223 /// <summary>
224 /// Generate the XAML markup for the default style.
225 /// </summary>
226 /// <returns>Generated XAML markup.</returns>
227 public string GenerateXaml()
228 {
229 // Create the ResourceDictionary
230 string defaultNamespace = XNamespace.Xml.NamespaceName;
231 Namespaces.TryGetValue("", out defaultNamespace);
232 XElement resources = new XElement(XName.Get(RootElement, defaultNamespace));
233
234 // Add the shared namespaces
235 foreach (KeyValuePair<string, string> @namespace in Namespaces)
236 {
237 // The default namespace will be added automatically
238 if (string.IsNullOrEmpty(@namespace.Key))
239 {
240 continue;
241 }
242 resources.Add(new XAttribute(
243 XName.Get(@namespace.Key, XNamespace.Xmlns.NamespaceName),
244 @namespace.Value));
245 }
246
247 // Add the resources
248 foreach (KeyValuePair<string, XElement> element in Resources)
249 {
250 resources.Add(
251 new XText(Environment.NewLine + Environment.NewLine + " "),
252 new XComment(" " + element.Key + " "),
253 new XText(Environment.NewLine + " "),
254 element.Value);
255 }
256
257 resources.Add(new XText(Environment.NewLine + Environment.NewLine));
258
259 // Create the document
260 XDocument document = new XDocument(
261 // TODO: Pull this copyright header from some shared location
262 new XComment(Environment.NewLine +
263 "// (c) Copyright Microsoft Corporation." + Environment.NewLine +
264 "// This source is subject to the Microsoft Public License (Ms-PL)." + Environment.NewLine +
265 "// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details." + Environment.NewLine +
266 "// All other rights reserved." + Environment.NewLine),
267 new XText(Environment.NewLine + Environment.NewLine),
268 new XComment(Environment.NewLine +
269 "// WARNING:" + Environment.NewLine +
270 "// " + Environment.NewLine +
271 "// This XAML was automatically generated by merging the individual default" + Environment.NewLine +
272 "// styles. Changes to this file may cause incorrect behavior and will be lost" + Environment.NewLine +
273 "// if the XAML is regenerated." + Environment.NewLine),
274 new XText(Environment.NewLine + Environment.NewLine),
275 resources);
276
277 return document.ToString();
278 }
279
280 /// <summary>
281 /// Generate the XAML markup for the default style.
282 /// </summary>
283 /// <returns>Generated XAML markup.</returns>
284 public override string ToString()
285 {
286 return GenerateXaml();
287 }
288 }
289 }
290
在实际操作中,发现当我定义了两个Button的自定义样式,他们的TargetType都是Button,但是x:Key不同,可是最后生成时还是发生了错误,提示重复。检查了代码,感觉以下两个方法有些问题,所以进行了修改,改为按照key、name、targertype的顺序进行键的取值,而不是先判断targertype,相关代码如下,修改部分用红色标出:
2 {
3 DefaultStyle style = new DefaultStyle();
4 style.DefaultStylePath = path;
5
6 string xaml = File.ReadAllText(path);
7 XElement root = XElement.Parse(xaml, LoadOptions.PreserveWhitespace);
8 if (root.Name.LocalName == RootElement)
9 {
10 // Get the namespaces
11 foreach (XAttribute attribute in root.Attributes())
12 {
13 if (attribute.Name.LocalName == "xmlns")
14 {
15 style.Namespaces.Add("", attribute.Value);
16 }
17 else if (attribute.Name.NamespaceName == XNamespace.Xmlns.NamespaceName)
18 {
19 style.Namespaces.Add(attribute.Name.LocalName, attribute.Value);
20 }
21 }
22
23 // Get the styles and shared resources
24 foreach (XElement element in root.Elements())
25 {
26 string name = (element.Name.LocalName == "Style") ?
27 GetAttribute(element, "TargetType", "Key", "Name") :
28 GetAttribute(element, "Key", "Name");
29 if (style.Resources.ContainsKey(name))
30 {
31 throw new InvalidOperationException(string.Format(
32 CultureInfo.InvariantCulture,
33 "Resource \"{0}\" is used multiple times in {1} (possibly as a Key, Name, or TargetType)!",
34 name,
35 path));
36 }
37 style.Resources.Add(name, element);
38 style.MergeHistory[name] = path;
39 }
40 }
41
42 return style;
43 }
44
45 private static string GetAttribute(XElement element, params string[] attributes)
46 {
47 foreach (string name in attributes)
48 {
49 string value =
50 (from a in element.Attributes()
51 where a.Name.LocalName == name
52 select a.Value)
53 .FirstOrDefault();
54 if (name != null)
55 {
56 return value;
57 }
58 }
59 return "";
60 }
61
2 {
3 DefaultStyle style = new DefaultStyle();
4 style.DefaultStylePath = path;
5
6 string xaml = File.ReadAllText(path);
7 XElement root = XElement.Parse(xaml, LoadOptions.PreserveWhitespace);
8 if (root.Name.LocalName == RootElement)
9 {
10 // Get the namespaces
11 foreach (XAttribute attribute in root.Attributes())
12 {
13 if (attribute.Name.LocalName == "xmlns")
14 {
15 style.Namespaces.Add("", attribute.Value);
16 }
17 else if (attribute.Name.NamespaceName == XNamespace.Xmlns.NamespaceName)
18 {
19 style.Namespaces.Add(attribute.Name.LocalName, attribute.Value);
20 }
21 }
22
23 // Get the styles and shared resources
24 foreach (XElement element in root.Elements())
25 {
26 //此处进行了修改
27 string name = (element.Name.LocalName == "Style") ?
28 GetAttribute(element, "Key", "Name", "TargetType") :
29 GetAttribute(element, "Key", "Name");
30 if (style.Resources.ContainsKey(name))
31 {
32 throw new InvalidOperationException(string.Format(
33 CultureInfo.InvariantCulture,
34 "Resource \"{0}\" is used multiple times in {1} (possibly as a Key, Name, or TargetType)!",
35 name,
36 path));
37 }
38 style.Resources.Add(name, element);
39 style.MergeHistory[name] = path;
40 }
41 }
42
43 return style;
44 }
45
46 private static string GetAttribute(XElement element, params string[] attributes)
47 {
48 foreach (string name in attributes)
49 {
50 string value =
51 (from a in element.Attributes()
52 where a.Name.LocalName == name
53 select a.Value)
54 .FirstOrDefault();
55 //此处进行了修改
56 if (value != null)
57 {
58 return value;
59 }
60 }
61 return "";
62 }
63
OK,代码添加完毕,编译后就得到了一个自定义任务的实现类,接下来就是对要进行自定任务运行的项目文件进行编辑。
首先,在VS中右键Unload你要编辑的项目,然后右键选择编辑改项目,在<Project节点的下面加上如下的自定义任务的声明,如下:
2 <UsingTask
3 TaskName="Engineering.Build.Tasks.MergeDefaultStylesTask"
4 AssemblyFile="$(EngineeringResources)\Engineering.Build.dll" />
5
其中TaskName为刚才新建的Task类的全名,AssemblyFile为该类所在的程序集的物理地址,这里使用了一个预先的符号,你需要将其改成自己的实际地址。
然后,在之前的定义下面添加一个ItemGroup,这样可以让VS识别到这个Build Action,定义如下:
2 <ItemGroup Condition="'$(BuildingInsideVisualStudio)'=='true'">
3 <AvailableItemName Include="DefaultStyle" >
4 <Visible>false</Visible>
5 </AvailableItemName>
6 </ItemGroup>
7
注意这里跟原文不同的是添加了一个<Visible>false</Visible>属性,你可以尝试将其去掉,会发现在项目中多出了一个名为DefaultStyle的文件。
最后,添加自定义任务的执行,这也是我卡住的地方,最后发现可能是由于项目文件是在VS中生成的原因,原文中的Targer定义之后不起作用,而是需要将其定义放到VS生成的项目文件所指定的位置,具体位置如下:
2 Other similar extension points exist, see Microsoft.Common.targets. -->
3 <Target Name="BeforeBuild">
4 </Target>
5 <Target Name="AfterBuild" Inputs="@(DefaultStyle)" Outputs="$(ProjectDir)\Themes\Generic.xaml">
6 <MergeDefaultStylesTask DefaultStyles="@(DefaultStyle)" ProjectDirectory="$(ProjectDir)" />
7 </Target>
8
按照字面的意思,我选择了在编译成功后执行我们的自定义任务,关于为什么必须在这里添加的任务才会执行,由于我对于MSBuild方面知识的匮乏,无法给大家一个解释,我猜测可能是下面这句话的原因:
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Silverlight\v3.0\Microsoft.Silverlight.CSharp.targets" />
时间有限,暂时还不想将时间放在了解MSBuild,希望有知道的朋友能够给以回复。
好了,按照上面的流程就完成了整个任务的定义,以后你只需要在修改了某个资源文件后,点击Build,该任务就会自动帮你把所有Build Action设置为DefaultStyle的资源文件进行合并。
PS:目前只能在有相应的资源文件被修改后Build才会进行合并操作,而在原文中还有一个任务,能够在Rebuild的时候强制合并资源文件,但是我添加后并没有起作用,如果有了解的朋友,还希望能够在回复中给与指出,不甚感激!