C# 插件 DLL 的本地化资源路径

发布于 2024-11-18 22:42:17 字数 424 浏览 1 评论 0原文

在我的 C# 应用程序中,我有一个插件机制,可以从配置 XML 文件中指定的不同路径加载插件 DLL。我的应用程序是可本地化的。主程序集(*.exe)以标准 .NET 方式在 exe 旁边具有本地化语言的附属程序集(例如 .\en\en-US\main.resources.dll; <代码>.\de\de_DE\main.resources.dll等);

我开始本地化一个插件,发现附属程序集必须放在 exe 旁边的文件夹中。当将它放在插件 DLL 旁边时,资源管理器找不到它。

但是,由于我的插件是可互换的,并且可能位于不同的文件夹中,因此我非常愿意将本地化资源程序集放在插件旁边,而不是放在 exe 旁边。

这可能吗?!?!

我可以接受的另一种选择是将本地化资源嵌入到 DLL 中。这可能吗?

干杯, 菲利克斯

In my C# application, I've a plugin mechanism that loads plugin DLLs from different pathes as specified in a configuration XML file. My application is localizable. The main assembly (the *.exe) has satellite assemblies for the localized languages next to the exe in the standard .NET way (e.g. .\en\en-US\main.resources.dll; .\de\de_DE\main.resources.dll; etc.).

I started localizing a plugin and had to discover that the satellite assembly has to be put in the folders next to the exe. When putting it next to the plugin DLL, the resource manager doesn't find it.

However, since my plugins are interchangable and potentially in different folders, I would highly prefer to put the localized resource assemblies next to the plugins and not to the exe.

Is this possible?!?!

An alternative I could live with would be to embed the localized resources into the DLLs. Is this possible??

Cheers,
Felix

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

不即不离 2024-11-25 22:42:17

我在为我们公司开发产品时遇到了这个问题。我在任何地方都没有找到答案,所以我将在这里发布我的解决方案,以防其他人发现自己处于同样的情况。

从 .NET 4.0 开始,这个问题有了一个解决方案,因为附属程序集现在被传递到 AssemblyResolve 处理程序。如果您已经有一个可以从远程目录加载程序集的插件系统,那么您可能已经有一个程序集解析处理程序,您只需扩展它即可对卫星资源程序集使用不同的搜索行为。如果您没有,那么实现就非常重要,因为您基本上要对所有程序集搜索行为负责。我将发布一个可行的解决方案的完整代码,这样无论哪种方式你都会被覆盖。首先,您需要将 AssemblyResolve 处理程序挂接到某处,如下所示:

AppDomain.CurrentDomain.AssemblyResolve += ResolveAssemblyReference;

然后假设您有几个变量来保存主应用程序和插件目录的路径信息,如下所示:

string _processAssemblyDirectoryPath;
List<string> _assemblySearchPaths;

然后您需要一个小帮助器方法看起来有点像这样:

static Assembly LoadAssembly(string assemblyPath)
{
    // If the target assembly is already loaded, return the existing assembly instance.
    Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
    Assembly targetAssembly = loadedAssemblies.FirstOrDefault((x) => !x.IsDynamic && String.Equals(x.Location, assemblyPath, StringComparison.OrdinalIgnoreCase));
    if (targetAssembly != null)
    {
        return targetAssembly;
    }

    // Attempt to load the target assembly
    return Assembly.LoadFile(assemblyPath);
}

最后,您需要所有重要的 AssemblyResolve 事件处理程序,它看起来有点像这样:

