WPF应用程序中的程序集资源与其他.NET应用程序中的程序集资源在本质上是相同的。基本概念是为项目添加文件,从而Visual studio可将其嵌入到编译过的应用程序的EXE或DLL文件中。WPF程序集资源与其他应用程序中的程序集资源之间的重要区别是引用他们的寻址系统不同。
在前面章节已讨论过程序集资源的工作原理。因为每次编译应用程序时,项目中的每个XAML文件都转换为解析效率更高的BAML文件。这些BAML文件作为独立资源嵌入到程序集中。添加自己的资源同样很容易。
一、添加资源
可通过向项目添加文件,并在Properties窗口中将其Build Action属性设置为Resource来添加自己的资源。这是需要完成的全部工作——这确实是好消息。
为更加合理地组织资源,可在项目中创建子文件夹(在Solution Explorer中右击项目名称,然后选择Add|New Folder菜单项),然后使用这些子文件夹组织不同类型的资源。
以这种方式添加的资源易于更新。只需要替换文件并重新编译应用程序即可。例如,可在Windows浏览器中将所有新文件复制到指定文件夹中。只要替换在项目中包含的文件的内容,就不必在Visual Studio中再采取任何其他特殊步骤(除了实际编译应用程序外)。
为成功地使用程序集资源,务必注意以下两点:
不能将Build Action属性错误地设置为Embedded Resource。尽管所有程序集资源都被定义为嵌入的资源,但Embedded Resource生成操作会在另一个更难访问的位置放置二进制数据。在WPF应用程序中,假定总是使用Resource生成类型。
不要将Project Properties窗口中使用Resource选项卡。WPF不支持这种类型的资源URI。
好奇的编程人员自然希望了解嵌入到程序集中的资源到底发生了什么变化。WPF将他们和其他BAML资源合并到单独的流中。单独的资源流使用以下格式命名AssemblyName.g.resources。
如果想要实际查看在编译过的程序集中嵌入的资源,可使用反编译工具。例如,使用Reflector(http://reflector.net)的更出色工具来深入挖掘资源。
除所有图像和音频文件外,还可看到用于应用程序中窗口的BAML资源。在WPF中,文件中的空格不会引起问题,因为Visual Studio足够智能,它能够正确地略过他们。当应用程序被编译过之后,你可能还会注意到文件名变成了小写形式。
二、检索资源
显然,添加资源非常容易,但到底如何使用他们呢?可以采用多种方法来使用资源。
低级方法是检索封装数据的StreamResourceInfo对象,然后决定如何使用该对象。可通过代码,使用静态方法Application.GetResourceStream()完成该工作。例如,下面的代码为winter.jpg图像获取StreamResourceInfo对象:
StreamResourceInfo sri=Application.GetResourceStream(new Uri("image/winter.jpg",UriKind.Relative));
一旦得到StreamResourceInfo对象,就可以得到两部分信息。ContentType属性返回一个描述数据类型的字符串——在该例中是image/jpg。Stream属性返回一个UnmanagedMemoryStream对象,可使用该对象读取数据,一次读取一个字节。
GetResourceStream()的确是一个很有用的辅助方法,它封装了ResourceManager类和ResourceSet类。这些类是.NET Framework资源体系的核心,自从.NET 1.0开始就提供了这些类。如果不使用GetResourceStream()方法,就需要具体访问AssemblyName.g.resources资源流(这是存储所有WPF资源的地方),并查找所需的对象。下面是完成这一操作的非常简单的代码:
Assembly assembly=Assembly.GetAssembly(this.GetType()); string resourceName=assembly.GetName().Name+".g"; ResourceManager rm=new ResourceManager(resourceName,assembly); using(ResourceSet set=rm.GetResourceSet(CultureInfo.CurrentCulture,tur,true)) { UnmanagedMemoryStream s; s=(UnmanagedMemoryStream)set.GetOjbect("images/winter.jpg",true); }
通过ResourceManager类和ResourceSet类还可完成其他一些Application类自身不能完成的工作。例如,下面的代码片段会向你现实在AssemblyName.g.resources资源流中所有嵌入资源的名称:
Assembly assembly=Assembly.GetAssembly(this.GetType()); string resourceName=assembly.GetName().Name+".g"; ResourceManager rm=new ResourceManager(resourceName,assembly); using(ResourceSet set=rm.GetResourceSet(CultureInfo.CurrentCulture,true,ture)) { foreach(DictionaryEntry res in set) { MessageBox.Show(res.Key.ToSting()); } }
虽然GetResourceStream()方法可提供帮助,但直接检索资源还可能会遇到麻烦,问题是使用该方法得到的相对低级的UnmanagedMemoryStream对象,该对象本身没有什么用处,需要将它转换成一些更有意义的数据,例如具有属性和方法的高级对象。
WPF提供了几个专门使用资源的类。这些类不要求提取资源(这非常混乱且不是类型安全的),他们使用资源的名称访问资源。例如,如果希望在WPF的Image元素中显示Blue.jpg图像,可使用下面的标记:
<Image Source="Images/Blue.jpg">Image>
注意反斜杠变成了正斜杠,因为这是WPF作用URI的约定(实际上这两种方式都可行,但为了连贯起见,建议使用正斜杠)。
可使用代码完成相同的工作。对于Image元素,只需要将Source属性设置为BitmapImage对象,该对象使用URI确定希望显示的图像的位置,可以像下面这样指定完全限定的文件路径:
img.Source = new BitmapImage(new Uri("d:\images\winter.jpg",));
但如果使用相对URI,就可从程序集中提取不同资源,并将他们传递给图像,而且不需要使用UnmanagedMemoryStream对象:
img.Source = new BitmapImage(new Uri("images/winter.jpg", UriKind.Relative));
该技术通过在基本应用程序URI的末尾处加上images/winter.jpg构造了URI。大多数情况下不需要考虑URI语法——只要遵循相对URI,剩下的工作就由程序集负责了。然而有些情况下,更详细理解URI系统的非常重要的,当希望访问嵌入到另一个程序集中额资源时更是如此。
三、pack URI
WPF使用pack URI语法寻址编译过的资源(比如用于页面的BAML)。上一节的Image对象和标签使用相对URI来引用资源,如下所示:
images/winter.jpg
这与下面更繁琐的绝对URI是等效的:
pack://application:,,,/images/winter.jpg
当为一幅图像设置源时可使用这种绝对URI,尽管这种方法没有任何优点:
img.Source = new BitmapImage(new Uri("pack://application:,,,/images/winter.jpg"));
pack URI语法来自XPS(XML Paper Specification,XML页面规范)标准。它看起来非常奇怪,因为它在一个URI中嵌入了另一个URI。三个逗号实际上时三个转义的斜杠。换句话说,上面显示的包含应用成功需URI的pack URI是以application:///开头的。
位于其他程序集中的资源
使用pack URI还可检索嵌入到另一个库中的资源(换句话说,在应用程序中使用的DLL程序集中的资源)。这种情况下需要使用如下语言:
pack://application:,,,/AssemblyName;component/ResourceName
例如,如果图像呗嵌入到引用的名为ImageLibrary的程序集中,将需要使用如下URI:
img.Source=new BitmapImage(new Uri("pack://application:,,,/ImageLibrary;component/images/winter.jpg"));
或从更实用的角度看,可使用等价的相对URI:
img.Source=new BitmapImage(new Uri("ImageLibrary;component/images/winter.jpg",UriKind.Relative));
如果使用强命名的程序集,可使用包含版本和/或公钥标记的限定程序集引用代替程序集的名称。使用分号隔离每段信息,并在版本号数字之前添加字符v.下面是一个使用版本号的示例:
image.Source=new BitmapImage(new Uri("ImageLibrary;v1.25;component/images/winter.jpg",UriKind.Relative));
下面的示例同时使用了版本号和公钥标记:
image.Source=new BitmapImage(new Uri("ImageLibrary;v1.25;dc642a7f5bd64912;component/images/winter.jpg",UriKind.Relative));
四、内容文件
当嵌入文件作为资源时,会将文件放到编译过的程序集中,并且可以确保文件总是可用的。对于部署而言这是理想选择,并且可避免可能存在的问题。然而在有些情况下,使用这种方法并不方便:
- 希望改变资源文件,又不想重新编译应用程序。
- 资源文件非常大。
- 资源文件是可选的,并且可以不随程序集一起部署。
- 资源是声音文件。
显然,可事业能够应用程序部署文件,并为应用程序添加代码,进而从硬盘驱动器中读取这些文件来解决该问题。然而,WPF还有更方便的选择,使这一过程更加容易管理。可将这些未编译的文件专门标记为内容文件。
不能将内容文件嵌入到程序集中。然而,WPF为程序集添加了AssemblyAssociatedContentFile特性,公告每个内容文件的存在。该特性还记录了每个内容文件相对可执行文件的位置(指示内容文件是否和可执行文件位于同一文件夹中,或者位于某个子文件夹中)。最方便的是,当为能够理解资源的元素(如Image类)使用内容文件时,可使用相同的URI系统。
为测试该技术,为项目添加声音文件,在Solution Exporer中选择该文件,并在Properties窗口中将Build Action属性改为Content,确保将Copy to Output Directory属性设置为Copy Always,以确保当生产项目时将声音文件复制到输出目录中。
现在可使用相对URI,将MediaElement元素指向内容文件:
<MediaElement Name="Sound" Source="Sounds/start.wav" LoadedBehavior="Manual">MediaElement>