如何强制WPF使用带有程序集强名称的资源URI?哎呀!

19

好的,这真的很恼人。我之前已经注意到WPF生成的加载XAML资源的代码似乎没有使用强名称,因此可能会在需要支持WPF程序集并行版本的场景下出现问题。

这事情确实是这样,现在它给我带来了问题 - 我有一个插件系统,应该支持安装仅在其版本号(其程序集版本)不同的插件的并行安装。当然,.NET可以支持这一点,因为即使具有相同的DLL文件名,只要它们被强命名并且具有不同的公钥/私钥或不同的程序集版本号,则会确定程序集具有不同的标识。

现在,如果我们查看Visual Studio自动生成的Windows和UserControls的代码,我们会在自动生成的文件中看到以下内容:

/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponent() {
    if (_contentLoaded) {
        return;
    }
    _contentLoaded = true;
    System.Uri resourceLocater = new System.Uri("/Sensormatic.AMK1000.Panel;component/views/servicepanelui.xaml", System.UriKind.Relative);

    #line 1 "..\..\..\Views\ServicePanelUI.xaml"
    System.Windows.Application.LoadComponent(this, resourceLocater);

    #line default
    #line hidden
}

注意创建资源定位器的那一行 - 它使用的是相对URI,没有指定包含xaml资源的程序集的强名称或版本。

我认为LoadComponent可能会检查调用程序集的标识并使用它的公钥和版本细节,或者检查包含“this”参数类型的程序集的标识。

看起来情况并非如此-如果您有两个具有不同版本号(但具有相同文件名)的程序集,则可以获得IOException,并显示消息“无法找到资源X”(对于上面的示例为“无法找到资源'views/servicepanelui.xaml'”)。

更糟糕的是,我几乎确定这也意味着具有相同文件名但具有不同公钥/私钥(即来自不同发布者)的程序集也会导致此错误。

那么,有人知道如何解决这个问题吗?如何使WPF符合强名称标准。

请注意,就我而言,这是一个WPF错误。您不应该只是为了避免此错误而使用Appdomain隔离。


嗨Phil,我也遇到了同样的问题。你找到解决方案了吗? - Manish Basantani
有人在使用WPF自定义控件时遇到过类似的主题问题(通过资源字典)吗?如果是,有解决方案吗? - akjoshi
这个问题有进展吗?肯定有人能够创建没有这个问题的WPF库。我需要做到这一点,但确实遇到了这个问题。 - CathalMF
7个回答

6
您可以在项目文件中设置以下内容,以更改生成代码中的URI:
<PropertyGroup>
  <AssemblyVersion>1.0.0.0</AssemblyVersion>
  <AssemblyPublicKeyToken>[YOUR_PUBLIC_KEY_TOKEN]</AssemblyPublicKeyToken>
</PropertyGroup>

