Pleasanterで楽しく学ぼうC# (1)

# 1. はじめに
## この記事について
こんにちは。インプリムのhmineです。

今回は、PleasanterのGitHubに興味がある皆さまと、C#のシステム開発初学者から中級者へのレベルアップをめざす皆さまに向けて記事を執筆しました。

## 記事の全体像
記事の前半では、まず「プログラミング言語の書き方」を習得した先にある、「システムを開発するスキル」を取得する方法論について考えます。続けて、それと弊社（株式会社インプリム）のプロダクトである「Pleasanter」がどう関わるかという観点から、Pleasanterのソースコードの特徴をご説明します。

記事の後半では、最近更新されたソースコードの引用・解説を行います。

- **この記事で解説するC#の記法**
  - LINQの`Enmutable.Where`メソッドによるフィルタ処理（絞り込み処理）

最後まで読んでただいた際、「プリザンターのソースコードリポジトリを見てみたい」「GitHub上でOSSに貢献してみたい」といったご検討をいただけると嬉しいです！

# 2. Pleasanterのデバッグ環境構築の勧め
## プログラミング言語の習得とシステム開発
「本や講座でプログラミング言語を学ぶ」という方法論は、多くのエンジニアの方が実践しているか、少なくともやってみようとしたことがあるやり方だと思います。

そこで避けられない問題は「本や講座のサンプルコードと業務システムのソースコードの間には、規模の面で大きな乖離がある」ということです。業務システムが抱えるファイル数や、その1ファイル当たりの行数は、本や講座のサンプルコードと比較すると非常に巨大であるはずです。

はじめてプログラマーの業務に就いた方には、最初の数年間は「巨大な資源を取り扱うにはどのように仕事に取り組めばよいか」を覚えていくミッションが待っています。

## Pleasanterのパブリックなソースコードを活用しよう！
PleasanterのGitHubリポジトリをご紹介します。以下のリンク先をご覧ください。

https://github.com/Implem/Implem.Pleasanter

GitHubリポジトリのREADMEは英語で書かれていますが、Qiita上には日本語の記事も充実していますのでご安心ください！　もしお時間があれば、ぜひ以下の手順の通りVisual Studio上でPleasanterが動く開発環境をつくっていただきたいです！

https://qiita.com/Implem/items/f0704ebaee8f061fc94a

Pleasanterは社会インフラを支える企業様に導入をいただいている、信頼性の高いプロダクトです。以下の記事で導入事例をいくつかご紹介しています。

https://pleasanter.org/cases

以上のように、**どなたでもアクセスできるリポジトリに、現役の業務システムのソースコードが公開されています**ので、Pleasanterのソースコードは、C#によるシステム開発の規模感をイメージしたいと考えるエンジニアの皆さま（レベルは問わず）の一助になるのではないかと思います。

## Pleasanterのソースコードの"クセ"
Pleasanterには、本や講座のソースコードとは少し違った考え方で書かれてる処理が多く存在します。これは、インプリム創始者の内田がPleasanterを個人開発でローンチした際に[^1]、プログラムの基盤であるフレームワークもゼロからコーディングしたためです。

この件については、今後のQiitaの記事にて、Pleasanterのソースコードがもつ独自のコーディング手法についての解説ができればと思います！
[^1]: 個人開発について、本人によるLTの内容がログミーTechで公開されています。
「無償公開していたからこそ、サブスクで売上が立てられた」個人開発のオープンソースを元に法人化＆マネタイズ - https://logmi.jp/tech/articles/330082

# 3. Pleasanterのソース解説
それでは実際に最近のPleasanterのコミット履歴を元に、簡単にソースリーディングをしてみましょう。

## LINQによる集合のフィルタ処理（絞り込み処理）
以下のページは、Microsoftが公開している`Enmutable.Where`メソッドのリファレンスです。

https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.where?view=net-8.0

C#の正式な仕様は、このページのようなMicrosoftのドキュメントで確認します。
ただし、「このメソッドを利用すると結局何が嬉しいの？」という疑問については、Pleasanterのような業務システムのソースコードを見るほうが理解しやすい場合も多いです。

`Enmutable.Where`メソッドがPleasanterでどのように使われているか見てみましょう。

## Pleasanterのコード上の`Enmutable.Where`メソッド
Pleasanter_1.4.1.0（2024年2月13日リリース）のコミット差分を見てみましょう。

