sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# Prism DialogParameter の実装(学習)

Prism ライブラリーには、「Dialog Service」というものがあります。

利用してみるとこれがとても便利。また、ポップアップしたウィンドウに対して値渡しをする仕組みとしても面白いと思います。仕組みとして考えられているので、自分でもこうした拡張を予想した引数の受け渡しの仕組みは真似して自分の設計パターンのひとつに加えたいと思いました。

そんなわけで、DialogParameter がどういう感じの実装になっているのか写経をしてみます。

実装

DialogParameter は、インターフェースに従って実装されています。実装はこんな感じ。

なんで、IDialogParameter を作成して、DialogParameterBase を実装して、それを継承する DialogParameter を実装する流れでした。(DialogParameter はむしろ何も実装がないので、ここも拡張可能)

IDialogParameter を写経した例は、以下のとおりです。

/// <summary>
/// <see cref="IParameters"/> インターフェースは、データの引数(パラメーター)を定義します。
/// </summary>
/// <remarks>
/// <see cref="IParameters"/> インターフェースの実装クラスは、引数(パラメーター)となるデータの取得/設定をサポートします。
/// </remarks>
public interface IParameters
{
    /// <summary>
    /// コレクションにキーと値のペアを追加します。
    /// </summary>
    /// <param name="key">コレクションの中でパラメーターの値を参照するためのキー。</param>
    /// <param name="value">格納するパラメーターの値。</param>
    void Add(string key, object value);

    /// <summary>
    /// コレクションに指定したキーの名前が存在しているかどうかを示す値を取得します。
    /// </summary>
    /// <param name="key">確認をするキーの名前。</param>
    /// <returns>キーが存在するとき <c>true</c>、それ以外のときは <c>false</c> を返却します。</returns>
    bool ContainsKey(string key);

    /// <summary>
    /// コレクションに含まれるパラメーターの数を取得します。
    /// </summary>
    int Count { get; }

    /// <summary>
    /// キーによって参照されるパラメーターの値を取得します。
    /// </summary>
    /// <typeparam name="T">パラメーターの型。</typeparam>
    /// <param name="key">パラメーターの値を参照するキー。</param>
    /// <returns><typeparamref name="T"/> 型の(キーに一致する)パラメーターの値。</returns>
    T? GetValue<T>(string key);

    /// <summary>
    /// キーによって参照されるすべてのパラメーターの値を取得します。
    /// </summary>
    /// <typeparam name="T">パラメーターの型。</typeparam>
    /// <param name="key">パラメーターの値を参照するキー。</param>
    /// <returns><typeparamref name="T"/> 型の(キーに一致する)すべてのパラメーターの値。</returns>
    IEnumerable<T?> GetValues<T>(string key);

    /// <summary>
    /// コレクションに参照するキーが存在する場合、パラメーターの値を取得します。
    /// </summary>
    /// <typeparam name="T">パラメーターの型。</typeparam>
    /// <param name="key">パラメーターの値を参照するキー。</param>
    /// <param name="value">
    /// このメソッドから制御がもどるとき、参照するキーが見つかった場合、指定した(キーに一致する)値が格納されます。
    /// それ以外のときは、<typeparamref name="T"/> 型に対応する既定の値。このパラメーターは初期化されずに渡されます。
    /// </param>
    /// <returns>キーに一致するパラメーターが存在するときは <c>true</c>、それ以外のときは <c>false</c> を返却します。</returns>
    bool TryGetValue<T>(string key, out T? value);
}

よくある実装だと思います。ここから同じ階層 Dialogs にある DialogParameter を見てみると、Prism.CommonDialogParameterBase を継承していました。(IDialogParameter が複数存在している……)

基本的な実装はここを見ればよいのですが、List の操作では拡張が用意されています。

ソースを追う上で、めんどくさいなと感じたところはここくらいでした。List からデータを取り出すコレクションは List であり、かつ、データの取得も単純な foreach です。なので、キーで値を取得する仕組みですが、大量データの set/get することを想定していない作りだと思います。

値を取り出す部分は TryGetValueInternal が一番のロジックポイントだと思いました。ほとんどは、GetType() == type で示す if 文の2つ目で処理されるはず。(3つ目からは、気を利かせた処置っぽい気もしました)

private static bool TryGetValueInternal(KeyValuePair<string, object> kvp, Type type, out object value)
{
    value = GetDefault(type);
    var success = false;
    if (kvp.Value == null)
    {
        success = true;
    }
    else if (kvp.Value.GetType() == type)
    {
        success = true;
        value = kvp.Value;
    }
    else if (type.IsAssignableFrom(kvp.Value.GetType()))
    {
        success = true;
        value = kvp.Value;
    }
    else if (type.IsEnum)
    {
        var valueAsString = kvp.Value.ToString();
        if (Enum.IsDefined(type, valueAsString))
        {
            success = true;
            value = Enum.Parse(type, valueAsString);
        }
        else if (int.TryParse(valueAsString, out var numericValue))
        {
            success = true;
            value = Enum.ToObject(type, numericValue);
        }
    }

    if (!success && type.GetInterface("System.IConvertible") != null)
    {
        success = true;
        value = Convert.ChangeType(kvp.Value, type);
    }

    return success;
}

写経をやってみると、ライブラリの理解が深まることのほかに、コードを書いた人の設計の癖が見えたり、C# のどのくらいのバージョンで書いていたのか(どのバージョンで書けているのか)だったり、改めて XML コメントの記述の癖を見たり、自分だと普段使わないメソッドを活用してたり……と面白いし勉強になる。

インデントやスペースの癖、ガード節の使い方や、if 文の深さ、マニアックな attribute などなど。

車輪の再開発が良くないって話は、主に「非効率」だからなんだけど、勉強でいうと話は全然違ってメリットいっぱい。

サンプル

自分でも写経してみたので、動作するか動かしてみる。

単純だけど、実際の動きはこの延長だと思います。サンプルを作成したので公開します。

internal class Program
{
    static void Main(string[] args)
    {
        var program = new Program();

        program.Run();
    }

    private void Run()
    {
        var parameter = new Parameter();

        SetTest(parameter);
        GetTest(parameter);
    }

    private void SetTest(Parameter parameter)
    {
        parameter.Add("key1", "test data");
        parameter.Add("key2", 1);
        parameter.Add("key3", Enum1.Sample3);
        parameter.Add("key4", new Class1 { Data1 = 111, Data2 = "test 222" });
    }

    private void GetTest(Parameter parameter)
    {
        Console.WriteLine($"count = {parameter.Count}");
        Console.WriteLine(parameter["key1"]);
        Console.WriteLine(parameter["key2"]);
        Console.WriteLine(parameter["key3"]);
        Console.WriteLine(parameter["key4"]);

        var isSuccessed1 = parameter.TryGetValue<Enum1>("key3", out var e);

        if (isSuccessed1)
        {
            Console.WriteLine(e);
        }

        var isSuccessed2 = parameter.TryGetValue<Class1>("key4", out var c);

        if (isSuccessed2 && c != null)
        {
            Console.WriteLine(c.Data1);
            Console.WriteLine(c.Data2);
        }
    }
}

参考