Pull to refresh

Валидация форм в декларативном стиле (C#)

Reading time 5 min
Views 26K
Я думаю, что многие из вас сталкивались с задачей валидации данных в формах. Это долго, утомительно и часто требует значительных усилий. Порой мне кажется, что ребята из Редмонта издеваются над нами, предлагая проводить валидацию так. Шучу, конечно, но этот метод мы использовать не будем.

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



Здесь есть несколько обычных текстовых полей, поле ввода числа и поле для e-mail'a. Зададим следующие правила для нашей формы:

Поля Фамилия, Имя и Отчество должны быть заполнены как минимум одним печатным (не whitespace) символом:
txtSurname
    .ValidateControl()
    .IsNotNullOrWhitespace();

txtName
    .ValidateControl()
    .IsNotNullOrWhitespace();

txtMiddleName
    .ValidateControl()
    .IsNotNullOrWhitespace();


Возраст должен быть не менее 16 лет. Если указанный возраст менее 21 года – необходимо вывести предупреждение, но разрешить сохранить форму:
nmAge
    .ValidateControl()
    .IsTrue(ctl => ctl.Value >= 16, "Возраст должен быть не менее 16 лет.", ValidationType.Required)
    .IsTrue(ctl => ctl.Value >= 21, "Некоторый контент (21+) для вас будет недоступен.", ValidationType.Optional);


Поле e-mail должно быть заполнено корректным значением (или по крайней мере похожим на e-mail):
txtEMail
    .ValidateControl()
    .IsValidEMail(false);


Если все поля заполнены правильно – разрешить нажатие кнопки «Сохранить», иначе – нет:
butSave
    .ValidateControl()
    .EnableByValidationResult();


Код класса формы одной простыней
public partial class frmMain : Form
    {
        public frmMain()
        {
            InitializeComponent();

            // проверим, что текстовые поля заполнены 
            txtSurname
                .ValidateControl()
                .IsNotNullOrWhitespace();

            txtName
                .ValidateControl()
                .IsNotNullOrWhitespace();

            txtMiddleName
                .ValidateControl()
                .IsNotNullOrWhitespace();

            // зададим жесткое ограничение в 16 лет
            // и не жесткое ограничение (предупреждение не препятствующее вводу формы) в 21 год
            nmAge
                .ValidateControl()
                .IsTrue(ctl => ctl.Value >= 16, "Возраст должен быть не менее 16 лет.", ValidationType.Required)
                .IsTrue(ctl => ctl.Value >= 21, "Некоторый контент (21+) для вас будет недоступен.", ValidationType.Optional);

            // включим проверку на ввод корректного e-mail'а в этом поле
            txtEMail
                .ValidateControl()
                .IsValidEMail(false);

            // по результатам валидации будем разрешать/запрещать указанную кнопку
            butSave
                .ValidateControl()
                .EnableByValidationResult();
        }
    }



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



В третьих: кнопка «Сохранить» будет доступна к нажатию только после успешной валидации данных.

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



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

Активацию контролов будем делать так:
// список чекбоксов с категориями
var categoryCheckBoxes = pnlCategories.Controls.Cast<CheckBox>();

// управление состоянием контролов в зависимости от включенных чекбоксов
dtBegin.EnableByTimer(() => chkFilterByDate.Checked);
dtEnd.EnableByTimer(() => chkFilterByDate.Checked);
pnlCategories.EnableByTimer(() => chkFilterByCategory.Checked);
pnlTextFilter.EnableByTimer(() => chkFilterByText.Checked);


Теперь правила валидации. Если фильтр по дате включен, то начальная дата должна быть меньше либо равна конечной. Начальная дата не может быть раньше 1990 года. Валидация будет происходить в обоих DatePicker'ах, но индикация будет отображаться только на dtEnd:
dtEnd
    .ValidateControl()
    .IsTrue(ctl => !chkFilterByDate.Checked || dtBegin.Value >= new DateTime(1990, 1, 1), 
            "Начальная дата отбора не может быть раньше 1990 года")
    .IsTrue(ctl => !chkFilterByDate.Checked || dtBegin.Value <= dtEnd.Value, 
            "Начальная дата должна быть меньше или равной конечной");


Если осуществляется фильтрация по категориям, то необходимо выбрать как минимум одну категорию:
pnlCategories
    .ValidateControl()
    .IsTrue(ctl => !chkFilterByCategory.Checked || categoryCheckBoxes.Any(c => c.Checked), 
            "Необходимо выбрать категорию");


Если осуществляется поиск текста, то необходимо задать текст и выбрать где его искать:
pnlTextFilter
    .ValidateControl()
    .IsTrue(ctl => !chkFilterByText.Checked || chkSearchTextInBody.Checked || chkSearchTextInHeader.Checked, 
            "Необходимо выбрать места поиска текста")
    .IsTrue(ctl => !chkFilterByText.Checked || !string.IsNullOrWhiteSpace(txtSearchText.Text), 
            "Необходимо задать текст");


Также, для успешной валидации формы, должен быть задан хотя бы один из фильтров для поиска:
gbSearchParameters
    .ValidateControl()
    .IsTrue(ctl => chkFilterByCategory.Checked || chkFilterByDate.Checked || chkFilterByText.Checked,
            "Необходимо задать условия поиска.");


Ну и по результатам валидации формы активируем главную действующую кнопку:
butSearch
    .ValidateControl()
    .EnableByValidationResult();


Полный код второй формы
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Teleavtomatika.Forms;

namespace Teleavtomatika_Form_Validation
{
    public partial class frmMain2 : Form
    {
        public frmMain2()
        {
            InitializeComponent();

            // список чекбоксов с категориями
            var categoryCheckBoxes = pnlCategories.Controls.Cast<CheckBox>();

            // управление состоянием контролов в зависимости от включенных чекбоксов
            dtBegin.EnableByTimer(() => chkFilterByDate.Checked);
            dtEnd.EnableByTimer(() => chkFilterByDate.Checked);
            pnlCategories.EnableByTimer(() => chkFilterByCategory.Checked);
            pnlTextFilter.EnableByTimer(() => chkFilterByText.Checked);

            // теперь правила валидации:
            // если фильтр по дате включен, то начальная дата должна быть меньше либо равна конечной
            // начальная дата не может быть раньше 1990 года
            // валидация будет происходить в обоих DatePicker'ах, но индикация будет отображаться только на dtEnd
            dtEnd
                .ValidateControl()
                .IsTrue(ctl => !chkFilterByDate.Checked || dtBegin.Value >= new DateTime(1990, 1, 1),
                        "Начальная дата отбора не может быть раньше 1990 года")
                .IsTrue(ctl => !chkFilterByDate.Checked || dtBegin.Value <= dtEnd.Value,
                        "Начальная дата должна быть меньше или равной конечной");

            // если осуществляется фильтрация по категориям
            // то необходимо выбрать как минимум одну категорию
            pnlCategories
                .ValidateControl()
                .IsTrue(ctl => !chkFilterByCategory.Checked || categoryCheckBoxes.Any(c => c.Checked),
                        "Необходимо выбрать категорию");

            // если осуществляется поиск текста
            // то необходимо задать текст
            // и выбрать где его искать
            pnlTextFilter
                .ValidateControl()
                .IsTrue(ctl => !chkFilterByText.Checked || chkSearchTextInBody.Checked || chkSearchTextInHeader.Checked,
                        "Необходимо выбрать места поиска текста")
                .IsTrue(ctl => !chkFilterByText.Checked || !string.IsNullOrWhiteSpace(txtSearchText.Text),
                        "Необходимо задать текст");

            // должен быть задан хотя-бы один из фильтров для поиска
            gbSearchParameters
                .ValidateControl()
                .IsTrue(ctl => chkFilterByCategory.Checked || chkFilterByDate.Checked || chkFilterByText.Checked,
                        "Необходимо задать условия поиска.");

            // Ну и по результатам валидации формы активируем главную действующую кнопку "Найти":
            butSearch
                .ValidateControl()
                .EnableByValidationResult();
        }
    }
}



Посмотреть как это работает в живую можно на видео:


Исходники тут.
Tags:
Hubs:
+6
Comments 14
Comments Comments 14

Articles