1
你怎么知道的?这太...嗯,深奥了。这甚至没有得到官方支持,但它确实有效! - user195275
我不喜欢这个解决方案,如果我改变版本(不自动重构)或者我的调试机器和构建服务器上的密钥不同怎么办? - L.Trabacchin
我已经困扰于这个问题超过一周了,我非常苦恼!你的答案对我来说像魔法般起作用,帮了我很多!谢谢!!(还请注意引用了这个线程的网站 - marc wellman
我遇到了一个问题,表现为“无法加载文件或程序集'<AssemblyName>,Version=1.0.0.0,Culture=neutral'或其某个依赖项。所定位的程序集清单定义与程序集引用不匹配。(HRESULT 异常: 0x80131040)”。原来是我覆盖了“AssemblyVersion”属性。 - rossng

5
我遇到了同样的问题,这可能是一个可行的解决方案:
每当使用.xaml页面创建控件时,在附加的.cs文件构造函数中,在InitializeComponent()之前添加以下代码:
contentLoaded = true; var assemblyName = GetType().Assembly.GetName(); System.Windows.Application.LoadComponent(GetType(), new Uri( string.Format("/{0};v{1};component{2}/{3}.xaml", assemblyName.Name, assemblyName.Version, [[[namespace]]], type.Name ), UriKind.Relative))
其中[[[namespace]]]输入类的完整命名空间,但不包括Visual Studio项目的默认命名空间。
(注意:在connect上有一个打开的支持单元 https://connect.microsoft.com/VisualStudio/feedback/details/668914/xaml-generated-code-uses-resource-uri-without-assembly-strong-name

在 [[[命名空间]]] 中,重要的并不是命名空间本身,而是所加载页面相对于项目的位置(通常与根目录剥离后的命名空间相一致)。 - L.Trabacchin

3
我倾向于认为这可能是一个bug,或者至少是XAML工具的不足之处。也许你应该在Connect上报告它。
我没有尝试过,但这里有几个潜在的解决方法:
  1. 注入一个预构建步骤来自动修改.g.cs文件,以使用指定完整程序集信息的pack URIsAssemblyShortName[;Version][;PublicKey];component/Path
  2. 附加到AppDomain.AssemblyResolve以帮助CLR找到正确的程序集

谢谢Kent,虽然我希望有一种内置的处理方式来处理这个问题!(尽管我没有找到它)。我不认为#2会有所帮助——我很确定汇编解析请求只会有弱名称,因为这是URI指定的,然后我不知道应该使用哪个程序集。#1可能有效,但显然有点丑陋。顺便说一句,truss做得很好。我正在我的当前项目中使用它。 - Phil
连接到assemblyresolve,却不知道要加载哪一个,这对我来说似乎没有用处,也许你可以使用请求的程序集(如果有指定的话)。 - L.Trabacchin

1
我们也遇到了同样的问题,但是我们只需要为解决方案中的某些特定项目设置程序集版本。
因为我喜欢像user195275建议的那样为构建设置版本号的想法,所以我研究了如何为单个csproj文件做到这一点。
所以结合以下线程如何从assemblyInfo.cs读取程序集版本?,我们得出了以下解决方案:
<Target Name="BeforeBuild">
    <ReadLinesFromFile File="$(MSBuildProjectDirectory)\Properties\AssemblyInfo.cs">
        <Output TaskParameter="Lines"
                ItemName="ItemsFromFile"/>
    </ReadLinesFromFile>

    <PropertyGroup>
        <Pattern>\[assembly: AssemblyVersion\(.(\d+)\.(\d+)\.(\d+)\.(\d+)</Pattern>
        <In>@(ItemsFromFile)</In>
        <Out>$([System.Text.RegularExpressions.Regex]::Match($(In), $(Pattern)))</Out>
    </PropertyGroup>

    <CreateProperty Value="$(Out.Remove(0, 28))">
        <Output TaskParameter="Value" PropertyName="AssemblyVersion"/>
    </CreateProperty>
</Target>

它的作用:从AssemblyInfo.cs文件中解析版本号,并将其设置为属性,就像Aaron Martens的答案一样。这使得我们的版本号只需要在一个地方进行维护。

感谢您的回答,在经过3天的激烈搜索后,我们终于解决了一个类似的问题(基于COM的插件,加载相同WPF程序集的不同版本)。这段代码还可以防止我们每次增加程序集版本时都要编辑csproj文件! - edupeux

1
这段代码基于Riccardo的回答,在VS2010中对我有用。
首先,我定义了一个加载器方法,可以从我的XAML构造函数中调用。
namespace Utility
{
    public class Utility
    {
        public static void LoadXaml(Object obj)
        {
            var type = obj.GetType();
            var assemblyName = type.Assembly.GetName();
            var uristring = string.Format("/{0};v{1};component/{2}.xaml",
                assemblyName.Name,
                assemblyName.Version,
                type.Name);
            var uri = new Uri(uristring, UriKind.Relative);
            System.Windows.Application.LoadComponent(obj, uri);
        }
    }
}

然后,在每个XAML控件的构造函数中,我用以下代码替换了InitializeComponent():

        _contentLoaded = true;
        Utility.Utility.LoadXaml(this);
        InitializeComponent();

我注意到我的一些RelativeSource绑定停止工作了,但是我能够解决这个问题。


通过这样做,我得到了一个异常:无法找到资源 'basicwizard.xaml'。其中basicwizard是我的窗口类。 - chincheta73

1
如果你的构建是自动化的,你也可以通过向msbuild过程传递/p:AssemblyVersion=$version参数来实现。

我已经困扰于这个问题超过一周,非常苦恼!你的答案对我起了巨大的帮助,解决了我的问题!非常感谢!!(同时注意引用了你的答案的这个网站 - marc wellman

1

我一直在 VS2012 中努力解决这个问题。我无法让 Riccardo 的解决方案在这个环境中工作。他代码的这个变种……

_contentLoaded = true;
var assemblyName = GetType().Assembly.GetName();
Application.LoadComponent(this, new Uri(String.Format("/{0};v{1};component/CustomersFrame.xaml", assemblyName.Name, assemblyName.Version), UriKind.Relative));

...解决了“无法定位资源”的问题,但在子元素中遇到了以下错误:“无法注册命名对象。在此范围内无法注册重复名称“search”。”

Aaron Marten的解决方案对我有效。很抱歉我不能发表评论或点赞,因为我的声望不够。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接