sh1’s diary

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

C# コーディングガイドライン&プラクティス 2021

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

この記事は、Microsoft MVP のひとり Vincent Maverick Sanchez Durano のブログ記事「C# Coding Guidelines and Practices - 2021」を個人的に雑訳したものです。

この記事では、一般的なソフトウェアエンジニアリング ガイドラインを紹介します。これらのガイドラインのほとんどは、業界で一般的に使用されているものであり、これらを使用することで、あなたのコードが他の人にも読みやすくなります。

コーディングの標準は 任意のもの (arbitrary) であることは事実です。しかし、メンテナンス制の高いプロジェクトを成功させるためにどのようなコーディングスタイルに従うのかではなくて、一貫性を保つことが重要です。

ここでは、コードをどのようにインデント(タブ、スペース、{} の位置)するべきかということではなくて、管理しやすいコードを書くためのガイドラインです。

とはいえ、もしあなたがチームを率いているなら、あるいはひとりの開発者であってもより良いものを目指したいと思っているなら、コーディングガイドラインを用意することは、より良いものを実現する優れたスタートとなります。

それでは早速、本題に入っていきます。

Tips

#1

つぎのような if-else 文は避けること:

bool result;
if (condition)
{
   result = true;
}
else
{
   result = false;
}

代わりに三項演算子 (ternary conditional operator) を使います:

bool result = condition ? true: false;

コードはよりすっきりして、読みやすく理解しやすくなりました。それらに加えて、より簡潔です。

#2

つぎのように null チェックのために if 文を使うことを避けること:

if (something != null)
{
    if (other != null)
    {
        return whatever;
    }
}

代わりに null 条件演算子 (null conditional) を使います:

return something?.other?.whatever;

コードは、よりクリーンで簡潔なものになります。

#3

null チェックのためにややこしい if-else 文を使うことを避けること:

if (something != null)
{
    if (other != null)
    {
        return whatever;
    }
    else 
    {
        return string.empty;
    }
}
else
{
    return string.empty;
}

代わりに ?? 演算子 (null coalescing) を使います:

return something?.other?.whatever ?? string.empty;

#4

オブジェクトのデフォルト値が null のとき、つぎのようなコードを使わないこと:

int? number = null;
var n = number.HasValue ? number : 0;

代わりに ?? 演算子 (null coalescing) か、GetValueOrDefault メソッドを使うこと:

var n = number ?? 0;
or
var n = number.GetValueOrDefault();

#5

nullable な変数のチェックは等値演算子 == や HasValue を使わないこと:

int? number = null;

if (number == null)
{
    //do something
}

if (!number.HasValue)
{
    //do something
}

is を使うことで、さらに改善することができます:

int? number = null;

if (number is null)
{
    //do something
}

意図がはっきりするので、とても読みやすくなります。

#6

単純な if, for, foreach 文でも、{} のないコードを避けること:

if(conditioin) action;

中括弧がないと、2行目を誤って追加してしまうことで、if 文に含まれていないのに含まれていると勘違いするコードを書きます。

必ず中括弧を使用しましょう:

if (condition) { action; }

//or better
if (condition) 
{ 
    action; 
}

#7

次のような if-else 文の連続を使わないこと:

if (condition)
{
   //do something
}
else if(condition)
{
   //do something
}
else if(condition)
{
   //do something
}
else(condition)
{
   //do something else
}

代わりに switch を使うこと:

switch(condition)
{
   case 1:
      //do something
      break;
   case 2:
      //do something
      break;
   case 3:
      //do something
      break;
   default:
      //do something else
     break;
}

ただし、可能なら従来の switch よりも switch の式 (switch statements) のほうが好ましい:

condition switch
{
    1 => //do something;
    2 => //do something;
    3 => //do something;
    _ => //do something else;
}

より簡潔で、読みやすくて理解しやすくなりました。(注:C# 8.0 からの機能です)

例外:switch よりも if-else 文のほうが意味を持つことがあります。例えば、条件の異なるオブジェクトや複雑な条件が含まれている場合などです。どちらがよいかは判断に任せます。

#8

リソースを使うオブジェクト、IDisposable インターフェースを実装したオブジェクトを扱うときは、必ず using ステートメントを使用すること:

using (MemoryStream stream = new MemoryStream()) 
{
    // do something
}

C# 8.0 で導入された新しい using を使っても良い:

using var stream = new MemoryStream();
// do something

新しい using は、メソッドの中で中括弧の数を減らし、リソースを破棄する箇所を簡単に見出すことができます。詳細は「Microsoft Docs - "pattern-based using" and "using declarations"」を参照します。

#9

テキストを + で連結しないこと:

string name = "Vianne";
string greetings = "Hello " + name + "!";

代わりに string.Format() 、または、文字列補間 $ (tring interpolation) を使用すること:

string name = "Vynn";
string greetings = string.Format("Hello {0}!", name);
or
string name = "Vjor";
string greeting = $"Hello, {name}!;

簡潔で読みやすいコードになります。

どっちもコンパイルすると同じコードの意味になっていたから。ただし、C# 10.0 からは文字列補間の関係から後者の $ を使ったほうがパフォーマンスに優れる場合がある。

#10

単純なオブジェクトのテキストをフォーマットするときは、なるべく string.Format() を避けること:

var date = DateTime.Now;
string greetings = string.Format("Today is {0}, the time is {1:HH:mm} now.", date.DayOfWeek, date);

代わりに文字列補間を使うこと:

var date = DateTime.Now;
string greetings = $"Today is {date.DayOfWeek}, the time is {date:HH:mm} now.");

簡潔で理解しやすいコードになります。しかし、string.Format() を使ったほうがよい場合もあります。例えば、複雑な書式設定やデータ操作をする場合などです。適用するべきだとあなたが判断するときに string.Formart() を使用してください。

#11

変数を定義するとき、(特に複雑なオブジェクトに対して)型を指定することを避けること:

List<Repository.DataAccessLayer.Whatever> listOfBlah = _repo.DataAccessLayer.GetWhatever();

代わりに var を使う。他のローカル変数も同じ:

var listOfBlah = _repo.DataAccessLayer.GetWhatever();
and
var students = new List<Students>(); 
var memoryStream = new MemoryStream();
var dateUntilProgramExpiry = DateTime.Now; 

#12

中括弧を使った1行のメソッド実装は避けること:

public string Greeter(string name)
{
    return $"Hello {name}!";
}

代わりに => (Expression-bodied) を使った実装にする:

public string Greeter(string name) => $"Hello {name}!";

読みやすさを維持しつつ、より簡潔になります。

#13

つぎのようなオブジェクトの初期化は避けること:

Person person = new Person();
person.FirstName = "Vianne";
person.LastName = "Durano";

代わりにオブジェクトやコレクションの初期化子を使用します。

var person = new Person { 
    FirstName = "Vianne",
    LastName = "Durano"
};

プロパティは中括弧の中で定義されているので、より自然に読むことができるうえ、意図もはっきりする。

#14

単純な2つのプロパティの値を持つだけの結果セットのクラスを作るのを避けること:

public Person GetName()
{
    var person = new Person
    {
        FirstName = "Vincent",
        LastName = "Durano"
    };
    
    return person;
}

代わりに Tuple を使うこと:

public (string FirstName, string LastName) GetName()
{
    return ("Vincent", "Durano");
}

#15

変換(conversion, transformation)・検証・フォーマット・解析などの一般的なタスクを実行する Extension Methods を作成してみてください。

つぎのようにはしません:

string dateString = "40/1001/2021";
var isDateValid = DateTime.TryParse(dateString, our var date);

このコードでも問題なく変換をすることができます。しかし、基本的な変換をするだけにしては、少々(コードが)長くなっています。

プロジェクトの様々な箇所で同じ変換のコードが散らかっている様を想像してみてください。これでは、コードが乱雑になって開発にかかる時間が長くなってしまう恐れがあります。

