web-dev-qa-db-ja.com

単一アセンブリの多言語Windowsフォームの展開(ILMergeおよびサテライトアセンブリ/ローカリゼーション)-可能ですか?

Visual Studio 2008で構築された単純なWindowsフォーム(C#、. NET 2.0)アプリケーションがあります。

複数のUI言語をサポートし、フォームの「Localizable」プロパティとカルチャ固有の.resxファイルを使用すると、ローカリゼーションの側面がシームレスかつ簡単に機能します。 Visual Studioはカルチャ固有のresxファイルをサテライトアセンブリに自動的にコンパイルするため、コンパイルしたアプリケーションフォルダーには、これらのサテライトアセンブリを含むカルチャ固有のサブフォルダーがあります。

アプリケーションを単一のアセンブリとしてデプロイ(コピー)し、複数の文化固有のリソースセットを含める機能を保持したいと思います。

ILMerge (または ILRepack )を使用すると、サテライトアセンブリをメインの実行可能アセンブリにマージできますが、標準の.NET ResourceManagerフォールバックメカニズムでは、次のカルチャ固有のリソースが見つかりませんメインアセンブリにコンパイルされました。

興味深いことに、マージされた(実行可能な)アセンブリを取得し、そのコピーをカルチャー固有のサブフォルダーに配置すると、すべてが機能します!同様に、 Reflector (または ILSpy )を使用すると、マージされたアセンブリのメインリソースとカルチャ固有のリソースを確認できます。しかし、メインアセンブリをカルチャ固有のサブフォルダにコピーすると、とにかくマージの目的が無効になります。本当に、単一のアセンブリのコピーが1つだけ必要です...

GACとカルチャ名の付いたサブフォルダではなく、同じアセンブリでカルチャ固有のリソースを探すためにResourceManagerフォールバックメカニズムをハイジャックまたは影響する方法があるかどうか。次の記事で説明されているフォールバックメカニズムを確認しましたが、それがどのように変更されるかについての手掛かりはありません: ResourceManagerに関するBCLチームのブログ記事

誰かが何か考えを持っていますか?これはオンラインで比較的よくある質問のようです(たとえば、Stack Overflowの別の質問: " ILMergeおよびローカライズされたリソースアセンブリ ")。しかし、信頼できる回答はどこにも見つかりませんでした。


更新1:基本的なソリューション

以下のcasperOneの推奨事項 に続いて、私は最終的にこの作業を行うことができました。

CasperOneが唯一の回答を提供したので、ここにソリューションコードを配置します。自分のコードを追加したくありません。

「InternalGetResourceSet」メソッドに実装されているフレームワークリソース検索フォールバックメカニズムから根性を引き出し、同じアセンブリでfirstメカニズムを使用して検索することで、それを機能させることができました。現在のアセンブリでリソースが見つからない場合は、基本メソッドを呼び出してデフォルトの検索メカニズムを開始します(下記の@Wouterのコメントに感謝します)。

これを行うために、「ComponentResourceManager」クラスを派生させ、1つのメソッドのみをオーバーライドしました(そしてプライベートフレームワークメソッドを再実装しました)。

class SingleAssemblyComponentResourceManager : 
    System.ComponentModel.ComponentResourceManager
{
    private Type _contextTypeInfo;
    private CultureInfo _neutralResourcesCulture;

    public SingleAssemblyComponentResourceManager(Type t)
        : base(t)
    {
        _contextTypeInfo = t;
    }

    protected override ResourceSet InternalGetResourceSet(CultureInfo culture, 
        bool createIfNotExists, bool tryParents)
    {
        ResourceSet rs = (ResourceSet)this.ResourceSets[culture];
        if (rs == null)
        {
            Stream store = null;
            string resourceFileName = null;

            //lazy-load default language (without caring about duplicate assignment in race conditions, no harm done);
            if (this._neutralResourcesCulture == null)
            {
                this._neutralResourcesCulture = 
                    GetNeutralResourcesLanguage(this.MainAssembly);
            }

            // if we're asking for the default language, then ask for the
            // invariant (non-specific) resources.
            if (_neutralResourcesCulture.Equals(culture))
                culture = CultureInfo.InvariantCulture;
            resourceFileName = GetResourceFileName(culture);

            store = this.MainAssembly.GetManifestResourceStream(
                this._contextTypeInfo, resourceFileName);

            //If we found the appropriate resources in the local Assembly
            if (store != null)
            {
                rs = new ResourceSet(store);
                //save for later.
                AddResourceSet(this.ResourceSets, culture, ref rs);
            }
            else
            {
                rs = base.InternalGetResourceSet(culture, createIfNotExists, tryParents);
            }
        }
        return rs;
    }

    //private method in framework, had to be re-specified here.
    private static void AddResourceSet(Hashtable localResourceSets, 
        CultureInfo culture, ref ResourceSet rs)
    {
        lock (localResourceSets)
        {
            ResourceSet objA = (ResourceSet)localResourceSets[culture];
            if (objA != null)
            {
                if (!object.Equals(objA, rs))
                {
                    rs.Dispose();
                    rs = objA;
                }
            }
            else
            {
                localResourceSets.Add(culture, rs);
            }
        }
    }
}

このクラスを実際に使用するには、Visual Studioによって作成された「XXX.Designer.cs」ファイル内のSystem.ComponentModel.ComponentResourceManagerを置き換える必要があります-設計されたフォームを変更するたびにこれを行う必要があります-Visual Studioによって置き換えられます自動的にコーディングします。 (この問題については、「 MyResourceManagerを使用するようにWindowsフォームデザイナをカスタマイズする で説明しました。よりエレガントな解決策は見つかりませんでした-ビルド前のステップで fart.exe を使用しています。自動交換します。)


更新2:別の実用的な考慮事項-2つ以上の言語

上記の解決策を報告したとき、私は実際には2つの言語しかサポートしておらず、ILMergeは私のサテライトアセンブリを最終的なマージ済みアセンブリにマージするというすばらしい仕事をしていました。

最近私はmultiple二次言語、したがって複数のサテライトアセンブリがある同様のプロジェクトに取り組み始めました、そしてILMergeは非常に奇妙なことをしていました:複数のサテライトアセンブリをマージする代わりに要求、それは最初のサテライトアセンブリを複数回マージしていました!

例:コマンドライン:

"c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1InputProg.exe %1es\InputProg.resources.dll %1fr\InputProg.resources.dll

そのコマンドラインで、私はマージされたアセンブリで次のリソースのセットを取得していました(ILSpyデコンパイラーで確認)。

InputProg.resources
InputProg.es.resources
InputProg.es.resources <-- Duplicated!

何度か試してみたところ、単一のコマンドライン呼び出しで同じ名前の複数のファイルに遭遇すると、これはILMergeのバグにすぎないことに気づきました。解決策は、各サテライトアセンブリを異なるコマンドライン呼び出しでマージすることです。

"c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1es\InputProg.resources.dll
"c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1fr\InputProg.resources.dll

これを行うと、最終的なアセンブリで結果として得られるリソースは正しいです。

InputProg.resources
InputProg.es.resources
InputProg.fr.resources

最後に、これが明確になる場合に備えて、完全なビルド後のバッチファイルを次に示します。

"%ProgramFiles%\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1es\InputProg.resources.dll 
IF %ERRORLEVEL% NEQ 0 GOTO END

"%ProgramFiles%\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1fr\InputProg.resources.dll 
IF %ERRORLEVEL% NEQ 0 GOTO END

del %1InputProg.exe 
del %1InputProg.pdb 
del %1TempProg.exe 
del %1TempProg.pdb 
del %1es\*.* /Q 
del %1fr\*.* /Q 
:END

更新3:ILRepack

別の簡単なメモ-ILMergeで私を悩ませた点の1つは、追加の独自仕様のMicrosoftツールであり、デフォルトではVisual Studioにインストールされないため、追加の依存関係があり、サードパーティが開始するのが少し難しくなります。私のオープンソースプロジェクトで。

私は最近 ILRepack を発見しました。これは、これまでのところ私(ドロップイン置換)と同じように機能し、プロジェクトソースと共に自由に配布できるオープンソース(Apache 2.0)の同等物です。


これが誰かに役立つことを願っています!

54
Tao

この動作を確認できる唯一の方法は、 ResourceManager から派生するクラスを作成し、 InternalGetResourceSet および GetResourceFileName メソッド。そこから、 CultureInfo インスタンスを指定すると、リソースが取得される場所をオーバーライドできるはずです。

24
casperOne

別のアプローチ:

1)resource.DLLをプロジェクトの埋め込みリソースとして追加します。

