none
プロパティのカテゴリ名を多言語化したい RRS feed

  • 質問

  • Visual C# 2017
    .NET Framework 4.5

    自作したクラスがあります。
    色々なクラスの中に、デザイナー上で編集できるプロパティがあり、CategoryやDescriptionなどがあるわけですが、それらを多言語化したいと思っています。

    まずはCategoryと思い、CategoryAttributeを継承してGetLocalizedString()をオーバーライドすれば任意の文字を、プロパティフィールドごとにハードコードしなくて済む、というところまでは行きついたのですが、それをカルチャーに合わせたリソースなどから表示させる、ということができません。

    どうすると多言語化できるのでしょうか?
    今のコードは以下のような状態です。

        public class MetCategoryAttribute : CategoryAttribute
        {
            public MetCategoryAttribute() { }
    
            public MetCategoryAttribute(string category) : base(category) { }
    
            protected override string GetLocalizedString(string value)
            {
                var category = base.GetLocalizedString(value);
    
                if (category == null)
                {
                    // 意味ないやつ
                    //System.Resources.ResourceManager rm = new System.Resources.ResourceManager("Metroit.ComponentModel.MetCategoryRes", this.GetType().Assembly);
                    //category = rm.GetString(value);
                    category = MetCategoryRes.ResourceManager.GetString(value);
                }
    
                return category;
            }
        }

    // クラス内のプロパティ宣言
    [Browsable(true)]
    [MetCategory("MetAppearance")]
    [DefaultValue(typeof(Color), "WindowText")]
    [Description("コントロールの前景色を取得または設定します。")]
    public Color BaseForeColor

    // リソースファイルの中身
    MetAppearance  拡張 表示


    2017年11月17日 5:13

回答

  • コード的には問題なさそうです。

    ちょっと調べました(ただしVisual Studio Community 2015で)。

    ---------------------------------

    まず基本の確認ですが、既定のカルチャのリソースはアセンブリに直接マニフェストリソースとして格納され、それ以外のカルチャのリソースは、アセンブリの存在するフォルダの [カルチャ] サブフォルダに、[リソース名].resources.dll として作成されます(サテライトアセンブリ)。実行時にはこのフォルダ構成を前提として、サテライトアセンブリのロードが行われます。

    さて、ソリューションにDLLプロジェクトとEXEプロジェクトを作成します。

    DLLプロジェクトはLocalizableCategoryAttributeクラスを定義し、またUserControl1クラスを定義してLocalizedCategory属性を付けたTestプロパティを持たせます。この属性に渡すリソースを、.resxと.ja-JP.resxに定義しました。

    次にEXEプロジェクトにDLLプロジェクトをプロジェクト参照させ、EXEプロジェクトのForm1にUserControl1を配置します。

    このとき、Visual Studioは、DLL.dllを、%LOCALAPPDATA%\Microsoft\VisualStudio\14.0\ProjectAssemblies\[ランダムな名前]\DLL.dll にコピーして、このDLL.dllをロードするようになっています(シャドウコピーって奴ですね)。

    問題なのは、このときシャドウコピーの対象になっているのがDLL.dllのみであり、サテライトアセンブリはコピーの対象にならない、というところのようです。ResouceManagerは実際にロードされているdll(=シャドウコピーされたDLL)を基準にしてサテライトアセンブリを探すので、当然見つからず、結果として常に既定のカルチャのリソースを返すことになります。UserControl1.Testプロパティのカテゴリは.resxに記述しているものをになってしまいます。

    実際、DLLプロジェクトのビルド後に、ProjectAssembliesの下に新たに作成されたフォルダの下にja-JP/[リソース名].resources.dllを配置し、それからEXEプロジェクトでForm1のUserControl1.Testを確認すれば、カテゴリがローカライズされていることを確認しました。

    ---------------------------------------

    簡単な解決は思いつきませんでした。

    なにかVS拡張で上記のシャドウコピー時の処理をフォローできるのかも知れませんが、私は専門外なので何とも言えません。

    あとは、プロジェクト参照するのでは無く、DLLは別にビルドしてサテライトアセンブリごとGACに配置し、それをEXE側がDLL参照する、と言う形にするのは有効そうです(確認していませんが)。

    <追記>ここまでDLLとEXEを分けた場合を書きましたが、EXEに全部乗っけた場合でも、シャドウコピーが発生しサテライトアセンブリがコピーされない、というのは変わりません。この場合GACって回避策はなくなりますねえ。</追記>

    • 編集済み Hongliang 2017年11月18日 9:35
    • 回答としてマーク takiru 2017年12月15日 5:57
    2017年11月18日 9:03

