C#のWinFormsでコンボボックスのイベントを確認する2。「バリデーション」

C# コンピュータ
C#

コンボボックスはプルダウンメニューの様な選択肢を選択する機能と文字を入力するテキストボックスのような機能が複合されたコントロールです。選択と入力で保持する値が別の方法で変更される可能性があり、イベントの処理を考えると思考を放棄したくなります。

今回もコンボボックスをエクスプローラーのアドレスバーに見立てドライブ(とカレントディレクトリのパンくず)の選択とカレントディレクトリの入力をするサンプルを作成してみました。

ファイル名:FileSystemManager.cs
ファイルシステムを操作するルーチンをこちらにまとめてみました。

using System.Diagnostics;

namespace ComboBoxSample;

public class FileSystemManager
{
    public string CurrentDirectory = @"";

    public FileSystemManager()
    {
        CurrentDirectory = Directory.GetCurrentDirectory();
    }
    static public bool CheckDirectoryPath(string path)
    {
        return Directory.Exists(path);
    }
    /// <summary>
    /// ドライブの一覧を返す
    /// </summary>
    /// <returns>ドライブとカレントディレクトリの階層リスト</returns>
    public List<string> GetDriveListAndBreadcrumb()
    {
        List<string> result = new();
        string currentRoot = Path.GetPathRoot(CurrentDirectory) ?? "";

        foreach(var drive in Directory.GetLogicalDrives())
        {
            result.Add(drive);

            if (drive.ToUpper() == currentRoot.ToUpper())
            {
                string tmp = CurrentDirectory;
                Stack<string> tmps = new();
                while(currentRoot.ToUpper() != tmp.ToUpper())
                {
                    tmps.Push(tmp);
                    tmp = Path.GetDirectoryName(tmp) ?? currentRoot;
                }
                result.AddRange(tmps);
            }
        }
        return result;
    }
}

ファイル名:Form1.Designer.cs
フォーム上に配置するコントロールの初期化をこちらで行っています。

namespace ComboBoxSample;

partial class Form1
{
    /// <summary>
    ///  Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    ///  Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);

    }

    #region Windows Form Designer generated code

    /// <summary>
    ///  Required method for Designer support - do not modify
    ///  the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.components = new System.ComponentModel.Container();
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(800, 450);
        this.Text = "Form1";
    }

    #endregion
    // コンボボックスの生成
    ComboBox combobox1 = new()
    {
        Dock = DockStyle.Top,
    };
    // ボタン
    Button dummpyBtn1 = new()
    {
        Dock = DockStyle.Fill,
    };
}

ファイル名:Form1.cs
フォームのイベント関連をこちらに記述していて、このサンプル(記事)のメインといいますか、試行錯誤した部分にあたります。

using System.Reflection.Metadata.Ecma335;
using System.Diagnostics;

namespace ComboBoxSample;

public partial class Form1 : Form
{
    FileSystemManager fsm = new();

    // コンボボックスの更新
    void updateCombobox()
    {
        combobox1.BeginUpdate();
        combobox1.Items.Clear();
        foreach(var f in fsm.GetDriveListAndBreadcrumb())
        {
            combobox1.Items.Add(f);
        }
        int i = combobox1.Items.IndexOf(fsm.CurrentDirectory);
        combobox1.SelectedIndex = i;
        combobox1.EndUpdate();
    }
    public Form1()
    {
        InitializeComponent();

        // コントロールの追加
        Controls.AddRange(new Control[]
        {
            combobox1,
            dummpyBtn1,
        });

        // フォームロード
        Load += (sender, e) =>
        {
            // コンボボックスの更新(初回)
            updateCombobox();
        };

        // コンボボックスの選択が変更されたイベント
        combobox1.SelectedValueChanged  += (sender, e) =>
        {
            if (combobox1.SelectedItem is null) return;

            // イベントの無限ループ防止
            var dir = combobox1.SelectedItem.ToString() ?? "";
            if (dir == "" || fsm.CurrentDirectory == dir ) return;

            // カレントディレクトリ変更
            fsm.CurrentDirectory = dir;
            // コンボボックスの更新
            updateCombobox();
        };
        // コンボボックスのバリデーション        
        combobox1.Validating += (sender, e) => 
        {
            var dir = combobox1.Text;
            if (!FileSystemManager.CheckDirectoryPath(dir))
            {
                MessageBox.Show($"{dir}は無効なパス。", "えらー");
                combobox1.Text = fsm.CurrentDirectory;
                e.Cancel = true;
                return;
            }
        };
        // コンボボックスのバリデーション終了後
        combobox1.Validated += (sender, e) =>
        {
            fsm.CurrentDirectory = combobox1.Text;
            updateCombobox();
        };
    }
}



試行錯誤の結果、選択の変更イベントはSelectedValueChangedで処理し、バリデーション(入力文字の検証)はValidatingValidatedで行っています。

前提として選択による値の変更ではValidatingValidatedは発生しないようです。
Validatingはバリデーションの処理を記述し、e.Cancel = trueをセットしてreturn
するとバリデーションが失敗したことになります。サンプルでは入力値がディレクトリとして存在しているかチェックしています。(入力値の検証としては良くないと思いますが面倒なので…)

Validatedはバリデーションに成功した場合発生するイベントです。バリデーションエラーのメッセージを表示している場合などそれをクリアするコードを記述するようです。サンプルではカレントディレクトリの変更とコンボボックスの更新処理をしています。

サンプルではcombobox1.SelectedItemcombobox1.Textは同じ値(カレントディレクトリ)がセットされるようにしていますが、基本的には別に考える必要がありそうです。

ボタンオブジェクトを配置していますがこれはダミーで、コントロールがフォーム上にコンボボックス1つだけだとフォーカスの喪失が発生させられず(フォームを閉じるときに発生していた)その手前で発生するValidatedも発生しなかったためです。

スクリーンショットを撮っていて気が付いたのですが、サンプルのプログラムではバリデーションが失敗した状態でフォームを閉じることが出来ませんでした。なんらかの対策が必要です。とりあえずe.Cancel = trueの手前でcombobox1.Textにカレントディレクトリをセットしお茶を濁すことにします。

コメント