2)AppDomain.CurrentDomain.ResourceResolveのイベントハンドラーを追加します。このハンドラは、リソースが見つからないときに起動します。

  internal static System.Reflection.Assembly CurrentDomain_ResourceResolve(object sender, ResolveEventArgs args)
        {
            try
            {
                if (args.Name.StartsWith("your.resource.namespace"))
                {
                    return LoadResourcesAssyFromResource(System.Threading.Thread.CurrentThread.CurrentUICulture, "name of your the resource that contains dll");
                }
                return null;
            }
            catch (Exception ex)
            {
                return null;
            }
        }

3)LoadResourceAssyFromResourceを次のように実装する必要があります

private Assembly LoadResourceAssyFromResource( Culture culture, ResourceName resName)
        {
                    //var x = Assembly.GetExecutingAssembly().GetManifestResourceNames();

                    using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resName))
                    {
                        if (stream == null)
                        {
                            //throw new Exception("Could not find resource: " + resourceName);
                            return null;
                        }

                        Byte[] assemblyData = new Byte[stream.Length];

                        stream.Read(assemblyData, 0, assemblyData.Length);

                        var ass = Assembly.Load(assemblyData);

                        return ass;
        }

}

1
jm.

ちょっとした考え。

ステップを実行し、SingleAssemblyComponentResourceManagerを作成しました

