Создаем свой тип, часть 2

Задача:

  1. Создать класс реализующий операции в соответствии с заданием
  2. Протестировать операции
  3. Создать GUI приложение, а-ля калькулятор

для следующего задания:

0

Мера длины, задаваемая в виде пары (значение, тип), допустимые типы: метры, км, астрономические единицы, парсеки

  • сложение
  • вычитание
  • умножение на число
  • сравнение двух объемов
  • вывод значения в любом типе

В первой части мы создали класс для работы с длинами заданной в разных мерах.

Теперь можно приступить к реализации интерфейса (он же калькулятор) для того чтобы любой пользователь мог бы поиграться с нашим типом.

Мы не будем сразу сильно переусложнять интерфейс и реализуем, пока что, калькулятор для манипуляции длинами заданными в метрах

Проектируем простой калькулятор

Нам потребуется три поля для ввода:

  • поле для первого слагаемого
  • поле для второго слагаемого
  • поле для результата

и еще выпадающий список где можно будет выбрать операцию, которую мы применяем к слагаемым:

Я не хочу, чтобы у меня была какая-та кнопка по нажатию на которую у меня будет все пересчитываться. А хочу, чтобы пересчет происходил мгновенно, то есть я ввел или поменял какое-то число и сразу получил результат, поменял операцию и сразу получил пересчет.

Поэтому я буду производить пересчет в событии TextChanged, это событие поумолчанию для TextBox поэтому я можно кликнуть на него два раза чтобы добавить обработчик.

Кликаем дважды на txtFirst

и так видим:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void txtFirst_TextChanged(object sender, EventArgs e)
    {
        // наш новый обработчик, который вызывается, когда кто-нибудь что-то вводит в поле для ввода
    }
}

добавим код для расчёта

private void txtFirst_TextChanged(object sender, EventArgs e)
{
    try
    {
        // считали значения с полей для ввода и сконвертили в числа
        var firstValue = double.Parse(txtFirst.Text);
        var secondValue = double.Parse(txtSecond.Text);
        
        // на основании значений создали экземпляры нашего класса Length 
        var firstLength = new Length(firstValue, MeasureType.m);
        var secondLength = new Length(secondValue, MeasureType.m);

        // сложили две длины
        var sumLength = firstLength + secondLength;

        // записали в поле txtResult длину в строковом виде
        txtResult.Text = sumLength.Verbose();
    } catch (FormatException) {
        // если тип преобразовать не смогли
    }
}

можно запустить и проверить:

как мы видим пересчет происходит только когда мы редактируем первое поле, а нам очевидно хочется, чтобы пересчет происходил и при вводе в поле txtSecond. Поэтому добавим обработчик для txtSecond (кликнем на него дважды)

public partial class Form1 : Form
{
    // ...

    private void txtFirst_TextChanged(object sender, EventArgs e)
    {
        // ...
    }

    private void txtSecond_TextChanged(object sender, EventArgs e)
    {
        // добавился обработчик
    }
}

теперь можно заполнить его тем же кодом что и в txtFirst_TextChanged, то есть так:

public partial class Form1 : Form
{
    // ...

    private void txtFirst_TextChanged(object sender, EventArgs e)
    {
        try
        {
            var firstValue = double.Parse(txtFirst.Text);
            var secondValue = double.Parse(txtSecond.Text);

            var firstLength = new Length(firstValue, MeasureType.m);
            var secondLength = new Length(secondValue, MeasureType.m);
    
            var sumLength = firstLength + secondLength;
    
            txtResult.Text = sumLength.Verbose();
        } catch (FormatException) {
            // если тип преобразовать не смогли
        }
    }

    private void txtSecond_TextChanged(object sender, EventArgs e)
    {
        try
        {
            var firstValue = double.Parse(txtFirst.Text);
            var secondValue = double.Parse(txtSecond.Text);
            
            var firstLength = new Length(firstValue, MeasureType.m);
            var secondLength = new Length(secondValue, MeasureType.m);
    
            var sumLength = firstLength + secondLength;
    
            txtResult.Text = sumLength.Verbose();
        } catch (FormatException) {
            // если тип преобразовать не смогли
        }
    }
}

и попробовать запустить:

теперь все хорошо, если оба поля заполнены, то при смене значения в любом из них идет пересчет результирующего значения. Но есть одно «но», у нас в коде два раза встречается один и тот же кусок кода. А так как мы дальше планируем усложнять пересчет то нам придется при каждом изменении вносить правки в оба куска. Что есть зло.