Assembly ResolveAssemblyReference(object sender, ResolveEventArgs args)
{
    // Obtain information about the requested assembly
    AssemblyName targetAssemblyName = new AssemblyName(args.Name);
    string targetAssemblyFileName = targetAssemblyName.Name + ".dll";

    // Handle satellite assembly load requests. Note that prior to .NET 4.0, satellite assemblies didn't get
    // passed to AssemblyResolve handlers. When this was changed, there is a specific guarantee that if null is
    // returned, normal load procedures will be followed for the satellite assembly, IE, it will be located and
    // loaded in the same manner as if this event handler wasn't registered. This isn't sufficient for us
    // though, as the normal load behaviour doesn't correctly locate satellite assemblies where the owning
    // assembly has been loaded using Assembly.LoadFile where the assembly is located in a different folder to
    // the process assembly. We handle that here by performing the satellite assembly search process ourselves.
    // Also note that satellite assemblies are formally documented as requiring the file name extension of
    // ".resources.dll", so detecting satellite assembly load requests by comparing with this known string is a
    // valid approach.
    if (targetAssemblyFileName.EndsWith(".resources.dll"))
    {
        // Retrieve the owning assembly which is requesting the satellite assembly
        string owningAssemblyName = targetAssemblyFileName.Replace(".resources.dll", ".dll");
        Assembly owningAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((x) => x.Location.EndsWith(owningAssemblyName));
        if (owningAssembly == null)
        {
            return null;
        }

        // Retrieve the directory containing the owning assembly
        string owningAssemblyDirectory = Path.GetDirectoryName(owningAssembly.Location);

        // Search for the required satellite assembly in resource subdirectories, and load it if found.
        CultureInfo searchCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
        while (searchCulture != CultureInfo.InvariantCulture)
        {
            string resourceAssemblyPath = Path.Combine(owningAssemblyDirectory, searchCulture.Name, targetAssemblyFileName);
            if (File.Exists(resourceAssemblyPath))
            {
                Assembly resourceAssembly = LoadAssembly(resourceAssemblyPath);
                if (resourceAssembly != null)
                {
                    return resourceAssembly;
                }
            }
            searchCulture = searchCulture.Parent;
        }
        return null;
    }

    // If the target assembly exists in the same directory as the requesting assembly, attempt to load it now.
    string requestingAssemblyPath = (args.RequestingAssembly != null) ? args.RequestingAssembly.Location : String.Empty;
    if (!String.IsNullOrEmpty(requestingAssemblyPath))
    {
        string callingAssemblyDirectory = Path.GetDirectoryName(requestingAssemblyPath);
        string targetAssemblyInCallingDirectoryPath = Path.Combine(callingAssemblyDirectory, targetAssemblyFileName);
        if (File.Exists(targetAssemblyInCallingDirectoryPath))
        {
            try
            {
                return LoadAssembly(targetAssemblyInCallingDirectoryPath);
            }
            catch (Exception ex)
            {
                // Log an error
                return null;
            }
        }
    }

    // If the target assembly exists in the same directory as the process executable, attempt to load it now.
    string processDirectory = _processAssemblyDirectoryPath;
    string targetAssemblyInProcessDirectoryPath = Path.Combine(processDirectory, targetAssemblyFileName);
    if (File.Exists(targetAssemblyInProcessDirectoryPath))
    {
        try
        {
            return LoadAssembly(targetAssemblyInProcessDirectoryPath);
        }
        catch (Exception ex)
        {
            // Log an error
            return null;
        }
    }

    // Build a list of all assemblies with the requested name in the defined list of assembly search paths
    Dictionary<string, AssemblyName> assemblyVersionInfo = new Dictionary<string, AssemblyName>();
    foreach (string assemblyDir in _assemblySearchPaths)
    {
        // If the target assembly doesn't exist in this path, skip it.
        string assemblyPath = Path.Combine(assemblyDir, targetAssemblyFileName);
        if (!File.Exists(assemblyPath))
        {
            continue;
        }

        // Attempt to retrieve detailed information on the name and version of the target assembly
        AssemblyName matchAssemblyName;
        try
        {
            matchAssemblyName = AssemblyName.GetAssemblyName(assemblyPath);
        }
        catch (Exception)
        {
            continue;
        }

        // Add this assembly to the list of possible target assemblies
        assemblyVersionInfo.Add(assemblyPath, matchAssemblyName);
    }

    // Look for an exact match of the target version
    string matchAssemblyPath = assemblyVersionInfo.Where((x) => x.Value == targetAssemblyName).Select((x) => x.Key).FirstOrDefault();
    if (matchAssemblyPath == null)
    {
        // If no exact target version match exists, look for the highest available version.
        Dictionary<string, AssemblyName> assemblyVersionInfoOrdered = assemblyVersionInfo.OrderByDescending((x) => x.Value.Version).ToDictionary((x) => x.Key, (x) => x.Value);
        matchAssemblyPath = assemblyVersionInfoOrdered.Select((x) => x.Key).FirstOrDefault();
    }

    // If no matching assembly was found, log an error, and abort any further processing.
    if (matchAssemblyPath == null)
    {
        return null;
    }

    // If the target assembly is already loaded, return the existing assembly instance.
    Assembly loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((x) => String.Equals(x.Location, matchAssemblyPath, StringComparison.OrdinalIgnoreCase));
    if (loadedAssembly != null)
    {
        return loadedAssembly;
    }

    // Attempt to load the target assembly
    try
    {
        return LoadAssembly(matchAssemblyPath);
    }
    catch (Exception ex)
    {
        // Log an error
    }
    return null;
}

