17. 程序集_03_资源和附属程序集

1 个月前 · 来自专栏 C# .NET

资源和附属程序集 Resources and Satellite Assemblies

应用程序通常不仅包含可执行代码,还包含文本、图像或 XML 文件等内容。此类内容可以通过资源resource在程序集中表示。资源有两个重叠的用例:

  • 合并不能进入源代码的数据,例如图像
  • 在多语言应用程序中存储可能需要翻译的数据

程序集资源最终是一个带有名称的字节流。您可以将程序集视为包含以字符串为键的字节数组字典。如果反汇编包含名为 banner.jpg 的资源和名为 data.xml 的资源的程序集,您可以在 ildasm 中看到这一点:

.mresource public banner.jpg
    // Offset: 0x00000F58 Length: 0x000004F6
.mresource public data.xml
    // Offset: 0x00001458 Length: 0x0000027E
}

在这种情况下,banner.jpg 和 data.xml 直接包含在程序集中——每个都作为自己的嵌入式资源。这是最简单的工作方式。

.NET 还允许您通过中间 .resources 容器添加内容。这些专为保存可能需要翻译成不同语言的内容而设计。本地化的 .resources 可以打包为单独的附属程序集,这些程序集会根据用户的操作系统语言在运行时自动获取。

图 17-2 展示了一个程序集,其中包含两个直接嵌入的资源,以及一个名为 welcome.resources 的 .resources 容器,我们为此创建了两个本地化附属程序集。

直接嵌入资源 Directly Embedding Resources

Windows 应用商店应用不支持将资源嵌入到程序集中。相反,将任何额外文件添加到您的部署包,并通过从您的应用程序 StorageFolder (Package.Current.InstalledLocation) 读取来访问它们。

要使用 Visual Studio 直接嵌入资源:

  • 将文件添加到您的项目中。
  • 将其构建操作设置为嵌入式资源。

Visual Studio 始终在资源名称前加上项目的默认命名空间以及包含该文件的任何子文件夹的名称。因此,如果您的项目的默认命名空间是 Westwind.Reports 并且您的文件在文件夹图片中名为 banner.jpg,则资源名称将为 Westwind.Reports.pictures.banner.jpg。

要检索资源,请对包含该资源的程序集调用 GetManifestResourceStream。这将返回一个流,然后您可以像其他任何流一样读取它:

Assembly a = Assembly.GetEntryAssembly();
using (Stream s = a.GetManifestResourceStream ("TestProject.data.xml"))
using (XmlReader r = XmlReader.Create (s))
System.Drawing.Image image;
using (Stream s = a.GetManifestResourceStream ("TestProject.banner.jpg"))
	image = System.Drawing.Image.FromStream (s);

返回的流是可搜索的,因此您也可以这样做:

byte[] data;
using (Stream s = a.GetManifestResourceStream ("TestProject.banner.jpg"))
		data = new BinaryReader (s).ReadBytes ((int) s.Length);

如果您使用 Visual Studio 嵌入资源,则必须记住包含基于命名空间的前缀。为帮助避免错误,您可以使用类型在单独的参数中指定前缀。类型的命名空间用作前缀:

using (Stream s = a.GetManifestResourceStream (typeof (X), "data.xml"))

X 可以是任何具有所需资源命名空间的类型(通常是同一项目文件夹中的类型)。 GetManifestResourceNames 返回程序集中所有资源的名称。

.resources Files

.resources 文件是潜在可本地化内容的容器。 .resources 文件最终作为程序集中的嵌入式资源——就像任何其他类型的文件一样。区别在于您必须执行以下操作:

  • 首先将您的内容打包到 .resources 文件中
  • 通过 ResourceManager 或打包 URI 访问其内容,而不是 Get ManifestResourceStream

.resources 文件是二进制结构的,因此不是人为的可编辑;因此,您必须依赖 .NET 和 Visual Studio 提供的工具来使用它们。字符串或简单数据类型的标准方法是使用 .resx 格式,可以通过 Visual Studio 或 resgen 工具将其转换为 .resources 文件。 .resx 格式也适用于用于 Windows 窗体或 ASP.NET 应用程序的图像。

在 WPF 应用程序中,您必须使用 Visual Studio 的“资源”构建操作来获取需要由 URI 引用的图像或类似内容。无论是否需要本地化,这都适用。

我们将在以下部分中描述如何执行这些操作。

.resx Files

.resx 文件是一种用于生成 .resources 文件的设计时*design-time*格式。 .resx 文件使用 XML 并使用名称/值对进行结构化,如下所示:

<root>
	<data name="Greeting">
		<value>hello</value>
	</data>
	<data name="DefaultFontSize" type="System.Int32, mscorlib">
		<value>10</value>
	</data>
</root>

若要在 Visual Studio 中创建 .resx 文件,请添加资源文件类型的项目项。其余工作自动完成:

  • 创建正确的标题。
  • 提供设计器用于添加字符串、图像、文件和其他类型的数据。
  • .resx 文件自动转换为.resources 格式并在编译时嵌入到程序集中。
  • 编写了一个类来帮助您稍后访问数据。

资源设计器将图像添加为类型化图像对象 (System.Drawing.dll) 而不是字节数组,这使得它们不适用于 WPF 应用程序。

Reading .resources files

ResourceManager 类读取程序集中嵌入的 .resources 文件:

ResourceManager r = new ResourceManager ("welcome", Assembly.GetExecutingAssembly());