こうしたことを防ぐために、プロジェクト間で再利用可能な共通タスク(変換など)を実行するヘルパー/ユーティリティの関数を作成することを検討するべきです。例えば、さっきのコードはつぎのような拡張機能です:

public static class DateExtensions
{
     public static DateTime ToDateTime(this string value)
         => DateTime.TryParse(value, out var result) ? result : default;
}

これを用意することで、拡張メソッドをどこのコードからでも呼び出すことができます:

var date = "40/1001/2021".ToDateTime();

このコードは、コードを簡潔でわかりやすくして、利便性を高めています。このような一般的なタスクをする NuGet パッケージを作成しました。興味のある方はパッケージを入手できます。

#16

.NET の定義済のデータ型 Int32, String, Boolean の使用を避けること:

String firstName; 
Int32 orderCount; 
Boolean isCompleted; 

代わりに組み込み型 (built-in primitive data types) を使います:

string firstName; 
int orderCount; 
bool isCompleted; 

.NET Framework に準拠、コードをより自然に読みことができます。

#17

識別子の略語 (identifier abbreviations) としてイニシャルを使用しないこと。その主な理由は、同じような名称のクラスが存在すると、混乱や矛盾を引き起こす恐れがあるためです。

private readonly PersonManager _pm;
and
private readonly ProductManager _pm;

つぎのように、明解で簡潔な名前にします:

private readonly PersonManager _personManager;
private readonly ProductManager _productManager;

オブジェクトがなになのか明解に示したことで、よりわかりやすくなります。

#18

名前空間は明解に定義された構造で整理すること。一般的に名前空間は、プロジェクトのフォルダーの階層を反映させるべきです:

namespace ProjectName.App.Web;
namespace ProjectName.Services.Common;
namespace ProjectName.Services.Api.Payment;
namespace ProjectName.Services.Api.Ordering;
namespace ProjectName.Services.Worker.Ordering;

プロジェクトのコードをよく整理して、レイヤー間を簡単に移動できるようにします。

#19

クラスの名前には単数形、名詞、名詞句を使うこと:

public class Person
{
    //some code
}

public class BusinessLocation
{
    //some code
}

public class DocumentCollection
{
    //some code
}

こうすることで、あるオブジェクトが単一のアイテムの値を保持しているのか、コレクションを保持しているのかを簡単に判断することができます。

例えば、List<People> vs List<Person> を想像してみてください。リストやコレクションに複数形の名前を入れると、奇妙なことになります。

わかるとおもうけど、People が複数形です。

#20

名詞や形容詞句をプロパティの名前に使用すること。bool 型のプロパティや変数には "can, is, has" のような接頭辞 (prefix) をつけることができる:

public bool IsActive { get; set; }
public bool CanDelete { get; set; }

//variables
bool hasActiveSessions = false;
bool doesItemExist = true;

接頭辞を追加することで、呼び出す側での価値をより良くすることができます。

#21

クラス・メソッド・プロパティ・定数の変数名には Pascal Casing を使用すること:

public class ClassName 
{ 
    const int MaxPageSize = 100;
    
    public string PropertyName { get; set; } 
    
    public void MethodName() 
    { 
        //do something
    } 
} 

コードを Microsoft .NET Framework と一貫性のあるようにするためです。

Unity は注意が必要だと思います。

#22

メソッドの引数やローカル変数には Camel Casing を使用すること:

public void MethodName(CreatePersonRequestDto requestDto) 
{ 
       var firstName = requestDto.FirstName; 
} 

コードを Microsoft .NET Framework と一貫性のあるようにするためです。

#23

クラス・メソッド・プロパティは、意味のあるわかりやすい (self-explanatory) 名前をつけること:

int daysUntilProgramExpiry;

public List<Person> GetPersonProfileById(long personId)
{
       //do something
}

#24

非同期のメソッドは接尾辞 (suffix) に Async をつけること:

public async Task<List<Person>> GetPersonProfileByIdAsync(long personId)
{
     //do something
}

メソッドを見ただけで、同期 vs 非同期を見分けることができます。

#25

インターフェースの名前は接頭辞に I をつけること:

public interface IPersonManager 
{ 
   //...
} 

インターフェースとクラスを簡単に区別するためです。実際、インターフェースを定義する標準(的な手法)として知られます。

#26

グローバル変数やクラスのメンバー変数には _(アンダースコア)を接頭辞につけること:

private readonly ILogger<ClassName> _logger;
private long _rowsAffected;
private IEnumerable<Persons> _people;

ローカルまたはグローバルな変数/識別子を簡単に区別できるようにします。

#27

(コーディングとして)クラスの一番はじめにすべてのメンバー変数とフィールドを宣言すること:

private static string _externalIdType;
private readonly ILogger<PersonManager> _logger;
private int _age;

一般的に受け入れられている手法です。変数宣言を探す手間を省くことができます。

#28

すべての private メソッドを public メソッドの後に定義すること:

public class SomeClass
{
    private void SomePublicMethodA()
    {

    }

    // rest of your public methods here
    // ...

    private void SomePrivateMethodA()
    {

    }

    private void SomePrivateMethodB()
    {

    }
}

28(おそらく 27 のこと)と同じ理由です。メソッドの宣言を探す手間を省くことができます。

#29

コードを region でまとめないこと:

#region Private Members
    private void SomePrivateMethodA()
    {

    }

    private void SomePrivateMethodB()
    {

    }
#endregion

このやり方は、気がつかないうちにコードを肥大化させる可能性があるコード(の臭い)です。

確かに、私もクラスの中のコードをまとめるために region を何度も使ったことがあります。region の機能は、隠したコード行を知覚的に最大化(再表示)すること以外に、特別な機能や価値を持たないことも気づいています。

複数の開発者でプロジェクトを進めると、他の開発者が自分のコードに region を追加して、時間が経つにつれてコードが(知らず知らずのうちに)どんどんと大きくなっていることがあります。

よい習慣としてクラスはできるだけ小さくすることが推奨されます。

クラスの中にかなりの数のプライベートメソッドがあるときは、それらを別クラスに分割することができます。

#30

一般的によく知られている場合にのみ、短縮した名前 (short-hand names) を使うこと:

private readonly CreateQuestionDefinitionRequestDto _requestDto;

変数/パラメーターがリクエストオブジェクトであることがわかっているときに「createQuestionDefinitionRequestDto」という変数名をつけることは、長すぎるでしょう。

FTP, UI, IO も同じことが言えます。一般的に知られている範囲であれば略語を使ってもよいですが、そうではないなら逆効果(悪影響)になります。

#31

識別子の名前の間に _(アンダースコア)を入れるのを避けること:

public PersonManager person_Manager;
private long rows_Affected;
private DateTime row_updated_date_time;

C# は postgres ではないからです……冗談です。

Microsost .NET Frameworkの規約との一貫性と、コードをより自然に読めるようにするためです。また、下線が見えない「下線ストレス (underline stress)」を避けることができる。

#32

定数や readonly の変数に SCREAMING CAPS を使用しないこと:

public static const string EXTERNALIDTYPE = "ABC"; 
public static const string ENVIRONMENT_VARIABLE_NAME = "TEST"; 

あまりにもたくさん注目を集めてしまうためです。

#33

(インターフェースを除いた)識別子にはハンガリアン記法やその他の識別記法を使用しないこと:

int iCounter; 
string strName;
string spCreateUsers; 
OrderingService svcOrdering;

Visual Studio のコードエディターは、オブジェクトの型を判断するための便利な tooltips がすでに提供されています。一般的に、識別子の(名前の)中に型の指標となるものを含めることは避けたほうが良いです。

#34

enum 型の名前には "Enum" のような接尾辞を使用しないこと。複数形の名称をしないこと:

public enum BeerType
{
    Lager,
    Ale,
    Ipa,
    Porter,
    Pilsner
} 

