То что я сейчас покажу, сломает мозг, но зато потом настолько понравится, что вы не сможете без этого писать код. Это называется Привязка данных (Data Binding). Штука очень полезная, при чем настолько, что в WPF и других более мощных UI движках без нее в принципе невозможно ничего вменяемого написать. А в Winforms про нее знают единицы.
Как вы обычно пишете код:
- Создаете контрол
- Если в контроле что-то произошло, создаете обработчик
- В обработчике что-то считаете
- Записываете результат в другие контролы
Как пишут код с привязками данных
- Создаете контрол
- Создаете модель данных, реализующую интерфейс
INotifyPropertyChnaged
- Привязываете контрол к свойству в модели данных, то есть сообщаете контролу, где брать данные для показа
- Меняете значение в модели данных, а контрол обновляется сам
Вы уже начали уносить из класса окна логику работы приложения в другие классы - отлично, значит привязки вам понравятся, так как они позволяют делать это очень легко.
К делу, вот реализация INotifyPropertyChnaged
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Выглядит страшновато, но пока с этим классом разбираться не надо, просто добавьте его в проект отдельным файлом.
Далее я начну писать приложение, которое будет считать периметр прямоугольника, оно содержит два контрола TextBox, в них я буду вводить вводные данные и один Label, который будет отображать результат.
Для этого, создам вот такую модель данных
public class Measures : NotifyPropertyChanged
{
private int _width;
private int _height;
private int _perimeter;
public int Width
{
get => _width;
set
{
_width = value;
OnPropertyChanged();
CalculatePerimeter();
}
}
public int Height
{
get => _height;
set
{
_height = value;
OnPropertyChanged();
CalculatePerimeter();
}
}
private void CalculatePerimeter()
{
Perimeter = Width + Height * 2;
}
public int Perimeter
{
get => _perimeter;
private set
{
_perimeter = value;
OnPropertyChanged();
}
}
}
Она наследует класс NotifyPropertyChanged, и из него использует метод OnPropertyChanged(), он как раз и нужен для того чтобы сообщать контролам, что данные изменились, и Windows Forms такой подход к разработке поддерживает нативно. То есть всю работу по обновлению контролов я отдаю самому UI движку Winforms, а сам концентрируюсь на написании полезного кода.
Чтобы всё было очевидно, контролы я создал прямо в коде конструктора. То есть дизайнер формы я вообще не трогал. А это значит, что просто создав новое Winforms приложение и скопировав код из этого ответа, вы сможете запустить и попробовать всё то что я здесь показал.
Код формы:
public partial class Form1 : Form
{
private Measures _data;
private TextBox widthInput;
private TextBox heightInput;
private Label resultLabel;
public Form1()
{
InitializeComponent();
FlowLayoutPanel panel = new FlowLayoutPanel() { Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown };
widthInput = CreateTextBox();
panel.Controls.Add(widthInput);
heightInput = CreateTextBox();
panel.Controls.Add(heightInput);
resultLabel = new Label() { AutoSize = true };
panel.Controls.Add(resultLabel);
Controls.Add(panel);
Load += Form1_Load;
}
private TextBox CreateTextBox()
{
TextBox result = new TextBox() { Margin = new Padding(5), MaxLength = 5 };
result.TextChanged += Result_TextChanged;
return result;
}
private void Result_TextChanged(object sender, EventArgs e)
{
if (sender is TextBox tbx)
{
tbx.BackColor = tbx.Text.Length == 0 || int.TryParse(tbx.Text, out _) ? SystemColors.Window : Color.LightPink;
}
}
private void BindControls(Measures data)
{
widthInput.DataBindings.Clear();
widthInput.DataBindings.Add(nameof(TextBox.Text), data, nameof(Measures.Width), false, DataSourceUpdateMode.OnPropertyChanged);
heightInput.DataBindings.Clear();
heightInput.DataBindings.Add(nameof(TextBox.Text), data, nameof(Measures.Height), false, DataSourceUpdateMode.OnPropertyChanged);
resultLabel.DataBindings.Clear();
resultLabel.DataBindings.Add(nameof(Label.Text), data, nameof(Measures.Perimeter));
}
private void Form1_Load(object sender, EventArgs e)
{
_data = new Measures();
BindControls(_data);
}
}
Обратите внимание, у меня вообще нет обработчиков событий, связанных с логикой приложения. Во время работы приложения будут функционировать только привязки данных.
Отвечая на вопрос про логику - вот собствено она и есть, класс для данных создается однажды и редактируется из интерфейса напрямую. При этом вы сможете создать новый класс с данными, снова вызвать BindControls для новой модели данных и готово. Эта модель легко сериализуется, сохраняется или читается из БД и т.д., в ней нет ничего лишнего.
Выглядит это так.

(4 + 6) * 2 = 20 всё верно.
Кстати, при вводе неверных данных никаких исключений не возникает, просто вычисления не производятся. Я добавил обработчик Result_TextChanged только для того чтобы показать пользователю, что ввод неверен, для этого я меняю фон текстбокса на розовый.

Советую так же поступить. Тем более проверки KeyPress не защищают от ввода неверных данных, я легко смогу вставить в текстбокс мусор из буфера обмена через Ctrl+V.
Поиграйте с этим проектом, посмотрите как работает. Потом решите, подходит оно или нет.
Ранее я рассказывал, как привязывать коллекции в Winforms: Привязка данных в DataGridView
NumericUpDown(документация). Текущий вопрос можно удалить, затем получше подготовиться, и задать новый уже конкретно сфокусированный на одной проблеме. – aepot Sep 22 '22 at 17:37