それでは、なぜあなたはサテライトアセンブリを組み込みアセンブリに含めるのに苦労するのですか?

ResourceName.es.resx自体を、プロジェクト内の別のリソースへのバイナリファイルとして。

コードを書き直すよりも

       store = this.MainAssembly.GetManifestResourceStream(
            this._contextTypeInfo, resourceFileName);

//If we found the appropriate resources in the local Assembly
if (store != null)
{
    rs = new ResourceSet(store);

このコードを使用(テストされていませんが、動作するはずです)

// we expect the "main" resource file to have a binary resource
// with name of the local (linked at compile time of course)
// which points to the localized resource
var content = Properties.Resources.ResourceManager.GetObject("es");
if (content != null)
{
    using (var stream = new MemoryStream(content))
    using (var reader = new ResourceReader(stream))
    {
        rs = new ResourceSet(reader);
    }
}

これは、ilmergeプロセスに衛星アセンブルを含める努力を時代遅れにするはずです。

0

コメントが十分なスペースを提供しなかったため、回答として投稿されました:

OPソリューションでは、ニュートラルカルチャ(en-USではなくen)のリソースを見つけることができませんでした。したがって、私はInternalGetResourceSetを拡張して、私のために役立った中立的な文化を検索しました。これにより、リージョンを定義していないリソースを見つけることができます。これは実際には、リソースファイルをILMergingしていないときに通常のresourceformatterが示す動作と同じです。

//Try looking for the neutral culture if the specific culture was not found
if (store == null && !culture.IsNeutralCulture)
{
    resourceFileName = GetResourceFileName(culture.Parent);

    store = this.MainAssembly.GetManifestResourceStream(
                    this._contextTypeInfo, resourceFileName);
}

これにより、SingleAssemblyComponentResourceManagerの次のコードが生成されます

class SingleAssemblyComponentResourceManager : 
    System.ComponentModel.ComponentResourceManager
{
    private Type _contextTypeInfo;
    private CultureInfo _neutralResourcesCulture;

    public SingleAssemblyComponentResourceManager(Type t)
        : base(t)
    {
        _contextTypeInfo = t;
    }

    protected override ResourceSet InternalGetResourceSet(CultureInfo culture, 
        bool createIfNotExists, bool tryParents)
    {
        ResourceSet rs = (ResourceSet)this.ResourceSets[culture];
        if (rs == null)
        {
            Stream store = null;
            string resourceFileName = null;

            //lazy-load default language (without caring about duplicate assignment in race conditions, no harm done);
            if (this._neutralResourcesCulture == null)
            {
                this._neutralResourcesCulture = 
                    GetNeutralResourcesLanguage(this.MainAssembly);
            }

            // if we're asking for the default language, then ask for the
            // invariant (non-specific) resources.
            if (_neutralResourcesCulture.Equals(culture))
                culture = CultureInfo.InvariantCulture;
            resourceFileName = GetResourceFileName(culture);

            store = this.MainAssembly.GetManifestResourceStream(
                this._contextTypeInfo, resourceFileName);

            //Try looking for the neutral culture if the specific culture was not found
            if (store == null && !culture.IsNeutralCulture)
            {
                resourceFileName = GetResourceFileName(culture.Parent);

                store = this.MainAssembly.GetManifestResourceStream(
                    this._contextTypeInfo, resourceFileName);
            }                

            //If we found the appropriate resources in the local Assembly
            if (store != null)
            {
                rs = new ResourceSet(store);
                //save for later.
                AddResourceSet(this.ResourceSets, culture, ref rs);
            }
            else
            {
                rs = base.InternalGetResourceSet(culture, createIfNotExists, tryParents);
            }
        }
        return rs;
    }

    //private method in framework, had to be re-specified here.
    private static void AddResourceSet(Hashtable localResourceSets, 
        CultureInfo culture, ref ResourceSet rs)
    {
        lock (localResourceSets)
        {
            ResourceSet objA = (ResourceSet)localResourceSets[culture];
            if (objA != null)
            {
                if (!object.Equals(objA, rs))
                {
                    rs.Dispose();
                    rs = objA;
                }
            }
            else
            {
                localResourceSets.Add(culture, rs);
            }
        }
    }
}
0
Marwie

あなたの問題の一部について提案があります。具体的には、.Designer.csファイルを更新してComponentResourceManagerをSingleAssemblyComponentResourceManagerに置き換えるステップの解決策。

  1. InitializeComponent()メソッドを.Designer.csから実装ファイル(#regionを含む)に移動します。 Visual Studioはそのセクションを自動生成し続けますが、私の知る限り問題はありません。

  2. ComponentResourceManagerがSingleAssemblyComponentResourceManagerにエイリアスされるように、実装ファイルの先頭でC#エイリアスを使用します。

残念ながら、これを完全にテストすることはできませんでした。問題に対する別の解決策を見つけたので、次に進みました。私はそれがあなたを助けることを願っています。

0
Josh Buedel