sh1’s diary

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

C# 再試行パターンを実装する

f:id:shikaku_sh:20210823173353p:plain:w300

DB の操作 (DML) のような 99.99...% 成功する操作は信頼性が高いため(面倒で)リトライ処理を実装しないことがあるかと思いますが、実際はごく稀に失敗することがあって、失敗時の処理を横着したことでより面倒な障害につながるケースがあります。

通化したリトライ処理を作成しておいて、なるべく安全性の高いシステムを作成しておこう……という記事です。

再試行パターン (Retry pattern)

通化したリトライ処理、言い換えるとリトライ処理のパターン化です。これは、珍しいテクニックではないです。

アプリケーションを一時的な障害に対応できるようにするパターンです。 アプリケーションまたはサービスが、(リソースに取得・接続しようとするなど)失敗した操作を透過的に再試行します。

再試行パターンのポイントは、つぎのようなところ:

  • 再試行する
    要求した処理が失敗したときに、同じ処理を成功するまで再試行します。処理に成功した場合、再試行ををそれ以上しません。
  • キャンセルする
    処理を繰り返しても成功しない場合、操作をキャンセルして例外を報告します。たとえば、間違った資格情報で認証を何度繰り返しても、成功することは無いのでキャンセルすることが適切です。
  • 時間をおいて再試行する
    要求した処理が、一時的な障害(ビジー状態など)である場合、状態が解消されるまで時間を必要とすることがあります。要求する処理は待機してから再試行します。

加えて、再試行に失敗したときは、その操作の詳細を出力できると助けになります。

補足

それほど重要ではない処理なら、何度も繰り返し再試行するとアプリケーションのスループット(単位時間あたりに処理できる量)に影響を与えるため、さっさと失敗したことを通知する、Fail Fast のほうがよい場合もあります。

たとえば、Qiita の「C#でリトライ処理を共通化してみた」という記事では、リトライ処理の間隔を Math.Pow を使って指数的に時間を伸ばす処理をしていますが、おそらくこれはバッチのような処理の場合に適切だと思います。

一方で、ユーザーが処理する場合は、「後でもう一度試してください」くらいのメッセージを表示しておくほうが、スループットを考慮すると適切かもしれないよね、ということです。(実際は、待たずに再試行を連打マンになる人も多いわけですが)

おおきな障害で再試行が必要な場合は、再試行パターンでは対応しづらいはずです。サーキットブレーカーパターンなどを参照してみてください。

実装

シンプルな実装例だとこんな感じになると思います。

繰り返し処理を共通化するのにあわせて、DB 操作の安全性を高めることができる可能性があると考えて Lock しています。(用途に応じて、なくてもいいと思います)

Task 内で発生した例外は AggregateException として返却されますが、n回繰り返した間に発生した間に発生した例外をまとめて返却することになるので、AggregateException を作り直しています。

private static readonly object _Locker = new ();

public static async Task InvokeAsync(Action action, int attempts, int sleepMilliseconds,
                        [CallerMemberName] string memberName = "", 
                        [CallerFilePath] string sourceFilePath = "", 
                        [CallerLineNumber] int sourceLineNumber = 0)
{
    var aggregates = new List<Exception>();

    while (true)
    {
        try
        {
            lock (_Locker)
            {
                var task = Task.Run(() => action());
                task.Wait();
            }

            break;
        }
        catch (Exception ex)
        {
            if (ex is AggregateException aggregate)
            {
                for (int i = 0; i < aggregate.InnerExceptions.Count; i++)
                {
                    aggregates.Add(aggregate.InnerExceptions[i]);
                }
            }
            else 
            {
                aggregates.Add(ex);
            }

            if (--attempts <= 0)
            {
                Debug.WriteLine($"could not invoke. caller: {memberName} - {sourceFilePath} ({sourceLineNumber})");
                throw new AggregateException(aggregates);
            }

            Debug.WriteLine($"{ex.GetType()} caught: retry after {sleepMilliseconds} ms (left try: {attempts})");
            await Task.Delay(sleepMilliseconds);
        }
    }
}

メソッドの呼び出し元情報を取得する Caller パラメーターを付与しておけば、再試行に失敗したときにログを出力することができます。

どのメソッドが実行されて、そのメソッドはどのファイルで何行目かまでわかります。こうしたログを強化できると共通化したメリットが大きくなると思います。

could not invoke. caller: Test_Action - C:\Users\source\repos\Samples\RetryTest\TestProject\Tests.cs (16)

テスト

TestAction メソッドを4回試行して、最後に Exception が発生することを確認できます。実行間隔は1秒なので、約3秒で実行されることも確認できます。

public class Tests
{
    [Test]
    public void Test_Action()
    {
        Assert.ThrowsAsync<AggregateException>(() => Retry.InvokeAsync(TestAction));
    }

    public void TestAction()
    {
        throw new Exception();
    }
}

もうすこし細かくテスト:

[Test]
public async Task Test_Action()
{
    try
    {
        await Retry.InvokeAsync(TestAction);
    }
    catch (AggregateException e)
    {
        Assert.AreEqual(e.Flatten().InnerExceptions.Where(p => p is Exception).Count(), 4);
    }
}

f:id:shikaku_sh:20220105171819p:plain

async/await を使わずに、Task 型の変数に渡して Task.Wait() で待機させる方法でもよいですが、AggregateException が階層的になってしまい一覧性がわるい。なので Flatten メソッドを使って平坦化してやると扱いやすくなります。

サンプル

GitHub にサンプルコードをあげています。

参考

現実的なマルチスレッド (ロック) 対策

f:id:shikaku_sh:20210823173353p:plain:w300

2021 年末現在、C# におけるマルチスレッド対策はいくつかの方法があります。ニーズに合わせて使い分けることができるように、このような状態になっているのだと思いますが、パフォーマンスなどを意識した結果、誤った実装をしてしまう恐れがあります。

そんなわけで、マルチスレッド対策について勉強した内容がとても参考になったので、個人的にまとめておく内容になります。

参考元「【C#】マルチスレッド関連操作の詳説」は、とても勉強になる記事だと思います。

マルチスレッドとロックとは?

複数のスレッドを利用して動作するプログラムで、スレッド A とスレッド B から同じ変数 x に対して書き込みが発生したとします。

f:id:shikaku_sh:20211224174309p:plain:w500

図の例だと、2つのスレッド A, B で変数 x の値をインクリメントしています。それぞれ1回ずつ変数をインクリメントしているので、変数 x の値は2になってほしいのですが、結果は1です。例の場合だと、スレッド A の書き込みが上書きされてしまったため、意味のない処理になってしまっています。