该事件处理程序的第一部分处理卫星资源程序集,然后是我用于常规程序集的搜索行为。这应该足以帮助任何人从头开始运行这样的系统。

I ran into this issue when working on a product for our company. I didn't find an answer anywhere, so I'm going to post my solution to it here in case someone else finds themselves in the same situation.

As of .NET 4.0 there is a solution to this issue, because satellite assemblies now get passed to the AssemblyResolve handler. If you already have a plugin system where assemblies can be loaded from remote directories, you'll probably already have an assembly resolve handler in place, you just need to extend it to use a different search behaviour for satellite resource assemblies. If you don't have one, the implementation is non-trivial since you basically take responsibility for all assembly search behaviour. I'll post the complete code for a working solution so either way you'd be covered. First of all, you need to hook your AssemblyResolve handler somewhere, like this:

AppDomain.CurrentDomain.AssemblyResolve += ResolveAssemblyReference;

Then assuming you've got a couple of variables to hold path information for your main application and your plugin directories, like this:

string _processAssemblyDirectoryPath;
List<string> _assemblySearchPaths;

Then you need a little helper method that looks a little like this:

static Assembly LoadAssembly(string assemblyPath)
{
    // If the target assembly is already loaded, return the existing assembly instance.
    Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
    Assembly targetAssembly = loadedAssemblies.FirstOrDefault((x) => !x.IsDynamic && String.Equals(x.Location, assemblyPath, StringComparison.OrdinalIgnoreCase));
    if (targetAssembly != null)
    {
        return targetAssembly;
    }

    // Attempt to load the target assembly
    return Assembly.LoadFile(assemblyPath);
}

And finally you need the all important AssemblyResolve event handler, which looks a little something like this:

Assembly ResolveAssemblyReference(object sender, ResolveEventArgs args)
{
    // Obtain information about the requested assembly
    AssemblyName targetAssemblyName = new AssemblyName(args.Name);
    string targetAssemblyFileName = targetAssemblyName.Name + ".dll";

    // Handle satellite assembly load requests. Note that prior to .NET 4.0, satellite assemblies didn't get
    // passed to AssemblyResolve handlers. When this was changed, there is a specific guarantee that if null is
    // returned, normal load procedures will be followed for the satellite assembly, IE, it will be located and
    // loaded in the same manner as if this event handler wasn't registered. This isn't sufficient for us
    // though, as the normal load behaviour doesn't correctly locate satellite assemblies where the owning
    // assembly has been loaded using Assembly.LoadFile where the assembly is located in a different folder to
    // the process assembly. We handle that here by performing the satellite assembly search process ourselves.
    // Also note that satellite assemblies are formally documented as requiring the file name extension of
    // ".resources.dll", so detecting satellite assembly load requests by comparing with this known string is a
    // valid approach.
    if (targetAssemblyFileName.EndsWith(".resources.dll"))
    {
        // Retrieve the owning assembly which is requesting the satellite assembly
        string owningAssemblyName = targetAssemblyFileName.Replace(".resources.dll", ".dll");
        Assembly owningAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((x) => x.Location.EndsWith(owningAssemblyName));
        if (owningAssembly == null)
        {
            return null;
        }

        // Retrieve the directory containing the owning assembly
        string owningAssemblyDirectory = Path.GetDirectoryName(owningAssembly.Location);

        // Search for the required satellite assembly in resource subdirectories, and load it if found.
        CultureInfo searchCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
        while (searchCulture != CultureInfo.InvariantCulture)
        {
            string resourceAssemblyPath = Path.Combine(owningAssemblyDirectory, searchCulture.Name, targetAssemblyFileName);
            if (File.Exists(resourceAssemblyPath))
            {
                Assembly resourceAssembly = LoadAssembly(resourceAssemblyPath);
                if (resourceAssembly != null)
                {
                    return resourceAssembly;
                }
            }
            searchCulture = searchCulture.Parent;
        }
        return null;
    }

    // If the target assembly exists in the same directory as the requesting assembly, attempt to load it now.
    string requestingAssemblyPath = (args.RequestingAssembly != null) ? args.RequestingAssembly.Location : String.Empty;
    if (!String.IsNullOrEmpty(requestingAssemblyPath))
    {
        string callingAssemblyDirectory = Path.GetDirectoryName(requestingAssemblyPath);
        string targetAssemblyInCallingDirectoryPath = Path.Combine(callingAssemblyDirectory, targetAssemblyFileName);
        if (File.Exists(targetAssemblyInCallingDirectoryPath))
        {
            try
            {
                return LoadAssembly(targetAssemblyInCallingDirectoryPath);
            }
            catch (Exception ex)
            {
                // Log an error
                return null;
            }
        }
    }

    // If the target assembly exists in the same directory as the process executable, attempt to load it now.
    string processDirectory = _processAssemblyDirectoryPath;
    string targetAssemblyInProcessDirectoryPath = Path.Combine(processDirectory, targetAssemblyFileName);
    if (File.Exists(targetAssemblyInProcessDirectoryPath))
    {
        try
        {
            return LoadAssembly(targetAssemblyInProcessDirectoryPath);
        }
        catch (Exception ex)
        {
            // Log an error
            return null;
        }
    }

    // Build a list of all assemblies with the requested name in the defined list of assembly search paths
    Dictionary<string, AssemblyName> assemblyVersionInfo = new Dictionary<string, AssemblyName>();
    foreach (string assemblyDir in _assemblySearchPaths)
    {
        // If the target assembly doesn't exist in this path, skip it.
        string assemblyPath = Path.Combine(assemblyDir, targetAssemblyFileName);
        if (!File.Exists(assemblyPath))
        {
            continue;
        }

        // Attempt to retrieve detailed information on the name and version of the target assembly
        AssemblyName matchAssemblyName;
        try
        {
            matchAssemblyName = AssemblyName.GetAssemblyName(assemblyPath);
        }
        catch (Exception)
        {
            continue;
        }

        // Add this assembly to the list of possible target assemblies
        assemblyVersionInfo.Add(assemblyPath, matchAssemblyName);
    }

    // Look for an exact match of the target version
    string matchAssemblyPath = assemblyVersionInfo.Where((x) => x.Value == targetAssemblyName).Select((x) => x.Key).FirstOrDefault();
    if (matchAssemblyPath == null)
    {
        // If no exact target version match exists, look for the highest available version.
        Dictionary<string, AssemblyName> assemblyVersionInfoOrdered = assemblyVersionInfo.OrderByDescending((x) => x.Value.Version).ToDictionary((x) => x.Key, (x) => x.Value);
        matchAssemblyPath = assemblyVersionInfoOrdered.Select((x) => x.Key).FirstOrDefault();
    }

    // If no matching assembly was found, log an error, and abort any further processing.
    if (matchAssemblyPath == null)
    {
        return null;
    }

    // If the target assembly is already loaded, return the existing assembly instance.
    Assembly loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((x) => String.Equals(x.Location, matchAssemblyPath, StringComparison.OrdinalIgnoreCase));
    if (loadedAssembly != null)
    {
        return loadedAssembly;
    }

    // Attempt to load the target assembly
    try
    {
        return LoadAssembly(matchAssemblyPath);
    }
    catch (Exception ex)
    {
        // Log an error
    }
    return null;
}

The first part of that event handler deals with satellite resource assemblies, then the search behaviour I use for regular assemblies follows that. This should be enough to help anyone get a system like this working from scratch.

以歌曲疗慰 2024-11-25 22:42:17

好的,如果您希望将自己与标准本地化资源绑定“分离”,并且希望能够自由地从任何位置加载程序集,则选项之一是

a) 实现一个接口以与该程序集中的翻译进行交互

b) 使用 Assembly.加载函数可从您想要的位置加载您想要的.NET 程序集

Ok If you want "detach" yoursefl from standart Localization resource binding, and want to have freedom to load an assembly from any location, one of the options is to

a) implement an interface to interact with translations within that assembly

b) use Assembly.Load function to load .NET assembly you want from location you want

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文