繰り返しですが、Microsoft .NET Framework との一貫性を保つためで、識別子の中に型の指標となるものを入れることを避けています。

#35

不変のオブジェクトには、record 型を使うこと。record は C# 9.0 で導入された新機能で、コードをシンプルにします。例えば、つぎのようなコード(これは record ではなくクラス)です:

public class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

(このクラスは)record 型を使って、つぎのように書くことができる。

public record Person(string FirstName, string LastName);

record 型を使用することで、定型的なコードが自動的に生成することが出来る。コードを簡潔に保つことができます。record 型は DTO, Commands といった不変のデータを定義する際にとても役立ちます。

この機能についての詳細は、つぎを参照します。

感想

一貫性については、リーダブルコードだと「個人的な好みと一貫性」で、ベタープログラマだと「見かけのよい状態を維持する」でそれぞれ説明があります。

32 は個人的に慣例的なところがあるので難しい気がしました。

C# 8.0 や 9.0 の書き方がガイドラインに載ってくる感じが新しいですね。C# 9.0 は 2020 年 11 月ごろのリリースだったけど、元の記事は2月に書かれているため、すぐに 9.0 のスタイルも取り入れていることがわかります。

そもそも、C# の新機能はオープンソース化してからフィードバックが早くなり、言語として変化の流れも昔より早い。コーダーも進化を早める必要があるのと、リリースされたなら基本的には使ったほうが便利なはず。(じゃないと、フィードバックのやりとりの意味がわからない)

ただし、Visual Studio をさっさと新しいやつに入れ替えてね、ってなる。

2021 年も終わりが近づく今日だと .NET6 も出て、C# 10.0 も新機能としてフォローし始めるころ。

  • record は record struct の値型レコードが追加
  • 文字列補間が改善
  • 名前空間を宣言するようにできる
  • global using が便利

など、コーディングガイドラインはまたちょっと考えたほうがよいところがありそうです。

全体に影響がありそうで、わかりやすいのは:

  • 名前空間の宣言の仕方
  • global using
  • (文字列補間)

個人的に、名前空間の宣言の仕方は好感が持てる。インデントの問題で書き換えたい。(自動生成コードが邪魔だ)

補足

2, 3 は以下のミスかも。

class Somthing
{
    public Other other;
}
class Other
{
    public object whatever;
}

// 上の定義なら以下のように書ける
var something = new Something { other = new Other() };

if (something != null)
{
    if (something.other != null)
    {
        return something.other.whatever;
    }
}

or

return something?.other?.whatever;

11は好みがありそう。

var list = new List<int>();
or
List<int> list = new();

一応、IL 上での違いはないです。

public class C {
    public void M() 
    {
        var list1 = new List<int>();
        List<int> list2 = new();

        Console.WriteLine(list1);
        Console.WriteLine(list2);        
    }
}
.method public hidebysig 
instance void M () cil managed 
{
// Method begins at RVA 0x2050
// Code size 28 (0x1c)
.maxstack 1
.locals init (
    [0] class [System.Private.CoreLib]System.Collections.Generic.List`1<int32> list1,
    [1] class [System.Private.CoreLib]System.Collections.Generic.List`1<int32> list2
)

IL_0000: nop
IL_0001: newobj instance void class [System.Private.CoreLib]System.Collections.Generic.List`1<int32>::.ctor()
IL_0006: stloc.0
IL_0007: newobj instance void class [System.Private.CoreLib]System.Collections.Generic.List`1<int32>::.ctor()
IL_000c: stloc.1
IL_000d: ldloc.0
IL_000e: call void [System.Console]System.Console::WriteLine(object)
IL_0013: nop
IL_0014: ldloc.1
IL_0015: call void [System.Console]System.Console::WriteLine(object)
IL_001a: nop
IL_001b: ret
} // end of method C::M

参考

2021 年 会社勤めの買ってよかったモノ、まとめ(好きなモノ10選)

はてなブログ10周年特別お題「10にまつわる4つのお題」の「好きな◯◯10選」ということで、ちょっと早いけど 2021 年の好きな(買ってよかった)モノ10選をまとめてみました。(毎年やっているやつですね)

今年は結構マニアックというか、個性的なものも多かった気がします。

1位 筋トレグッツ

健康を目的としてジムにゆるく週に1~2回(2~3H)で通っています。

それなりに真面目な話として、体を鍛えること以上に健康に寄与する習慣って、睡眠と食事くらい基本的なものしかないけど、どっちも平常以上になりづらい気がする。より良くなるイメージがない。

運動は明らかに体のラインがよくなるし、様々なリスクを軽減させ、何なら睡眠や食事にもよい影響を与えるわけで、今よりもよくなるリターンが分かりやすい。いまだと、いろんなデータを取ってくれるので、ゲームみたいな感覚で出来るのもいい。

テレワークの増加にあわせて、テレワークの運動量追加のために購入。

風呂前か後に20~30回くらい使ってます。3日に1日はスクワットを追加、寝る前に目もとエステをしながら寝たままストレッチがセット。

プロテインは最初いらないかも。少しして効果的な筋トレが出来る(ジムの人に褒めてもらったとか)ようになってからでもいい気がします。

2位 ロジクール MX ANYWHERE 3

仕事や趣味でキーボードやマウスを使うなら良いものを使え、この宗教を信仰しています。

前はロジクール MX ANYWHERE 2 を利用していましたが、USB-C に対応したので即更新。あわせて、ボタンを静音化するとよいと思います。カチカチ音はヘッドホンをしていても気になることがある。

メルカリで「kailh 静音マイクロスイッチ」で検索してスイッチを追加購入して、はんだごて (soldering iron) を使って改造してみると幸せになれるかも。

f:id:shikaku_sh:20211112175130j:plain:w500
ANYWHERE2 だとこんな感じに改造

f:id:shikaku_sh:20211112175211j:plain:w500
ANYWHERE3 だとこんな中身でした

3位 自作 PC (AMD 仕様)

(2回目以降は特に)Windows11 の更新も Windows10 アカウントで出来るため、OS を買いなおす必要が無い昨今、自作のハードルが低いと思う。

グラフィックボードが高いというものの、楽天だと「0,5の付く日」と「〇〇セール」の組み合わせ、AMAZON だと「ブラックフライデー」などと組み合わせることで、高いってほどじゃなかった。

会社の PC を DELL XPS シリーズ (RTX3060ti) に更新したときの価格より、自作で同レベルのグラボで組んだ価格のほうが安かったりした。(大量の付与ポイントを含めてだけど)なお、Windows11 で組みました。あとなんといっても自作ケースかっこええ。

4位 Anker Soundcore Liberty Air 2 Pro

外に出るときは大体ニット帽を被っているので、落とす心配もないか、ということでスポーツタイプ(左右のイヤーが線で繋がってるやつ)から変更。

音質はスポーツタイプに比べると良くなりました。ただやっぱり通信が悪くなるときもあるんで、安定して高品質に聴くもんじゃないのかもしれません。個人的には、再発性のある突発性難聴持ちだったりするので低品質すぎないイヤホンを選定しています。

ただこれ人混みのある場所だと「バチッ」と耳障りなノイズが乗ることがありました。怖い。Anker のやつは割とそうだと思うけど、ほかのメーカーもそうなんだろうか。

5位 Anker Magnetic Cable Holder マグネット式

カメラを持っている人が増えていると思うけど、Windows Hello は、この組み合わせで指紋認証しています。

デスクトップ PC の指紋認証は、リーダーを USB に直差しだと遠かったり、触りづらかったりするので、ケーブルホルダーにリーダーをまとめておきます。便利なんでおススメ。

マグネットで固定されてるだけなんで、すげー便利。テープでべちょっとならないです。

6位 グレゴリー ショルダーバッグ クイックトゥーゴーポケット HDナイロン