https://github.com/Implem/Implem.Pleasanter/commit/30b2eb9edea8078e82f2a48b0a8db4643e2729a8

上のリンクをクリックし、以下の手順でコードを確認してください。

1. Ctrl+Fキーを同時に押してブラウザの検索欄を開く
1. ブラウザの検索欄に`Implem.Pleasanter/Libraries/Settings/SiteSettings.cs`と入力してSiteSettings.csファイルの先頭まで移動する
1. 2870行目を確認する

``` Implem.Pleasanter/Libraries/Settings/SiteSettings.cs
2870  +        .Where(o => FilterColumn(
2871  +            context: context,
2872  +            ss: this,
2873  +            def: o,
2874  +            selection: selection,
2875  +            keyWord: keyWord))
```

`.Where`を利用しているコードが見つかりました！

`.Where`の引数に記述されているラムダ演算子（`=>`）の左辺は、フィルタされる集合から取り出した単一のデータです。右辺で同じクラスの`FilterColumn`というメソッドを呼び出していますが、これは`o`を参照し True/False のいずれかのboolの返却値を戻す条件判定処理です。`SiteSettings.cs`に`FilterColumn`メソッドの内容が書いてありますので探してみてください。

一旦Pleasanterのコードは置いておき、仮に`o`が数値であるシンプルな処理を考えます。`.Where(o => o > 0)`というコードを実行することで、数値の集合が絞り込まれて、0を超える数値の集合が生成されます。`o > 0`もTrue/False のいずれかのboolの戻り値を戻す条件判定式です。

以上より、`SiteSettings.cs`の`FilterColumn`メソッドは、`o > 0`というシンプルな判定式と同じようにラムダ演算子（`=>`）の右辺に記述できることを理解していただければと思います。

## 「まだ`Enmutable.Where`メソッドが分からないかも…」
という皆さまも、とりあえず記事の先を読み進めていただいてOKです！

この先はVisual Studioを実際に動かす作業となります。最後まで動かしてみた後で再度上記のMicrosoftのリファレンスの記事から読み直してみてください！

## Visual Studioのデバッグ機能を使ってみよう！
### "「エディタ」の選択肢一覧をフィルタする機能" について
先ほどご覧いただいたコードは、 **"「エディタ」の選択肢一覧をフィルタする機能"** で追加された処理です。この機能の大まかな説明としては、下図の[選択肢一覧]の中にあるリスト（初期状態で150項目以上）を、[検索ダイアログ]の操作を元にフィルタする（＝絞り込む）機能です。

[選択肢一覧]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/bad20b9a-a735-c9f8-d891-f35e76193128.png)

[検索ダイアログ]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/d871b385-c198-9bf9-5719-b9cc568e0b92.png)

### 「エディタ」タブを開くまで
Visual Studio上でPleasanterの環境構築をした状態であれば、以下の手順を進めることで "「エディタ」の選択肢一覧をフィルタする機能" を操作できます。

まずは、テーブルを新規作成して「エディタ」タブを開いてみましょう。