そこで、スレッド A の処理の終了を待ってから、スレッド B の処理を開始するというようにして、下図のようにスレッド間の協調を取ります。

f:id:shikaku_sh:20211224174353p:plain:w500

処理の終了を待つという仕組みに利用されるのが、ロックです。

マルチスレッド処理の準備

例のようなテスト値を複数のスレッドからインクリメントするサンプルプログラムを作成してみます。

まず、実行コードがこちら。

public class Program
{
    const int _ThreadCount = 4;
    const int _TestValue = 1000000; // _ThreadCount で割れる数でテストすること
    const int _TestCount = 100;      // 実行回数(実行時間と AverageLog に影響)

    public static void Main(string[] args)
    {
        Console.WriteLine($"Run test {_TestCount:#,0} times. Info(thread count:{_ThreadCount:#,0} value:{_TestValue:#,0})");

        ThreadPool.SetMinThreads(_ThreadCount, _ThreadCount);
        ThreadPool.SetMaxThreads(_ThreadCount, _ThreadCount);

        var processes = new List<IProcess>
        {
            new NoLockedProcess(),
        };

        foreach (var process in processes)
        {
            for (int i = 0; i < _TestCount; i++)
            {
                process.Run(_ThreadCount, _TestValue, false);
            }
        }

        Console.WriteLine("--");

        foreach (var process in processes)
        {
            process.WriteAverageLog();
        }
    }
}

定義した NoLockedProcess クラスのインターフェースがこちら。Main から呼び出されているのは IProcess インターフェースのメソッドだけです。

Run メソッドは、マルチスレッド処理を実行し、実効速度を計測します。 WriteAverageLog メソッドは、Run メソッドを実行した際の計測結果を出力します。

public interface IProcess
{
    public void Run(int threadCount, int testValue, bool needLog);
    public void WriteAverageLog();
}

定義した NoLockedProcess クラスの基底(抽象)クラスがこちら。注意して読むポイントは、Tasks の部分でマルチスレッド処理を実行しているのがわかると思います。

スレッドのワーク(処理)をする部分は Increments メソッドで _Value の値をインクリメントするのですが、実装は抽象化していますので、NoLockedProcess で実装します。

public abstract class ProcessBase : IProcess
{
    protected int _Value = 0;

    private List<(int, long)> _ExecutedInfos = new ();

    public abstract string Name { get; }
    public abstract void Increment(int counts);

    public void Run(int threadCount, int testValue, bool needLog)
    {
        if (needLog)
        {
            Console.WriteLine($"Start {Name} - thread no:{Thread.CurrentThread.ManagedThreadId:D3}");
        }

        var tasks = new List<Task>();
        var loops = testValue / threadCount; // test 値を指定数のスレッドで割って、加算プロセスを実行する
        var stopWatch = new Stopwatch();

        _Value = 0;

        stopWatch.Start();

        for (int i = 0; i < threadCount; ++i)
        {
            var task = Task.Run(() => 
            {
                Increment(loops);
            });

            tasks.Add(task);
        }

        Task.WhenAll(tasks).Wait();
        stopWatch.Stop();

        // 結果を保存
        _ExecutedInfos.Add((_Value, stopWatch.ElapsedMilliseconds));

        if (needLog)
        {
            Console.WriteLine($"End {Name} - thread no:{Thread.CurrentThread.ManagedThreadId:00} value: {_Value:#,0} time: {stopWatch.ElapsedMilliseconds:#,0} ms");
        }
    }

    public void WriteAverageLog()
    {
        var averagedValue = _ExecutedInfos.Average(p => p.Item1);
        var maxValue = _ExecutedInfos.Max(p => p.Item1);
        var minValue = _ExecutedInfos.Min(p => p.Item1);
        var averagedTime = _ExecutedInfos.Average(p => p.Item2);
        var maxTime = _ExecutedInfos.Max(p => p.Item2);
        var minTime = _ExecutedInfos.Min(p => p.Item2);

        Console.WriteLine($"Average {Name} - value: {averagedValue:#,0}(Max:{maxValue:#,0} Min:{minValue:#,0}), time: {averagedTime:#,0}(Max:{maxTime:#,0} Min:{minTime:#,0})");
    }
}

このコードのやりたいことは、テスト値の回数だけ変数 _Value の値をインクリメントするけど、スレッド数で分担して処理します。テスト値が100、スレッド数が2の場合は、変数 _Value の値を各スレッドで50回ずつインクリメントします。

ロックしないマルチスレッド処理

単純に _Value の値を引数の数だけ繰り返しています。

public class NoLockedProcess : ProcessBase
{
    public override string Name => nameof(NoLockedProcess);

    public override void Increment(int counts)
    {
        for (int i = 0; i < counts; i++)
        {
            _Value += 1;
        }
    }
}

実行するとどうなるでしょうか。 試行回数 100 回 (インクリメント回数: 1,000,000 スレッド数:4)で WriteAverageLog の結果を見てみます。

Average NoLockedProcess - value: 386,931(Max:798,288 Min:250,000), time: 1(Max:8 Min:1)

一番大切なことは、 value の値が 1,000,000 になりません。なので、一番最初の図で示したように値の書き込みが正しく行われていないことがわかります。

タスク処理の気を付けないといけない怖い部分が詰まっているコードですね。

lock

一番代表的なロック制御の機構 lock を利用したコードを確認します。

マルチスレッド処理をする場合、最初に検討していいやり方になります。

public class LockProcess : ProcessBase
{
    private object _Locker = new object();
    public override string Name => nameof(LockProcess);

    public override void Increment(int counts)
    {
        for (int i = 0; i < counts; i++)
        {
            lock (_Locker)
            {
                _Value += 1;
            }
        }
    }
}

実行結果がこちら。

Average LockProcess - value: 1,000,000(Max:1,000,000 Min:1,000,000), time: 41(Max:48 Min:38)

一番大切なことは、 value の値が 1,000,000 になりました。

スレッド A の処理の終了を待ってから、スレッド B の処理を開始する、という仕組みをロックで正しく実現できていますね。

lock はとても優れた仕組みでマルチスレッド処理における書きやすさと安全面で最も優れたプランになります。

パフォーマンス面でも実行結果のとおり、それほど致命的な遅延に繋がりません。

注意すること

ロック内の箇所で操作するオブジェクトを、別スレッドからロックしていない箇所から操作してしまった場合、値の保証ができなくなるため注意が必要です。