自転車に乗るから、スマートフォンをポケットに入れたい。でも、このズボンはポケットが浅かったり夏は服にポケットが無い……みたいなことがあって、普段はリュックサック(2020 年購入)なんで肩掛けの部分にこれを追加した。

もともと普段からジムに行くための着替え・水筒、参考書、小説などを持っていることが多いのでリュックサックは結構重いです。なんで、重さとか誤差。

スマホの重量が気になるなら、トレーニングで使うダンベルを重くすれば分からなくなるだろ、っていう考えの人。

7位 Anker PowerPort Strip PD 6

電源タップを今年は更新。通常だと3~5年程度が交換目安と言われていますが、実際の使用感とは離れたものだと思います。

自分は直接火災被害を被ったことはありませんが、保険に入る前にやることあるだろ派で、それなりの電源タップを購入するし、埃対策に安全プラグカバーを付けたりもしますし、ケーブルも利用します。(机下だと吊り下げもする)

タコ足配線の回数も制限する。

8位 印鑑

ふるさと納税で印鑑を購入。

徐々に印鑑は無くなるだろう……と言うけど、結局必要なシーンが今日の明日に無くなるわけでもないので、ふるさと納税を利用して入手。

ざっくりまとめた感じ、男性だとこうなる:

種類 サイズ 書体
認印 12 mm 下以外
銀行印 12 mm 印相体 or 篆書体
実印 15 mm 篆書体

認印は雑に使えるやつなんで、通常だとシャチハタくらいでもいい。なんで、銀行印と実印を作っておくと吉。 書体が読みづらいのは仕様で、簡単にコピーされないようにって意味を含むので、機械で作ったものより職人が作った印鑑を推奨……ってことになってる。

銀行印はクレジットカードを含め、大きなお金に関する際に利用する印鑑なんで、一番実用性は高そう。実印は滅多に使うことないけど人生の節目に必要な感じ。

ただ自分で普通に買うと高いので、ふるさと納税を利用して購入すれば実質ポイントが付いて無料に近い価格で高級品が購入できると思います。

9位 PC スピーカー

PC スピーカーは、それなりに良いものを購入しておくことをおススメします。というのも、不要になったときにメルカリ等で出品しても値段がつくからです。当然モノにも依るけど。(いいスピーカーはそれほど販売されるわけじゃないので。新製品の数そのものも少ないです)

個人的な都合だと、人間は音楽を正常な音でずっと聴けるわけじゃないです。いい音で音楽を聴けることに贅沢を感じています。(病気もあって、1年に20日~60日は音を正しく聴くことが出来ないので……)

10位 目もとエス

年齢の増加と関係して目薬を差す回数が増えてきた気がします。若い頃よりも目のクマも分かりやすく出るらしいという話もあるし、冬の乾燥の目対策を含めて購入。

眠るほど気持ちいいってほどじゃないけど慣れると邪魔でもない。目の周りのストレッチもしてくれるので、朝起きたときの目元の状態が明らかにいい。

疲れ目のゴロゴロ感の機会は、減ったかもしれない感じです。

あと、寝る前まで PC やスマホを弄る癖があったのも強制的に改善します。

総括

2021 年はコロナ禍が続くなか、ある意味で家の中の自由な時間がたくさんありました。しかし、思いに反して勉強もブログのアウトプットも例年を下回る結果となりました。メリハリをつけることが難しく、良くも悪くも「停滞」を感じる一年でした。

Unity は途中から更新できず、自分に対して残念な思いをした。Prism の勉強を進めるなど、資格以外のところで学びができたのは実際的なところがあり、よかったと思う。

運動に関しては、夏頃までジムも空き時間を狙うため回数減となり、健康的にすこし不摂生なところがあったと思います。体重は去年の冬に増えた分から増加も減少もなかった。無酸素運動の量の増加から筋肉量は微増した。

ポエム的なところだと、人生観を感じるところがでてきた。これからをどういう風にしたいとか、どうなりたくないとか。取捨選択できない選択も増えてきており悲しく思うこともあれば、裕福な選択ができるようになったこともある。悲喜こもごもなのだろう。

現時点で来年度の学びたいことや目標のようなものは定まっておらず、自分を律することが難しいかもしれない。臆病な性格から行動を始めるのに人より時間がかかると思う。今更臆病を克服できるのかわからないので、すくなくとも早めに調べはじめる癖をつけるようにしたい。結果的に稼働がはやくなると思う。

参考

はてなブログのイベント参加の記事です。

WPF PasswordBox のバインドとその対策 (Windows Hello)

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

PasswordBox コントロールの Password プロパティは仕様のため、バインディングをすることができません。まずもって、バインディングをすることはコンセプト的に非推奨です。

セキュリティ対策の一環のため、バインディングを禁止したのはパスワードの平文をメモリー内に持ちたくありませんでした。PasswordBox は SecurePassword プロパティを追加した経緯があります。SecurePassword は暗号化したパスワードをメモリー内に持つことが目的の機能でした。

なので、SecurePassword とバインディングすればいいのか、となりそうですが、現在は SecurePassword のプロパティ自体、非推奨になっています。

We don't recommend that you use the SecureString class for new development. For more information, see SecureString shouldn't be used on GitHub.

SecurePassword の実体は SecureString で実装されていますが、結局以下の問題となります:

  • .NET はどこかで文字列をプレーンテキストに変換する必要があるため、完全に防ぐことはできなかった
  • .NET Framework 以外では暗号化されません
  • .NET Core では、もうモリー内で暗号化されない

そんなわけで SecurePassword は死んだ仕様となり、簡単にパスワードを使うなら結局 Password プロパティでいいや、ってことになるかどうかは後述のとおりで、今は別の選択ができています。

非推奨でも Password プロパティを Binding する

実際のところ、平文でパスワードを扱わざるをえないなら、もうバインディングしてもいいやってときは、こんな感じでバインディングできると思います。

<PasswordBox local:AttachedProperty.BindablePassword="{Binding Text2}"/>
class AttachedProperty
{
    private static readonly DependencyProperty IsAttachedProperty =
        DependencyProperty.RegisterAttached(
            "IsAttached",
            typeof(bool),
            typeof(AttachedProperty),
            new FrameworkPropertyMetadata(
                false,
                FrameworkPropertyMetadataOptions.None,
                (s, e) =>
                {
                    if (s is PasswordBox passwordBox)
                    {
                        if (passwordBox == null)
                        {
                            return;
                        }

                        if ((bool)e.OldValue)
                        {
                            passwordBox.PasswordChanged -= PasswordBox_PasswordChanged;
                        }

                        if ((bool)e.NewValue)
                        {
                            passwordBox.PasswordChanged += PasswordBox_PasswordChanged;
                        }
                    }
                })
            );

    private static readonly DependencyProperty BindablePasswordProperty =
        DependencyProperty.RegisterAttached(
            "BindablePassword",
            typeof(string),
            typeof(AttachedProperty),
            new FrameworkPropertyMetadata(
                "",
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                (s, e) =>
                {
                    if (s is PasswordBox passwordBox)
                    {
                        var newPassword = (string)e.NewValue;

                        if (GetIsAttached(passwordBox) == false)
                        {
                            SetIsAttached(passwordBox, true);
                        }

                        // 例外
                        if (string.IsNullOrEmpty(passwordBox.Password) && string.IsNullOrEmpty(newPassword) ||
                            passwordBox.Password == newPassword)
                        {
                            return;
                        }

                        passwordBox.PasswordChanged -= PasswordBox_PasswordChanged;
                        passwordBox.Password = newPassword;
                        passwordBox.PasswordChanged += PasswordBox_PasswordChanged;
                    }
                })
            );

    public static bool GetIsAttached(DependencyObject d)
    {
        return (bool)d.GetValue(IsAttachedProperty);
    }