1. Pleasanterを起動、ログインする。手順は以下の記事（主に後半部）を参照
    - [Qiita - プリザンターをVisual Studioでデバッグする](https://qiita.com/Implem/items/f0704ebaee8f061fc94a)
1. テーブルを1つ新規作成する。手順は以下を参照
    -  [ユーザマニュアル - 操作ガイド（基本編） - テーブル作成](https://www.pleasanter.org/manual/basic-operations-table) 
1. 新規作成したテーブルについて、項目追加を行う画面である「エディタ」タブを開く。手順は以下を参照
    -  [ユーザマニュアル - 操作ガイド（基本編） - 編集画面の項目追加](https://www.pleasanter.org/manual/basic-operations-table)

### 検索ダイアログを使ってみよう
手順は以下のマニュアルの **"「選択肢一覧」のフィルタ機能"** の項に書かれています。

お手元のブラウザで検索ダイアログを操作し、選択肢一覧の内容が変化することを確認してください。検索ダイアログは各種フィルタ条件に対応しているので、クリックするボタンを変えたり、テキストボックスに入力する文字列を変えたりして、色々な入力パターンを試してみましょう。

https://www.pleasanter.org/manual/table-management-editor-columns

### 🐛フィルタ処理（絞り込み処理）のデバッグ
ここまでで検索ダイアログの動きをひととおり確認できましたか？

それでは、Visual Studioのデバッグ機能で`Enmutable.Where`メソッドを使用したフィルタ処理の動きを見てみましょう！　以下の順序で画面とVisual Studioを操作してください。

1. ブラウザ上で検索ダイアログを開く
1. Visual Studioで`Implem.Pleasanter/Libraries/Settings/SiteSettings.cs`を開く
1. Visual Studioで`private bool FilterColumn`の文字列を指定し、上記の.csファイルの検索を行って`FilterColumn`メソッドの内容を参照する
1. Visual Studioで`FilterColumn`メソッドの開始行にブレークポイントを付ける
    - 下記 **[`FilterColumn`メソッド記述箇所のイメージ]** の07行目に付ける
    - 【備考】ブレークポイントを付けるタイミングが早すぎると、検索ダイアログが開いていない状態で何度も`FilterColumn`メソッドが呼ばれ、先に進めなくなってしまう場合があります。そのときは、一度ブレークポイントを無効化または削除 → 「続行」ボタンをクリックし、画面を操作できるようになったことを確認してから、正しいタイミングでブレークポイントを再度付けてください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/536928b9-5e19-3d96-610c-138240d939fb.png)
1. ブラウザ上で検索ダイアログの **「分類」ボタン** をクリックする
1. ブラウザ上で画面の動作が止まったことと、Visual Studioのブレークポイントで処理が停止したことを確認する
1. メソッド内で第3引数の **def** を確認できるように「ローカル」タブを開く
   - 【備考】「ローカル」タブを使用した変数の確認方法のような、「Visual Studioの使い方」については紙面の都合があり、ここでは最低限の解説のみ記載しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/6cf37af8-ff10-7251-511b-68f00272e149.png)
1. F10キーを押して／「ステップオーバー」ボタンをクリックして処理を1行単位で進める
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/0a17e19b-7d57-948e-c97b-71361625abce.png)
1. 処理が`FilterColumn`メソッドの終端に到達した際、F10キー／「ステップオーバー」ボタンクリックを継続し、以下の事項を確認する
    - [ ] **次に実行するコード（行）はどこか？**
    - [ ] **「ローカル」タブに表示される`def`の内容をウォッチしてわかることは何か？**
1. ブラウザで画面を操作可能な状態に戻したい場合は、ブレークポイントを無効化または削除 → 「続行」ボタンをクリックする

#### [`FilterColumn`メソッド記述箇所のイメージ]
※一部のコードを省略した上で、行の採番はこの記事独自で行っています。
※ソースコードは2024年5月1日時点の内容です。
``` Implem.Pleasanter/Libraries/Settings/SiteSettings.cs
01        private bool FilterColumn(
02            Context context,
03            SiteSettings ss,
04            ColumnDefinition def,
05            string selection,
06            string keyWord)
07        {                       // ←←←★★★ ブレークポイント付ける行です！ ★★★
08            switch (selection)
09            {
10                case "KeyWord":
11                    var keyWords = keyWord.Replace("　", " ").Split(" ");
12                    return keyWords.All(o => def.ColumnName.Contains(
13                    // 以下略
14                case "Basic":
15                    return new List<string>
16                    // 以下略
17                case "Class":
18                case "Num":
19                case "Date":
20                case "Description":
21                case "Check":
22                case "Attachments":
23                    return def.ColumnName.StartsWith(selection);
24                default:
25                    return true;
26            }
27        }
```

### 💡デバッグを実行してわかること1　～"繰り返して絞り込む"
先ほどの問いについて、答えは準備できましたか？　この後解説がはじまりますので、ぜひあらかじめご自身の目で処理の動きを追ってみてください。

> 9．処理が`FilterColumn`メソッドの終端に到達した際、F10キー／「ステップオーバー」ボタンクリックを継続し、以下の事項を確認する
>    - [ ] 次に実行するコード（行）はどこか？
>    - [ ] 「ローカル」タブに表示される`def`の内容をウォッチしてわかることは何か？

#### Q. `FilterColumn`メソッドの終端に到達した際、次に実行するコード（行）はどこか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/1fd63882-88d4-0451-6051-360b4e7b356b.png)