下のコードのように、_Data は Test2 でロックせずに操作していますが、これはよくないです。

class Data
{
    public void Run();
}

class Bug
{
    private readonly object _Locker = new ();
    private Data _Data;

    public void Test1()
    {
        lock (_Locker)
        {
            _Data = new ();
        }
    }

    public void Test2()
    {
        var data = _Data;
        data.Run();
    }
}

ロックしてから処理しないとダメ。

public void Test2()
{
    Data data;

    lock (_Locker)
    {
        data = _Data;
    }

    data.Run();
}

Interlocked

Interlocked は、int, uint, long, ulong, double, float など変数の値(変数単体)を変更するときにだけロックをかけるやり方です。

public class InterlockedProcess : ProcessBase
{
    public override string Name => nameof(InterlockedProcess);

    public override void Increment(int counts)
    {
        for (int i = 0; i < counts; i++)
        {
            Interlocked.Increment(ref _Value);
        }
    }
}

lock よりもずっと負荷が小さい。注意しないといけないのは2つ以上の処理をロックすることができません。今回のようなシンプルなケースでの利用になります。

Average InterlockedProcess - value: 1,000,000(Max:1,000,000 Min:1,000,000), time: 20(Max:23 Min:17)

ReaderWriterLockSlim

書き込み処理だけをロックして、読み取り処理をロックを介さず高速化する仕組みを作るときに利用するクラスです。

lock よりも高速に実行したいニーズがあり、書き込み処理よりも読み取り処理が多数である場合に有効な仕組みです。

public class ReaderWriterLockSlimProcess : ProcessBase
{
    public ReaderWriterLockSlim _Locker = new ();
    public override string Name => nameof(ReaderWriterLockSlimProcess);

    public override void Increment(int counts)
    {
        for (int i = 0; i < counts; i++)
        {
            _Locker.EnterWriteLock();

            try
            {
                _Value += 1;
            }
            finally
            { 
                _Locker.ExitWriteLock(); 
            }
        }
    }
}

実行結果もほとんど lock と変わりません。

Average ReaderWriterLockSlimProcess - value: 1,000,000(Max:1,000,000 Min:1,000,000), time: 39(Max:71 Min:24)

テストのやり方は読み取りが多数ではないため、ReaderWriterLockSlim の高速性をチェックするテストコードにはなっていないと思います。

SemaphoreSlim

まず、Semaphoreセマフォ)という概念を把握しないとちょっとわかりづらいです。

並列処理を実行するときの並列処理数を制御する仕組みですが、並列処理する数を「1」個に設定すると、ロックをしているのと実質的には同じです。

ユースケースをよく考えて利用したいクラスです。

このあたりが転じて、ロックの中に async/await が存在していても利用可能なロック制御、つまり、ロック開始スレッドとロック終了スレッドが異なっていても動作する、といった応用的な使い方もあります。

public class SemaphoreSlimProcess : ProcessBase
{
    private SemaphoreSlim _Locker = new (1, 1);
    public override string Name => nameof(SemaphoreSlimProcess);

    public override void Increment(int counts)
    {
        for (int i = 0; i < counts; i++)
        {
            _Locker.Wait();

            try
            {
                _Value += 1;
                // Volatile.Write(ref _Value, Volatile.Read(ref _Value) + 1);
            }
            finally
            {
                _Locker.Release();
            }
        }
    }
}

実行結果は、それほど変わりません。

Average SemaphoreSlimProcess - value: 1,000,000(Max:1,000,000 Min:1,000,000), time: 53(Max:58 Min:51)

Mutex

プロセス間でのロック制御に利用するクラスです。非常に低速なので、利用には注意が必要です。

public class MutexProcess : ProcessBase
{
    private Mutex _Locker = new ();
    public override string Name => nameof(MutexProcess);

    public override void Increment(int counts)
    {
        for (int i = 0; i < counts; i++)
        {
            _Locker.WaitOne();

            try
            {
                _Value += 1;
            }
            finally
            {
                _Locker.ReleaseMutex();
            }
        }
    }
}
Average ReaderWriterLockSlimProcess - value: 1,000,000(Max:1,000,000 Min:1,000,000), time: 3,440(Max:3,703 Min:3,213)

テストコードでも、他コードが 50ms 程度で動作するのに対して 3000 ms、つまり3秒以上必要としました。単純なマルチスレッドのロック対策として採用しない必要があります。

ある意味厄介なことは、どのコードもほとんど差がありません。 なんとなく前に書いたコードの感覚からどのロック制御を採用するのか検討しようと思っても、同じようなコードなんでわかりづらいです。

それぞれのロックの違いを整理しておいたほうがいいです。

実行結果のまとめ

1M(100万)回インクリメントを4つのスレッドで分担して処理した場合の結果をまとめます。平均処理時間は前述の処理を 100 回実行した平均です。

ロックの種類 インクリメント結果の値 平均処理時間 (ms)
ロックなし 461,603 1 ms
lock 1,000,000 37 ms
Interlocked 1,000,000 20 ms
ReaderWriterLockSlim 1,000,000 40 ms
SemaphoreSlim 1,000,000 53 ms
Mutex 1,000,000 3,703 ms

ロックなしは、値を正しくインクリメントできませんでした。

f:id:shikaku_sh:20211224174851p:plain:w500

実行環境:

f:id:shikaku_sh:20211224174907p:plain:w500

補足

Volatile 修飾子

Volatile は、コンパイラの最適化によるマルチスレッド関連のバグから守るための仕組みで利用されることがあります。たとえば:

var sample = new Sample();

Task.Run(async () =>
{
    await Task.Deley(TimeSpan.FromSeconds(60)) // heavy work
    sample.IsFinished = true;
})

while (!sample.IsFinished)
{
    // do something
}
class Sample
{
    public bool IsFinished; // volatile
}

コンパイラがシングルスレッドで動作することを前提にした最適化をしてしまうため、while がいつまで経っても終了しない(終了条件無しに最適化してしまう)場合があります。こうした際に Volatile は適切です。

注意しないといけないことは、Volatile(キーワード and class)は acquire/release なメモリバリアに対応しますが、sequential consistency ではありません。Interlocked は sequential consistency です。

  • acquire メモリバリアは、acquire よりもに書かれたメモリー操作命令が、コンパイラの最適化などによってコード順序が変化されることはない
  • release メモリバリアは、release よりもに書かれたメモリー操作命令が、コンパイラの最適化などによってコード順序が変化されることはない
  • sequential consistency は、並列で動いているコードが直列で動いているように見えること(通常はこっちを使うことになってる)

