На примере стандартной задачи валидации ввода данных в проекте на WinForms рассмотрим ряд интересных технических приемов, в частности для работы с потоками, которые могут пригодиться и для других задач.
Итак, предлагаем пользователю заполнить 4-е текстовых поля, двух типов данных. Два поля строковые и два числовые.

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

Первое что нам тут надо сделать это не дать возможность писать ничего кроме цифр в те два поля, которые могут иметь только цифровые значения, для этого повесим события KeyPress на оба этих поля и реализуем события следующим образом:
private void txtINN_KeyPress(object sender, KeyPressEventArgs e)
{
char number = e.KeyChar;
if (!Char.IsDigit(number) && number != 8) // цифры и клавиша BackSpace
{
e.Handled = true;
}
}
private void txtCash_KeyPress(object sender, KeyPressEventArgs e)
{
char number = e.KeyChar;
if (!Char.IsDigit(number) && number != 8) // цифры и клавиша BackSpace
{
e.Handled = true;
}
}
number != 8 дает возможность стирать написанное используя BackSpace. А вы спросите, как же стрелки, кнопка Delete и т.д.? Да, согласен, надо переделать, и можно даже короче, и вообще на оба контрола достаточного одного обработчика:
private void txtINN_KeyPress(object sender, KeyPressEventArgs e)
{
char ch = e.KeyChar;
e.Handled = !Char.IsDigit(ch) && !Char.IsControl(ch);
}
Теперь можно использовать любые кнопки типа Up, Dwn, Del, Back, для которых Char.IsControl(ch) вернет true.
Далее хотим проверить, что все наши TextBox поля были не пустыми, для этого при нажатии кнопки Start вызовем метод ValidateData
bool ValidateData()
{
validate = 0;
foreach (Control t in this.Controls)
{
if (t.Name.StartsWith("txt"))
{
if (t.Text.Length == 0)
{
t.PlaceholderText = "ЗАПОЛНИТЕ ЭТО ПОЛЕ!";
validate++;
}
}
}
return validate == 0;
}
Функция ValidateData вернет false если хоть один TextBox будет пустой и нам его надо захайлайтить. За это будет отвечать метод MarkInvalid. В итоге мы хотим поморгать цветом Barder, но в TextBox нет возможности менять цвет рамки, поэтому, для начала сделаем метод MarkInvalid по простому, зададим свойству BackColor пустого TextBox значение Color.Red
private void btnStart_Click(object sender, EventArgs e)
{
if (ValidateData())
{
this.Visible = false;
FormMain frmain = new FormMain();
frmain.Show();
}
else
{
lblErrorValidate.Text = "Ошибка Валидации! Заполните поля!";
MarkInvalid();
}
}
MarkInvalid вызовется, если ValidateData вернет false
void MarkInvalid()
{
foreach (Control t in this.Controls)
{
if (t.Name.StartsWith("txt"))
{
if (t.Text.Length == 0)
t.BackColor = Color.Red;
}
}
}
Как говориться просто и сердито. Но мы то хотим не просто, мы хотим что бы красным стал бортик пустого TextBox и не просто поменял цвет, а еще бы и могал. К сожалению нет у TextBox свойства для цвета Border, что делать?
Идея такая, под каждый TextBox подложим контрол Panel, вернее надо положить все TextBox внутрь Panel, для каждого своей, но так что бы Panel была на пиксель шире и выше, то есть по чуть чуть вылезать со всех сторон. Её то мы и будем красить в красный цвет, что бы получить эффект закраски рамки.
Функции ValidateData и MarkInvalid придется переписать, так как наши TextBox-ы теперь напрямую не принадлежать форме, они вложены каждый в свою Panel:
bool ValidateData()
{
validate = 0;
foreach (Control p in this.Controls)
{
if (p.Name.StartsWith("panel"))
{
TextBox t = (TextBox)p.Controls[0];
if (t.Text.Length == 0)
{
t.PlaceholderText = "ЗАПОЛНИТЕ ЭТО ПОЛЕ!";
validate++;
}
}
}
return validate == 0;
}
void MarkInvalid()
{
foreach (Control p in this.Controls)
{
if (p.Name.StartsWith("panel"))
{
TextBox t = (TextBox)p.Controls[0];
if (t.Text.Length == 0)
if (p.BackColor == Color.Transparent)
p.BackColor = Color.Red;
else
p.BackColor = Color.Transparent;
else
p.BackColor = Color.Transparent;
}
}
}
То есть теперь, изначально пробегаем не по TextBox контролам формы, а по всем Panel и уже внутри каждой панельки берем вложенный Textbox для проверки на пустоту. И меняем теперь свойство BackColor не у TextBox, а у той панельки в которую он вложен.
А еще мы хотим, что бы “рамка” у текстбокса не просто стала красной, а заморгала. Для этого запустим метод MarkInvalid в отдельном потоке, так что бы в нем закрутился бесконечный цикл), вот так:
private void btnStart_Click(object sender, EventArgs e)
{
if (ValidateData())
{
this.Visible = false;
FormMain frmain = new FormMain();
frmain.Show();
}
else
{
lblErrorValidate.Text = "Ошибка Валидации! Заполните поля!";
Task.Run(() => MarkInvalid());
}
}
Запускаем MarkInvalid в отдельном потоке:
Task.Run(() => MarkInvalid());
Внутри MarkInvalid запускаем цикл с остановкой на пол секунды:
void MarkInvalid()
{
while(validate > 0)
{
Invoke(new Action(() => DoMark()));
Thread.Sleep(500);
}
}
void DoMark()
{
foreach (Control p in this.Controls)
{
if (p.Name.StartsWith("panel"))
{
TextBox t = (TextBox)p.Controls[0];
if (t.Text.Length == 0)
if (p.BackColor == Color.Transparent)
p.BackColor = Color.Red;
else
p.BackColor = Color.Transparent;
else
p.BackColor = Color.Transparent;
}
}
}
Обратите внимание, что метод MarkInvalid работает в своем собственном, отдельном потоке, но в нем мы меняем значения свойств контролов, которые лежат на форме и могут изменяться только из главного потока, то есть из потока формы. Что бы обойти это ограничение, нам надо обернуть вызов DoMark в специальные потоковые скобки:
Invoke(new Action(() => DoMark()));
Красота, теперь все работает как надо.

Если интересно, код проекта можно смотреть на github.