#### A. `.Where`の右辺にある判定処理を全項目分、繰り返し実行する。
1. 呼び出し元に戻りました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/2bed2cca-ca9d-0c7e-e1d6-d1035c456a2f.png)
1. 先に`.Where`でフィルタする別の条件があるため、その行が実行されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/adfa6bbb-2b7f-860c-dd2c-f698a1ed5342.png)
---- 脇道の解説 ----
※読み飛ばして 3. に進んで構いません。
    - 上図の黄色くなった行に記載されている`.Contains`の判定は、「既に有効化され、画面の「現在の設定」側にある項目を除外する判定処理」です。
    - その1行上のコードを読んでみます。これは、フィルタされる対象全体を取得する、`ColumnDefinitionHash.EditorDefinitions(context: context)`。実は、画面上の「現在の設定」と「選択肢一覧」を統合したリスト＝該当のテーブルで有効化しうる全項目を取得する処理です。
    - 再び上図の黄色くなった行を見てください。これは1つ目のフィルタ処理であるため、先に実行されました。
    - この記事で取り上げている`FilterColumn`メソッドのフィルタ処理は、上図の黄色くなった行の`.Where`の処理で除外されなかったオブジェクトに対する2つ目のフィルタ処理となります。
    - つまりどういうこと？
→LINQで`.Where`をメソッドチェーンで記述した際の正確な実行順がわかりました。
・×：1つ目のフィルタをすべて繰り返す → 2つ目のフィルタをすべて繰り返す
・○：右記処理を繰り返す〈1つ目のフィルタ処理 → 2つ目のフィルタ処理〉
1. 上記の1つ目のフィルタ処理で除外されなかった項目を処理している場合は、再度`FilterColumn`メソッドの呼び出し元に到達します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/2bed2cca-ca9d-0c7e-e1d6-d1035c456a2f.png)
1. `FilterColumn`メソッドにブレークポイントが付いているため、再び`FilterColumn`メソッドの中に入ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/7befbf3e-1ff5-7b55-c01d-98f911d21a95.png)

---
#### Q. 「ローカル」タブに表示される`def`の内容をウォッチしてわかることは何か？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/6cf37af8-ff10-7251-511b-68f00272e149.png)

#### A. `FilterColumn`メソッドの繰り返し実行で、ColumnName等が異なるオブジェクトを順番に参照している。
デバッグすると、`FilterColumn`メソッドの繰り返し実行により、`ColumnName`に「"Locked"→"ClassA"→"ClassB"→"ClassC"…」が設定されているオブジェクトを順番に参照していることがわかります。なお、"ClassZ"の次は"NumA"のオブジェクトとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/893bf083-7aab-928b-5ebc-d14b6e6fd5e7.png)

`ColumnName`について、今回はマニュアルの引用で解説します。
（引用元：[テーブルの管理：エディタ：エディタの項目の設定](https://www.pleasanter.org/manual/table-management-editor-columns)）

> **キーワード検索のルール**
> 1. 「項目名」「表示名」「カラム名」のいずれかがキーワードと一致する項目を検索します。
>     - 「項目名」：「分類A」等のデフォルト名称。
>     - 「表示名」：「テーブルの管理：エディタ：項目の詳細設定：表示名」で設定した名称。
>     - 「カラム名」：「ClassA」などデータベース上のカラム名。

上記の引用にある「カラム名」は、デバッグで確認した`ColumnName`と同じものです。今回見ている`FilterColumn`メソッドの引数`ColumnDefinition def`には、メソッドが繰り返し呼び出される度に、「選択肢一覧」の中身が一つずつ渡っています。`FilterColumn`メソッドの実行中に引数`ColumnDefinition def`＞`ColumnName`を参照すると、「選択肢一覧」にある各選択肢の「カラム名」を調べることができます。

##### 〈番外編〉`ColumnName`について
---- かなり脇道なので、読み飛ばしても構いません！----

Pleasanterに作成したテーブルのレコードを保存する仕組みについて簡単に説明します。

お手元のRDBMS管理ツールを使用して、RDBMS上にある「Issues」および「Results」というテーブルに「Locked／ClassA／ClassB／ClassC／…」といったカラムがあることを確認してください。このようにPleasanterのレコードは、RDBMS上において、「ロック」項目の入力→「Locked」カラムに登録／「分類A」項目の入力→「ClassA」カラムに登録／…といった方式により、「登録先のカラムは項目ごとに固定」というルールで保存されています。

この「カラム名」の考え方は、レコードを扱う処理（＝システム利用者向け機能）だけでなく、今回のようなテーブルの設定内容を扱う処理（＝システム管理者向け機能）にも使われています。「選択肢一覧」は画面上に日本語名が表示されていますが、プログラム上ではカラム名で取り扱えるように処理が組まれているのです。

#### まとめ - 📝`Enmutable.Where`の繰り返し処理について 

[「選択肢一覧」に対する`Enmutable.Where`（フィルタ処理）のイメージ]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/3291060d-41d6-c0ed-bed2-564f17862f2e.png)

