Silverlight开发中的疑难杂症-控件设计篇-如何自动合并控件的默认样式

WPF中开发自定义控件时,可以将控件的默认样式放在以“<控件类型>.Generic.xaml”的形式命名的资源文件中,从而分离各个自定义控件的默认样式的定义,减少单个Generic.xaml文件的复杂度。

但是在Silverlight控件开发时,却发现无法采用上面的方法来实现这一效果,尝试了许久都没有找到其他的办法实现这一效果。郁闷之中,突然想起看一下Silverlight Toolkit中是如何解决这一问题的,结果惊讶的发现它也是将所有的默认样式都堆积在了Generic.xaml一个文件当中,感觉相当的不可思议。但是,仔细一看,发现在Generic.xaml文件的开头有如下的一段话:

XML
<!--
// 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 我在这里只介绍如何简单的实现这一功能。

       首先,新建一个类库项目,在里面添加文章中提到的两个类,代码如下:

MergeDefaultStylesTask
  1 // (c) Copyright Microsoft Corporation.   
  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 { getset; }
 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 { getset; }
 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 
DefaultStyle
  1 // (c) Copyright Microsoft Corporation.
  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 { getset; }
 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<stringstring> Namespaces { getprivate 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 { getprivate set; }
 45 
 46         /// <summary>
 47         /// Gets or sets the history tracking which resources originated from
 48         /// which files.
 49         /// </summary>
 50         private Dictionary<stringstring> MergeHistory { getset; }
 51 
 52         /// <summary>
 53         /// Initializes a new instance of the DefaultStyle class.
 54         /// </summary>
 55         protected DefaultStyle()
 56         {
 57             Namespaces = new SortedDictionary<stringstring>(StringComparer.OrdinalIgnoreCase);
 58             Resources = new SortedDictionary<string, XElement>(StringComparer.OrdinalIgnoreCase);
 59             MergeHistory = new Dictionary<stringstring>(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<stringstring> 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<stringstring> @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不同,可是最后生成时还是发生了错误,提示重复。检查了代码,感觉以下两个方法有些问题,所以进行了修改,改为按照keynametargertype的顺序进行键的取值,而不是先判断targertype,相关代码如下,修改部分用红色标出:

原文代码
 1 public static DefaultStyle Load(string path)
 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 
修改后的代码
 1 public static DefaultStyle Load(string path)
 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节点的下面加上如下的自定义任务的声明,如下:

自定义任务的声明
1 <Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
2   <UsingTask
3   TaskName="Engineering.Build.Tasks.MergeDefaultStylesTask"
4   AssemblyFile="$(EngineeringResources)\Engineering.Build.dll" />
5 

其中TaskName为刚才新建的Task类的全名,AssemblyFile为该类所在的程序集的物理地址,这里使用了一个预先的符号,你需要将其改成自己的实际地址。

       然后,在之前的定义下面添加一个ItemGroup,这样可以让VS识别到这个Build Action,定义如下:

Build Action
1   <!-- Add "DefaultStyle" as a Build Action in Visual Studio -->
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生成的项目文件所指定的位置,具体位置如下:

Targer
1 <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
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的时候强制合并资源文件,但是我添加后并没有起作用,如果有了解的朋友,还希望能够在回复中给与指出,不甚感激!

 

posted on 2010-03-06 23:06  yingql  阅读(1303)  评论(11编辑  收藏  举报

导航