(如果资源是在 Visual Studio 中编译的,则第一个参数必须以名称空间为前缀。)

然后您可以通过调用 GetString 或 GetObject 来访问其中的内容:

string greeting = r.GetString ("Greeting");
int fontSize = (int) r.GetObject ("DefaultFontSize");
Image image = (Image) r.GetObject ("flag.png");

枚举内容.resources 文件:

ResourceManager r = new ResourceManager (...);
ResourceSet set = r.GetResourceSet (CultureInfo.CurrentUICulture, true, true);
foreach (System.Collections.DictionaryEntry entry in set)
		Console.WriteLine (entry.Key);

Creating a pack URI resource in Visual Studio

在 WPF 应用程序中,XAML 文件需要能够通过 URI 访问资源。例如:

<Button>
	<Image Height="50" Source="flag.png"/>
</Button>

或者,如果资源在另一个程序集中:

<Button>
	<Image Height="50" Source="UtilsAssembly;Component/flag.png"/>
</Button>

(Component 是一个文字关键字。)

要创建可以以这种方式加载的资源,您不能使用 .resx 文件。相反,您必须将文件添加到您的项目并将它们的构建操作设置为 Resource(而不是 Embedded Resource)。然后,Visual Studio 将它们编译成一个名为 <AssemblyName>.g.resources 的 .resources 文件,这也是已编译的 XAML (.baml) 文件的所在。

要以编程方式加载 URI 键控资源,请调用 Application.GetResource Stream:

Uri u = new Uri ("flag.png", UriKind.Relative);
using (Stream s = Application.GetResourceStream (u).Stream)

注意我们使用了相对 URI。您还可以完全按照以下格式使用绝对 URI(三个逗号不是拼写错误):

Uri u = new Uri ("pack://application:,,,/flag.png");

如果您更愿意指定一个 Assembly 对象,则可以使用 ResourceManager 来检索内容:

Assembly a = Assembly.GetExecutingAssembly();
ResourceManager r = new ResourceManager (a.GetName().Name + ".g", a);
using (Stream s = r.GetStream ("flag.png"))

ResourceManager 还可以让您枚举的内容给定程序集中的 .g.resources 容器。

附属程序集 Satellite Assemblies

嵌入在 .resources 中的数据是可本地化的。

当您的应用程序运行在以不同语言显示所有内容的 Windows 版本上时,资源本地化就很重要。为了保持一致性,您的应用程序也应该使用相同的语言。

典型的设置如下:

  • 主程序集包含默认或后备语言的 .resources。
  • 单独的附属程序集包含翻译成不同语言的本地化资源。

当您的应用程序运行时,.NET 会检查当前操作系统的语言(来自 CultureInfo.CurrentUICulture)。每当您使用 ResourceManager 请求资源时,运行时都会查找本地化的附属程序集。如果一个可用——并且它包含你请求的资源密钥——它被用来代替主程序集的版本。

这意味着您可以通过添加新的附属程序集来增强语言支持,而无需更改主程序集。

附属程序集不能包含可执行代码,只能包含资源。

附属程序集部署在程序集文件夹的子目录中,如下所示:

programBaseFolder\\MyProgram.exe
\\MyLibrary.exe
\\XX\\MyProgram.resources.dll
\\XX\\MyLibrary.resources.dll

XX 指的是两个字母的语言代码(例如德语的“de”)或语言和区域代码(例如 Great 中的英语的“en-GB”英国)。此命名系统允许 CLR 自动查找并加载正确的附属程序集。

Building satellite assemblies

回想一下我们之前的 .resx 示例,其中包括以下内容:

<root>
	<data name="Greeting">
		<value>hello</value>
	</data>
</root>

然后我们在运行时检索问候语,如下所示:

ResourceManager r = new ResourceManager ("welcome", Assembly.GetExecutingAssembly());
Console.Write (r.GetString ("Greeting"));

假设我们希望它在德语版 Windows 上运行时改为写“hallo”。第一步是添加另一个名为 welcome.de.resx 的 .resx 文件,用 hello 代替 hallo:

<root>
	<data name="Greeting">
		<value>hallo<value>
	</data>
</root>

在 Visual Studio 中,这就是您需要做的所有事情——当您重建时,会自动创建一个名为 MyApp.resources.dll 的附属程序集在名为 de 的子目录中。

Testing satellite assemblies

要模拟在具有不同语言的操作系统上运行,您必须使用 Thread 类更改 CurrentUICulture:

System.Threading.Thread.CurrentThread.CurrentUICulture
	= new System.Globalization.CultureInfo ("de");

CultureInfo.CurrentUICulture 是同一属性的只读版本。

Visual Studio designer support

Visual Studio 中的设计器为本地化组件和视觉元素提供扩展支持。 WPF 设计器有自己的本地化工作流程;其他基于组件的设计人员使用仅限设计时的属性来使组件或 Windows 窗体控件看起来具有语言属性。要为另一种语言定制,只需更改语言属性,然后开始修改组件。属性为 Localizable 的控件的所有属性都将保存到该语言的 .resx 文件中。您可以随时通过更改 Language 属性在语言之间切换。

文化和亚文化 Cultures and Subcultures

文化分为文化和亚文化。一种文化代表一种特定的语言;亚文化代表该语言的区域变体。 .NET 运行时遵循 RFC1766 标准,该标准用两个字母的代码表示文化和亚文化。以下是英语和德语文化的代码:

En