    public static void SetIsAttached(DependencyObject d, bool value)
    {
        d.SetValue(IsAttachedProperty, value);
    }

    public static string GetBindablePassword(DependencyObject d)
    {
        return (string)d.GetValue(BindablePasswordProperty);
    }

    public static void SetBindablePassword(DependencyObject d, string value)
    {
        d.SetValue(BindablePasswordProperty, value);
    }

    private static void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
    {
        var passwordBox = sender as PasswordBox;

        if (passwordBox == null)
        {
            return;
        }

        SetBindablePassword(passwordBox, passwordBox.Password);
    }
}

Windows Hello を利用した認証

パスワードの認証から Web アカウントマネージャー のような GitHubTwitter アカウントを利用して認証を受ける方法もあります。(ストア連携が必要になります)

Web アカウントマネージャーよりも楽に使える認証パターンとして、Windows Hello を利用した PIN や指紋認証によるログインを実装することも検討したいです。ログイン設定を OS 管理にできるため、省力化にも繋がると思います。

.NET5 の場合は、UWP (Win RT) から機能をつかうことになります。Visual Studio のプロジェクトファイル .csproj を開いて以下のように編集します:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Prism.Unity" Version="8.1.97" />
    <PackageReference Include="Prism.Wpf" Version="8.1.97" />
  </ItemGroup>

</Project>

TargetFramework のところを編集しただけです。デフォルトだとプロパティ値が net5.0-windows のようになっていると思います。対応を始める Windows10 のバージョンを明記することで指紋認証に使うクラスが利用できるようになります。

実際に使ってみたサンプルコードはつぎのとおり:

var ucvAvailability = await UserConsentVerifier.CheckAvailabilityAsync();

if (ucvAvailability == UserConsentVerifierAvailability.Available)
{
    var consentResult = await UserConsentVerifier.RequestVerificationAsync("Please provide fingerprint verification.");

    if (consentResult == UserConsentVerificationResult.Verified)
    {
        Debug.WriteLine("OK");
    }
}
  • CheckAvailabilityAsync()
    WindowsHello に対応しているかチェックする
  • RequestVerificationAsync(string)
    認証のウィンドウを表示する

非同期を使うので、Prism なんかのコマンドを利用すると楽:

TestFingerPrintCommand = new DelegateCommand(async () =>
{
    var ucvAvailability = await UserConsentVerifier.CheckAvailabilityAsync();
    ...
});

実際に動かしてみた感じはサンプルを参照。

エラー発生するとき

ちなみに、.NET5 で Microsoft.Windows.SDK.Contracts NuGet パッケージをインストールするとエラーになります。

f:id:shikaku_sh:20211104161336p:plain:w500

.NET 5 以上のターゲットを設定する場合、Windows Metadata コンポーネントを直接参照することはできません。 詳細については、 「https://aka.ms/netsdk1130」をご参照ください。

or

SupportedOSPlatformVersion 10.0.xxxxxx.0 を TargetPlatformVersion 7.0 より大きくすることはできません。

上述のとおり、.csproj を編集してください。

サンプル

f:id:shikaku_sh:20211104161243g:plain:w500

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

参考

Prism EventAggregator はユニットテストに Moq を使うこと

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

この記事は、ARCTOUCH のブログ記事「Using Moq for unit testing with Prism EventAggregator」を個人的に雑訳したものです。

前回記事では、Publisher-Subscriber パターンと EventAggregator を使用するメリット、そして、プロジェクトでの使用方法についてを説明しました。さて、EventAggregator をコード上で使用する方法を学んだので、ユニットテスト単体テスト)を書いてみましょう。ユニットテストは、高品質なソフトウェアをリリースする上で重要になります。(なんで、)EventAggregator をどのように使用したのかをテストする必要があります。しかし、これにはいくつかの課題があります。テストをするべき最も重要なシナリオは、つぎの2つです:

  • Publisher(パブリッシャー)
    イベントが正しいパラメーターで正常に発行されたかどうかを確認します(該当するなら)
  • Subscriber(サブスクライバー、購読者)
    受信したイベントが正しく処理されたかどうかを確認する

これら(パブリッシャーとサブスクライバー)のテストをするために、Prism EventAggregator を使ったコード実装を(本番環境でも、テストコードであっても)利用することができると思います。EventAggregator 自体は依存関係がほとんどないため、おそらくうまくテストできるはずです……が、そのようにするべきではありません。

ユニットテストでは、テストするコードをあらゆる依存関係から分離したいためです。

(本番環境でも、テストコードであっても)EventAggregator を使ってしまうと、テストがユニットテストではなくて、統合テスト (integration test) のようになってしまいます。そこで、モックされた IEventAggregator を使うことにします。

ここでは Moq Library を使用します。

Moq を使ったテストのやり方

まず、EventAggregator の例を設定しましょう。イベント、パブリッシャー、サブスクライバーから始めます。

簡単なパラメーター <string> を持ちます(イベント):

public class MyTextEvent : PubSubEvent<string>
{
}

ユーザーがボタンを押下したときにイベントを発行します(パブリッシャー):

public class MyPublisherViewModel
{
    private readonly IEventAggregator _eventAggregator;

    public MyPublisherViewModel(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;

        OnButtonClickCommand = new DelegateCommand(PublishTextEvent);
    }

    // ...

    private void PublishTextEvent()
    {
        _eventAggregator
        .GetEvent<MyTextEvent>()
        .Publish("text to send");
    }
}

受信したテキストの値をパブリックなプロパティに設定します(サブスクライバー):

public class MySubscriberViewModel
{
    private readonly IEventAggregator _eventAggregator;

    public MySubscriberViewModel(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;

        _eventAggregator
        .GetEvent<MyTextEvent>()
        .Subscribe(HandleMyTextEvent);
    }

    public string Text { get; set; }

    private void HandleMyTextEvent(string text)
    {
        Text = text;
    }
}

パブリッシャーのテスト

一番簡単なテストから始めましょう。ユーザーがボタンをクリックしたときに、イベントが正しいテキストでパブリッシュされるかどうかを検証します。つまりテストは以下のような構造になります:

  • Arrange
    必要なモックをセットアップして PublisherViewModelインスタンス化します。
  • Act
    OnButtonClickedCommand を実行する
  • Assert
    Publish(“text to send”) が一度だけ呼ばれたことを確認する

まず、IEventAggregator のモックを ViewModel のコンストラクターに注入する必要があります:

var eventAggregatorMock = new Mock<IEventAggregator>();
var viewModel = new MyPublisherViewModel(eventAggregatorMock.object);

設定するメソッドは、GetEvent<MyTextEvent>() メソッドだけです。これは MyTextEvent イベントのインスタンスを返却する必要があります。このうえで、なんらかを検証することになるので、イベントもモックになります:

var mockedEvent = new Mock<MyTextEvent>();
eventAggregatorMock
  .Setup(x => x.GetEvent<MyTextEvent>())
  .Returns(mockedEvent.Object);

最後に、Publish() が正しい引数で呼び出されているかを確認しましょう。完成したテストは次のようになります:

// Arrange
var mockedEvent = new Mock<MyTextEvent>();
var eventAggregatorMock = new Mock<IEventAggregator>();
eventAggregatorMock
  .Setup(x => x.GetEvent<MyTextEvent>())
  .Returns(mockedEvent.Object);

var viewModel = new MyPublisherViewModel(eventAggregatorMock.object);

// Act
viewModel.OnButtonClickCommand.Execute()

// Assert
mockedEvent.Verify(x => x.Publish("text to send"), Times.Once);

最初のユニットテストができました。さて、次のテストに進みましょう。

サブスクライバーのテスト

サブスクライバーのテストはすこし難しいです。似たような構成でやってみます:

  • Arrange
    必要なモックをセットアップして、SubscriberViewModelインスタンス化する
  • Act
    イベントを処理するコードを実行する
  • Assert
    Text プロパティに期待する値を持つことを確認する

