実務で使うフォルダ生成アプリの作成

# はじめに
最近アウトプットの大事さに気づき、どんな些細なことでもいいからどんどんアウトプットしていこうと思ったので、
第一歩としてこの記事を書きます。<br>
また、たたき台として作成しているので難易度や文章についてはご容赦いただきたいです…

# 今回作成するアプリについて（要件定義）
- 実務で案件にアサインされたときに、案件フォルダを作って管理しているが、そのフォーマットがバラバラなことに気持ち悪さを感じたので、
フォーマットを統一させたい。
- 手動でディレクトリを作成するのが手間
- フォームアプリなので言語はC#

# ユースケース
1. ユーザーは案件を管理するディレクトリを選択する
2. ユーザーはフォルダのフォーマットを決める（例：01_開発)
3. システムは入力されたフォーマットをもとにフォルダを生成する
4. システムは入力されたフォーマットを記憶する

# 画面レイアウトを決める
デザイナーでポチポチパーツを置いていきます。
フォルダ名を決めるところは+ボタンを実装したいのでデザイナから定義はしません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641298/a22f8edb-49dc-afd1-c406-7091b7f5e8f6.png)

# コーディング
## アプリの設定ファイル
```xml:App.config
<configuration>
    <appSettings>
		<!-- 本体設定 -->
        <!-- パネル初期表示 -->
		<add key="Display"  value="5" />
        <!-- パネル最大表示 -->
        <add key="MaxDisplay"  value="10" />
		
		<!-- ディレクトリの設定値 -->
        <add key="Base"  value="" />
		
        <add key="Number1"  value="1" />
        <add key="Number2"  value="2" />
        <add key="Number3"  value="3" />
        <add key="Number4"  value="4" />
        <add key="Number5"  value="5" />
        <add key="Number6"  value="6" />
        <add key="Number7"  value="7" />
        <add key="Number8"  value="8" />
        <add key="Number9"  value="9" />
        <add key="Number10" value="10" />
        <add key="Value1"  value="" />
        <add key="Value2"  value="" />
        <add key="Value3"  value="" />
        <add key="Value4"  value="" />
        <add key="Value5"  value="" />
        <add key="Value6"  value="" />
        <add key="Value7"  value="" />
        <add key="Value8"  value="" />
        <add key="Value9"  value="" />
        <add key="Value10"  value="" />
    </appSettings>
</configuration>
```

## Folderクラス作成
```C#:Folder.cs
public class Folder
{
    public Folder(int id, int num, string name)
    {
        this.Id = id;
        this.Num = num;
        this.Name = name;
    }
    public int Id { get; set; }
    public int Num { get; set; }
    public string Name { get; set; }

    public string GetFolderName()
    {
        return $"{this.Num.ToString("00")}_{this.Name}";
    }

    public void Create(string baseDirectory)
    {
        if (string.IsNullOrEmpty(this.Name))
        {
            return;
        }
        string path = Path.Combine(baseDirectory, this.GetFolderName());
        DirectoryInfo directoryInfo = new DirectoryInfo(path);
        if (!directoryInfo.Exists)
        {
            directoryInfo.Create();
        }

    }
}
```
GetFolderNameは"01_開発"などのフォーマットを返す関数、
Createはディレクトリを生成する関数です。