[`Enmutable.Where`（フィルタ処理）の結果のイメージ]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658415/48c9761e-b267-b0fd-f4f7-b494fdedfd73.png)

`Enmutable.Where`を使うとシンプルなコードを書けますね！　繰り返し構文として有名なfor文と比較すると記述する行数が少なく、読みやすいですね！

### 💡デバッグを実行してわかること2　～この選択肢はTrue or False？
`Enmutable.Where`によるフィルタ処理では、集合を構成している要素（オブジェクト）ひとつひとつを参照して、所定の判定処理がTrueである要素（オブジェクト）のみの集合を生成していたことが、伝わりましたでしょうか？

最後に、`FilterColumn`メソッドで行われている条件判定の内容も少しだけ見てみましょう！

ソースコードを再掲します。

``` Implem.Pleasanter/Libraries/Settings/SiteSettings.cs
01        private bool FilterColumn(
02            Context context,
03            SiteSettings ss,
04            ColumnDefinition def,
05            string selection,
06            string keyWord)
07        {
08            switch (selection)    // ←←←★★★ ここから読みます！ ★★★
09            {
10                case "KeyWord":
11                    var keyWords = keyWord.Replace("　", " ").Split(" ");
12                    return keyWords.All(o => def.ColumnName.Contains(
13                    // 以下略
14                case "Basic":
15                    return new List<string>
16                    // 以下略
17                case "Class":
18                case "Num":
19                case "Date":
20                case "Description":
21                case "Check":
22                case "Attachments":
23                    return def.ColumnName.StartsWith(selection);
24                default:
25                    return true;
26            }
27        }
```

08行目で判定している`selection`は、検索ダイアログでクリックしたボタンを判定する条件分岐です。「分類」ボタンをクリックすると`selection`には"Class"が設定されます。（※この記事の趣旨とまったく異なる技術要素であるため、解説はありません。ご了承ください）

したがって、次に実行されるのは23行目です。

23行目に見え覚えのあるコードが書かれています。繰り返し処理の対象となる各選択肢がもつ`def.ColumnName`の値が「"Locked"→"ClassA"→"ClassB"→"ClassC"…」等であることは、先ほど確認した通りです！　これらの文字列に対して`.StartsWith(selection)`を実行。つまり「選択肢のカラム名が"Class"という文字列ではじまっているか？」が、「分類」ボタンをクリックした場合に処理している判定条件なのです。

#### 「分類」ボタンクリック時、`def.ColumnName`に対する`FilterColumn`メソッドの結果
- "Locked" → 返却値：False
- "ClassA" → 返却値：True
- "ClassB" → 返却値：True
- "ClassC" → 返却値：True　…
- "NumA" → 返却値：False　…

#### 📝さらに調べてみたい皆さまへ
検索ダイアログの「分類」ボタン以外（かつ「キャンセル」「×」ボタン以外）をクリックした際も、`FilterColumn`メソッドによる条件判定が実行されています。どのような判定が実行されているか、考えてみてください。調べていくと`Enumerable.All`というLINQのメソッドについて理解が進むかと思います！

## 今回の解説はここまでです！
"「エディタ」の選択肢一覧をフィルタする機能" 追加の対応では、他にも様々な処理をC#やJavaScriptで記述し、機能をリリースすることができました。この記事では、その中でも最も重要なフィルタ処理について解説しました。

**「選択肢一覧」に対するフィルタ処理の本体は、LINQの`Enmutable.Where`メソッドである** ということが伝われば嬉しいです！

# 4. おわりに
この記事の手順でつまったところがあれば、お気軽にコメントをください！

また、「Pleasanterを動かしてみたよ！」というご報告の記事を公開いただけると、わたしたち開発チームにとっての励みになりますので、ぜひ公開をお願いします！
さらに、Pleasanterの活用方法や、ソースコード本体への機能追加についても、皆さまのアイデアをお待ちしています！