ここで面倒になるのは、Act の段階です。イベントを実行するためのコードは HandleMyTextEvent(string text) です。このメソッドは private なので、直接呼び出すことができないのです。

テストのためにアクセス修飾子を変更してはいけないため、このメソッドを public にすべきではありません。なので、なにか別のアプローチを考えないといけません。

なんとかして、private メソッドへ参照することで、マニュアルで呼び出すことが可能になります。幸いなことに、Moq テストはこのやり方を可能にする便利なメソッドが用意されています。

それは Callback() メソッドです。

上のテストと同じように IEventAggregatorCallback() と、MyTextEventCallback() のモックを作成し、GetEvent<MyTextEvent>() メソッドを設定します。

つぎに、MyTextEvent.Subscribe(Action eventHandler) を設定します。Returns<T>(T value) を呼び出す代わりに Callback<T>(Action<T> callbackAction) を呼び出します。

コールバックメソッドは、設定されているメソッドが呼び出されるたびに Action を受け取ります。もし、設定されているメソッドがなにか引数を受け取った場合は、それらも Action に渡されます。

今回の例では、Subscribe()Action<Action> という引数を受け取るため、コールバックのアクションは Action になります。言い換えると、コールバックのアクションは SubscriberViewModelコンストラクターPublish() に渡された Action への参照を受け取ることになります。

参照を保存しておけば、あとから起動することができるようになります。すこし複雑に聞こえますが、コードで説明します:

Action<string> eventHandlerAction;
var mockedEvent = new Mock<MyTextEvent>();

mockedEvent
  .Setup(x => x.Subscribe(It.IsAny<Action<string>>()))
  // ここで渡されたアクションを変数に格納して、テストでアクセスできるようにする
  .Callback(action => eventHandlerAction = action);

残念ながら、このコードでは例外が発生します。MoqSubscribe(Action<string>action) を設定できません。オプションのパラメーターをすべて使用して設定をする必要があります。

mockedEvent
  .Setup(x => x.Subscribe(
    It.IsAny<Action<string>>(),      // The event handler
    It.IsAny<ThreadOption>(),        // threadOption
    It.IsAny<bool>(),                // keepSubscriberReferenceAlive
    It.IsAny<Predicate<string>>()))  // filter
    // action の引数以外は興味ないので、捨てています
  .Callback((action, _, __, ___) => eventHandlerAction = action); 

これでイベントへの参照ができたので、Act のフェースを完了させることができます。テストの完成形はこのようになります:

// Arrange
Action<string> eventHandlerAction;

var mockedEvent = new Mock<MyTextEvent>();
mockedEvent
  .Setup(x => x.Subscribe(
    It.IsAny<Action<string>>(),     // The event handler
    It.IsAny<ThreadOption>(),       // threadOption
    It.IsAny<bool>(),               // keepSubscriberReferenceAlive
    It.IsAny<Predicate<string>>())) // filter
  .Callback((action, _, __, ___) => eventHandlerAction = action);

var eventAggregatorMock = new Mock<IEventAggregator>();
eventAggregatorMock
.Setup(x => x.GetEvent<MyTextEvent>())
.Returns(mockedEvent.Object);

var viewModel = new MyPublisherViewModel(eventAggregatorMock.object);

// Act
eventHandlerAction.Invoke("expected");

// Assert
myTextEvent.Equal("expected", viewModel.Text);

ようやくサブスクライバーユニットテストが完成しました。しかし、これはコードの量が多いです。もっとエレガントなやり方があるはずです。

そこで、EventAggregator_Mocker が登場します。

Nuget パッケージ EventAggregator_Mocker

リファクタリングという魔法と拡張メソッドという神秘を利用して、私はこの nuget パッケージを作成しました。

なんで、このパッケージは ARCTOUCH の人が作ったパッケージになります。GitHub の公開もありますが、そこまでメジャーなパッケージではないです。雑訳のレベルも翻訳ツールを利用して加速させました。

このパッケージには Mock<IEventAggregator> の2つの拡張メソッドが含まれています:

  • パラメーターのないイベントのモック用
    Mock<TEvent> RegisterNewMockedEvent<TEvent>(Action<Action>onSubscribeAction = null)
  • パラメーターのあるイベントのモック用
    Mock<TEvent> RegisterNewMockedEvent<TEvent, TParam>(Action<Action<TParam>> onSubscribeAction = null)

これらは、いずれもモックされた IEventAggragator に登録されている(モックされた)イベントを返却します。また、これらはそれぞれコールバックのアクションとして使用されるオプションのパラメーター Action を持っているため、イベントを処理するアクションへの参照を取得することができます。

このパッケージを使用すると、上記のユニットテストはつぎのようになります:

Publisher:

// Arrange
var eventAggregatorMock = new Mock<IEventAggregator>();
// イベントハンドラへ参照する必要はないので、Action を渡しません
Mock<MyTextEvent> mockedEvent = eventAggregatorMock.RegisterNewMockedEvent();
var viewModel = new MyPublisherViewModel(eventAggregatorMock.object);

// Act
viewModel.OnButtonClickCommand.Execute();

// Assert
mockedEvent.Verify(x => x.Publish("text to send"), Times.Once);

Subscriber:

// Arrange
Action<string> eventHandlerAction;
var eventAggregatorMock = new Mock<IEventAggregator>();
// // イベントは必要ないので、戻り値を破棄しています
_ = eventAggregatorMock.RegisterNewMockedEvent<MyTextEvent>(action => eventHandlerAction = action);
var viewModel = new MyPublisherViewModel(eventAggregatorMock.object);

// Act
eventHandlerAction.Invoke("expected");

// Assert
myTextEvent.Equal("expected", viewModel.Text);

コードブロックは、前より短く読みやすいものになりました。パッケージのソースコードは「ここ」にあります。実際に動作させているサンプルは「ここ」。

今回の Prism EventAggregator シリーズがお役に立てれば幸いです。

参考

Prism EventAggregator を使用する方法

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

この記事は、ARCTOUCH のブログ記事「How to use Prism EventAggregator for .NET app development」を個人的に雑訳したものです。

前回記事では、.NET アプリ開発に Prism EventAggregator を使用する理由、Publisher-Subscriber パターン、そして、使用するシナリオについて説明しました。今回は、具体的な使用方法をコードと共に紹介します。

環境構築 (Installation?)

必要ありません。EventAggregator は Prism.Core の一部分なので、すでに Prism を使っているプロジェクトであれば、EventAggregator の準備は OK です。

コンストラクターの注入 (inject)

EventAggregator は、IEventAggregator というインターフェースを実装しています。これを(Prism の IoC に登録された) 任意のクラスで使用するためには、コンストラクターIEventAggregatorインスタンスを要求するだけです。

public MyViewModel(IEventAggregator eventAggregator)
{
    _eventAggregator = eventAggregator;
}

Prism は ViewModel の構築時に、自動的に EventAggregatorインスタンスを受け渡しをします。他に登録や設定をする必要はありません。

イベントの作成

つぎに、送信するなにか、つまり、イベントが必要になります。イベントには2種類あります:

  • パラメーターのあるイベント
  • パラメーターのないイベント

パラメーターのないイベントを作成する場合は、通常だと新しいクラスを作成して PubSubEvent を継承します。

public class MyEvent : PubSubEvent
{
}

パラメーターのあるイベントを作成する場合は、ジェネリック型の PubSubEvent<T> を継承します。ここで、T は渡したいパラメーターの型になります。T は、プリミティブな型や複雑なオブジェクトであったりすることができます。

イベントの購読

EventAggregator とイベントの準備ができたので、今度はイベントをイベントを購読します。まず、EventAggregatorGetEvent<TEvent>() メソッドを呼び出して、イベントのインスタンスを返します。次に、イベントを処理するメソッドや Action 型を渡して、イベントの Subscribe(...) を呼び出します。