## メイン画面
```C#:MainForm.cs
public partial class MainForm : Form
{
    List<Folder> _folderList = new List<Folder>();

    public MainForm()
    {
        InitializeComponent();

        baseDirectory.Text = LoadSetting("Base");

        int length = int.Parse(LoadSetting("Display"));

        for (int i=0; i<length;i++)
        {
            CreatePanel(true);
        }
    }

    private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        SaveSetting();
    }

    private void BaseDirectrySelectBtn_Click(object sender, EventArgs e)
    {
        FolderBrowserDialog fbd = new FolderBrowserDialog();
        // デフォルトルートフォルダ
        fbd.RootFolder = Environment.SpecialFolder.Desktop;

        DialogResult result =  fbd.ShowDialog(this);
        if (result == DialogResult.OK)
        {
            string path = fbd.SelectedPath;

            this.baseDirectory.Text = path;
        }
    }

    private void CreateButton_Click(object sender, EventArgs e)
    {
        if (string.IsNullOrEmpty(this.baseDirectory.Text))
        {
            MessageBox.Show("ディレクトリを指定してください",
                            "エラー",
                            MessageBoxButtons.OK,
                            MessageBoxIcon.Error);
            return;
        }

        _folderList.Clear();

        int i = 1;
        Control.ControlCollection controls = this.folderGroup.Controls;
        foreach (Control control in controls)
        {
            if (control.GetType() != typeof(Panel))
            {
                continue;
            }
            
            Control.ControlCollection panel = control.Controls;
            NumericUpDown numeric = (NumericUpDown)panel[0];
            TextBox name = (TextBox)panel[1];

            Folder folder = new Folder(i, (int)numeric.Value, name.Text);
            folder.Create(baseDirectory.Text);
            _folderList.Add(folder);
            i++;
        }
    }

    private void AddButton_Click(object sender, EventArgs e)
    {
        int controlCount = this.folderGroup.Controls.Count;
        if (controlCount >= int.Parse(LoadSetting("MaxDisplay")))
        {
            return;
        }
        CreatePanel();
    }

    private void RemoveButton_Click(object sender, EventArgs e)
    {
        int controlCount = this.folderGroup.Controls.Count;
        if (controlCount <= 1)
        {
            return;
        }
        RemovePanel();
    }

    private string LoadSetting(string key)
    {
        Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
        return config.AppSettings.Settings[key]?.Value ?? null;
    }

    private void SaveSetting()
    {
        // 作成していなければ更新しない
        if (_folderList.Count == 0)
        {
            return;
        }

        Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
        // ベースディレクトリの保存
        config.AppSettings.Settings["Base"].Value = baseDirectory.Text;

        int length = int.Parse(LoadSetting("MaxDisplay"));

        for (int i = 1; i <= length; i++)
        {
            config.AppSettings.Settings[$"Number{i}"].Value = i.ToString();
            config.AppSettings.Settings[$"Value{i}"].Value = "";
        }

        foreach (Folder folder in _folderList)
        {
            config.AppSettings.Settings[$"Number{folder.Id}"].Value = folder.Num.ToString();
            config.AppSettings.Settings[$"Value{folder.Id}"].Value = folder.Name;
        }
        config.AppSettings.Settings["Display"].Value = _folderList.Count.ToString();
        config.Save();
    }

    private void CreatePanel(bool isInit = false)
    {
        // コントロールを取得する
        Control.ControlCollection collection = this.folderGroup.Controls;


        int y = collection.Count != 0 ? collection[collection.Count - 1].Location.Y + 35 : 10;
        int id = collection.Count != 0 ? collection.Count + 1 : 1;
        int num = isInit ? int.Parse(LoadSetting($"Number{id}")) : id;
        string value = isInit ? LoadSetting($"Value{id}") : "";

        Panel panel = new Panel();

        NumericUpDown directoryNumber = new NumericUpDown();
        directoryNumber.Name = $"directoryNumber{id}";
        directoryNumber.Value = num;
        directoryNumber.Size = new Size(35, 20);
        directoryNumber.Location = new Point(10, 5);
        directoryNumber.Minimum = 1;
        directoryNumber.Maximum = 99;

        TextBox directory = new TextBox();
        directory.Name = $"directory{id}";
        directory.Text = value;
        directory.Size = new Size(270, 25);
        directory.Location = new Point(50, 5);
        directory.TextChanged += CheckFileName;

        panel.Controls.Add(directoryNumber);
        panel.Controls.Add(directory);
        panel.Location = new Point(5, y);
        panel.Size = new Size(380, 30);

        this.folderGroup.Controls.Add(panel);
    }

    private void RemovePanel()
    {
        this.folderGroup.Controls[this.folderGroup.Controls.Count - 1].Dispose();
    }

    private void CheckFileName(object sender, EventArgs e)
    {
        TextBox textbox = (TextBox)sender;
        string name = textbox.Text;

        ToolTip toolTip = new ToolTip();
        toolTip.ShowAlways = true;

        char[] invalidChars = Path.GetInvalidFileNameChars();
        if( name.IndexOfAny(invalidChars) > -1
        || Regex.IsMatch(name, @"(^|\\|/)(CON|PRN|AUX|NUL|CLOCK\$|COM[0-9]|LPT[0-9])(\.|\\|/|$)", RegexOptions.IgnoreCase))
        {
            textbox.BackColor = Color.PaleVioletRed;
            toolTip.SetToolTip(textbox, "使用出来ない文字が含まれています。");
            this.createButton.Enabled = false;
            return;
        }

        textbox.BackColor = Color.White;
        toolTip.SetToolTip(textbox, null);
        this.createButton.Enabled = true;
        return;
    }
}
```

