LoginSignup
31
29

More than 5 years have passed since last update.

C# アプリ設定クラスの微妙さを何とかする

Posted at

ポータブルアプリ最高!(゚∀゚)

いきなり脱線しますがポータブルアプリ最高ですよね!
.NET は過去の Windows アプリに比べて XCOPY 配置で済むようなアプリが作りやすくなったと思います。
portableapps.com のように、プログラム本体と設定ファイルを同じ場所に配置して XCOPY で持ち運べるようにしたアプリを揃えたサイトもありますから、XCOPY なアプリはそれなりに需要があると思います。
さらに設定ファイルが実行ファイルのあるフォルダ配下に配置されるタイプならば OS の再インストール後にもインストールと再設定の手間が掛からなくていいですよね。
主に俺得です。

ちょっとお試しで使ってみようと思ったときに、インストーラーになっていると敬遠してしまいませんか?
だってちょっと油断すると変なもの一緒に入れてくるソフトって結構多いじゃないですか。
hao123 とか hao123、あとは hao123 とかね!(゚д゚)
インストール時に入れるか聞いてくるものはまだ良心的で、なかにはそっと潜り込ませてくる酷いソフトもあったりします。
インストーラーは権限昇格を求めてきますから、インストールするには許可するしかないし、結果としてアレなものが一緒に入るわけですから、なんのために UAC なんだろうと思ってしまいます。

さて、タイトルと関係ない雑談はここまでにして本題に入ります。

app.config

設定情報を保存するなら大体この方法で事足りる気がします。
Windows アプリケーションの構成ファイル(app.config)はファイル名も保存場所も実行ファイルに依存していますが、小さなアプリケーションならお手軽で、my.app (Visual Basic) の機能を使えば終了時に保存するメソッドすら呼ばなくても勝手にやってくれるキャッチーさで素敵。
設定クラスはプロジェクトに複数追加できますし、簡単な設定情報ならコレでもいい気がしますが、独自のクラスを含めたり階層構造を持たせたり、ちょっと凝ったことをするとデザイナは使えないのでびみょ~なコードを自分で書くことになります。

微妙なコード
[global::System.Configuration.UserScopedSettingAttribute()]
public Heroine Tomochan
{
    get
    {
        return ((Heroine)(this["Tomochan"]));
    }
    set
    {
        this["Tomochan"] = value;
    }
}

文字列リテラルの部分、びみょーですよね(´・ω・`)
クラス名とかプロパティ名は微妙じゃないです。
括弧が妙に多いのはデザイナが自動生成するコードをコピペしたからです。

微妙なコード書くくらいなら自分で作ってみよっと

コーディングの最中に おもいつきで プロパティ名やメソッド名を変更することは、希によくありますよね。
そんなときに上記のような微妙なコードだと、Visual Studio がいくらイケメンでも、文字列リテラルはリファクタリングしてくれません。
せっかく自分で作るのだから、微妙なコードは回避出来るようにして、「車輪の再発明」の言い訳にしますw

以下は私の作った設定クラスの利用例です。

自作クラスの場合
private Heroine _tomochan;
[DataMember]
public Heroine Tomochan
{
    get
    {
        return _tomochan;
    }
    set
    {
         _tomochan = value;
         OnPropertyChanged(a => a.Tomochan);
    }
}

なんで app.config が微妙なコードになるのか、自分で作ってみるとよく分かります。
最初のコードとの違いはフィールドを宣言する必要があることや、シリアライズ用の属性を付けていることもありますが、一番の違いは OnPropertyChanged の呼び出しです。

INotifyPropertyChanged を実装するクラスは、プロパティの変更をイベントで通知する必要があるのですが、イベント引数の PropertyChangedEventArgs はプロパティ名の文字列が必要です。
そのために "Tomochan" を渡してたんですね。
以下は .NET の ApplicationSettingsBase のソース抜粋です。

ApplicationSettingsBase.cs
/// <devdoc>
///     Overriden from SettingsBase to support validation event.
/// </devdoc>
public override object this[string propertyName] {
    get {
        if (IsSynchronized) {
            lock (this) {
                return GetPropertyValue(propertyName);
            }
        }
        else {
            return GetPropertyValue(propertyName);
        }

    }
    set {
        SettingChangingEventArgs e = new SettingChangingEventArgs(propertyName, this.GetType().FullName, SettingsKey, value, false);
        OnSettingChanging(this, e);

        if (!e.Cancel) {
            base[propertyName] = value;
            //
            PropertyChangedEventArgs pe = new PropertyChangedEventArgs(propertyName);
            OnPropertyChanged(this, pe);
        }
    }
}

なるほど、INotifyPropertyChanged の実装だけでなく、排他処理もここで対応できるようにしているんですね。
プロパティごとに、このコードを書くくらいなら、文字列リテラルが入るのもやむなしという考えも分からないでもないです。
リファクタリング漏れよりも、排他処理が漏れるバグのほうが発見しにくくて危険だもんね。
コレを踏まえて本気で改良してみた結果がこれです。

自作クラス改
[DataMember]
public Heroine Tomochan
{
    get { return Get(a => a.Tomochan); }
    set { Set(a => a.Tomochan, value); }
}

プロパティの値は内部でオブジェクト型変数に格納しています。
OnPropertyChanged のためにラムダ式を渡して、Get / Set メソッド内部で排他処理するようにしてみました。
微妙なコードが消えて、コード量的にも悪くないかなーってくらいになりました。
一度自分で作ってから .NET Framework のソースを読むと勉強になるなぁ(´∀`)