public class MySubscriberViewModel
{
    public MySubscriberViewModel(IEventAggregator eventAggregator)
    {  
        eventAggregator
        .GetEvent<MyEvent>()
        .Subscribe(HandleMyEvent);
    }

    private void HandleMyEvent()
    {
        // Do some stuff here
    }
}

パラメーターのあるイベントを処理する場合も同様です:

public class MySubscriberViewModel
{
    public MySubscriberViewModel(IEventAggregator eventAggregator)
    {
        eventAggregator
        .GetEvent<MyParameterEvent>()
        .Subscribe(HandleMyParameterEvent);
    }
    private void HandleMyEvent(MyParameter parameter)
    {
        // Do some stuff with the parameter here
    }
}

イベントの発行

イベントが処理(受け取り)されるようになったので、あとはイベントを発行するだけです。これを行うためには、再び GetEvent<TEvent>() メソッドを呼び出して、発行したいイベントのインスタンスを取得する必要があり、取得したインスタンスから、今回は Publish(...) メソッドを呼び出します。

public class MyPublisherViewModel
{
    private readonly IEventAggregator _eventAggregator;

    public MyPublisherViewModel(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;
    }

    private void PublishEvent()
    {
        _eventAggregator
        .GetEvent<MyEvent>()
        .Publish();
    }
}
public class MyPublisherViewModel
{
    private readonly IEventAggregator _eventAggregator;

    public MyPublisherViewModel(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;
    }

    private void PublishParameterEvent()
    {
        _eventAggregator
        .GetEvent<MyParameterEvent>()
        .Publish(new MyParameter());
    }
}

イベントの購読解除

最後に、イベントの購読を解除したいときは、Unsubscribe() メソッドを使ってイベントの購読を簡単に解除できます。

_eventAggregator
.GetEvent<MyEvent>()
.Unsubscribe(HandleMyEvent);

高度なこと (Advanced stuff)

これまで説明した内容で、95% のケースは十分にカバーできるはずです。しかし、さらに制御をしたい場合も、EventAggregator でカバーできます。Subscribe() メソッドには、オプションのパラメーターがあるので、これを使って EventAggregator の動作を構築することができます:

  • threadOption
    どのスレッドでイベントが処理されるのかを指定する列挙型オプションです。3つの値があります。
    1. PublisherThread (Default)
    2. UIThread
    3. BackgroundThread
  • keepSubscriberReferenceAlive
    デフォルトでは false に設定されているパラメーターです。EventAggregator が他に参照を持たない場合でも、サブスクライバーの参照を保持します。ガベージコレクションされることを防ぎたい場合は、このパラメーターを true に設定します。(この設定によって Xamarin.MessagingCenter と同様の動作をします)
  • filter
    サブスクライバーがイベントを受信すべきかどうかを評価する bool 値を返却するラムダ式を受け取るフィルター。(この機能は、パラメーターを持つイベントにのみ適用できます)
_eventAggregator
  .GetEvent<MyParameterEvent>()
  .Subscribe(
    HandleMyEvent,              // イベント受信時の実行メソッド
    ThreadOption.UIThread,      // UI スレッドで実行する
    true,                       // 強参照を保持する
    value => value == "valid"); // 実行メソッドはパラメーターが `valid` のときだけ実行される

このとおり、EventAggregator は簡単に使える強力なツールです。サンプルを確認したい人は、つぎのリポジトリーを確認してください。

参考

Prism EventAggregator をなぜ使うべきか

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

この記事は、ARCTOUCH のブログ記事「Why you should use Prism EventAggregator in .NET app development」を個人的に雑訳したものです。EventAggregator については「過去の記事」で紹介しています。

Prism は、.NET アプリを開発するための豊富な機能を提供するライブラリーで、C# 開発者のコーディングライフをより快適にする多くの優れた機能を備えています。その機能のひとつが EventAggregator です。この記事では、EventAggregator とはなにか、なぜ便利なのか、そして、どこで使うべきなのかを説明します。

EventAggregator is なに?

要するに、EventAggregator とは Publisher-Subscriber パターンを Prism で実装したものです。Publisher-Subscriber パターンとは、アプリケーションの非同期通信を容易にするため設計されたメッセージングのパターンのことです。具体的には、リンクすることが困難なコンポーネント間でメッセージをやり取りするといった、問題を解決します。

このパターンの核となるものは、event bus に対して発行されるイベントです。event bus は、そのイベントを1つ以上のサブスクライバー (subscribers) に渡します。サブスクライバーは、受け取ったイベントを自由に扱うことができます。

より詳細な情報は「こちら」をご覧ください。

f:id:shikaku_sh:20211027173429p:plain
Publisher-Subscriber パターン

.NET アプリ開発に Prism EventAggregator をなぜ使用するのか?

Publisher-Subscriber モデルには、さまざまなメリットがあります:

  • 分離 (Decoupled)
    パブリッシャーとサブスクライバーはお互いのことを知りません
  • 非同期 (Asynchronous)
    パブリッシャーは、サブスクライバーがイベントの処理を終えるまで待つ必要がないので、メッセージを素早く送信し、自分の処理を続けることができる
  • 関心の分離 (Separation of concerns)
    パブリッシャーはサブスクライバーが何をしているのか知る必要はありません。また、サブスクライバーはイベントがどこで発生したのか知る必要はありません。
  • 拡張性 (Scalable)
    パブリッシャー、サブスクライバー、イベントの数がどれだけ多くても、必要なのは、ひとつのイベントバスへの参照だけ

EventAggregator は、上記の利点に加えて .NET アプリの開発に役に立つ利点を持ちます:

  • モリーリークの防止 (Prevent memory leaks)
    EventAggregator は、廃棄されたサブスクライバの参照を保持しません。マニュアルでサブスクライブを解除する必要もありません
  • 複雑なオブジェクトの送信 (Send complex objects)
    イベントを発行する際に複雑なオブジェクトをパラメーターとして送信して、イベントを処理する際にサブスクライバーにより多くの情報を提供する
  • スレッドの柔軟性 (Thread flexibility)
    受信したイベントを処理するスレッドを選択できる
  • イベントのフィルタリング (Filter events)
    サブスクライバーは、処理するイベントと無視するイベントを選択できる

Prism EventAggregator をいつ利用するのか?

要約すると、Publisher-Subscriber パターンは、リンクすることが難しい、または、現実的ではないコンポーネント間の通信用に設計されたメッセージングのパターンです。

Prism のプロジェクトでは、EventAggregator は、2つ以上の ViewModel の間や、お互いに参照を持たないサービスの間でメッセージを送受信するためによく利用されます。

また、1つのイベントを異なる多くのサブスクライバーで処理する必要があって、それぞれのサブスクライバーにパブリッシャーの参照を渡さない(現実的ではない)ときにも利用されます。

Xamarin.Forms の EventAggregator

Xamarin.Forms に慣れている人は、「待って、Xamarin.Forms には、これに似たものがすでにあるんじゃないか?」と思うかもしれません。確かに、Xamarin.Forms には MessagingCenter という独自の Publisher-Subscriber パターンの実装があります。しかし、以下の理由から私は Prism の EventAggregator が気に入っています:

  • モリーリーク (Memory leaks)
    MessagingCenter は、マニュアルでサブスクライブを解除しないと、参照を保持します。(「Xamarin MessagingCenter memory leaks」で検索すると、パフォーマンスの問題に関する多くのフォーラム投稿を見つけることができます)
  • 柔軟性 (Flexibility)
    MessagingCenter は、EventAggregator のようなスレッドの柔軟性やイベントフィルターはありません
  • テストの容易性 (Testability)
    MessagingCenter は、static メソッドを使用しているため、単体テストの作成が難しいです。Prism では、コンストラクターEventAggregatorインスタンスを注入することができるので、簡単にモックを作成することができます

