このページの最終更新日 2020/10/25

コントロールの共有化

1、重複コードはなくそう!

 既に何かしらの機能を持っているプログラムコードを、綺麗なコードに修正していくような作業のことを「リファクタリング」と一般的に呼ばれます。そしてリファクタリングの教科書を読んでいると必ず「重複コード」は避けるべきだと書かれています。そして執筆者も重複コードはほとんどのケースにおいて抹殺すべきものだと考えています。
 しかしコントロールの操作に対する重複コードを、一箇所に纏めていくことは少し困難を伴う場合が多いです。ここでは具体的にpic.1のようなフォームを考えてみます。入力値1と入力値2の2つのテキストボックスがあり、両方に整数値が入力されている場合には正常にプログラムが実行されます。

- pic.1 -

 入力値1に整数値が入力されていないとpic.2のようにエラーとなり、入力値1のテキストボックスがオレンジ色で表示されます。

- pic.2 -

 入力値2に整数値が入力されていないとpic.3のようにエラーとなり、入力値2のテキストボックスがオレンジ色で表示されます。

- pic.3 -

 これらの機能を満たすプログラムコードをcode.2に示します。ただしcode.2だと整数値が入力されていてもオーバーフローだと「整数値が入力されていません」というエラーになるし、大文字で1234と入力されていてもエラーになるし、入力値1も入力値2も数値が入力されていない場合は入力値1だけのエラーしか感知出来ないなど色々問題もあるのですが、この節ではコントロール操作のコード共有のテクニックを提示することが目的なので、細かい箇所には目をつぶって話を進めることにします。

- code.1 -
public partial class FormControlShare : Form
{
    public FormControlShare()
    {
        InitializeComponent();
        this.btnExe.Click += new EventHandler(ExecuteButtonClick);
    }

