ファイル/ディレクトリ選択をユーザーコントロール化

# はじめに
自作アプリケーションを開発する上で、ファイルを選択する or ディレクトリを選択する場面が多くある。

例)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3331961/27fd4135-e4e0-e1ba-069a-ce5e35c29a70.png)

たかがコントロール３つを配置し
・入力対象の概要を書く：Label
・ダイアログで選択したファイル/ディレクトリを出力：TextBox
・OpenFileDialog/FolderBrowserDialogを呼ぶ：Button
これらを毎回毎回書くのがとても面倒臭くなった
※コピペツールを使うという手もあるが、それでは意味がない

無いのならば、作ってしまえ！

# 環境
:::note info
実施環境
・Windows 10 Professional
・Visual Studio 2022 Community Ver.17.9.0
・C#
・.NET 8(5以降ならば動く筈)
:::

:::note info
ユーザーコントロールに設置するのは以下の通り
・lblTitle:Label
・txtInput:TextBox
・btnSelect:Button
:::

```c#:FileSelector.cs(ユーザーコントロール)
// デザイナー側は省略
using System.ComponentModel;
using System.Reflection;
using System.IO;

namespace InputParts
{
    /// <summary>ボタン押下時の挙動</summary>
    public enum SelectorType
    {
        File = 0,               // OpenFileDialog
        Directory = 1,          // FolderBrowserDialog
    }

    public partial class FileSelector : UserControl
    {
        // 固定値及びメンバ変数宣言
        private const string CATEGORY_NAME = "CustomProperty";  // プロパティウィンドウのグループ分け、後述
        private SelectorType _selectorType = SelectorType.File; //
        private string _dialogTitle = "ファイルを選択";           // ダイアログタイトル
        private string _returnPath = string.Empty;              // 選択アイテムのフルパス
        private string _initialDir = string.Empty;              // 初期ディレクトリ
        private string _filter = "すべてのファイル(*.*)|*.*";     // SelectorType.Directoryの時は無視

        public FileSelector()
        {
            InitializeComponent();
        }

#region デザイナー公開プロパティ
        /// <summary>ラベル</summary>
        [Browsable(true)]
        [Category(CATEGORY_NAME)]
        [Description("ラベルに表示する文字列")]
        public string Title{ get => lblTitle.Text; set => lblTitle.Text = value; }

        /// <summary>テキストボックス</summary>
        [Browsable(true)]
        [Category(CATEGORY_NAME)]
        [Description("テキストボックスの内容を読取専用にするか")]
        public bool ReadOnly{ get => txtInput.ReadOnly; set => txtInput.ReadOnly = value; }

        [Browsable(false)]
        public string SelectedPath{ get => txtInput.Text; set => txtInput.Text = value; }

        /// <summary>
        /// ボタン
        /// </summary>
        [Browsable(true)]
        [Category(CATEGORY_NAME)]
        [Description("選択タイプ")]
        public SelectorType SelectType{ get => _selectorType; set => _selectorType = value; }

        [Browsable(true)]
        [Category(CATEGORY_NAME)]
        [Description("ダイアログタイトルを指定")]
        public string DialogTitle{ get => _dialogTitle; set => _dialogTitle = value; }
#endregion

#region デザイナー非公開プロパティ
        /// <summary>
        /// 表示フィルタ
        /// Example:"テキストファイル(*.txt)|*.txt|すべてのファイル(*.*)|*.*";
        /// </summary>
        [Browsable(false)]
        public string Filter{ get => _filter; set => _filter = value; }
#endregion

#region イベント
        private void btnSelect_Click(object sender, EventArgs e)
        {
            var FullPath = string.Empty;
            switch(this.SelectType)
            {
                case SelectorType.File:
                    FullPath = DialogSelectFile(SelectedPath);
                    break;

                case SelectorType.Directory:
                    FullPath = DialogSelectDirectory(SelectedPath);
                    break;
            }

            if(FullPath == string.Empty) return;    // キャンセル時は無視
            this.SelectedPath = FullPath;
        }
#endregion

#region ダイアログ表示
        /// <summary>ファイル選択ダイアログ表示</summary>
        private string DialogSelectFile(string CurrentValue)
        {
            DialogPathInitialize(ref _returnPath, ref _initialDir, CurrentValue);
            using(var OFD = new OpenFileDialog())
            {
                OFD.Title = this.DialogTitle;
                OFD.Filter = this.Filter; 
                OFD.FilterIndex = 0;
                OFD.InitialDirectory = _initialDir;
                OFD.Multiselect = false;
                OFD.CheckFileExists = true;
                OFD.CheckPathExists = true;                
                OFD.FileName = CurrentValue;
                if(OFD.ShowDialog() == DialogResult.OK){ _returnPath = OFD.FileName; }
            }
            return _returnPath;
        }

        /// <summary>フォルダ選択ダイアログ表示</summary>
        private string DialogSelectDirectory(string CurrentValue)
        {
            DialogPathInitialize(ref _returnPath, ref _initialDir, CurrentValue);
            using(var FBD = new FolderBrowserDialog())
            {
                //FBD.Description = this.DialogTitle;
                FBD.InitialDirectory = _initialDir;
                if(FBD.ShowDialog() == DialogResult.OK)
                { 
                    //_returnPath = FBD.SelectedPath;
                    _returnPath = $"{FBD.SelectedPath}\\";
                }
            }
            return _returnPath;
        }

        /// <summary>初期化の記述が煩雑になるので一括化</summary>
        private void DialogPathInitialize(ref string SelPath, ref string InitDir, string CurVal)
        {
            SelPath = string.Empty; InitDir = CurVal;
            if(string.IsNullOrEmpty(InitDir)){ InitDir = GetExecLocalDir(); }
        }
#endregion

#region 汎用
        /// <summary>実行ファイルの親ディレクトリパスを返す</summary>
#pragma warning disable CS8602
        private string GetExecLocalDir() => Directory.GetParent(GetExecFullPath()).FullName;
#pragma warning restore CS8602
        /// <summary>実行ファイルパスを返す</summary>
        private string GetExecFullPath() => Assembly.GetExecutingAssembly().Location;
#endregion        
    }
}
```
ビルド後、以下の様になっていればOK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3331961/540835f9-cbc1-8994-3bb8-d1e50a0af17e.png)

# プロパティ
コード内で「CustomProperty」とした理由は独自プロパティである事を明確化する為
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3331961/7f2817d7-968b-0435-25cb-3947c736790c.png)

# 心残り
FolderBrowserDialogの「フォルダーの選択(ダイアログタイトル)」を変えたかったが諦めた。
参考元に記載した「C# Winforms フォルダ選択ダイアログ」の方法で解決しそうだが…
SHFolderForBrowser(API)を使うのでは手軽とは程遠く、且つ労力に見合わないので断念

https://oshiete.goo.ne.jp/qa/3355240.html

# 参考元
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.usercontrol?view=windowsdesktop-8.0

[FolderBrowserDialog.cs](https://github.com/dotnet/winforms/blob/main/src/System.Windows.Forms/src/System/Windows/Forms/Dialogs/CommonDialogs/FolderBrowserDialog.cs)

https://qiita.com/ryojio2k/items/d6b1c39ca360373d9025

https://qiita.com/mototoke/items/d8df78abbc4cf60e8e42

http://chokuto.ifdef.jp/urawaza/struct/BROWSEINFO.html

https://qiita.com/Nossa/items/c2226232b31d7665267f

※2024/11/07　今更誤字を見つけたので修正