ラムダ式をわたす意味

プロパティ名を取得するために式木を利用しています。
かなり悩みましたが .NET 4.0 では他にいい方法はないんじゃないかと思います。
.NET 4.5 が使える環境なら CallerMemberNameAttribute を使って呼び出し元のプロパティやメソッド名を取得できます。
さらにソースファイルの名前や行番号まで取得できるようになっていてログ出力がはかどりそうですね!

スタックトレースを駆け上がって取得する方法も考えたけど、よく考えたら最適化されてインライン展開されると期待する結果を得られないので危険です。
MethodImplAttribute とか DebuggableAttribute で最適化を抑止するようにコンパイラに指示する方法もありますが、プロパティを追加するたびに属性を書くのはイケてないなーと思ってやめました。

作ったクラスの使い方まとめ

PortableSettingsBase クラスを継承した設定クラスを作成して、プロパティを実装していきます。

AppSettings.cs
[PortableSettingsPath(DirectoryName = @".\conf\", FileName = "Sample.conf")]
[DataContract]
public class AppSettings : PortableSettingsBase<AppSettings>
{
    public static readonly AppSettings Instance = Load();

    protected override void OnPropertyChanging(PropertyChangingEventArgs args)
    {
        Trace.WriteLine(args.Name);
        base.OnPropertyChanging(args);
    }

    protected override void OnLoaded(EventArgs args)
    {
        // ファイルがない初期状態のときに null が嫌ならこんな感じで適当に
        UserId = UserId ?? "";
        Font = Font ?? new Font("メイリオ", 9);
        base.OnLoaded(args);
    }

    [DataMember]
    public string UserId
    {
        get { return Get(a => a.UserId); }
        set { Set(a => a.UserId, value); }
    }

    [DataMember]
    public SerializableFont Font
    {
        get { return Get(a => a.Font); }
        set { Set(a => a.Font, value); }
    }
}

static void Main()
{
    AppSettings.Instance.UserId = "hoge";
    AppSettings.Instance.Save();
}

設定ファイルの保存場所は、PortableSettingsPath 属性で指定するか、Load メソッドで決定します。
何も指定しなければ実行ファイルと同じ場所に作成されます。
複数の設定ファイルを使う場合はちゃんとパスを指定しないと困ったことになるので注意です。
シリアライズするプロパティに DataMember 属性をつけて、読み込み時は静的メソッドの Load でインスタンスを取得できます。
保存するときは Save メソッドを呼び出せばシリアライズされます。
シングルトンにする場合は、適当に静的なフィールドにでも突っ込めばいいと思います。
出力形式は XML ですが、JSON かバイナリも選択できます。
実装するクラスのコンストラクタで何かするのはやめた方がいいです、呼ばれませんからね(´・ω・`)
もっと素敵なアイデアあったら教えてください(o*。_。)oペコッ

PortableSettingsBase.cs
IPortableSettingsSerializer.cs

31
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
29