    private void ExecuteButtonClick(object sender, EventArgs e)
    {
        try
        {
            int userInput1;
            int userInput2;

            if (!int.TryParse(this.tbNumber1.Text, out userInput1))
            {
                this.tbNumber1.BackColor = Color.Orange;
                throw new Exception("入力値1が整数値ではありません。");
            }
            this.tbNumber1.BackColor = Color.White;

            if (!int.TryParse(this.tbNumber2.Text, out userInput2))
            {
                this.tbNumber2.BackColor = Color.Orange;
                throw new Exception("入力値2が整数値ではありません。");
            }
            this.tbNumber2.BackColor = Color.White;

            var showUserInput = new StringBuilder();
            showUserInput.Append(string.Format("入力値1は「{0}」です。\r\n", userInput1));
            showUserInput.Append(string.Format("入力値2は「{0}」です。\r\n", userInput2));
            MessageBox.Show(showUserInput.ToString(), "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
} 

 さて、code.1ではテキストボックスに対するエラー処理16~28行目が重複していますね。重複するコードを見かけたら一箇所に纏めていきましょう、というのがコードを綺麗に書く鉄則です。
 ここでcode.2のようにテキストボックスに対する処理を1つのCheckUserInputメソッドに纏めて重複コードを減らすようにします。

- code.2 -
public partial class FormControlShare : Form
{
    public FormControlShare()
    {
        InitializeComponent();
        this.btnExe.Click += new EventHandler(ExecuteButtonClick);
    }

    private void ExecuteButtonClick(object sender, EventArgs e)
    {
        try
        {
            int userInput1 = CheckUserInput(this.tbNumber1, "入力値1");
            int userInput2 = CheckUserInput(this.tbNumber2, "入力値2");

            var showUserInput = new StringBuilder();
            showUserInput.Append(string.Format("入力値1は「{0}」です。\r\n", userInput1));
            showUserInput.Append(string.Format("入力値2は「{0}」です。\r\n", userInput2));
            MessageBox.Show(showUserInput.ToString(), "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private int CheckUserInput(TextBox tbNumber, string textBoxName)
    {
         int userInput;
         if (!int.TryParse(tbNumber.Text, out userInput))
         {
            tbNumber.BackColor = Color.Orange;
            throw new Exception(textBoxName + "が整数値ではありません。");
         }
         tbNumber.BackColor = Color.White;
         return userInput;
    }
}       

 コード1よりかは少しスリムになった気はします。しかしフォームのクラスの中に各々のコントロールの処理を書いていくと、コードが複雑化し読みづらくなることが多いものです。 そこでコントロールの処理を共有化するようなテクニックをいくつか押さえておいてほしいのです。まずは、コントロールの継承について解説を進めていきます。


2、コントロールの継承

 ではまずコントロールの継承の方法を書いています。次のようにアプリケーションの中でTextBoxクラスを継承したExpandTextBoxクラスを作成します。 C#ではクラスの雛形が自動作成された段階では、Sysytem.Windows.Forms、System.Drawing名前空間がインポートされていないため 6・7行目でインポートしておきます。 このコード3内での「this.Text」や「this.BackColor」というのは、当然ながら継承元のクラスであるTextBoxのTextプロパティやBackColorプロパティのことを指しています。 この継承されたクラスの中に、コントロールを識別するプロパティ「TextBoxName」と、整数値が入力されていない場合にエラーをぶん投げるメソッド「IsIntOrError」を記述します。

- code.3 -
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;

namespace AppShareControl
{
    class ExpandTextBox : TextBox
    {
        public string TextBoxName { get; set; }

        public int IsIntOrError()
        {
            int userInput;
            if (!int.TryParse(this.Text, out userInput))
            {
                this.BackColor = Color.Orange;
                throw new Exception(this.TextBoxName + "が整数値ではありません。");
            }

            this.BackColor = Color.White;
            return userInput;
        }
    }
} 

 Visual Studioでの開発環境においてこのようにコントロールを継承したクラスは、継承元のコントロールと同じようにツールボックスの一覧から使用することが出来ます。コードを書いた段階ではまだ使用出来ません、ソリューションのビルドが通ることでpic.4のように自分で作成したクラス(ExpandTextBox)が表示され、他のコントロールと同様に使用することが出来ます。

- pic.4 -

 さらにExpandTextBoxクラスの中で宣言されたパブリックなプロパティは、IDEのプロパティ変更欄から打ち込むことができます。ここでは入力値1に対応するExpandTextBoxのTextBoxNameプロパティには「入力値1」を、ここでは入力値2に対応するExpandTextBoxのTextBoxNameプロパティには「入力値2」と記述します。

- pic.5 -

 Formクラスに記述すべきコードは次のcode.4のようになります。TextBoxに入力された値のチェックの記述はExpandTextBoxクラスに移動させることで、コードの役割とコードの書かれる場所の対応が明確になってより可読性の高いコードが書けるようになります。

- code.4 -
public partial class FormControlShare : Form
{
    public FormControlShare()
    {
        InitializeComponent();
        this.btnExe.Click += new EventHandler(ExecuteButtonClick);
    }

    private void ExecuteButtonClick(object sender, EventArgs e)
    {
        try
        {
            int userInput1 = this.etbNumber1.IsIntOrError();
            int userInput2 = this.etbNumber2.IsIntOrError();

            var showUserInput = new StringBuilder();
            showUserInput.Append(string.Format("入力値1は「{0}」です。\r\n", userInput1));
            showUserInput.Append(string.Format("入力値2は「{0}」です。\r\n", userInput2));
            MessageBox.Show(showUserInput.ToString(), "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
} 

3、複数のコントロールで1つのセットの場合

 先ほどは、1つのテキストボックスに対するコードの共有化を行いました。次は、1つのテキストボックスと1つのボタンがセットになっている場合を考えます。「選択」ボタンをクリックするとOpenFileDialogが開き、ファイルを選択するとファイルパスが対応するテキストボックスに入力されます。また、「実行」ボタンをクリックすると、2つのテキストボックス両方に存在するファイルパスが入力されていた場合は正常にプログラムが実行されてファイルパスを表示しますが、存在するファイルパスが入力されていない場合はテキストボックスをオレンジ色にしてエラーメッセージを表示します。このように1つのテキストボックスと1つのボタンが組み合わさって1つのセットになっており、このセットが複数ある場合を考えます。

- pic.6 -

 code.5はなるべく重複コードがないように書いたものです。

- code.5 -
using IO = System.IO;
    
public partial class FormControlShare : Form
{
    public FormControlShare()
    {
        InitializeComponent();
        this.btnExe.Click += new EventHandler(ExecuteButtonClick);
        this.btnFile1.Click += new EventHandler(SelectFile1);
        this.btnFile2.Click += new EventHandler(SelectFile2);
    }

    private void SelectFile1(object sender, EventArgs e)
    {
        SelectFileCommonMethod(this.tbFile1);
    }

    private void SelectFile2(object sender, EventArgs e)
    {
        SelectFileCommonMethod(this.tbFile2);
    }

    private void SelectFileCommonMethod(TextBox tbFile)
    {
        var ofd = new OpenFileDialog();
        ofd.Filter = "TextFile(*.txt)|*.txt|AllFile(*.*)|*.*";
        if (ofd.ShowDialog() != DialogResult.OK) { return; }

        tbFile.Text = ofd.FileName;
    }

    private string GetFilePath(TextBox tbFile, string fileName)
    {
        if (!IO.File.Exists(tbFile.Text))
        {
            tbFile.BackColor = Color.Orange;
            throw new Exception(fileName + "が存在しません。");
        }
        tbFile.BackColor = Color.White;
        return tbFile.Text;
    }

    private void ExecuteButtonClick(object sender, EventArgs e)
    {
        try
        {
            string filePath1 = GetFilePath(this.tbFile1, "ファイル1");
            string filePath2 = GetFilePath(this.tbFile2, "ファイル2");

            var showUserInput = new StringBuilder();
            showUserInput.Append(string.Format("ファイル1のパスは「{0}」です。\r\n", filePath1));
            showUserInput.Append(string.Format("ファイル2のパスは「{0}」です。\r\n", filePath2));
            MessageBox.Show(showUserInput.ToString(), "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
}   

 そこまで酷いコードではないとは思いますが、はやりフォームクラスの中に色々な処理を混ぜ込むと見づらくなってきます。しかし先ほどのように単純にコントロールの継承を行ったクラスを作るのもコントロールが組み合わさってくるような場合には難しくなってきます。このように組み合わさったコントロールのセットが複数ある場合に、コードを共有化する方法としてユーザーコントロールを使用する方法が挙げられます。

4、ユーザーコントロールを使用する

 ユーザーコントロールを使用するためには、Visual Studioでアプリケーション名を右クリックして、「追加」>「新しい項目」とクリックしていきpic.7のように表示された項目の中から「ユーザーコントロール」を選択します。

- pic.7 -

 ユーザーコントールの名前は「SelectFileControl」とします。ユーザーコントロールのデザイナーで、ファイルパスが入力されるテキストボックス「tbFile」と、クリックすることでファイルを選択出来るようになるボタン「btnFile」を配置します。このように複数のコントロールが配置されたユーザーコントロールは、あたかも1つのコントロールのように使用することが出来ます。

- pic.8 -

 ユーザーコントロールのプログラムコードは次のようになります。code.5でFormクラスの中に混ぜこぜに書かれていた、ボタンを押してファイルを選択するコードと、テキストボックスに存在するファイルパスが入力されていない場合にエラーを発生させるコードを、「SelectFileControl」ユーザーコントロールの中に持ってくるようにします。

- code.6 -
using IO = System.IO;

public partial class SelectFileControl : UserControl
{
    public string FileName { get; set; }

    public SelectFileControl()
    {
        InitializeComponent();
        this.btnFile.Click += new EventHandler(SelectFile);
    }

    private void SelectFile(object sender, EventArgs e)
    {
        var ofd = new OpenFileDialog();
        ofd.Filter = "TextFile(*.txt)|*.txt|AllFile(*.*)|*.*";
        if (ofd.ShowDialog() != DialogResult.OK) { return; }
        this.tbFile.Text = ofd.FileName;
    }
    
    public string GetFilePath()
    {
        if(!IO.File.Exists(this.tbFile.Text))
        {
            this.tbFile.BackColor = Color.Orange;
            throw new Exception(this.FileName + "が存在しません。");
        }

        this.tbFile.BackColor = Color.White;
        return this.tbFile.Text;
    }
} 

- pic.9 -

 code.6を書き終わったら一旦ビルドを通すようにします。すると先ほどテキストボックスの継承を行ったときと同様に、ツールボックスに「SelectFileControl」という項目が表示されるようになります。この「SelectFileControl」も他のコントロールと同様に、IDEデザイナー上でFormにペタペタと貼りつけることが出来るようになります。Formクラスのコードはcode.7のようになります。ファイル操作に関するテキストボックスとボタンのコードを「SelectFileControl」ユーザコントロールへ移動させることで、Formのデザインとコードの対応が解かりやすくなり、読みやすいコードになりました。

- code.7 -
 public partial class FormControlShare : Form
{
    public FormControlShare()
    {
        InitializeComponent();
        this.btnExe.Click += new EventHandler(ExecuteButtonClick);
    }

    private void ExecuteButtonClick(object sender, EventArgs e)
    {
        try
        {
            string filePath1 = this.selectFileControl1.GetFilePath();
            string filePath2 = this.selectFileControl2.GetFilePath();

            var showUserInput = new StringBuilder();
            showUserInput.Append(string.Format("ファイル1のパスは「{0}」です。\r\n", filePath1));
            showUserInput.Append(string.Format("ファイル2のパスは「{0}」です。\r\n", filePath2));
            MessageBox.Show(showUserInput.ToString(), "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
}