すべての返信

  • ・リソースは、MetCategoryAttributeを定義するプロジェクトの、プロパティ→リソース から設定する

    という前提であれば、[ルート名前空間].Properties.Resources クラスが自動生成されますので、new ResourceManager(tyepof(Resources)).GetString(リソース名)で取得できます。

    カスタムCategoryAttributeクラスのコンストラクタには、任意のTypeとリソース名を受け取れるコンストラクタを用意すればよいでしょう。

    カスタムCategoryAttributeの利用者は、「自身のプロジェクトで定義されている」Resourcesクラスとアセンブリ名を渡します。

    // カスタムCategoryAttributeクラスを定義するアセンブリでもこの属性を使用するなら、internalなコンストラクタとしてクラス名だけを受け取るものを用意すれば便利ですね。
    2017年11月17日 5:33
  • どうもうまくできません・・・。
    以下コードの場合、どうするとできるでしょうか?

    各プロパティフィールドにリソースを指定させるというよりかは、通常のCategoryAttributeの指定方法ができて、
    勝手に多言語化される、という方法が理想なんですが。
    利用者ごとに新たな名前のリソースを定義して、新たなカテゴリーを追加できる、というようなイメージではなくて、
    通常のCategoryAttributeでは解釈されないいくつかの特定の名前を、多言語化したいだけなんですが。

    併せて、対象言語に対する定義がなかった場合、既定値リソースを使いたいです。
    (WinFormsのLocalizable=True, Language=(既定値)の動作)

    構成

    Metroit ├Properties │ ├MetCategoryRes.ja-JP.resx │ └MetCategoryRes.resx └ComponentModel └MetCategoryAttribute.cs

    Metroit.Windows.Forms
      └MetTextBox.cs
    MetCategoryRes.ja-JP.resx
    名前           値
    MetAppearance  Metroit拡張 表示
    MetCategoryRes.resx
    名前           値
    MetAppearance  Metroit Extension Appearance

    MetCategoryAttribute.cs

    namespace Metroit.ComponentModel
    {
        public class MetCategoryAttribute : CategoryAttribute
        {
            public MetCategoryAttribute() { }
    
            public MetCategoryAttribute(string category) : base(category) { }
    
            protected override string GetLocalizedString(string value)
            {
                var category = base.GetLocalizedString(value);
    
                if (category == null)
                {
                    var res = new System.Resources.ResourceManager(typeof(Metroit.Properties.MetCategoryRes));
                    category = res.GetString(value);
                }
    
                return category;
            }
        }
    }

    MetTextBox.cs

    namespace Metroit.Windows.Forms
    {
        public class MetTextBox : TextBox, ISupportInitialize
        {
            [Browsable(true)]
            [MetCategory("MetAppearance")]
            [DefaultValue(typeof(Color), "WindowText")]
            [Description("コントロールの前景色を取得または設定します。")]
            public Color BaseForeColor
            {
                get { return this.baseForeColor; }
                set
                {
                    this.baseForeColor = value;
    
                    // 背景色・文字色の変更
                    this.ChangeDisplayColor();
                }
            }
        }
    }

    2017年11月18日 6:24
  • コード的には問題なさそうです。

    ちょっと調べました(ただしVisual Studio Community 2015で)。

    ---------------------------------

    まず基本の確認ですが、既定のカルチャのリソースはアセンブリに直接マニフェストリソースとして格納され、それ以外のカルチャのリソースは、アセンブリの存在するフォルダの [カルチャ] サブフォルダに、[リソース名].resources.dll として作成されます(サテライトアセンブリ)。実行時にはこのフォルダ構成を前提として、サテライトアセンブリのロードが行われます。

    さて、ソリューションにDLLプロジェクトとEXEプロジェクトを作成します。

    DLLプロジェクトはLocalizableCategoryAttributeクラスを定義し、またUserControl1クラスを定義してLocalizedCategory属性を付けたTestプロパティを持たせます。この属性に渡すリソースを、.resxと.ja-JP.resxに定義しました。

    次にEXEプロジェクトにDLLプロジェクトをプロジェクト参照させ、EXEプロジェクトのForm1にUserControl1を配置します。

    このとき、Visual Studioは、DLL.dllを、%LOCALAPPDATA%\Microsoft\VisualStudio\14.0\ProjectAssemblies\[ランダムな名前]\DLL.dll にコピーして、このDLL.dllをロードするようになっています(シャドウコピーって奴ですね)。

    問題なのは、このときシャドウコピーの対象になっているのがDLL.dllのみであり、サテライトアセンブリはコピーの対象にならない、というところのようです。ResouceManagerは実際にロードされているdll(=シャドウコピーされたDLL)を基準にしてサテライトアセンブリを探すので、当然見つからず、結果として常に既定のカルチャのリソースを返すことになります。UserControl1.Testプロパティのカテゴリは.resxに記述しているものをになってしまいます。

    実際、DLLプロジェクトのビルド後に、ProjectAssembliesの下に新たに作成されたフォルダの下にja-JP/[リソース名].resources.dllを配置し、それからEXEプロジェクトでForm1のUserControl1.Testを確認すれば、カテゴリがローカライズされていることを確認しました。

    ---------------------------------------

    簡単な解決は思いつきませんでした。

    なにかVS拡張で上記のシャドウコピー時の処理をフォローできるのかも知れませんが、私は専門外なので何とも言えません。

    あとは、プロジェクト参照するのでは無く、DLLは別にビルドしてサテライトアセンブリごとGACに配置し、それをEXE側がDLL参照する、と言う形にするのは有効そうです(確認していませんが)。

    <追記>ここまでDLLとEXEを分けた場合を書きましたが、EXEに全部乗っけた場合でも、シャドウコピーが発生しサテライトアセンブリがコピーされない、というのは変わりません。この場合GACって回避策はなくなりますねえ。</追記>

    • 編集済み Hongliang 2017年11月18日 9:35
    • 回答としてマーク takiru 2017年12月15日 5:57
    2017年11月18日 9:03
  • Hongliang さん

    めちゃくちゃ調査、ありがとうございます。

    言われてみれば確かに、ということから、ローカライズする方法まで確認できました。

    しかしながら、作成したDLLを参照設定したプロジェクトを開く度に、ランダムに生成されるフォルダー内にサテライトアセンブリを投入しなければならない、という手順は極めて煩雑かつ困難です。

    私が想定している利用方法は以下になります。
     1.私が、多言語化されたDLLを作成、配布する。
     2.他人が、新規にプロジェクトを作成し、上記DLLを参照設定する。

    よって、今回の質問ではリソースを利用する(実質的にサテライトアセンブリになってしまっていますが)説明になってしまっていますが、リソースの利用に限らず、上記手順を踏まなくても多言語化する方法論などありますでしょうか?

    今回の例で言えば、MetCategoryAttributeクラス内に、ロケールを取得して、それぞれハードコードしないとダメでしょうか?

    何度も質問してしまって恐縮ですが、お知恵を拝借ください。

    2017年11月18日 9:47