コード順序を保証してくれる仕組みによって、そのスレッドのコードだけで見ると一見無駄に見えても、他のスレッドで値が更新されている可能性のある変数の最適化を回避するために利用されることになります。

ただし、acquire/release なメモリバリアであるため、単純なロジックにのみ利用することが望ましく、できるだけ Volatile キーワードではなく Volatile クラスを利用しましょう。(そして、値の書き込みは Interlocked を使うこと)

ちなみに、lock は acquire/release や sequential consistency を保証するものではなくて、スレッドがロックした部分を実行中の間、他のスレッドからアクセスできないことを保証するものです。

原子性

原子性は、それ以上に処理の分割が無いことです。

uint a;

a = 0xffffffff;

C# では、uint など 32 bit 以下のプリミティブ型+参照型は書き込み時の原子性が保証されているため、普段意識しないと思います。

GUID (128 bit) のように、ひとつのデータ型が大きい場合、0x0000ffff や 0xffff0000 のように中途半端な書き込み状態のデータを読み込んでしまう恐れがあります。これは、原子性が保証されない=処理が分割されている、ということになります。

C++ はプリミティブ型でも原子性を保証しないので、注意が必要。

なので、マルチスレッドの場合で値を操作するときは sequential consistency と原子性を保証してくれる Interlocked を使う。

サンプル

GitHub に今回のサンプルコードを公開しています。

参考

C# パターンマッチングのデコンパイルコードを確認する

f:id:shikaku_sh:20210823173353p:plain:w300

C# Advent Calendar 2021 を見てると、C# のパターンマッチングが便利らしい。

でも、パターンマッチングの中身が実際どんな判定(動き)してるのかよくわからないし、なんか怖い。わけわからん動きをするバグの狂気に陥りそう。

そんなわけでパターンマッチングのデコンパイルコードを確認してみました。そこそこ実際のうごきを把握できる(はずな)ので経験値をかせげると思います。

デコンパイルコードの確認には下図のとおり「sharplab」を利用しています。(あと IL は長くなるので記事では掲載外)

f:id:shikaku_sh:20211222163335p:plain:w500
sharplab

C# は 2021 年末現在 C# 10.0 ですが、パターンマッチングは C# 7.0 から 10.0 のなかでアップデートが進んでいます。最新の .NET 6 などでテストしてみるとよいと思います。

Visual Studio 2019 だと .NET 5 まで対応。C# 9.0 までのパターンマッチングで遊ぶことができます。デフォルトだと C# 10.0 の「プロパティパターンの拡張」は利用できないはずです。

パターンマッチングの比較

古式ゆかしい if 文と、パターンマッチングを利用した if 文です。二つの式は同じうごきをしますが、デコンパイルした C# のコードに違いはあるのかチェックしてみる。(クラスとメソッドはなくてもいいのですが、デコンパイルコードと比較しやすいように掲載)

using System;

public class Test
{
    public static void Main() 
    {
        var a = (x:1, y:2);
        
        if (a.x == 1 && a.y == 2)
        {
            Console.WriteLine("A");
        }
        
        if (a is { x: 1, y: 2 })
        {
            Console.WriteLine("B");
        }
    }
}

下が、C# Decompile のコードです。完全に生成されたコードが一致していました。なんで、見た目が違うだけで同じコードだとわかりました。

public class Test
{
    public static void Main()
    {
        ValueTuple<int, int> valueTuple = new ValueTuple<int, int>(1, 2);
        if (valueTuple.Item1 == 1 && valueTuple.Item2 == 2)
        {
            Console.WriteLine("A");
        }
        if (valueTuple.Item1 == 1 && valueTuple.Item2 == 2)
        {
            Console.WriteLine("B");
        }
    }
}

パターンマッチングのなかで _ をつかって、なんでもいい値を表現することがあると思います。

using System;

public class Test
{
    public static void Main() 
    {
        var p = (x: 10, y: 20);
        
        if (p.Item1 == 10) Console.WriteLine("A");
        if (p is {x: 10, y: _}) Console.WriteLine("B");
        if (p is {x: 10 }) Console.WriteLine("C");
    }
}

BC を出力する式は、同じ意味だとなんとなくわかると思います。下のように生成コードも同じです。(逆にこの場合は書いていても人の読みやすさだけに効果があり、実行時の処理コストは無い)

public class Test
{
    public static void Main()
    {
        ValueTuple<int, int> valueTuple = new ValueTuple<int, int>(10, 20);
        if (valueTuple.Item1 == 10)
        {
            Console.WriteLine("A");
        }
        if (valueTuple.Item1 == 10)
        {
            Console.WriteLine("B");
        }
        if (valueTuple.Item1 == 10)
        {
            Console.WriteLine("C");
        }
    }
}

null チェックの比較

知らないと絶対意味がわからないパターンマッチングの null チェック。

using System;

public class Test
{
    public static void Main() 
    {
        int? p = null;
        
        if (p != null) Console.WriteLine("A");
        if (p is not null) Console.WriteLine("B");
        if (p is {}) Console.WriteLine("C");

    }
}

完全に同じコードになります。安心した。わけのわからない追加コードはありません。

public class Test
{
    public static void Main()
    {
        Nullable<int> num = null;
        if (num.HasValue)
        {
            Console.WriteLine("A");
        }
        if (num.HasValue)
        {
            Console.WriteLine("B");
        }
        if (num.HasValue)
        {
            Console.WriteLine("C");
        }
    }
}

型パターン

型パターンも知らなかったら、コードが読めなくなるやつだと思います。 従来書いてたコードとすこし異なっていて機械的なコードが生成されました。が、常識の範囲内のごく普通のコードです。

using System;

public class Test
{
    public static void Main() 
    {
        object i = 1;
        
        if (i is int)
        {
            int i2 = (int)i;
            Console.WriteLine(i2);
        }
        
        if (i is int i3)
        {
            Console.WriteLine(i3);
        }
    }
}
public class Test
{
    public static void Main()
    {
        object obj = 1;

        // i2 のテストコード
        if (obj is int)
        {
            int value = (int)obj;
            Console.WriteLine(value);
        }

        // i3 のテストコード
        int value2 = default(int);
        int num;
        if (obj is int)
        {
            value2 = (int)obj;
            num = 1;
        }
        else
        {
            num = 0;
        }
        if (num != 0)
        {
            Console.WriteLine(value2);
        }
    }
}