ちょっとした注意点

ここまでの内容で EventAggregator の柔軟性や有用性について、納得していただけたなら幸いですが、(いつものように、)いくつかの注意点があります。

EventAggregator はサブスクライバーへの弱参照しか持ちませんが、パブリッシャーがパラメーターを渡し、サブスクライバーがその参照を持ち続けている場合は強参照として持つ恐れがあります。このような場合は、サブスクライバーが disposed されたときにマニュアルでサブスクライブを解除するようにしましょう。

また、EventAggregator の Decoupled な(分離された)構造は素晴らしいものですが、使いすぎると複雑さが増してしまいます。

パブリッシャーとサブスクライバーの間には関心の分離がありますが、イベント自体は関心の分離がありません。(イベントは、すべて同じ EventAggregator に保持されます)

多くのイベントを扱う大きなプロジェクトでは、これがコミュニケーションの流れを不明瞭にする恐れがあります。私のアドバイスとしては、EventAggregator は、必要なときにだけ使うようにしましょう。プロジェクト全体を EventAggregator を中心に構築することはやめておきましょう。

なんでもそうですが、節度を守ることが大切です。(Like with everything in life, moderation is key.)

参考

WPF Prism サンプルコードの学習6(公式外サンプル1)

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

前回まで Prism の公式サンプルを確認しました。補足として、Microsoftokazukiさんもサンプルを公開してくれていたので、こちらも見ていこうと思います。

サンプルは 2017 年 1 月公開のものなので、少し古いです。注意しましょう。

1. Bootstrap

これはサンプル1の内容で、完全にカバーできていると思います。

2. ViewModelLocator

モジュール化していませんが、View と ViewModel を紐づけるミニマムなサンプルです。

Dependency 属性が解決できないかもしれませんが、現在と名前空間が異なると思います。

using Microsoft.Practices.Unity;
using ViewModelLocatorSampleApp.Models;

namespace ViewModelLocatorSampleApp.ViewModels
{
    class ShellViewModel
    {
        [Dependency]
        public MessageProvider MessageProvider { get; set; }
    }
}
using Unity;
using ViewModelLocatorSampleApp.Models;

namespace ViewModelLocatorSampleApp.ViewModels
{
    class ShellViewModel
    {
        [Dependency]
        public MessageProvider MessageProvider { get; set; }
    }
}

3. Module

これはサンプル2の内容です。

4. MVVM の基本クラス

7.1 Modules.AppConfig のやり方でモジュールを実装したやり方です。コマンドは、11 DelegateCommand でフォローしています。

新しい要素は、ErrorsContainer です。

<StackPanel>
    <Label Content="入力"
            Target="{Binding ElementName=TextBoxInput}" />
    <TextBox x:Name="TextBoxInput" 
                Text="{Binding Input, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    <TextBlock x:Name="TextBlockErrorMessage" 
                Text="{Binding ElementName=TextBoxInput, Path=(Validation.Errors)/ErrorContent}"/>
</StackPanel>
class ErrorsContainerSampleViewModel : BindableBase, INotifyDataErrorInfo
{
    public string HeaderText { get; } = "ErrorContainerSample";

    private string input;

    [Required(ErrorMessage = "入力してください")]
    public string Input
    {
        get { return this.input; }
        set { this.SetProperty(ref this.input, value); }
    }


    public ErrorsContainerSampleViewModel()
    {
        this.ErrorsContainer = new ErrorsContainer<string>(
            x => this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(x)));
    }

    #region Validation
    private ErrorsContainer<string> ErrorsContainer { get; }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    protected override bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if(!base.SetProperty<T>(ref storage, value, propertyName))
        {
            return false;
        }

        var context = new ValidationContext(this)
        {
            MemberName = propertyName
        };

        var errors = new List<ValidationResult>();
        if (!Validator.TryValidateProperty(value, context, errors))
        {
            this.ErrorsContainer.SetErrors(propertyName, errors.Select(x => x.ErrorMessage));
        }
        else
        {
            this.ErrorsContainer.ClearErrors(propertyName);
        }

        return true;
    }

    public bool HasErrors
    {
        get
        {
            return this.ErrorsContainer.HasErrors;
        }
    }

    public IEnumerable GetErrors(string propertyName)
    {
        return this.ErrorsContainer.GetErrors(propertyName);
    }
    #endregion
}

Prism には、INotifyDataErrorInfo の実装を補助する ErrorsContainer が追加されていて、これを利用すると簡単に入力値の検証をすることができるようになる。

には、INotifyDataErrorInfo は以下のインターフェースを実装する必要がある:

  • bool HasErrors { get; }
  • event EventHandler ErrorsChanged;
  • IEnumerable GetErrors(string propertyName);

DataAnnotations は前からある機能なので詳細を割愛しますが、以下のようにインターフェースの実装を ErrorsContainer に対応をおまかせできる。

<Grid>
<Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox x:Name="TextBoxInput" Grid.Row="0" 
            Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            />
<TextBlock Grid.Row="1"
            Text="{Binding ElementName=TextBoxInput, Path=(Validation.Errors)/ErrorContent}"
            Foreground="Red" Margin="10"/>
</Grid>
[Required(ErrorMessage = "入力してください")]
public string Text
{
    get => _text;
    set
    {
        CheckErrors(value);
        SetProperty(ref _text, value); 
    }
}

public MainWindowViewModel()
{
    ErrorsContainer = new ErrorsContainer<string>(
        p => this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(p))
    );
}

private ErrorsContainer<string> ErrorsContainer { get; }

public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public bool HasErrors => ErrorsContainer.HasErrors;
public IEnumerable GetErrors(string propertyName) => ErrorsContainer.GetErrors(propertyName);

public void CheckErrors(string value, [CallerMemberName] string propertyName = null)
{
    var context = new ValidationContext(this)
    {
        MemberName = propertyName
    };

    var errors = new List<ValidationResult>();

    if (!Validator.TryValidateProperty(value, context, errors))
    {
        ErrorsContainer.SetErrors(propertyName, errors.Select(x => x.ErrorMessage));
    }
    else
    {
        ErrorsContainer.ClearErrors(propertyName);
    }
}

ErrorsContainer にまとめつつ、プロパティ名でさらにまとめてあるので、Text プロパティのエラー検知に加えて、Text2, Text3... と検知したいプロパティを増やしても、INotifyDataErrorInfo インターフェースの実装はもちろん、CheckErrors も修正なしで運用することができる。

CheckErrors は、以下のように SetProperty を override してやってしまうことも。
- protected override bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = null)

5. InteractionRequest

おそらくこれは、公式サンプルから消された 25. Interactivity - NotificationRequest に相当する機能だと思います。

IInteractionRequestAware というインターフェースは存在していませんし、INotification というインターフェースも存在しません。(おそらく)

現在は非推奨ということでよいと思います。

6. Navigation

  1. Navigation と同じ内容だけど、コーディングされている内容がバージョン違いでちょっと異なっています。あと、KeepAlive も含まれていたりするので、まとめてある。

7. EventAggregator

  1. UsingEventAggregator と同じ内容。書き方としては、公式サンプルのほうがキレイになっているんで、見るべき点はないと思います。

8. ModuleLoadSeq

7.4 Modules - LoadManual のような内容。コーディングテクニックというよりも、そういうものなので、理解だけしておけばよいと思います。

9. RegionBehavior

IRegionBehavior は現在も存在しているインターフェースです。WPF の Behavior を Region でやるための機能です。サンプルでは IDispose を ViewModel で実行するようなものになっています。

これは公式サンプルにはないものなので、知っておいてもよいと思います。

10. ModuleCatalog

7.1 Modules - AppConfig のような内容です。コーディングテクニックというよりも、そういうものなので、理解だけしておけばよいと思います。

まとめ

2と9が、参考になる可能性があると思います。

参考