Поэтому мы, чтобы не мучатся, просто утащим код расчёта в отдельную функцию, а сам обработчики будут вызывать уже эту самую функцию. Такой подход погроммисты называют DRY (англ. сухой, но сухость тут не причем, это чтобы забугром проще запомнить было, на самом деле это аббревиатура от Don`t repeat yourself, то бишь, не повторяйся). И так чтобы не повторяться правим код:

public partial class Form1 : Form
{
    // ...
    
    // добавили функцию для рассчета
    private void Calculate()
    {
        try
        {
            var firstValue = double.Parse(txtFirst.Text);
            var secondValue = double.Parse(txtSecond.Text);
    
            var firstLength = new Length(firstValue, MeasureType.m);
            var secondLength = new Length(secondValue, MeasureType.m);
    
            var sumLength = firstLength + secondLength;
    
            txtResult.Text = sumLength.Verbose();
        }
        catch (FormatException)
        {
            // если тип преобразовать не смогли
        }
    }
    
    private void txtFirst_TextChanged(object sender, EventArgs e)
    {
        // старый код убрали, вставили вызов новой функции
        Calculate();
    }
    
    private void txtSecond_TextChanged(object sender, EventArgs e)
    {
        // старый код убрали, вставили вызов новой функции
        Calculate();
    }
}

если запустить, то получим ту же картину.

Добавляем выбор операции

Давайте теперь добавим возможность выбрать операции, а то у нас все плюс да плюс. Перейдем на редактор форму, выберем cmbOperation, найдем свойство Items и кликнем на него

и введем все допустимые операции

чет негусто, ну и ладно. И еще установим свойству Text значение +

так, теперь операцию выбрать можно, давайте теперь еще подправим функцию Calculate, чтобы выбранная операция учитывалась

public partial class Form1 : Form
{
    // ...
    
    // добавили функцию для расчёта
    private void Calculate()
    {
        try
        {
            // эти строчки не трогаем
            var firstValue = double.Parse(txtFirst.Text);
            var secondValue = double.Parse(txtSecond.Text);

            var firstLength = new Length(firstValue, MeasureType.m);
            var secondLength = new Length(secondValue, MeasureType.m);
            
            // заводим неинициализированную переменную под длину
            Length sumLength;
            
            // смотрим что выбрали в cmbOperation
            switch(cmbOperation.Text)
            {
                case "+":
                    // если плюсик выбрали, то складываем
                    sumLength = firstLength + secondLength;
                    break;
                case "-":
                    // если минус, то вычитаем
                    sumLength = firstLength - secondLength;
                    break;
                default:
                    // а если что-то другое, то просто 0 выводим,
                    // такое маловероятно, но надо указать иначе не скомпилится
                    sumLength = new Length(0, MeasureType.m);
                    break;
            }
            
            // это не трогаем
            txtResult.Text = sumLength.Verbose();
        }
        catch (FormatException)
        {
            // если тип преобразовать не смогли
        }
    }
    
    private void txtFirst_TextChanged(object sender, EventArgs e)
    {
        // ...
    }
    
    private void txtSecond_TextChanged(object sender, EventArgs e)
    {
        // ...
    }
}

запускаем и проверяем:

хм, чего-то не реагирует… а ну да! Мы ведь забыли добавить обработчик в переключения операции, короче идем на форму кликаем дважды на cmbOperation и добавляем код:

public partial class Form1 : Form
{
    // ...

    private void txtFirst_TextChanged(object sender, EventArgs e)
    {
        Calculate();
    }

    private void txtSecond_TextChanged(object sender, EventArgs e)
    {
        Calculate();
    }

    private void cmbOperation_SelectedIndexChanged(object sender, EventArgs e)
    {
        // добавили обработчик
        Calculate();
    }
}

и снова проверяем

вот теперь другое дело.

Добавляем возможность указывать тип

Теперь давайте доработает нашу форму, чтобы можно было указывать меру длины, а также меру результата. Переключаемся на форму и приводим ее к следующему виду:

Надо все эти комбобоксы заполнить, но вручную это делать лениво, поэтому лучше сделать заполнение в конструкторе формы и так правим код формы:

public partial class Form1 : Form
{
    public Form1()
    {
        // эту функцию не трогаем
        InitializeComponent();
        
        // список значений
        var measureItems = new string[]
        {
            "м.",
            "км.",
            "а.е.",
            "парсек",
        };
        
        // привязываем списки значений к каждому комбобоксу
        cmbFirstType.DataSource = new List<string>(measureItems);
        cmbSecondType.DataSource = new List<string>(measureItems);
        cmbResultType.DataSource = new List<string>(measureItems);
    }

    // ...
}

пробуем запустить и получаем:

правда пока изменение типов ни на что не влияет. Чтобы появилась реакция, надо добавить учет выбранного значения в функцию Calculate и так пробуем (пока не копируйте)

private void Calculate()
{
    try
    {   
        // эти строчки не трогаем
        var firstValue = double.Parse(txtFirst.Text);
        var secondValue = double.Parse(txtSecond.Text);
            
        // создаем три переменные под тип каждого поля
        MeasureType firstType, secondType, resultType;
        
        // в зависимости от выбранного в комбобоксе значения получаем тип
        // напоминаю, что мы это делаем потому что наш класс умеет работать
        // только с MeasureType, и передать ему строку а-ля "км." не получится
        switch(cmbFirstType.Text)
        {
            case "м.":
                firstType = MeasureType.m;
                break;
            case "км.":
                firstType = MeasureType.km;
                break;
            case "а.е.":
                firstType = MeasureType.au;
                break;
            case "парсек":
                firstType = MeasureType.ps;
                break;
            default:
                firstType = MeasureType.m;
                break;
        }

        switch (cmbSecondType.Text)
        {
            case "м.":
                firstType = MeasureType.m;
                break;
            case "км.":
                firstType = MeasureType.km;
                break;
            case "а.е.":
                firstType = MeasureType.au;
                break;
            case "парсек":
                firstType = MeasureType.ps;
                break;
            default:
                firstType = MeasureType.m;
                break;
        }

        switch (cmbResultType.Text)
        {
            case "м.":
                firstType = MeasureType.m;
                break;
            case "км.":
                firstType = MeasureType.km;
                break;
            case "а.е.":
                firstType = MeasureType.au;
                break;
            case "парсек":
                firstType = MeasureType.ps;
                break;
            default:
                firstType = MeasureType.m;
                break;
        }
        
        var firstLength = new Length(firstValue, MeasureType.m);
        var secondLength = new Length(secondValue, MeasureType.m);
        
        // ...
    }
    catch (FormatException)
    {
        // если тип преобразовать не смогли
    }
}

надеюсь вы этот код еще не скопировали, потому что, в целом, это ужасный код, мы снова натолкнулись на нарушения принципа DRY (не повторяться), тут у нас три почти одинаковых switch-блока. Они отличаются только комбобоксом который указывается в начале блока (cmbFirstType, cmbSecondType, cmbResultType).

Чтобы сделать код более красивым, мы вынесем этот расчет в отдельную функцию. Но так как у нас есть меняющийся элемент, то мы сделаем функцию с параметром. И так, получится:

public partial class Form1 : Form
{
    // ...

    // добавили функцию которая возвращает MeasureType
    // называется GetMeasureType
    // а сам тип рассчитывается на основании comboBox,
    // который передается в качестве аргумента
    private MeasureType GetMeasureType(ComboBox comboBox)
    {
        MeasureType measureType;
        switch (comboBox.Text)
        {
            case "м.":
                measureType = MeasureType.m;
                break;
            case "км.":
                measureType = MeasureType.km;
                break;
            case "а.е.":
                measureType = MeasureType.au;
                break;
            case "парсек":
                measureType = MeasureType.ps;
                break;
            default:
                measureType = MeasureType.m;
                break;
        }
        return measureType;
    }

    private void Calculate()
    {
        try
        {
            var firstValue = double.Parse(txtFirst.Text);
            var secondValue = double.Parse(txtSecond.Text);

             // вместо трех страшных свитчей, три вызова нашей новой функции
            MeasureType firstType = GetMeasureType(cmbFirstType);
            MeasureType secondType = GetMeasureType(cmbSecondType);
            MeasureType resultType = GetMeasureType(cmbResultType);
            
            // тут сразу тип полученный передаем в момент создания экземпляра класса
            var firstLength = new Length(firstValue, firstType);
            var secondLength = new Length(secondValue, secondType);

            Length sumLength;

            switch(cmbOperation.Text)
            {
                case "+":
                    sumLength = firstLength + secondLength;
                    break;
                case "-":
                    sumLength = firstLength - secondLength;
                    break;
                default:
                    sumLength = new Length(0, MeasureType.m);
                    break;
            }

            // тут конвертируем через To(resultType) в указанный тип
            txtResult.Text = sumLength.To(resultType).Verbose();
        }
        catch (FormatException)
        {
            // если тип преобразовать не смогли
        }
    }

    // ...
}

блин, опять забыл обработчики подключить, не реагирует на смену типа. Возвращаемся в редактор формы и тыкаем дважды на каждый из новых комобоксов, походу добавляя код:

private void cmbFirstType_SelectedIndexChanged(object sender, EventArgs e)
{
    // этот обработчик появится после двойного тыка на cmbFirstType
    Calculate();
}

private void cmbSecondType_SelectedIndexChanged(object sender, EventArgs e)
{
    // этот обработчик появится после двойного тыка на cmbSecondType
    Calculate();
}

private void cmbResultType_SelectedIndexChanged(object sender, EventArgs e)
{
    // этот обработчик появится после двойного тыка на cmbResultType
    Calculate();
}

вообще, вы можете сказать: “А вот вы нам говорили про DRY, а сами тут на повторяли так что повторение на повторении и повторением погоняет” – ну и все такое. Но тут просто надо знать меру, одно дело, когда мы внутри функции десять раз один и тот же код вставляем, и другое дело, когда у нас несколько независимых функций, и привязка удобно делается через интерфейс.

Уменьшаем количество обработчиков [можно пропустить]

Короче, так себе аргумент на самом деле. Можно и это все отDRYить, давайте переключимся на форму и тыкнем на любой комбобокс и взглянем в свойства обработчиков привязанных к методу. Там есть свойство SelectedIndexChanged, и если кликнуть на выпадающий список мы увидим список методов, которые можно привязать:

если вернуться к коду, то мы увидим, что это вот эти методы, которые все делают одно и тоже:

private void txtFirst_TextChanged(object sender, EventArgs e)
{
    Calculate();
}

private void txtSecond_TextChanged(object sender, EventArgs e)
{
    Calculate();
}

private void cmbOperation_SelectedIndexChanged(object sender, EventArgs e)
{
    Calculate();
}

private void cmbFirstType_SelectedIndexChanged(object sender, EventArgs e)
{
    Calculate();
}

private void cmbSecondType_SelectedIndexChanged(object sender, EventArgs e)
{
    Calculate();
}

private void cmbResultType_SelectedIndexChanged(object sender, EventArgs e)
{
    Calculate();
}

и, в общем, напрашивается вопрос, а нельзя ли их все одним заменить, и ответ: “не можно, а нужно!”.

Добавим новый обработчик и назовем его как-нибудь красиво:

public partial class Form1 : Form
{
    public Form1()
    {
        // ...
    }
    
    // ...
    
    /**
     * новый обработчик, тут главное параметры правильно прописать
     * то бишь, скопировать из любого обработчика выше
     */
    private void onValueChanged(object sender, EventArgs e)
    {
        // вызов функции все тот же
        Calculate();
    }
}

теперь пойдем на форму и привяжем этот обработчик ко всем комбобоксам

теперь тоже самое делаем с полями для ввода (у нас их два, третье – это результат, мы ему ничего не привязываем), только там правим обработчик TextChanged:

теперь снова возвращаемся на форму и удаляем старые обработчики:

public partial class Form1 : Form
{
    public Form1()
    {
        // ...
    }

    private MeasureType GetMeasureType(ComboBox comboBox)
    {
        // ...
    }

    private void Calculate()
    {
        // ...
    }

    /*****************************************************
     * тут было шесть обработчиков, которые мы удалили *
      \\|//   \\|///  \\\|//\\\|/// \|///  \\\|//  \\|//
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     *****************************************************/
     
    private void onValueChanged(object sender, EventArgs e)
    {
        Calculate();
    }
}

Итого

В общем можно в очередной раз запустить и проверить:

примерно так все это будет работать. И примерно так должен работать ваш калькулятор.

Если вы где-то заблудились, то вот какой итоговый код получится:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        var measureItems = new string[]
        {
            "м.",
            "км.",
            "а.е.",
            "парсек",
        };

        cmbFirstType.DataSource = new List<string>(measureItems);
        cmbSecondType.DataSource = new List<string>(measureItems);
        cmbResultType.DataSource = new List<string>(measureItems);
    }

    private MeasureType GetMeasureType(ComboBox comboBox)
    {
        MeasureType measureType;
        switch (comboBox.Text)
        {
            case "м.":
                measureType = MeasureType.m;
                break;
            case "км.":
                measureType = MeasureType.km;
                break;
            case "а.е.":
                measureType = MeasureType.au;
                break;
            case "парсек":
                measureType = MeasureType.ps;
                break;
            default:
                measureType = MeasureType.m;
                break;
        }
        return measureType;
    }

    private void Calculate()
    {
        try
        {
            var firstValue = double.Parse(txtFirst.Text);
            var secondValue = double.Parse(txtSecond.Text);

            MeasureType firstType = GetMeasureType(cmbFirstType);
            MeasureType secondType = GetMeasureType(cmbSecondType);
            MeasureType resultType = GetMeasureType(cmbResultType);

            var firstLength = new Length(firstValue, firstType);
            var secondLength = new Length(secondValue, secondType);

            Length sumLength;

            switch(cmbOperation.Text)
            {
                case "+":
                    sumLength = firstLength + secondLength;
                    break;
                case "-":
                    sumLength = firstLength - secondLength;
                    break;
                default:
                    sumLength = new Length(0, MeasureType.m);
                    break;
            }

            txtResult.Text = sumLength.To(resultType).Verbose();
        }
        catch (FormatException)
        {
            // если тип преобразовать не смогли
        }
    }

    private void onValueChanged(object sender, EventArgs e)
    {
        Calculate();
    }
}