WPF なんかだと IValueConverter みたいに、データを object 型にして渡してくるやつと格闘する機会に恵まれると思います。そういうときに型パターンやパターンマッチングを利用すると、(ガード節の部分なんかで)マシになると思います。

switch 文のパターンマッチング1

これもチェックしておきます。

using System;

public class Test
{
    public static void Main() 
    {
        var p = (x: 10, y: 20);
        
        var p1 = p switch
        {
            { x: 10 } => "A",
            { y: >= 20 } => "B",
            _ => "X"
        };
        
        Console.WriteLine(p1);
    }
}
public class Test
{
    public static void Main()
    {
        ValueTuple<int, int> valueTuple = new ValueTuple<int, int>(10, 20);
        int item = valueTuple.Item1;
        string text;
        if (item != 10)
        {
            int item2 = valueTuple.Item2;
            text = ((item2 < 20) ? "X" : "B");
        }
        else
        {
            text = "A";
        }
        string value = text;
        Console.WriteLine(value);
    }
}

もうひとつ似たようなパターンを見ておきます。ほとんど同じコードが生成されています。

using System;

public class Test
{
    public static void Main() 
    {
        var p = (x: 10, y: 20);
        string p2 = "";
        
        switch (p)
        {
            case var (x, _) when x == 10:
                p2 = "A";
                break;
            case var (_, y) when y >= 20:
                p2 = "B";
                break;
            default:
                p2 = "X";
                break;
        }
        
        Console.WriteLine(p2);
    }
}
public class Test
{
    public static void Main()
    {
        ValueTuple<int, int> valueTuple = new ValueTuple<int, int>(10, 20);
        string text = "";
        ValueTuple<int, int> valueTuple2 = valueTuple;
        ValueTuple<int, int> valueTuple3 = valueTuple2;
        int item = valueTuple3.Item1;
        if (item != 10)
        {
            int item2 = valueTuple3.Item2;
            text = ((item2 < 20) ? "X" : "B");
        }
        else
        {
            text = "A";
        }
        Console.WriteLine(text);
    }
}

switch 文のパターンマッチング2

型をチェックするパターンマッチングの例です。先の例と変わらないかも。

public class Test
{
    public static void Main() 
    {
        int a = 1;
        
        Method(a);
    }
    
    public static string Method(object o)
    {
        string result = "";
        
        switch (o)
        {
            case int i when i > 0:
                result = "A";
                break;
            case double d when d > 10:
                result = "B";
                break;
            default:
                result = "X";
                break;
        }
        
        return result;
    }
}
public class Test
{
    public static void Main()
    {
        int num = 1;
        Method(num);
    }

    public static string Method(object o)
    {
        string text = "";
        if (o is int)
        {
            int num = (int)o;
            if (num > 0)
            {
                return "A";
            }
        }
        else if (o is double)
        {
            double num2 = (double)o;
            if (num2 > 10.0)
            {
                return "B";
            }
        }
        return "X";
    }
}