順番に解説します。

## コンストラクタ
App.configの値を読み、各コントロールの初期値を入力しています。
```C#
int length = int.Parse(LoadSetting("Display"));

for (int i=0; i<length;i++)
{
    CreatePanel(true);
}
```
App.configのDisplayの値を読み、初期表示するパネルの数を決めます。

## MainForm_FormClosing
アプリが終了するときに呼び出されるイベントハンドラです。
SaveSettingメソッドを呼び出しています。

```C#
if (_folderList.Count == 0)
{
    return;
}
```
何も操作(フォルダを作成)せずにアプリを落とした時に設定値が更新されるのを防ぎます。

```C#
for (int i = 1; i <= length; i++)
{
    config.AppSettings.Settings[$"Number{i}"].Value = i.ToString();
    config.AppSettings.Settings[$"Value{i}"].Value = "";
}
```
設定を保存する際に以前よりパネルの数が少ないと、のちのループ処理にて設定値が更新されないため、一旦すべて初期化しています。

## CreateButton_Click
```C#
_folderList.Clear();
```
連続してフォルダを作成した時のためにここでいったんリストを初期化しています。
```C#
int i = 1;
Control.ControlCollection controls = this.folderGroup.Controls;
foreach (Control control in controls)
{
    if (control.GetType() != typeof(Panel))
    {
        continue;
    }
    
    Control.ControlCollection panel = control.Controls;
    NumericUpDown numeric = (NumericUpDown)panel[0];
    TextBox name = (TextBox)panel[1];

    Folder folder = new Folder(i, (int)numeric.Value, name.Text);
    folder.Create(baseDirectory.Text);
    _folderList.Add(folder);
    i++;
}
```
ディレクトリ名や番号を入力する各コントロールははPanelにまとめており、そのパネル群はGroupBox配下に生成するようにしています。<br>
そのため、GroupBoxの子コントロールを取得し、パネルのみを抽出し、添え字にて抽出しています。<br>
ここに関しては[find関数](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.control.controlcollection.find)などを使用したほうがおそらくより良いと思います。

その後、Folderクラスをインスタンス化し、Createを実行…となっています。

## CreatePanel
ディレクトリ名、番号を内包したPanelクラスを生成する関数です。
```C#
int y = collection.Count != 0 ? collection[collection.Count - 1].Location.Y + 35 : 10;
int id = collection.Count != 0 ? collection.Count + 1 : 1;
int num = isInit ? int.Parse(LoadSetting($"Number{id}")) : id;
string value = isInit ? LoadSetting($"Value{id}") : "";
```
初期処理のときにも同じ関数を使っているので、引数に初期処理かどうかを引数に渡しています。<br>
isInitがtrueだった場合はSettingから読み出し、そうでないときは初期値にてパネルを生成する。

```C#
directory.TextChanged += CheckFileName;
```
## CheckFileName
ディレクトリのテキストボックスにイベントハンドラを追加しています。
```C#
char[] invalidChars = Path.GetInvalidFileNameChars();
if( name.IndexOfAny(invalidChars) > -1
|| Regex.IsMatch(name, @"(^|\\|/)(CON|PRN|AUX|NUL|CLOCK\$|COM[0-9]|LPT[0-9])(\.|\\|/|$)", RegexOptions.IgnoreCase))
{
    textbox.BackColor = Color.PaleVioletRed;
    toolTip.SetToolTip(textbox, "使用出来ない文字が含まれています。");
    this.createButton.Enabled = false;
    return;
}

textbox.BackColor = Color.White;
toolTip.SetToolTip(textbox, null);
this.createButton.Enabled = true;
```
フォルダ名に使用できない文字が含まれていた場合に、背景色とツールチップで通知します。

# 終わりに
いざ自分で一からアプリを作ってみると、「あれ、これどうやって書いてたっけな。。。」ということがあり、たったこれだけのアプリなのによく迷走して、自分の技術力の低さを痛感しました。。。<br>

また、実装とリファクタリングを同時にやっていることが多いことに気づきました。<br>

気づいた時点で共通化等出来そうな処理をまとめているのは良いですが、最後まで完成しきっていないのに処理自体を変更していて、結果よくわからなくなりモチベーションの低下につながるということがありました。<br>
これに関しては、まずは終わらせることを第一に目指したほうが効率だったりモチベ維持にもつながると思うので寄り道せずに行きたいと思います。

作成したソースは[こちら](https://github.com/yourao424/FolderCreation)に投げてます。<br>

以上