ちなみに、when を and に変えたコード (C# 9.0) でも、生成コードは変わらなかったです。

using System;

public class Test
{
    public static void Main() 
    {
        int a = 1;
        
        Method(a);
    }
    
    public static string Method(object o)
    {
        string result = "";
        
        switch (o)
        {
            case int i and > 0:
                result = "A";
                break;
            case double d and > 10:
                result = "B";
                break;
            default:
                result = "X";
                break;
        }
        
        return result;
    }
}
public class Test
{
    public static void Main()
    {
        int num = 1;
        Method(num);
    }

    public static string Method(object o)
    {
        string text = "";
        if (o is int)
        {
            int num = (int)o;
            if (num > 0)
            {
                return "A";
            }
        }
        else if (o is double)
        {
            double num2 = (double)o;
            if (num2 > 10.0)
            {
                return "B";
            }
        }
        return "X";
    }
}

個人的な感想

デコンパイルしたコードを確認してわかったことは、パターンマッチングのコードから見たこともない恐ろしいコードが生成されることはありませんでした。パターンマッチングのコードは、覚えきれない何千もの恐怖の形状ではなかったので、僕は狂気に陥らずにすみそうです。

というよりも、パターンマッチングのコードは、今風にイメージチェンジしただけの既存コードでした。C# の非同期、async/await とコルーチンを整理したときほどの苦労はありません。何か今までのコードと違っていたり、添加されている新しいコードもないとわかって安心できました。

それから、パターンマッチングのコードが読めないときは、デコンパイルした C# コードを確認してみるのは勉強と助けになると思いました。

参考

Git tag の基本的な使い方

f:id:shikaku_sh:20211221175407p:plain:w300

Git のコミットにタグをつけておくと、GitHub なんかのサービスだと特にバージョン管理が見やすく、やりやすくなります。

タグ付けしたほうがよいタイミングが明らかにあるんだけど、うっかりつけ忘れることがあるので、やり方をまとめておきます。

master に対して単純にプッシュし続けるようなリポジトリー管理でも、特定のバージョン(開発やリリースの区切りがよいコミット)に目印をつけておくことで、それっぽくなります。おすすめ。

タグ (tag) の使い方

1.タグをつけるとき

git tag #tagname#
or
git -a tag #tagname#

軽量版 (lightweight) タグと -a を付与した注釈つき (annotated) タグです。詳細は公式を確認。多くの場合は、軽量版が利用されると思います。


2.タグを共有する(GitHub に反映する)

git push origin #tagname#
or
git push origin --tags

タグをつける方法は忘れないけど、共有し忘れてることがよくある。GitHub にリリースを上げたいときは、必ず共有しておくこと。--tags をつけると、ローカルにだけ存在しているタグをまとめて転送できる。


3.タグの情報を確認

git show #tagname#

注釈つきは軽量版よりも多くの情報を出力できます。

Visual Studio からタグをつけるとき

Visual Studio だと Visual Studio Code ほど丁度良いコンソール(ターミナル)がパッっと出てきてくれません。

しょうがないので、プロジェクトに合わせたパスでコマンドプロンプトを表示します。これでタグを作ることができます。

f:id:shikaku_sh:20211221175505p:plain:h300

タグ付けしてみる。

f:id:shikaku_sh:20211221175725p:plain:w600

f:id:shikaku_sh:20211221175610p:plain:w600

当たり前だけど git をインストールしておきましょう。

GitHub でリリースを作るとき

次の URL で新しいリリースを作れる。

https://github.com/#user#/#repository#/releases/new

git で作成したタグを選択してリリースする。ソースを zip でダウンロードできるようになる。

f:id:shikaku_sh:20211221175531p:plain:w600

参考

なんでプログラミングは簡単だけど、ソフトウェアエンジニアリングは難しいのか?

f:id:shikaku_sh:20211213144325j:plain:w150
Yujian Tang さんの記事が元です

Twitter を見てたら流れてきた記事が気になったので、個人的な和訳をメモした内容です。

ソフトウェアの分野に入りたいと思っている初心者は、プログラミングとソフトウェアエンジニアリングをよく混同してしまいます。これらは同じものではありません。 プログラミングはソフトウェアエンジニアリングの一部分です。一方でソフトウェアエンジニアリングにはプログラミング以上の要素が含まれています。ソフトウェアエンジニアリングとは、問題から始まって、その問題を解決するためのソフトウェアソリューションを設計、提供します

なんでプログラミングが簡単か? (Why is Programming Easy?)

プログラミングとは、正確にはなんでしょうか?

プログラミングは、あるタスクを実行するためのコードを書くことです。コンピューターサイエンスがまだ始まったばかりの頃、プログラミングはまだ難しいものでした。

昔のころはプログラムを組むことは難しかったです。パンチカードのようなものを使わなければいけませんでした。それから、コンピューターサイエンスの分野が進化するについて抽象度は上がっていきました。

アセンブリ言語では、レジスタを直接割り当てることができるようになりました。それから、COBOL, Fortran, PASCAL (著者 Yujian Tang が初めて使った言語!) といった言語に変わっていきました。

しばらくして、C#, C++, JAVA などのより読みやすいオブジェクト指向言語が登場しました。ここ数年のとこで、30 年以上前に開発された Python が台頭してきました。Python の構文はほとんど英語のようなものです。そのため、理解しやすく学習しやすいです。

ほんの少しの時間で Python のプログラミングを学ぶことができます。プログラミング言語は時代とともに学びやすく、使いやすくなっているだけではなく、無料のオンラインリソースも増えています。

ここでは、「Randam Number Generator」を作る方法について無料のガイドを見ることができます。すばらしい、5分もかからずプログラムを作れました。無料のオンラインリソースの数と Python の使いやすさによってインターネットと学習する意欲さえあれば、だれでもプログラミングできるようになりました。

なんでソフトウェアエンジニアリングが難しいか? (Why is Software Engineering Hard?)

プログラミングが出来るからといって、ソフトウェアエンジニアに自動的になるわけではありません。ソフトウェアエンジニアはプログラミングに多くの時間を費やしますが、上達するほど考えることに多くの時間を費やします。

ソフトウェアエンジニアとして成功するためには、「エンジニアリングとしての考え方」を身につける必要があります。

エンジニアリングの考え方をしなければいけないことが、なんでソフトウェアエンジニアリングを難しくするのでしょうか? ええ、それは、同じ理由で多くのエンジニアリング分野で難しいのです。少なくとも、アメリカの教育では一般的に教えられていません。

エンジニアリングは好奇心を必要とする分野であり、正しい質問をする必要があります。ソフトウェアエンジニアの心情は、どのようにしてプログラムを作るのかではなくて、問題を解決することに集中しなければいけません。諸々の問題ではなくて、正しい問題に対して、ということです。

ソフトウェアエンジニアリングは、単に外見上のプログラムの束を作ることのように見えるかもしれませんが、それらのプログラムがどのように繋がっているのか理解していなければいけません。

また、プログラムをどのように追加するのか、どのように修正するのか、そして、多くのソフトウェアエンジニアがおそらく苦手とするだろう、プログラムの文書化についても理解する必要があります。

個人的な感想:
初心者の壁と言うと、どうなんだろう。プログラミングは、オンラインラーニングほどの低さ・手軽さでも学習する意欲が「ある」みたいなところがある。ソフトウェアエンジニアリングは? みたいな話だとも思った。個人で解決しづらい部分が難しい。
今は人材が溢れているこの界隈、粗製の人材は「プログラミング」ができるんですよねっていう辛い話。

参考

ローカルアプリでも API キーの取り扱いに注意する

f:id:shikaku_sh:20211127154642p:plain:w200

ローカルのサンプルプログラムでも、GitHub にコミットするときなんかに API キーを使用していると困ることがあります。GitHub にサンプルコードをあげるときは、当然 API キーを隠します。で、API を使用する(実際に動作する)実行コードに書き換えるたびに Git の変更要素としてコミットのリストに挙げられてしまうことになります。あまり好ましい状態ではないですね。

書き換えが面倒……ではなくて、そもそも危険なんで止めたいわけです。(Private なリポジトリでも GitHub に管理してもらうことではなさそう)

というわけで、どうするべきなのか調べました。

ローカルのアプリ(サンプルコードを含む)もコードは GitHub に上げる時代になったけど、ネット上のサンプルコードは(特にローカルアプリにおいて)API キーの取り扱いの注意が抜けがちなんで、注意しようという記事です。

基本的な対策方針

Google Cloud - API キーの使用」を確認すると次の説明があります。

  • API キーをコードに直接埋め込まないでください。コードに埋め込まれた API キーは、誤って公開されてしまう可能性があります。たとえば、共有するコードからキーを削除し忘れた場合などです。API キーはアプリケーションに埋め込む代わりに、アプリケーションのソースツリー外部にある環境変数やファイル内に保存します。
  • アプリケーションのソースツリー内のファイルに API キーを保存しないでください。API キーをファイルに保存する場合は、キーがソースコード制御システム内に保存されないよう、ファイルをアプリケーションのソースツリー外部に保持するようにします。これは特に、GitHub のような公共のソースコード管理システムを使用する場合に重要です。
  • 不要な API キーを削除して、攻撃を受けるリスクを最小限に抑えます。
  • 公開する前に、コードを確認します。コードを公開する前に、コードに API キーやその他のプライベート情報が含まれていないことを確認してください。

抜粋しています。

そんなわけで、サンプルコードとは言っても、平文をコードに直接埋め込んでいるとサンプルの本質ではない、とは言え誤って公開されてしまう恐れがあるため、サンプルを作る側も見る側もいいことなさそう。(平文コードを絶対真似るべきじゃない)

パッと見たわかりやすさはいいけど、リスクが大きすぎる。リスクリターンが見合わないというわけです。

もしデベロッパ秘密鍵GitHub上に過って公開したら、悪意のあるハッカーがすぐに飛びついてくるでしょう。彼らはGitHub上のリポジトリに公開されているAWS秘密鍵を発見する仕組みをすでに自動化していて、これらの秘密鍵を使ってEC2インスタンスをスピンアップし、ビットコイン採掘をしてしまいます。

対策1(環境変数

ともかく、「ソースツリー外部に保持するようにします」ということが大切です。

一番シンプルな回答になるのは、環境変数API キーを含めるケースです。

f:id:shikaku_sh:20211127154845p:plain:w400

var key = Environment.GetEnvironmentVariable("RESAS_API", EnvironmentVariableTarget.User);

ローカルアプリだと定番の方法のひとつだと思います。API キーを間違って GitHub なんかにアップロードしづらくなります。

正直サンプルコードを掲載する例なら、これくらい(の冗長コード)なら気にならない気がします。一言、API キーは環境変数に格納しました、でいいので。

対策2(DotNetEnv)

f:id:shikaku_sh:20211127155408p:plain:w400
NuGet からインストール

ファイルに API のキーを入れて管理する場合は Json 形式も候補ですが、DotNetEnv もよいと思います。DotNetEnv は .env 設定ファイルの読み込みをサポートしています。

f:id:shikaku_sh:20211127155441p:plain:w200
出力フォルダーにコピーします

.env ファイルは、ファイル名をあまりつけないです。.env ファイルの書き方はこんな感じ:

RESAS_API = "abcDEF123456 (API キー)"
OTHER_PARAM = 12345

とてもシンプルに記述して、値を読み込むことができる特徴を持ちます。なので、.iniJson 形式とさして変わらないといえば、そのとおりですね。

GitHub にアップロードしないように .env ファイルを除外する設定を忘れたらいけない。.gitignore ファイルに次を追記します:

# Ignore files
.env

値を読み込むコードは次のとおり:

DotNetEnv.Env.Load(".env");
var key = DotNetEnv.Env.GetString("RESAS_API");

.env ファイルは慣例的に除外ファイル扱いをしますので、その点で僅かにですがこの手法がよいのかと思いました。

補足

AWS では git-secrets を利用することで、特定のキー情報と思われる文字列を含むファイルは、アップロードできない、エラーにする対策があります。

git-secrets は、aws の場合に簡単設定することができますが、細かく自分で設定を作れば応用することができると思います。(が、汎用化してチームに転用する例はあまり見たことがないです)

より良い対策なんだと思うのですが、現状はそこまで API キーの流出でとんでもない企業被害が発生したというケースよりも、個人で失敗して 100 万円前後の被害というケースが(見た目には)多いように思いました。なので、対策が個人レベルな感じになるのも否めない気がします。

参考

Prism 子ウィンドウ(ダイアログ)IDialogAware を表示する

f:id:shikaku_sh:20211013180838p:plain:w400

Prism を利用するプロジェクトで子ウィンドウ(ダイアログ)を表示する方法は、バージョンの進化によって変化してきているようです。

現在(下記のバージョン記載)では、ダイアログの機能が一番手早く子ウィンドウを利用するための機能として候補に挙がります。

Prism

f:id:shikaku_sh:20211119170248g:plain

  • Prism.Wpf 8.1.97
  • Prism.Unity 8.1.97

App.xaml

とりあえずエントリーポイントから見ていく。

通常どおり、初期表示するウィンドウは MainWindow で、コンテナにポップアップする画面を RegisterDialog メソッドを通じて追加(Dialog1.xaml)します。

<prism:PrismApplication x:Class="PrismPopupDialogTest.App"
                        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:prism="http://prismlibrary.com/"
                        xmlns:local="clr-namespace:PrismPopupDialogTest">
    <Application.Resources>
         
    </Application.Resources>
</prism:PrismApplication>
public partial class App
{
    protected override Window CreateShell() => Container.Resolve<MainWindow>();

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterDialog<Dialog1>();
    }

    protected override void OnInitialized()
    {
        base.OnInitialized();
    }
}

MainWindow (View + ViewModel)

f:id:shikaku_sh:20211119170108p:plain:w400

サンプルにこの画面を作成した。2パターンのダイアログを表示することを目的にしています。

<Window x:Class="PrismPopupDialogTest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:prism="http://prismlibrary.com/"
        xmlns:v="clr-namespace:PrismPopupDialogTest.Views"
        xmlns:vm="clr-namespace:PrismPopupDialogTest.ViewModels"
        prism:ViewModelLocator.AutoWireViewModel="True"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance vm:MainWindowViewModel}"
        Title="{Binding Title}" Height="300" Width="400">
    <StackPanel HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Orientation="Vertical">
        <TextBlock HorizontalAlignment="Center"
                   Text="{Binding SampleValue, StringFormat=サンプル値 : {0}}" />
        <Button Margin="10" Padding="6"
                Content="Popup Dialog1"
                Command="{Binding PopupDialog1Command}" />
        <Button Margin="10" Padding="6"
                Content="Popup Dialog2"
                Command="{Binding PopupDialog2Command}" />
    </StackPanel>
</Window>
public class MainWindowViewModel : BindableBase
{
    private IDialogService _DialogService;
    private string _Title = "Prism - PopupDialog Test";

    public string Title
    {
        get => _Title;
        set => SetProperty(ref _Title, value);
    }

    private int _SampleValue = 3;

    public int SampleValue
    {
        get => _SampleValue;
        set => SetProperty(ref _SampleValue, value);
    }

    public DelegateCommand PopupDialog1Command { get; }
    public DelegateCommand PopupDialog2Command { get; }

    /// <summary>
    /// <see cref="MainWindowViewModel"/> クラスの新しいインスタンスを初期化します。
    /// </summary>
    public MainWindowViewModel(IDialogService dialogService)
    {
        _DialogService = dialogService;

        PopupDialog1Command = new (() =>
        {
            Debug.WriteLine($"{nameof(PopupDialog1Command)} called.");
            
            var parameters = new DialogParameters
            {
                { "key1", "send data1" },
                { "key2", "send data2" },
                { "key3", SampleValue },
            };

            _DialogService.ShowDialog("Dialog1", parameters, (results) => 
            {
                SampleValue = results.Parameters.GetValue<int>("key3");

                Debug.WriteLine($"Dialog1 result called.");
            });
        });

        PopupDialog2Command = new(() =>
        {
            Debug.WriteLine($"{nameof(PopupDialog2Command)} called.");

            var parameters = new DialogParameters
            {
                { "key1", "send data1" },
                { "key2", "send data2" },
                { "key3", SampleValue },
            };

            _DialogService.ShowDialog("Dialog2", parameters, (results) =>
            {
                SampleValue = results.Parameters.GetValue<int>("key3");

                Debug.WriteLine($"Dialog2 result called.");
            });
        });

    }
}

ポイントは、コンストラクターIDialogService を受け取り、ShowDialog でダイアログを表示しています。この表示するダイアログは Window を継承するコントロールである必要はありません。UserControl でよいのです。

サンプルでは、DialogParameters でダイアログにデータを送りだし、ダイアログを消したあと results で結果を受け取ります。データの送受信は Parameters を通しています。

Dialog1

f:id:shikaku_sh:20211119170154p:plain

とりあえず、シンプルにダイアログを表示してみます。ダイアログに利用する ViewModel は BindableBase の継承のほかにも IDialogAware インターフェースを継承します。

IDialogAware インターフェースは、次の5つを実装する必要があります。

  • string Title { get; }
    ダイアログのタイトル
  • event Action RequestClose;
    ダイアログを閉じるときのイベント
  • bool CanCloseDialog();
    ダイアログを閉じることができるかを決める(通常は true でいい)
  • void OnDialogClosed();
    ダイアログを閉じたときに実行されるイベント
  • void OnDialogOpened(IDialogParameters parameters);
    ダイアログを開いたときに実行されるイベント(パラメーターはここで受け取る)
<UserControl x:Class="PrismPopupDialogTest.Views.Dialog1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:v="clr-namespace:PrismPopupDialogTest.Views"
             xmlns:vm="clr-namespace:PrismPopupDialogTest.ViewModels"
             prism:ViewModelLocator.AutoWireViewModel="True"
             mc:Ignorable="d"
             d:DataContext="{d:DesignInstance vm:Dialog1ViewModel}"
             d:DesignHeight="200" d:DesignWidth="200" 
             MinWidth="200" MinHeight="200">
    <StackPanel Orientation="Vertical"
                HorizontalAlignment="Center"
                VerticalAlignment="Center">
        <TextBlock Margin="10" Text="{Binding Sample1}" />
        <TextBlock Margin="10" Text="{Binding Sample2}" />
        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center">
            <Button Margin="10" Padding="6"
                    Content="↓"
                    Command="{Binding DownValueCommand}" />
            <TextBlock Margin="10"
                       VerticalAlignment="Center"
                       Text="{Binding Sample3}" />
            <Button Margin="10" Padding="6"
                    Content="↑"
                    Command="{Binding UpValueCommand}" />
        </StackPanel>
        <Button Margin="10" Padding="6"
                MinWidth="120"
                Content="Close"
                Command="{Binding CloseCommand}" />
    </StackPanel>
</UserControl>
public class Dialog1ViewModel : BindableBase, IDialogAware
{
    private string _Title = "Test Title";

    public event Action<IDialogResult> RequestClose;

    public string Title
    {
        get => _Title;
        set => SetProperty(ref _Title, value);
    }

    private string _Sample1;

    public string Sample1
    {
        get => _Sample1;
        set => SetProperty(ref _Sample1, value);
    }

    private string _Sample2;

    public string Sample2
    {
        get => _Sample2;
        set => SetProperty(ref _Sample2, value);
    }

    private int _Sample3;

    public int Sample3
    {
        get => _Sample3;
        set => SetProperty(ref _Sample3, value);
    }

    public DelegateCommand CloseCommand { get; }
    public DelegateCommand UpValueCommand { get; }
    public DelegateCommand DownValueCommand { get; }

    /// <summary>
    /// <see cref="Dialog1ViewModel"/> クラスの新しいインスタンスを初期化します。
    /// </summary>
    public Dialog1ViewModel()
    {
        CloseCommand = new (() =>
        {
            var result = new DialogResult();

            result.Parameters.Add("key3", Sample3);

            RequestClose?.Invoke(result);

            Debug.WriteLine($"{nameof(CloseCommand)} called.");
        });

        UpValueCommand = new (() =>
        {
            Sample3++;
        });

        DownValueCommand = new (() =>
        {
            Sample3--;
        });

    }

    public bool CanCloseDialog() => true;

    public void OnDialogOpened(IDialogParameters parameters)
    {
        Sample1 =  parameters.GetValue<string>("key1") ?? "error";
        Sample2 =  parameters.GetValue<string>("key2") ?? "error";
        Sample3 =  parameters.GetValue<int>("key3");

        Debug.WriteLine($"{nameof(OnDialogOpened)} called.");
    }

    public void OnDialogClosed()
    {

    }

}

開いてみるとわかるのですが、ダイアログは通常の Window であることがわかります。拡大、縮小、終了のボタンなどを持ちます。ツール用のウィンドウを利用したいときは、もう少しウィンドウ自体に手を入れる必要があります。

Dialog2

f:id:shikaku_sh:20211119170216p:plain

まず App.xaml.cs にダイアログを追加。Dialog2 という xaml ファイルを追加して、ViewModel は Dialog1 のものを流用することにします。(仮なんで)

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterDialog<Dialog1>();
    containerRegistry.RegisterDialog<Dialog2, Dialog1ViewModel>();
}

Dialog2 の xaml はこのような感じになります。ウィンドウのスタイルをタグで指定していることがわかります。

<UserControl x:Class="PrismPopupDialogTest.Views.Dialog2"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:v="clr-namespace:PrismPopupDialogTest.Views"
             xmlns:vm="clr-namespace:PrismPopupDialogTest.ViewModels"
             prism:ViewModelLocator.AutoWireViewModel="True"
             mc:Ignorable="d"
             d:DesignHeight="200" d:DesignWidth="200" 
             MinWidth="200" MinHeight="200">
    <prism:Dialog.WindowStyle>
        <Style TargetType="Window">
            <Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterOwner" />
            <Setter Property="WindowStyle" Value="ToolWindow" />
            <Setter Property="ResizeMode" Value="NoResize"/>
            <Setter Property="ShowInTaskbar" Value="False"/>
            <Setter Property="SizeToContent" Value="WidthAndHeight"/>
        </Style>
    </prism:Dialog.WindowStyle>
    <Grid>
        <v:Dialog1 DataContext="{Binding .}" />
    </Grid>
</UserControl>

Dialog1 の中に <prism:Dialog.WindowStyle> タグを追加してもよいのですが、汎用性を考えるとこのように分離しておくとよいと思います。Grid の中で直接 Dialog1 を指定しているのが気に入らないなら、Region を利用してもよいと思います。

サンプル

GitHub にサンプルを公開しています。

f:id:shikaku_sh:20211119170248g:plain

参考