Форум программистов, компьютерный форум, киберфорум
Наши страницы

C# Windows Forms

Войти
Регистрация
Восстановить пароль
 
Рейтинг: Рейтинг темы: голосов - 2, средняя оценка - 5.00
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
#1

Архитектура ПО в WinForms (FAQ & HowTo) - C#

21.10.2015, 17:24. Просмотров 23213. Ответов 11
Метки нет (Все метки)

Архитектура ПО в WinForms (FAQ & HowTo)

В этом FAQ в основном обсуждаются вопросы проектирования пользовательского интерфейса (GUI) и взаимодействия интерфейса с моделью данных.
Приводятся также некоторые типовые решения проектирования интерфейса для WinForms.

Это не FAQ по паттернам проектирования. Несмотря на то, что архитектура GUI имеет отношение к паттернам MVC, MVP, MVVC и другим, здесь эти паттерны не рассматриваются. Данное руководство нацелено на соблюдение элементарных правил составления программ, что-то вроде гигиены кода. Соблюдение правил описанных ниже позволит вам разрабатывать простой, красивый, расширяемый и эффективный код для ваших приложений.

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

Просьба все замечания и вопросы постить в отдельной теме:
http://www.cyberforum.ru/faq/thread1558546.html
31
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Similar
Эксперт
41792 / 34177 / 6122
Регистрация: 12.04.2006
Сообщений: 57,940
21.10.2015, 17:24
Здравствуйте! Я подобрал для вас темы с ответами на вопрос Архитектура ПО в WinForms (FAQ & HowTo) (C#):

Валидатор указывает на ошибочную ссылку, вместо & нужно & amp - C#
Использую браузер FF 8.0, на нем установлен HTML Vallidator. Так вот этот валидатор ругается типо я неправильно указываю ссылку, я пишу & а...

query='SELECT * FROM resume WHERE ' & ''' & RecSet('place')& ''' & '=' & '''& s_loc &''' & - что не так? - C# ASP.NET
упростил для краткости запрос. в чем ошибка? RecSet - это коннекшн. query='SELECT * FROM resume WHERE ' & ''' & RecSet('place')& '''...

MVVM & WinForms - C# WPF
у меня есть проект wpf, в котором в главное окно подгружаются юзер контролы, в зависимости от выбранного меню. всем этим делом управляет...

error '80020009' Îøèáêà. /lalala/profile.asp, line 28 - C# ASP.NET
При простейшем и сто раз работавшем скрипте, вылетает ошибка! след. содержания error '80020009' ...

Помогите найти драйвера для pci\ven_8086&DEV_266E&SUBSYS_A002145&REV_05\3&13C0B0C5&0&F2 - Компьютерное железо
pci\ven_8086&DEV_266E&SUBSYS_A002145&REV_05\3&13C0B0C5&0&F2 Мультимедиа аудиоконтроллер помогите плз найти...

Мультимедиа контролер PCI\VEN_14F1&DEV_8800&SUBSYS_EA3D14F1&REV_05\4&25700A26&0&3020 - Windows XP
Помогите пожалуйста найти драйвер на мультимедиа видеоконтролер ...

11
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 17:29  [ТС] #2
Разделение данных и интерфейса

Главное что вы должны знать об архитектуре ПО – это то, что пользовательский интерфейс и данные должны быть разделены.
Самое плохое что вы можете сделать – это разместить весь код программы в Form1.cs.

Ответьте себе на вопрос – зачем нужна ваша программа? Каков ее функционал? Функция любой программы – это хранение и обработка данных. Именно так вы должны смотреть на свою программу – как на обработчик данных. Интерфейс же хоть и большая, но не самая главная часть программы. Наверняка функция вашей программы – не нажатие на кнопки.

Начните разработку вашей программы с составления модели данных – классов, который будут хранить и обрабатывать ваши данные. Но не думайте о программе как о наборе формочек! Формочки зависят от данных а не наоборот. Разработайте модель данных, оформите ее в отдельные классы, а затем уже подумайте об интерфейсе.

Программа в представлении новичка выглядит так:

Антипример(так делать нельзя!)

Архитектура ПО в WinForms (FAQ & HowTo)


А должна программа выглядеть вот так:
Архитектура ПО в WinForms (FAQ & HowTo)

Модель данных должна быть независима от GUI. Никогда не передавайте контролы в модель данных, и не возвращайте контролы из нее. Модель вообще не должна ничего знать об интерфейсе. Не забывайте, что интерфейсов может быть много, а модель всегда одна. Интерфейса вообще может не быть, и ваша модель может работать как сервис. Более того, сегодня ваша программа работает локально, в связке с интерфейсом, а завтра вы захотите сделать серверный вариант вашей программы. Отдельная и независимая модель данных позволит вам это сделать. Сильно привязанная к GUI – нет.

Процессы в программе с точки зрения новичка:

Антипример(так делать нельзя!)

Архитектура ПО в WinForms (FAQ & HowTo)


А должны процессы выглядеть так:
Архитектура ПО в WinForms (FAQ & HowTo)

Антрипримеры (так делать нельзя!)

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

Крестики нолики - автор делает крестики-нолики. Данные хранит прямо в пикчербоксах

Подсчет количества чисел в listbox1 - у автора нет модели данных, хранит данные и делает вычисления прямо в ListBox-ах и DataGridView.

Копирование датагрида в массив - автор думает как скопировать данные из DGV в массив. Но позвольте, данные изначально должны быть в массиве! А DataGridView - всего лишь пассивный отображатель данных, как фото на стене. Вы ведь не задумываетесь как из фотографии моря получить море?

List<Contol> в byte array - автор пытается сериализовать массив контролов. Зачем - загадка. Кстати, все контролы - не сериализуемые объекты.

Заполнение dataGridView - автор не задумывается о модели данных. Для автора программа - это DataGridView. Ведь это такой классный контрол - в нем можно и считать и хранить! (это ирония)

Калькулятор Windows C# - это очередной калькулятор. Угадайте, есть там модель данных? Угадали! Нет. Числа хранятся в richTextBox1. Не ну а где же их еще хранить? (это тоже ирония)

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


Можно ли хранить данные в контролах?

Нет, нельзя! Все данные должны храниться в классах модели данных. Не храните данные в DataGridView, TreeView и т.п. Эти контролы должны лишь отображать данные модели, но не хранить их.

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

Почему нельзя хранить данные в контролах?
Потому что:

1) Контролы не предназначены для хранения данных. Контролы - это визуальные компоненты, цель которых - отображать данные и принимать данные от пользователя.

2) Контролы существуют только в рамках WinForms. Что вы будете делать если вам нужно будет хранить данные в серверном сервисе? Тоже создавать там DataGridView? Или допустим вы решите переписать программу с WinForms на WPF.

3) Контролы - очень тяжелые объекты. Они содержат множество свойств и полей, которые не нужны с точки зрения вашей модели данных. Кроме того за каждым контролом тянется вереница вызовов WinAPI. Если вы создадите сто или тысячу контролов на форме, форма умрет.

4) Храня данные в контролах вы начисто лишаете себя всего объектно-ориентированного программирования. Ведь у вас нет модели данных, а значит нет ни классов, ни наследования, ни инкапсуляции, ни полиморфизма.

5) Контролы не сериализуемы. Вы не можете их ни сохранить в файл, ни прочитать из файла.

6) Контролы, как правило, хранят данные в виде строк. Храня данные в контролах - вам придется постоянно делать тяжелые преобразования число-строка и в обратную сторону.

7) Храня данные в контроле вы не сможете фильтровать данные(потому что неподходящие по фильтру записи просто удалятся и вы их потом не восстановите), не сможете скрывать несущественные поля (потому что скрывать просто негде), не сможете предавать их в другие формы, потому что тогда вам придется передавать весь контрол.

Этот список можно продолжать еще очень долго. Если вы продолжаете хранить данные в контролах, остальной FAQ можете не читать, толку не будет.

Замечание

1) В редких случаях допускается хранение данных в контролах. Например, если вы пишите текстовый редактор типа Блокнот, объектом модели данных является просто строка текста, и ее можно хранить в TextBox. Но это скорее исключение из правил.


Можно ли проводить вычисления в контролах?

Нет, нельзя! Все вычисления делайте на уровне модели данных либо в специальных сервисных классах. Не делайте вычислений в DataGridView или других контролах!

Пояснения

1) Вычисления – сложная задача, и эта задача является частью модели предметной области. Роль интерфейса – пассивна и проста. Получил данные – показал данные – принял данные – передал в модель данных. Все. Сложные манипуляции с данными – вне компетенции интерфейса.


Где делать сортировку, фильтрацию?

В модели данных или в специальных классах - обертках.
Пример реализации смотрите в главе "Как сделать фильтрацию и сортировку грида в виртуальном режиме".

Замечание

Даже если вы используете DataGridView, то сортировка все равно делается не в DGV, а в DataView, через который DGV связан с данными. Сам DGV не умеет сортировать данные, это не его задача.

Пояснения

1) Задача контролов – отображение данных. Сортировка или фильтрация – это слишком сложные для них операции. Они лишь должны отображать готовый набор данных. Хотите сортировку – отсортируйте данные на уровне модели и отдайте контролу.

2) В простейших случаях сортировка допустима и в контролах. Например, выпадающий список в ComboBox может быть отсортирован средствами самого ComboBox.


Где делать проверку правильности ввода?

Простые проверки (типа проверки того, что введено именно число) – можно делать на уровне контролов или форм. См главы Проверка пользовательского ввода, Где нужно парсить строки.
Для более сложных проверок – используйте методы из модели данных. Не делайте сложных проверок в контролах или формах.

Пояснения

1) Проверка корректности данных – может быть сложной задачей, и эта задача является частью модели предметной области. Роль интерфейса – пассивна и проста. Получил данные – показал данные – принял данные – передал в модель данных. Все. Сложные манипуляции с данными и их проверку – вне компетенции интерфейса.
37
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 17:33  [ТС] #3
С чего нужно начинать разработку приложения?

С модели данных. Не думайте о приложении как о наборе кнопок или гридов. Не начинайте разработку с GUI. Сначала разработайте архитектуру приложения, классы модели данных и бизнес-логику, а только затем - GUI.
В идеале - ваше приложение должно быть работоспособным вообще без всякого GUI.

Пояснения

1) Не забывайте: данные - первичны, пользовательский интерфейс - вторичен. GUI вообще может отсутствовать. А модель есть всегда.

2) GUI зависит от модели данных, а не наоборот.

3) GUI меняется чаще чем модель данных. Если же меняется модель данных, то GUI тем более меняется.

4) Модель данных всегда одна, а пользовательских интерфейсов может быть много (WinForms, WPF, ASP.NET и т.д.).

Замечания

1) Здесь под "началом разработки" понимается тот момент, когда у вас уже есть готовое техническое задание (ТЗ) на разработку ПО. Однако, что бы разработать хотя бы приблизительное ТЗ, вам возможно придется делать макеты ПО для заказчика. Макет - это чистый GUI без всякой модели данных и без функционала. Он нужен для того, что бы точно выяснить чего ожидает заказчик от вашего приложения. А поскольку заказчик мыслит в терминах интерфейса, то согласование ТЗ также должно быть на уровне интерфейса. Нет смысла разрабатывать классы модели данных на этапе макетирования - заказчик все равно не поймет ваших моделей.

2) На самом деле вам скорее всего придется начинать разработку приложения с нуля несколько раз. Полностью правильно понять требования заказчика и разработать сразу правильную архитектуру - довольно сложно. Плюс сами требования могут меняться со временем.


Как разработать модель данных?

1) Запишите на бумаге все задачи которые должно выполнять приложение.

2) Из текста выпишите все имена существительные (сущности). Каждое из них - потенциально может стать классом.

3) Нарисуйте схему в которой все сущности связаны между собой. Связи могут быть следующих типов: "является" (например связь Яблоко - Фрукт), связь "содержит" или "состоит из" (например связь Машина - Колесо), и
функциональные связи: "использует", "делает" и др (например Машина - Дорога, или Клиент - Банк).

4) Оформите каждую сущность в виде класса. При этом связь "является" оформите как наследование, "содержит" - как агрегацию, остальные связи - пока проигнорируйте, либо создайте классы - связки (например класс КлиентБанк отражающий связь клиента с банком).

5) Реализуйте алгоритмы и процессы преобразования данных - как методы классов модели.

Замечания

1) Избегайте создания классов, включающих в себя слишком много данных или функций (God object). Разделяйте сложные и большие классы на более мелкие.

2) С другой стороны нужно избегать создания слишком большого числа классов. Не забывайте, что вы разрабатываете не подробный каталог "всего что может быть". Программа должна выполнять определенные функции и ваши классы должны быть нацелены на эффективное выполнение функционала программы. Например, если в предметной области есть разные сущности "Мерседес" и "Ауди", то совсем не обязательно создавать два класса, если функционал этих сущностей - одинаков.

3) Старайтесь разрабатывать классы так, что бы изменения или расширение технического задания требовали минимальных изменений в модели данных.

4) Не увлекайтесь чрезмерно наследованием. Хорошая архитектура не должна иметь более 2-3 уровней наследования.

5) Используйте принципы SOLID и KISS при разработке модели.

Пояснения

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

Advanced features

Модели Rich и Anemic

Помимо разделения данных и интерфейса, иногда бывает полезно разделять классы данных и классы, осуществляющие вычисления и преобразования данных. Существует два подхода к реализации модели данных. Первый подход называется Rich модель. Это классический подход ООП, в котором хранение, преобразования и вычисления данных объекта совмещены в одном классе. Альтернативный подход – называется Anemic модель. В этом подходе классы разделены на те, что хранят данные и на те, которые производят преобразования данных (сервисные классы).
Пример: Пусть нам нужно решить квадратное уравнение. В Rich доменная модель будет выглядеть следующим образом:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    class SquareEquation
    {
        public double A { get; set; }
        public double B { get; set; }
        public double C { get; set; }
 
        public Tuple<double, double> Solve()
        {
            var d = B * B - 4 * A * C;
 
            if (d < 0)
                return null;
 
            var x1 = (-B + Math.Sqrt(d)) / (2 * A);
            var x2 = (-B - Math.Sqrt(d)) / (2 * A);
 
            return new Tuple<double, double>(x1, x2);
        }
    }
Здесь решение уравнения является методом самого уравнения.
А в модели Anemic решение уравнения выносится в отдельный класс:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    class SquareEquation
    {
        public double A { get; set; }
        public double B { get; set; }
        public double C { get; set; }
    }
 
    class SquareEquationCalculator
    {
        public Tuple<double, double> Solve(SquareEquation e)
        {
            var d = e.B * e.B - 4 * e.A * e.C;
 
            if (d < 0)
                return null;
 
            var x1 = (-e.B + Math.Sqrt(d)) / (2 * e.A);
            var x2 = (-e.B - Math.Sqrt(d)) / (2 * e.A);
 
            return new Tuple<double, double>(x1, x2);
        }
    }
Если вы затрудняетесь выбрать какой подход вам использовать в вашей программе, руководствуйтесь правилом:
Если объекты доменной модели – сложные, а алгоритмы обработки – простые, используйте Rich модель.
Если объекты модели данных – простые, а алгоритмы обработки – сложные, используйте Anemic модель.

Замечания

1) Некоторые классики теории программирования выступают категорически против модели Anemic (например Мартин Фаулер)
2) В реальных программах редко встречается чистый Rich или чистый Anemic. Зачастую есть некая смесь с преобладанием одного из подходов.
3) Не стоит считать что Rich модель подразумевает что все вычисления должны быть только внутри объекта доменной модели. Rich также может создавать сервисные классы. Но Rich модель разрешает реализацию методов внутри доменного объекта, а Anemic этого всячески избегает.


Упражнение 1

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

Упражнение 2

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

Упражнение 3

Сделайте Упражнение 2, но при этом в банке должна храниться история всех транзакций (переводов) с возможностью отмены уже выполненного перевода.


Практические советы по реализации классов доменной модели

Не ленитесь делать классы – контейнеры. Например, если ваша модель содержит список студентов, то лучше разработать отдельный класс
C#
1
class Students: List<Student>{}
чем везде использовать просто List<Student>. Класс контейнер позволит вам сделать код более лаконичным за счет того, что часть функций по работе со списком можно инкапсулировать внутри класса Students. Тогда, например, вместо кода
C#
1
students.Add(new Student(“John Smith”))
можно реализовать отдельный метод Add в классе Students, и тогда можно будет писать более лаконично:
C#
1
students.Add(“John Smith”)
Аналогично в контейнерах удобно реализовывать специфичные методы для фильтрации, поиска, сортировки и т.п.

Используйте те контейнерные классы, которые наиболее подходят вашей модели. Не забывайте, что List<T> - не единственный контейнер. Кроме него еще есть Dictionary<T,W>, HashSet<T>, SortedList<T,W> и другие.
Не храните в List<T> те данные, которые по природе имеют уникальный ключ. Например, если вам нужно хранить базу паспортов, то храните ее в Dictionary<T,W> где ключом является уникальный номер паспорта. Это больше соответствует природе данных. И кроме того, это еще дает несколько преимуществ: вы сможете быстро находить паспорт по его номеру и вы не сможете хранить несколько паспортов с одинаковыми номерами.

Используйте те типы данных, которые наиболее точно соответствуют природе ваших данных.
Например, не нужно хранить дату как string или int. Храните дату в типе DateTime. Если вы работаете с деньгами, используйте тип Decimal, не используйте int или float.
Если вы работаете с координатами – используйте float или double, но не используйте int, даже если в текущем ТЗ у вас написано что координаты могут быть только целые. Сегодня целые, а завтра – дробные. ТЗ меняются а природа – нет.

Не используйте числовые типы для хранения данных, которые похожи на числа, но ими не являются. Например, номер банковской карты – похож на число, но по сути это просто идентификатор. И его лучше хранить как string. Аналогично – номер телефона.
Обратите внимание на то, что те идентификаторы, которые сейчас выглядят как числа, завтра могут быть представлены с буквенным префиксом. Если у вас они хранились как числа – возникнут проблемы. Другой случай – длина числового идентификатора выросла и теперь она у вас не помещается в числовой тип – снова проблема.
Как определить, что это идентификатор, а не число? Просто задайте себе вопрос – имеет ли смысл складывать или вычитать данные числа? Если смысла нет – значит это не числа, а идентификаторы. Например, может ли нам понадобиться складывать или вычитать номера банковских карт? Скорее всего нет. Значит это – не числа.
39
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 17:47  [ТС] #4
Использование UserControl

UserControl – лучшее решение для реализации интерфейсов в WinForms. Пользовательский контрол позволяет разбить GUI на независимые части, где каждый UserControl отвечает только за отображение одного объекта модели данных. Код в таком случае становится более простым, лаконичным и однородным.

Также:
  1. Использование UserControl позволяет повторно использовать код. Однажды написанный UserControl вы можете использовать во многих формах, внутри других UserControl, а также в других проектах.
  2. Вы можете отображать на форме несколько объектов одного класса – просто создав на форме необходимое число UserControl.
  3. Если модель данных меняется, вам достаточно исправить один UserControl, отвечающий за отображения измененного объекта. Код форм, и остальных контролов – останется неизменным.
  4. Распределить работу между разработчиками.
  5. Проектирование форм становится более простым. На форму кидаются несколько UserControl и код формы сводится только к обработке событий этих котролов и перенаправлении данных с одних контролов к другим. При этом вы легко можете добавлять и убирать крупные блоки интерфейса, менять их расположение, выравнивание и т.д.

Начинающие программисты редко используют UserControl, видимо считая их сложными. На самом деле создание UserControl ничем не отличается от создания новой формы. Разница лишь в том, что форма может быть открыта в виде отдельного окна, а контрол – нужно положить на другую форму.

Практическая реализация

Пусть нам нужно отобразить объект данных класса Data. Наиболее простой вариант пользовательского контрола может быть таким:
Пример
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    public partial class DataPanel : UserControl
    {
        // Свойство, хранящее отображаемый объект модели данных
        public Data Data { get; private set; }
 
        //Конструктор
        public DataPanel()
        {
            InitializeComponent();
        }
 
        //Занесение данных из Data в контролы
        public void Build(Data data)
        {
            Data = data;
 
            //здесь занесение полей Data в контролы
            //например:
            //tbName.Text = Data.Name;
        }
 
        //Занесение данных из контролов в Data
        public void UpdateData()
        {
            //здесь занесение значений контролов в поля Data
            //например:
            //Data.Name = tbName.Text;
        }
    }

Код содержит свойство Data, хранящее редактируемый объект, и два метода: Build(Data data) и UpdateData(). Первый из них – заносит данные из Data в контролы. Второй – наоборот – заносит данные из контролов в объект Data. Оба метода, и свойство – публичные, и могут вызываться извне.

Для использования этого UserControl, нужно разместить его на форме и в определенный момент вызвать метод Build(), передав ему объект для отображения (например после того, как пользователь кликнул на элемент в списке объектов). На событие Validating контрола – создать обработчик такого вида:
Пример
C#
1
2
3
4
5
6
7
8
9
10
11
        private void pnData_Validating(object sender, System.ComponentModel.CancelEventArgs e)
        {
            try
            {
                //применяем изменения к объекту модели данных
                (sender as DataPanel).UpdateData();
            }catch(Exception ex)
            {
                e.Cancel = true;//запрещаем покидать контрол, пока не будут введены правильные значения
            }
        }

Этот код будет заносить изменения в объект при выходе пользователя из UserControl. Возможен другой вариант – с кнопкой Apply/Save/Ok. Тогда нужно вызвать метод UpdateData из обработчика нажатия кнопки.

Примеры UserControl смотрите также в главе "Как сделать панель свойств" и в упражнениях к этой главе.

Замечания

1) Обрабатывать ошибки ввода (try/catch) можно и непосредственно внутри метода UpdateData(). Но в таком случае, метод должен возвращать bool сигнализирующий о том, что метод отработан удачно.

2) Для оповещения об изменениях в объекте в UserControl можно сделать событие DataChanged.

3) Обратите внимание, что созданный UserControl появляется в Toolbox VisualStudio только после того как приложение было скомпилировано.

4) Не передавайте объект данных в конструктор UserControl. Во-первых потому что один контрол может быть использован для редактирования нескольких разных объектов, а во-вторых, обновление контрола может быть вызвано несколько раз даже для одного и того же объекта. Кроме того, если ваш конструктор будет иметь параметры, вы не сможете редактировать его в дизайнере форм.

Advanced features

Часто бывает нужно изменять свойства объекта сразу после внесения изменений (например, менять свойство объекта непосредственно в процессе набора текста в TextBox).
Тогда можно реализовать следующую схему:
Пример
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    public partial class DataPanel : UserControl
    {
        // Свойство, хранящее отображаемый объект модели данных
        public Data Data { get; private set; }
        //событие изменения данных
        public event EventHandler DataChanged = delegate { };
        //счетчик режима обновления
        private int updating;
 
        public DataPanel()
        {
            InitializeComponent();
        }
 
        //Инициализация контролов, занесение данных из Data в контролы
        public void Build(Data data)
        {
            Data = data;
 
            updating++;//включаем режим обновления
 
            //здесь занесение полей Data в контролы
            //например:
            //tbName.Text = Data.Name;
 
            updating--;//выключаем режим обновления
        }
 
        //Занесение данных из контролов в Data
        public void UpdateData()
        {
            if (updating > 0) return;//мы находимся в режиме обновления, не обрабатываем
 
            //здесь занесение значений контролов в поля Data
            //например:
            //Data.Name = tbName.Text;
 
            //здесь также можно вызвать Build(), если нужно перестроить все контролы для новых данных:
            //Build(Data);
 
            //сигнализируем об изменении объекта
            OnDataChanged();
        }
 
        protected virtual void OnDataChanged()
        {
            //вызываем событие
            DataChanged(this, EventArgs.Empty);
        }
 
 
        //обработчик изменения текста TextBox
        private void tbName_TextChanged(object sender, EventArgs e)
        {
            UpdateData();
        }
    }

Счетчик updating используется для предотвращения зацикливания программы при обновлении данных в контролах.

Упражнение 1

Создайте приложение, которое считывает все картинки из папки Изображения (используйте метод Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)), отображает на форме все картинки, рядом с каждой картинкой выводится имя файла, и размер файла. Имя файла можно менять, соответствующий файл должен переименовываться.
Общий вид приложения:
Архитектура ПО в WinForms (FAQ & HowTo)
Решение: Example8.zip

Упражнение 2

Создайте приложение для расчета напряжения по известным значениям сопротивления и силы тока (Закон Ома: U = IR). Интерфейс программы должен позволять производить одновременный расчет для трех наборов параметров. Также программа должна вычислять и отображать суммарное напряжение для всех наборов данных (U = U1 + U2 + U3).
Запретить ввод сопротивления меньше 0.
Общий вид приложения:
Архитектура ПО в WinForms (FAQ & HowTo)
Решение: Example9.zip

Упражнение 3

Если вы реализовали Упражнение 2 с моделью Rich, то переделайте его на модель Anemic. Если у вас была модель Anemic (ну а вдруг)), то переделайте на модель Rich.
(Что такое Rich и Anemic смотрите в главе "Как разработать модель данных?")
Решение для Anemic: Example10.zip
Решение для Rich преведено в предыдущем упражнении.
30
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 17:57  [ТС] #5
Именование контролов

Давайте контролам осмысленные названия, отражающие суть действия или имя объекта, которые они хранят. Не оставляйте названия типа button1 или textbox1.

Имена кнопок должны отражать имя действия, которое выполнится при нажатии на кнопку: btSave – для операции сохранения, btOpen – для открытия, btCancel, btOk, btAdd, btDelete и т.д. Имя кнопки уже полностью говорит о том действии, которая она выполняет. Когда вы создадите обработчик нажатия на кнопку, его название будет btOpen_Click() – по одному названия понятно, что здесь должно быть открытие документа.

Префикс bt – нужен для обозначения того, что это является кнопкой. Некоторые другие префиксы: tb – для TextBox, RichTextBox, cb - для ComboBox, CheckBox, dgv – для DataGridView, lv - для ListView, mi – для MenuItem, rb – для RadioButton, pn – для Panel, UserControl, lb – для Label, ToolStripStatusLabel, pb – для PictureBox.
Имена текстовых полей должно совпадать с именем объекта, которые они хранят: tbName – хранит имя, tbAge – хранит возраст, tbCount, tbPosition и т.д.

Аналогично – другие типы контролов: cbSex – комбобокс выбора пола, lvFiles – ListView хранящий список файлов, dgvClients – DataGridView хранящий список клиентов и т.д.

Бывает так, что контрол не привязан к какому либо конкретному объекту или ему трудно дать осмысленное название. Например – главное меню, статус бар, закладки, таймеры. В таком случае можно дать название Main. Например: msMain – главное меню (MenuStrip), ssMain – статус бар (StatusStrip), tmMain – таймер (Timer), tcMain – закладки (TabControl), tvMain – дерево (TreeView) и т.д.

Если контрол хранит одно значение (это TextBox, ComboBox, CheckBox, RadioButton, Panel) – давайте имя в единственном числе, если же контрол хранит список (это DataGridView, ListView, ListBox, TabControl)– давайте имя во множественном числе.

Что касается контролов типа Label. Если вы не планируете каким либо образом обращаться к этим контролам из кода, то для них можно оставить исходное имя (label1, label2 и т.д). Если же вам нужно менять текст лейбы из кода – обязательно дайте осмысленное название.

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

Используйте для названий английские слова, в правильном числе и времени. Не используйте кириллицу или транслит.

Имена классов для форм, UserControl и кастомных контролов

Дайте главной форме имя MainForm. Если вы создаете еще несколько дополнительных форм, называйте их XxxForm. Например: ClientsForm – форма, отображающая список клиентов, PrintForm – форма печати, LoginForm – форма логина и т.д.

Аналогично для имен классов UserControl – давайте имена типа XxxPanel. Например ClientPanel – панель редактирования клиента, DocumentPanel – панель для отображения документа и т.д.

Если вы создаете кастомный контрол, используйте постфикс, совпадающий с классом от которого вы унаследовались. Например, если вы сделали свою кнопку унаследованную от Button, то имя класса должно быть XxxButton (например FlatButton или CheckButton).

Имена для динамически создаваемых контролов

Если вы создаете контрол как локальную переменную и не планируете хранить его как поле класса, то используйте просто префикс (bt, pn, tm) – для контролов и form – для форм.

Например, если вам нужно создать и открыть форму логина, пишите так:
C#
1
2
var form = new LoginForm();
form.ShowDialog();
Если вам нужно положить на форму UserControl, пишите так:
C#
1
2
var pn = new ClientsPanel();
Controls.Add(pn);
Поскольку это локальные переменные, не стоит переусложнять код, придумывая им специфические названия. Обычно код создания контрола короткий и заключается только в инициализации свойств и размещении контрола.

Пояснения

1) Осмысленные имена контролов облегчают написание и понимание кода. Ваш код не должен содержать фрагменты типа textBox12.Text или button2_Click() – они бессмысленны, из их названия невозможно определить что они делают.

2) Давайте осмысленные имена сразу, еще до того, как вы сделаете обработчики событий. Поскольку если вы сделали обработчик button1_Click(), то даже если вы переименуете button1 на btOpen, имя обработчика все равно останется button1_Click().

3) Префиксы нужны для того, что бы вы легко отличали имена контролов от имен переменных, хранящих данные. Если не использовать префиксы, то часто имена контролов будут совпадать с именами объектов модели данных, переменными и т.д. Например, если у вас есть список людей, то у вас в коде будет встречаться и Persons(имя класса) и persons (объект типа Persons, хранящий список) и person(локальная переменная хранящая конкретного человека). Если же вы еще и DataGridView отображающий список, назовете Persons, то наступит полная неразбериха в именах. Если же ваш DataGridView будет называться dgvPersons – сразу будет понятно, что это контрол.

4) Не стоит указывать префиксы, абсолютно точно указывающие на тип контрола. Префиксу достаточно просто указать примерный функционал. Например и TextBox и RichTextBox могут иметь префикс tb. В процессе разработки типы контролов часто меняются. Вместо TextBox, возможно вы захотите использовать RichTextBox, вместо Panel - GroupBox, вместо Label - ToolStripStatusLabel и т.д. Если каждый раз давать другой префикс – код будет слишком часто меняться, и этого делать не стоит.

5) Если вы сделали свой UserControl, давайте ему префикс pn. По сути, любой контейнер (Panel, GroupBox, UserControl) – является панелью. Не нужно придумывать префиксы для каждого вновь созданного UserControl, это будет запутывать код.

6) Если у вас получается несколько одноименных контролов (например два TabControl, которые должны иметь имя tcMain) – значит интерфейс переусложнен и стоит задуматься о разделении элементов интерфейса на несколько независимых UserControl.

7) Не стоит придумывать имена для динамических контролов и заносить их в свойство Name контрола. Это имя не нужно для работы контрола. Оно может потребоваться только если вы хотите делать поиск контролов на форме по имени заданному как строка. Но это плохой подход к работе с контролами. Если вам нужно создать контрол динамичеки и затем вы хотите обрщаться к нему, то создайте поле в вашей форме и обращайтесь к нему:
C#
1
2
3
4
5
6
7
8
DataGridView dgvUsers;//объявляем контрол как поле нашей формы
 
//где то в коде:
dgvUsers = new DataGridView();//динамическое создание грида
dgvUsers.Parent = this;//размещаем грид на форме
 
//еще где-то в коде
dgvUsers.AddRow(...);//добавляем строку в динамически созданный грид


Где и как обрабатывать исключения

Пишите блок try/catch только в интерфейсе программы - в обработчиках нажатия кнопок или других контролов. Не перехватывайте исключения в теле методов модели данных или бизнес-логики.
Не оставляйте блок catch пустым – выводите сообщение пользователю в виде MessageBox.Show(ex.Message).

Пояснения

1) Если класс не может самостоятельно решить проблему с исключением, он не должен его обрабатывать. Обработка такого исключения – является просто замалчиванием проблемы. Например, если уровень DAL не может отправить данные в БД и просто отловил исключение, то вышестоящие уровни (в том числе GUI и пользователь) будут уверены, что данные отправились. Что недопустимо.
Если же класс не перехватил исключение, оно автоматически будет пробрасываться вверх, вышестоящему слою. А самым вышестоящим слоем является GUI. Здесь исключение нужно перехватить и сообщать о нем пользователю. Неперехват исключения в GUI означает аварийное закрытие приложения.

2) Если у вас серверное приложение – исключение может быть отловлено самым вышестоящим слоем и отправлено пользователю по сети.

3) Перехват внутри классов модели возможен, если класс, который его перехватывает, может исправить ситуацию, либо исключение является частью логики программы. Заметьте также, что механизм исключений в C# является очень медленным и использовать исключения для логики программы – не рекомендуется.

Пример

C#
1
2
3
4
5
6
7
8
9
10
11
        //обработчик нажатия кнопки
        void bt_Click(object sender, EventArgs e)
        {
            try
            {
                //здесь вызов методов обработки данных
            }catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

Антипример (так делать нельзя!)

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    class Calculator
    {
        private double average;
 
        public void CalcAverage(int[] array)
        {
            try
            {
                var sum = 0;
                foreach (var v in array)//здесь возможно исключение array == null
                    sum += v;
 
                average = sum / array.Length;//здесь возможно деление на ноль
            }catch
            {
                //перехват исключения просто скрывает проблему.
                //здесь также нельзя выводить сообщения типа MessageBox.Show(ex.Message);
                //потому что общение с пользователем – вне компетенции этого класса
            }
        }
    }

Упражнение

Напишите правильную реализацию метода из антипримера.
Решение
Все исключения пробрасываются в вызывающий уровень:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
    class Calculator
    {
        private double average;
 
        public void CalcAverage(int[] array)
        {
            var sum = 0;
            foreach (var v in array)
                sum += v;
 
            average = sum / array.Length;        
        }
    }

Advanced features

1) Довольно утомительно в каждом обработчике писать однотипный код try{…}catch(Exception ex){ MessageBox.Show(ex.Message);}. Для того что бы облегчить себе жизнь, можно обрабатывать событие Application.ThreadException, в котором показывать пользователю сообщение об ошибке. После обработки данного события, главный поток вернет управление в форму и приложение продолжит работу.
Для срабатывания этого события не забывайте предварительно вызвать
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); в методе Main() из Program.cs.

2) Часто исключения обернуты друг в друга и содержатся в свойстве Exception.InnerException. В таком случае имеет смысл показывать пользователю самый глубокий Exception, именно тот, который содержит изначальную причину:

C#
1
2
3
4
//в обработчике исключения:
while (ex.InnerException != null)
   ex = ex.InnerException;
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
3) Иногда излишне частое выскакивание MessageBox раздражает пользователя. Поэтому для некритических ошибок, можно не вызывать MessageBox, а сообщать о проблеме в статус баре главной формы - в виде надписи красного цвета.

4) Не используйте генерацию исключения для выхода из приложения (например при неправильно введенном пароле). Исключение не всегда приводит к завершению приложения: может быть выдано окно в котором будет кнопка "Продолжить" и пользователь сможет продолжить работу в программе.


А как же обрабатывать исключения в потоках?

Отловите исключение с помощью try/catch в главном методе потока. В обработчике catch вызовите некий метод формы для информирования пользователя о проблеме.

Пояснения

1) Не UI потоки не могут пробросить исключение в главный GUI поток приложения. Поэтому вы должны отловить исключение в главном методе потока и вызвать метод формы, который сообщит пользователю о проблеме.
Поскольку исключение происходит не в главном потоке, то метод формы должен проверять InvokeRequired и делать перевызов себя через Invoke.

2) Вы должны обрабатывать все исключения, кроме ThreadAbortException и AppDomainUnloadedException. Первое возникает при остановке потока через метод Abort() и обрабатывается самим объектом Thread. Второе исключение возникает при выгрузке домена и его тоже обрабатывать не следует.

Пример

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    public partial class MainForm : Form
    {
        ....
 
        //обработчик кнопки, запускающей поток
        private void bt_Click(object sender, EventArgs e)
        {
            var thread = new Thread(Work);
            thread.Start();
        }
 
        //главный метод потока
        private void Work()
        {
            try
            {
                //здесь вызываются методы вашей модели данных
            }catch(Exception ex)
            {
                if (ex is ThreadAbortException || ex is AppDomainUnloadedException) throw;//пробрасываем эти исключения
                OnThreadException(ex);//сообщаем форме об исключениях в потоке
            }
        }
 
        //этот метод следует вызывать после перехвата исключения в потоках
        void OnThreadException(Exception ex)
        {
            if (InvokeRequired)
                Invoke((MethodInvoker)(() => OnThreadException(ex)));//перевызываем себя в главном потоке
            else
            {
                MessageBox.Show(ex.Message);//сообщаем пользователю о проблеме
            }
        }
    }

Advanced features

1) Необработанные исключения в потоках обычно приводят к завершению приложения. Но это поведение можно изменить, если прописать в app.config:
XML
1
2
3
4
  <runtime>
    <!-- the following setting prevents the host from closing when an unhandled exception is thrown  -->
    <legacyUnhandledExceptionPolicy enabled="true" />
  </runtime>
В таком случае исключения в потоках просто игнорируются и поток завершается. При этом главный поток приложения продолжает работу. Эта настройка не влияет на исключения в главном потоке GUI.
Так делать не рекомендуется, но если очень хочется...
25
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 18:05  [ТС] #6
Где нужно парсить строки

Парсинг строк, введенных пользователем - делайте только в интерфейсе программы (в формах или UserControl). Не передавайте нераспарсенные строки в объекты доменной модели. Передавайте туда только распарсенные данные.
Не делайте преобразования точек в запятые или наоборот при парсинге дробных чисел.

Парсинг текстовых файлов делайте в модели данных. Используйте инвариантную культуру CultureInfo.InvariantCulture при парсинге.

Пояснения

1) Парсинг введенных данных – не функция доменной модели или бизнес-логики. Модель работает с уже распарсенными объектами. Парсинг – функция GUI.

2) В процессе парсинга возможны ошибки ввода. Эти ошибки легче отлавливать и контролировать в слое GUI.

3) Сам парсинг зависит от настроек системы и пользовательского интерфейса. Например, разделителем дробной части числа может быть и точка и запятая. От этих настроек зависит правильное преобразование строки в число. Но это совсем не забота модели данных. Модель данных никак не должна зависеть от настроек пользовательского интерфейса. Да это и не всегда возможно – например если модель данных находится на удаленном сервере, который не знает настроек локальной машины и не может правильно распарсить строки в принципе.

4) Почему не нужно делать преобразования точек в запятые или наоборот: Во-первых пользователь обязан вводить числа в том формате, который указан в настройках его системы. Во-вторых, замена запятых на точки не гарантирует правильность чисел и может привести к трудноуловимым ошибкам. Например, в американском стандарте точка является разделителем дробной части числа, а запятая - разделителем разрядов числа. То есть такое число вполне корректно: 10,000.00. Если же вы принудительно замените точки на запятые - число станет некорректным ни в одной культуре.

5) Исключение составляет парсинг файлов (например CSV). Это не является частью GUI, поэтому парсингом файлов может заниматься модель данных. При этом, парсинг не должен зависеть от настроек системы или GUI. Ведь файлы могут быть получены с другой машины, с другими настройками. Да и сама модель данных – может находиться на сервере. При этом конечно подразумевается, что данные файла сохранены в InvariantCulture (как и должно быть).

Пример (парсинг пользовательского ввода дробного числа)

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        private List<float> numbers = new List<float>();
 
        //обработчик события click кнопки добавления
        private void btAdd_Click(object sender, EventArgs e)
        {
            try
            {
                var str = tbNumber.Text;
                var val = float.Parse(str);//парсим (не преобразуем запятые в точки! используем текущие настройки системы)
                numbers.Add(val);//добавляем распарсенное число в модель данных
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

Пример (парсинг файла данных - делается в доменной модели)

C#
1
2
3
4
5
6
7
8
9
10
11
12
    //класс модели данных
    class Numbers : List<float>
    {
        public void ParseFile(string fileName)
        {
            foreach(var line in File.ReadLines(fileName))
            {
                var val = float.Parse(line, CultureInfo.InvariantCulture);//парсим с универсальным форматом InvariantCulture! Этот формат не зависит от текущих настроек системы
                Add(val);//добавляем значение в свой список
            }
        }
    }


Добавлено через 1 минуту
Проверка пользовательского ввода

Если вам нужно проверить правильность ввода пользователем данных – сделайте это в обработчике кнопки Save или OK.
В обработчике Save проверьте правильность всех полей данных, если данные введены неправильно – сообщите пользователю через MessageBox. Форму при этом - не закрывайте.
Если это главная форма и у нее нет кнопки Save или OK, то обработайте событие Validating контрола в котором происходит ввод.
Не делайте проверку вводимых данных в обработчиках KeyPress, KeyDown и т.д.

Пояснения

Часто программисты делают проверку ввода пользователя непосредственно в процессе ввода (извините за каламбур). Например, делается обработка события KeyPress в котором проверяется допустимость данного символа в данном поле (например то, что пользователь вводит именно число а не произвольную строку). Такой подход иногда имеет право на жизнь, но в общем случае лучше так не делать. И вот почему:

1) Правильно обработать событие KeyPress или KeyDown непросто. Например, нужно всегда давать пользователю возможность нажимать кнопки BackSpace и Del, иначе он не сможет удалять символы. Нужно давать пользователю возможность использовать управляющие клавиши: Ctrl-A, Ctrl-V и т.д. А еще нужно разрешить клавиши Left, Right, Home и др. Все это довольно сложно и утомительно для программирования.

2) В случае изменения модели данных, вам придется менять и обработчик KeyPress. Например, раньше пользователь мог ввести дробное число и ввод точки – допускался. А потом концепция поменялась, и вводить можно только целые числа. Не забудьте поменять 100500 обработчиков KeyPress

3) Обработка KeyPress еще не гарантирует правильность ввода. Например, пользователь может оставить поле пустым, и KeyPress здесь не поможет. А еще пользователь может ввести несколько точек в числе и контролировать это в KeyPress – затруднительно. А еще пользователь может вставить значение через Ctrl-V и тоже будут проблемы.

4) Часто правильность вводимого значения зависит от других полей ввода. Поэтому пользователь может ввести неправильное значение, но затем переключить некий переключатель в соседнем поле, и текущее значение уже станет правильным.

5) При использовании отдельных обработчиков код валидации распорошен по всему коду. Данные легче валидировать в одном месте – в обработчике Save.

6) Часто обрабатывая KeyPress вы просто делаете двойную работу. Например, вы проверяете что пользователь вводит число, но в обработчике Save вам все равно придется делать преобразование типа var val = int.Parse(tb.Text); - который и так выпадет в Exception, если число введено неправильно. Т.о. вам придется контролировать ввод и в KeyPress и в Save.

7) Если это главная форма и в ней нет кнопки Save – используйте событие Validation вместо KeyPress. Событие Validation происходит при попытке пользователя покинуть поле ввода. Если значение введено неправильно – сообщите через MessageBox. Это лучше чем обработка KeyPress, потому что не нужно заботиться о служебных клавишах, но хуже чем Save, потому что см. пп 4, 5, 6. Кроме того, недостаток этого подхода в том, что сообщение об ошибке появляется тогда, когда пользователь этого не ожидает – при переключении на другой контрол. Это неудобно еще и потому, что пользователь не может даже открыть справку, потому что вы не выпустите его из поля ввода.
Если вы используете UserControl, вы можете обрабатывать его событие Validation, которые будет вызываться когда пользователь будет пытаться покинуть пределы контрола.

Пример

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    public partial class InputForm : Form
    {
        public double Value { get; set; }
 
        public InputForm()
        {
            InitializeComponent();
        }
 
        //парсинг и проверка на правильность
        private void UpdateValue()
        {
            Value = double.Parse(tbValue.Text);
        }
 
        //обработчик события click кнопки Save
        private void btSave_Click(object sender, EventArgs e)
        {
            try
            {
                UpdateValue();//парсинг и проверка на правильность
                DialogResult = DialogResult.OK;//выход из формы, если все введено правильно
            }
            catch(Exception ex)
            {
                MessageBox.Show(ex.Message);//выводим сообщение об ошибке и не закрываем форму
            }
        }
    }

Антипример (так делать не нужно!)

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    public partial class InputForm : Form
    {
        public double Value { get; set; }
 
        public InputForm()
        {
            InitializeComponent();
        }
 
        //обработчик события KeyPress поля ввода
        private void tbValue_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (Char.IsNumber(e.KeyChar) || (e.KeyChar == ','))
                return;
            else
                e.Handled = true;
        }
 
        //обработчик события click кнопки Save
        private void btSave_Click(object sender, EventArgs e)
        {
            Value = double.Parse(tbValue.Text);
            DialogResult = DialogResult.OK;
        }
    }

Упражнение

Укажите потенциальные проблемы кода, приведенного в антипримере.
Ответ

1) Невозможность ввести отрицательное значение, а также значение в экспоненциальном формате (1e-02).

2) Невозможно использовать в поле ввода клавиши Backspace, Ctrl-A, Ctrl-V, Ctrl-C и т.д.

3) Код позволяет вводить дробные числа только если разделитель дробной части – запятая. Если в настройках Windows стоит иной разделитель – например точка – код работать не будет.

4) В методе btSave_Click не обрабатываются исключения (а они возможны, например если пользователь оставил поле ввода пустым и нажал Save).

Advanced features

1) Если у вас несколько полей на форме, и вы хотите выдавать в сообщении имя поля, где произошла ошибка, то вам придется писать отдельные блоки try/catch для каждого поля. Это неудобно.
Простым, но немного костальным решением может быть такое:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
        string currentParseState { get; set; }
 
        //парсинг и проверка на правильность
        private void UpdateValues()
        {
            currentParseState = "Поле Name";
            Name = double.Parse(tbName.Text);
 
            currentParseState = "Поле Price";
            Price = double.Parse(tbPrice.Text);
 
            currentParseState = null;
        }
 
        //обработчик события click кнопки Save
        private void btSave_Click(object sender, EventArgs e)
        {
            try
            {
                UpdateValues();//парсинг и проверка на правильность
                DialogResult = DialogResult.OK;//выход из формы, если все введено правильно
            }
            catch(Exception ex)
            {
                MessageBox.Show(currentParseState + "\r\n" + ex.Message);//выводим сообщение об ошибке для конкретного поля
            }
        }
Плюсом данного решения является также то, что вам не нужно вносить блоки try/catch и MessageBox в отдельный метод UpdateValues (который может вызываться и из других частей кода, которые имеют свою логику обработки ошибок).
22
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 18:16  [ТС] #7
Виртуальный режим работы контролов

В виртуальном режиме контролы не хранят данные а запрашивают их из модели данных непосредственно в момент прорисовки. Это наиболее простой и эффективный способ отображения данных модели в контрол.
Виртуальный режим имеет следующие преимущества:
  1. Контролы не хранят данные самостоятельно, следовательно, экономится память.
  2. Поскольку у контрола в виртуальном режиме нет состояния, то не возникает ситуации рассинхронзации модели данных и GUI (когда в контролах одни значения, а в модели данных - другие).
  3. Вам не нужно постоянно записывать данные в контрол и читать их оттуда. Для обновления контрола достаточно вызвать метод Invalidate(). После чего контрол самостоятельно отразит текущую модель данных.
  4. Виртуальный режим незаменим для отображения больших объемов данных. Контролы WinForms достаточно медлительны и виртуальный режим – самый быстрый режим работы для них.

Пример

Отображение списка 1 млн точек типа Point в DataGridView в виртуальном режиме:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    public partial class MainForm : Form
    {
        private Points points;
 
        public MainForm()
        {
            InitializeComponent();
 
            BuildData();
        }
 
        private void BuildData()
        {
            points = new Points();
 
            //создаем модель данных
            var rnd = new Random();
            for (int i = 0; i < 1000000; i++)
                points.Add(new Point(rnd.Next(100), rnd.Next(100)));
 
            //инициализируем число строк грида
            dgvMain.RowCount = points.Count;
        }
 
        private void dgvMain_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
        {
            //отдаем данные
            switch(e.ColumnIndex)
            {
                case 0: e.Value = points[e.RowIndex].X; break;
                case 1: e.Value = points[e.RowIndex].Y; break;
            }            
        }
 
        private void dgvMain_CellValuePushed(object sender, DataGridViewCellValueEventArgs e)
        {
            //получаем данные
            try
            {
                switch (e.ColumnIndex)
                {
                    case 0: points[e.RowIndex] = new Point(int.Parse(e.Value.ToString()), points[e.RowIndex].Y); break;
                    case 1: points[e.RowIndex] = new Point(points[e.RowIndex].X, int.Parse(e.Value.ToString())); break;
                }
            }catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
    }
Проект целиком: Example11.zip
Архитектура ПО в WinForms (FAQ & HowTo)


Упражнение

В приведенном примере сделайте кнопки добавления и удаления записи.
Решение

Добавьте обработчики для кнопок:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
        private void btDelete_Click(object sender, EventArgs e)
        {
            if(dgvMain.CurrentCell != null)
            {
                //удаляем данные из модели
                points.RemoveAt(dgvMain.CurrentCell.RowIndex);
                //обновляем интерфейс
                UpdateGrid();
            }
        }
 
        private void btAdd_Click(object sender, EventArgs e)
        {
            //добавляем данные в модель
            points.Add(new Point());
            //обновляем интерфейс
            UpdateGrid();
        }
 
        void UpdateGrid()
        {
            dgvMain.RowCount = points.Count;
            dgvMain.Invalidate();
        }
25
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 18:33  [ТС] #8
HowTo: Форма добавления/редактирования записи таблицы

В форме редактирования создайте метод Build() который будет заносить значения из DataRow в контролы формы:
C#
1
2
3
4
5
public void Build(DataRow row)
{
    //здесь занесение данных в контролы формы...
    //например tbName.Text = row[0].Value.ToString();
}
В главной форме создайте новый DataRow и создайте новую форму редактирования. Вызовите метод Build(), передав в него новый DataRow. Откройте форму редактирования через ShowDialog().
В форме редактирования, если пользователь нажимает OK, то пропарсите данные и сделайте DialogResult = DialogResult.OK.
В первой форме проверяете, что ShowDialog вернул DialogResult.OK, и если это так - добавляйте созданный DataRow в таблицу.

Аналогично поступайте, если ваш источник данных – типизированная коллекция классов модели данных. Тогда вместо DataRow указывайте ваш тип данных.

Замечание

Есть также альтернативный (хотя и очень похожий, но более универсальный) способ реализации формы редактирования - через UserControl. Смотрите главу "Как сделать панель свойств"

Пояснения

1) Не передавайте в форму другие контролы или формы в качестве параметров. Передавайте в форму только данные. Этим вы уменьшаете зависимость контролов друг от друга.

2) Не передавайте данные в конструктор формы или контрола. Если вы будете это делать, то во-первых форма или контрол не откроются в дизайнере форм. А во-вторых вы не сможете перестроить контролы формы редактирования после того, как форма уже создана (а это часто бывает нужно).

3) Использование метода Build() позволяет вам не пересоздавать форму каждый раз. Вы можете создать форму один раз, а затем показывать ее, предварительно вызвав метод Build() для перестройки ее контролов под новые данные.

4) Рассмотренное решение позволяет использовать одну и ту же форму как для добавления данных, так и для их редактирования. Кроме того, форму редактирования можно вызывать из различных форм.

5) Рассмотренное решение поддерживает отмену операции, если вы нажимаете Cancel на форме (или закрываете ее крестиком). Даже если вы изменили значения в текстбоксах, но нажали Cancel, то введенные значения не попадут в исходные данные.

6) Если вы нажали OK, но данные были введены неправильно и форма ввода выдала сообщение об ошибке, то часть данных может все же попасть в DataRow, даже если вы потом нажмете Cancel. Для того, что бы избежать этой ситуации – необходимо делать клон объекта данных и передавать в форму редактирования его, вместо исходного объекта.

Пример

Главная форма
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    public partial class MainForm : Form
    {
        private DataTable table;
 
        public MainForm()
        {
            InitializeComponent();
            Build();
        }
 
        public void Build()
        {
            table = new DataTable();
            table.Columns.Add("Article");
            table.Columns.Add("Price");
 
            dgvMain.DataSource = table;            
        }
 
        private void btAddRecord_Click(object sender, EventArgs e)
        {
            //создаем временный объект данных
            var row = table.NewRow();
            //создаем форму редактирования
            var form = new InputForm();
            //строим форму для объекта данных
            form.Build(row);
            //показываем пользователю
            if (form.ShowDialog() == DialogResult.OK)
                table.Rows.Add(row); //добавляем в таблицу
        }
 
        private void btEdit_Click(object sender, EventArgs e)
        {
            if(dgvMain.CurrentRow != null)
            {
                //получаем объект данных 
                var row = (dgvMain.CurrentRow.DataBoundItem as DataRowView).Row;
                //создаем форму редактирования
                var form = new InputForm();
                //строим форму для объекта данных
                form.Build(row);
                //показываем пользователю
                form.ShowDialog();
            }
        }
    }
Форма редактирования
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
    public partial class InputForm : Form
    {
        //данные
        public DataRow Data { get; private set; }
 
        public InputForm()
        {
            InitializeComponent();
        }
 
        //занесение данных из объекта данных в контролы
        public void Build(DataRow data)
        {
            Data = data;//сохрняем объект в своем свойстве
 
            tbArticle.Text =  Data["Article"].ToString();
            tbPrice.Text = Data["Price"].ToString();
        }
 
        //парсинг и проверка на правильность
        private void UpdateValue()
        {
            Data["Article"] = tbArticle.Text;
            Data["Price"] = decimal.Parse(tbPrice.Text);
        }
 
        private void btOk_Click(object sender, EventArgs e)
        {
            try
            {
                UpdateValue();//парсинг и проверка на правильность
                DialogResult = DialogResult.OK;//выход из формы, если все введено правильно
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);//выводим сообщение об ошибке и не закрываем форму
            }
        }
 
        private void btCancel_Click(object sender, EventArgs e)
        {
            DialogResult = DialogResult.Cancel;//выход из формы
        }
    }
Проект целиком: Example1.zip

Упражнение

Переделайте приведенный выше пример так, что бы в качестве источника данных выступал список объектов типа List<PointF>, а DataGridView работал в виртуальном режиме.
Решение: Example2.zip


Архитектура ПО в WinForms (FAQ & HowTo)
19
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 18:37  [ТС] #9
HowTo: Как сделать панель свойств

Если вы хотите редактировать свойства объектов непосредственно в форме (без открытия диалогового окна), нужно сделать следующее:
  • Создать на главной форме пустую панель (пусть она называется pnProperties)
  • Создать UserControl для редактирования объектов (если типы объектов разные, то для каждого типа должна быть отдельный UserControl).
  • В UserControl создать публичный метод Build(T data), который будет передавать в UserControl объект, который нужно редактировать.
  • Когда пользователь выберет объект для редактирования, создайте нужный UserControl, вызовите у него метод Build() и положите контрол в pnProperties.

Пояснения

Реализация редакторов свойств через UserControl позволяет:

1) Разделить логику приложения на независимые части. Код отображения, парсинга и верификации объекта будет сосредоточен в одном отдельном контроле.

2) Реализуя редакторы свойств в отдельных контролах мы разгружаем код главной формы, делаем его более простым и легко модифицируемым. В идеале - главная форма вообще не должна содержать кода. Весь функционал должен быть сосредоточен в UserControls, лежащих на форме.

3) Разработанный единожды контрол можно использовать во многих местах, где вам потребуется редактирование объектов данного типа.

4) Если класс модели данных меняется, вам нужно будет изменить код только соответствующего UserControl. Код главной формы, остальных форм и контролов - останется прежним.

5) UserControl позволяют распределить разработку GUI между несколькими программистами.

Пример

Главная форма:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public partial class MainForm : Form
    {
        ....
 
        private void tvMain_AfterSelect(object sender, TreeViewEventArgs e)
        {
            //очищаем панель свойств
            pnProperties.Controls.Clear();
            //
            if(e.Node.Tag != null)
            {
                if (e.Node.Tag is Person) new PersonPanel { Parent = pnProperties, Dock = DockStyle.Fill, Applied = OnPropertyPanelApplied }.Build(e.Node.Tag as Person);
                if (e.Node.Tag is CreditCard) new CreditCardPanel { Parent = pnProperties, Dock = DockStyle.Fill, Applied = OnPropertyPanelApplied }.Build(e.Node.Tag as CreditCard);
            }
        }
 
        private void OnPropertyPanelApplied(object sender, EventArgs e)
        {
            if (tvMain.SelectedNode.Tag != null)
                tvMain.SelectedNode.Text = tvMain.SelectedNode.Tag.ToString();
        }
    }
Один из UserControl:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    public partial class PersonPanel : UserControl
    {
        public Person Data { get; private set; }
 
        public EventHandler Applied = delegate { };
 
        public PersonPanel()
        {
            InitializeComponent();
        }
 
        public void Build(Person person)
        {
            Data = person;
 
            tbName.Text = Data.Name;
            dtpBirthday.Value = Data.BirthDate;
        }
 
        public void Apply()
        {
            Data.Name = tbName.Text;
            Data.BirthDate = dtpBirthday.Value;
        }
 
        protected void OnApplied()
        {
            Applied(this, EventArgs.Empty);
        }
 
        private void btApply_Click(object sender, EventArgs e)
        {
            try
            {
                Apply();
                OnApplied();
            }catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
    }
Проект целиком: Example4.zip

Упражнение 1

Измените приведенный выше пример так, что бы кнопка Apply была активна только если пользователь изменил поля редактируемого объекта.

Упражнение 2

Измените приведенный выше пример так, что бы кнопки Apply не было, а измененные поля автоматически сохранялись в модель данных.
Подсказка 1

Используйте событие Validating для UserControl.

Подсказка 2

Используйте публичный метод Apply у UserControl. Создайте интерфейс IPropertyPanel.

Решение: Example5.zip

Упражнение 3

Сделайте так, что бы в редакторе свойств CareditCard можно было редактировать свойства держателя карты (класс Person). Редактирование свойств должно быть реализовано через диалоговое окно. Убедитесь, что UserControl для Person может быть использован как внутри главной формы, так и в диалоговых окнах.

Архитектура ПО в WinForms (FAQ & HowTo)
20
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 18:43  [ТС] #10
HowTo: Как сделать фильтрацию и сортировку грида в виртуальном режиме

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

Замечание

Если вы используете грид в режиме Data Binding c DataTable (то есть не виртуальный режим), то фильтрация, сортировка и поиск легко реализуются с помощью методов класса DataView.

Пояснения

1) Виртуальный режим позволяет удобно отображать большие объемы информации, связывая непосредственно модель данных и контролы. Однако, если связывать данные напрямую, то будет невозможным режим фильтрации данных. Для этого создается класс-обертка, которая содержит те же объекты, что и исходная модель данных, но отдает эти объекты в другом порядке и не все а только подходящие по фильтру.

Пример 1

Главная форма:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
    public partial class MainForm : Form
    {
        private Persons persons;
        private PersonsView personsView;
 
        public MainForm()
        {
            InitializeComponent();
 
            BuildDataSource();
            UpdateInterface();
        }
 
        private void BuildDataSource()
        {
            //создаем модель данных
            persons = new Persons();
            var rnd = new Random();
 
            for (int i = 0; i < 50000; i++)
            {
                persons.Add(new Person("Bob" + i, new DateTime(rnd.Next(1950, 1999), 3, 24)));
                persons.Add(new Person("John" + i, new DateTime(rnd.Next(1950, 1999), 5, 14)));
                persons.Add(new Person("Sam" + i, new DateTime(rnd.Next(1950, 1999), 1, 10)));
                persons.Add(new Person("David" + i, new DateTime(rnd.Next(1950, 1999), 8, 21)));
            }
 
            //создаем вспомогательный класс 
            personsView = new PersonsView(persons);
        }
 
        //обновляем интерфейс
        void UpdateInterface()
        {
            if (dgvMain.RowCount != personsView.Count)
            {
                dgvMain.RowCount = 0;
                dgvMain.RowCount = personsView.Count;
            }
            dgvMain.Invalidate();
            lbCount.Text = "Records: " + personsView.Count;
        }
 
        private void dgvMain_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
        {
            switch(e.ColumnIndex)
            {
                case 0: e.Value = personsView[e.RowIndex].Name; break;
                case 1: e.Value = personsView[e.RowIndex].Birthday.ToShortDateString(); break;
                case 2: e.Value = personsView[e.RowIndex].Age; break;
            }
        }
 
        private void dgvMain_CellValuePushed(object sender, DataGridViewCellValueEventArgs e)
        {
            try
            {
                switch (e.ColumnIndex)
                {
                    case 0: personsView[e.RowIndex].Name = e.Value.ToString(); break;
                    case 1: personsView[e.RowIndex].Birthday = DateTime.Parse(e.Value.ToString()); break;
                }
                UpdateInterface();
            }catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
 
        private void btSortByAge_Click(object sender, EventArgs e)
        {
            //сортируем view
            personsView.SortByAge();
            //обновляем интерфейс
            UpdateInterface();
        }
 
        private void btFilterByAge_Click(object sender, EventArgs e)
        {
            try
            {
                //парсим
                var from = int.Parse(tbFromAge.Text);
                var to = int.Parse(tbToAge.Text);
                //фильтруем view
                personsView.FilterByAge(from, to);
                //обновляем интерфейс
                UpdateInterface();
            }catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
    }
Класс PersonsView:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
    //Оберка над Persons с поддержкой фильтрации и сортировки
    public class PersonsView
    {
        //содержит исходную коллекцию данных
        private Persons persons;
        //содержит отсортированную и/или отфильтрованную коллекцию данных
        private List<Person> items = new List<Person>();
 
        public PersonsView(Persons persons)
        {
            this.persons = persons;
            Build();
        }
 
        public int Count
        {
            get { return items.Count; }
        }
 
        public void SortByAge()
        {
            //сортирвем свой список по полю age
            items.Sort((p1, p2) => p1.Age.CompareTo(p2.Age));
        }
 
        public void FilterByAge(int fromAge, int toAge)
        {
            //очищаем свой список
            items.Clear();
            //формируем список заново из тех объектов, который подходят под фильтр
            foreach (var p in persons)
                if (p.Age >= fromAge && p.Age <= toAge)
                    items.Add(p);
        }
 
        public void Build()
        {
            items.Clear();
            items.AddRange(persons);
        }
 
        public Person this[int index]
        {
            get { return items[index]; }
        }
    }
Проект целиком: Example6.zip

Пример 2 (альтернативная реализация view)

Данная реализация PersonsView более эффективна: промежуточный список не используется. Сортируется и фильтруется исходный список.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    //Обертка над Persons с поддержкой фильтрации и сортировки
    public class PersonsView
    {
        //содержит исходную коллекцию данных
        private Persons persons;
 
        public int Count { get; private set; }
 
        public PersonsView(Persons persons)
        {
            this.persons = persons;
            Build();
        }
 
        public void SortByAge()
        {
            //сортируем исходный список по полю age (только ту часть, что отфильтрована)
            persons.Sort(0, Count, new AgeComparer());
        }
 
        class AgeComparer: IComparer<Person>
        {
            public int Compare(Person p1, Person p2)
            {
                return p1.Age.CompareTo(p2.Age);
            }
        }
 
        public void FilterByAge(int fromAge, int toAge)
        {
            //сортируем исходный список так, что в начале будут объекты, которые подходят под фильтру
            Count = 0;
            for (int i = 0; i < persons.Count; i++)
            {
                var p = persons[i];
                if (p.Age >= fromAge && p.Age <= toAge)
                {
                    //элементы, подходяие по фильтру - ставим в начало списка
                    var temp = persons[Count];
                    persons[Count] = p;
                    persons[i] = temp;
                    //увеличиваем счетчик найденных элементов 
                    Count++;
                }
            }
        }
 
        public void Build()
        {
            Count = persons.Count;
        }
 
        public Person this[int index]
        {
            get { return persons[index]; }
        }
    }
Проект целиком: Example7.zip

Упражнение 1

Используя один из примеров приведенных выше, реализуйте отображение Persons с возможностью фильтрации и сортировки, но не в DataGridView, а в ListView в виртуальном режиме.

Упражнение 2

Используя один из примеров приведенных выше, реализуйте поиск Person по имени. Найденная запись должна стать выделенной, а грид должен прокрутиться к выделенной записи.


Архитектура ПО в WinForms (FAQ & HowTo)
20
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
21.10.2015, 18:46  [ТС] #11
HowTo: Как реализовать Save, SaveAs, Open, New

Просто делайте как описано в примере :o)

Пояснение

1) Приведенный в примере код контролирует имя текущего файла, состояние документа (изменен/не изменен) и в зависимости от этого управляет активностью пунктов меню save, saveAs, диалогов сохранения, открытия файла и т.д.

2) Код запрашивает сохранение документа при попытке заменить его новым, или при выходе из программы. При этом учитывается был ли в документ изменен. Также выдается повторный запрос на сохранение, если при сохранении возникли ошибки.

Пример

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
using System;
using System.IO;
using System.Windows.Forms;
 
namespace ExmapleNS
{
    public partial class MainForm : Form
    {
        private bool dataChanged = false;
        private string currentFileName;
 
        public MainForm()
        {
            InitializeComponent();
 
            UpdateInterface();
        }
 
        void UpdateInterface()
        {
            miSave.Enabled = dataChanged;
            Text = currentFileName ?? "New document";
        }
 
        //сохраняем документ (save or saveAs)
        DialogResult Save(string fileName)
        {
            try
            {
                if (fileName == null)
                {
                    //save as...
                    var sfd = new SaveFileDialog() {Filter = "Text file|*.txt"};
                    if (sfd.ShowDialog() == DialogResult.OK)
                        fileName = sfd.FileName;
                    else
                        return DialogResult.Cancel;
                }
                //сохраняем данные
                File.WriteAllText(fileName, tbMain.Text);
                //запоминаем имя файла, куда сохранили
                currentFileName = fileName;
                dataChanged = false;//сбрасываем флаг измененных данных
                //обновляем интерфейс
                UpdateInterface();
                //
                return DialogResult.OK;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                return DialogResult.No;
            }
        }
 
        private void miNew_Click(object sender, EventArgs e)
        {
            OnDocumentClosing();//спрашиваем пользователя, не хочет ли он сохранить текущий документ
 
            tbMain.Clear();
            currentFileName = null;
            dataChanged = false;
 
            UpdateInterface();
        }
 
        private void miSave_Click(object sender, EventArgs e)
        {
            Save(currentFileName);//save or save as ...
        }
 
        private void miSaveAs_Click(object sender, EventArgs e)
        {
            Save(null);//save as ...
        }
 
        private void miOpen_Click(object sender, EventArgs e)
        {
            OnDocumentClosing();//спрашиваем пользователя, не хочет ли он сохранить текущий документ
            var ofd = new OpenFileDialog { Filter = "Text files|*.txt" };
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                //сохраняем данные
                tbMain.Text = File.ReadAllText(ofd.FileName);
                //запоминаем имя файла, куда сохранили
                currentFileName = ofd.FileName;
                dataChanged = false;//сбрасываем флаг измененных данных
                //обновляем интерфейс
                UpdateInterface();
            }
        }
 
        //вызываем этот метод при закрытии документа
        void OnDocumentClosing()
        {
            if(dataChanged)
            while(MessageBox.Show("Сохранить текущий документ?", "Сохранение", MessageBoxButtons.OKCancel) == DialogResult.OK)
            {
                if (Save(currentFileName) != DialogResult.No)//if no exceptions
                    break;
            }
        }
 
        protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
        {
            OnDocumentClosing();//спрашиваем пользователя, не хочет ли он сохранить текущий документ
            base.OnClosing(e);
        }
 
        private void tbMain_TextChanged(object sender, EventArgs e)
        {
            dataChanged = true;
            UpdateInterface();
        }
    }
}
Проект целиком: Example3.zip

Упражнение

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


Архитектура ПО в WinForms (FAQ & HowTo)
20
Storm23
Эксперт .NETАвтор FAQ
5421 / 3278 / 1001
Регистрация: 11.01.2015
Сообщений: 4,363
Записей в блоге: 27
26.10.2015, 12:48  [ТС] #12
HowTo: Как сделать Undo, Redo

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

1) Создать интерфейс ICommand следующего вида:
ICommand
C#
1
2
3
4
5
6
    public interface ICommand
    {
        string Name { get; }
        void Execute();
        void UnExecute();
    }

2) Создать команды, реализующие ICommand для каждого действия, изменяющего модель данных. Любые изменения в модель можно будет вносить только(!) через команды. Пример команды, меняющей цвет прямоугольника в графическом редакторе:
ChangeColorCommand
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    public class ChangeColorCommand : ICommand
    {
        private Rects rects;
        private Color color;
        private Color prevColor;
 
        public ChangeColorCommand(Rects rects, Color color)
        {
            this.rects = rects;
            this.color = color;
        }
 
        public string Name
        {
            get { return "Change color"; }
        }
 
        public void Execute()
        {
            //запоминаем предыдущий цвет
            prevColor = rects.LastRect.Color;
            //присваиваем новый цвет
            rects.LastRect.Color = color;
            //сигнализируем об изменениях
            rects.OnChanged();
        }
 
        public void UnExecute()
        {
            //возвращаем предыдущий цвет
            rects.LastRect.Color = prevColor;
            //сигнализируем об изменениях
            rects.OnChanged();
        }
    }

Каждая команда должна уметь выполнять определенное действие над моделью данных и должна уметь отменять свое действие - таким образом приводя модель в исходное состояние.

3) Создать менеджер Undo/Redo, который будет содержать стек команд для Undo, и стек команд для Redo:
UndoRedoManager
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    public class UndoRedoManager
    {
        Stack<ICommand> UndoStack { get; set; }
        Stack<ICommand> RedoStack { get; set; }
 
        public void Undo()
        {
            if(UndoStack.Count > 0)
            {
                //изымаем команду из стека
                var command = UndoStack.Pop();
                //отменяем действие команды
                command.UnExecute();
                //заносим команду в стек Redo
                RedoStack.Push(command);
                //сигнализируем об изменениях
                StateChanged(this, EventArgs.Empty);
            }
        }
 
        public void Redo()
        {
            if (RedoStack.Count > 0)
            {
                //изымаем команду из стека
                var command = RedoStack.Pop();
                //выполняем действие команды
                command.Execute();
                //заносим команду в стек Undo
                UndoStack.Push(command);
                //сигнализируем об изменениях
                StateChanged(this, EventArgs.Empty);
            }
        }
 
        //выполняем команду
        public void Execute(ICommand command)
        {
            //выполняем команду
            command.Execute();
            //заносим в стек Undo
            UndoStack.Push(command);
            //очищаем стек Redo
            RedoStack.Clear();
            //сигнализируем об изменениях
            StateChanged(this, EventArgs.Empty);
        }
 
        ....
    }


Когда пользователь хочет изменить некое свойство модели, пользовательский интерфейс должен создать соответствующую команду и выполнить метод Execute() у менеджера UndoRedoManager.
При выполнении команды, менеджер выполняет команду и кладет ее в UndoStack. При этом RedoStack очищается.
Когда необходимо отменить последнее действие, UndoRedoManager изымает команду из UndoStack отменяет ее действие, и кладет ее в RedoStack.
Когда необходимо повторно выполнить отмененное действие (т.е. Redo) - UndoRedoManager изымет команду из RedoStack, выполняет ее и кладет в UndoStack.

Более подробно смотрите в прилагаемом примере.

Замечания

1) Если вы планируете реализовывать Undo/Redo в своем приложении, вы должны заранее реализовывать архитектуру соответствующим образом - применяя паттерн Command. Если же вы уже реализовали модель, а потом захотите прикрутить к ней Undo/Redo, то скорее всего ничего не получится.

2) Обратите внимание на то, что через Command должны производиться любые изменения модели. Если вы будете менять свойства модели напрямую, операция Undo не сможет нормально выполняться.

3) В примере описывается undo/redo через паттерн Command. В принципе, возможна и более простая альтернатива, без Command. Смысл ее в том, что перед каждым действием модель полностью сериализуется и сохраняется в стеке undo. Таким образом, в стеке сохраняется полное состояние модели. При отмене действия - модель полностью заменяется на восстановленную копию. Недостаток этого метода очевиден - слишком большие расходы памяти на стек, длительный процесс сериализации/десериализации.

4) В примере реализован бесконечный стек Undo. Но на практике эти стек Undo нужно ограничивать. В примере это не реализовано для упрощения кода. Если вы работаете в FW 4.0 и старше - можно использовать BlockingCollection для реализации ограниченного стека.


В примере реализован простой графический редактор, в котором можно создавать прямоугольники, двигать их и менять цвет. Реализован паттерн Command, реализованы операции Undo, Redo а также множественное Undo и множественное Redo.
33
Миниатюры
Архитектура ПО в WinForms (FAQ & HowTo)  
Вложения
Тип файла: zip Example12.zip (82.9 Кб, 130 просмотров)
26.10.2015, 12:48
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
26.10.2015, 12:48
Привет! Вот еще темы с ответами:

Нужен драйвера, код PCI\VEN_1039&DEV_7012&SUBSYS_0C98105B&REV_A0\3&B1BFB68&0&17 - Звук, акустика
всем привет !! уменя нет звука нужен драйвер код PCI\VEN_1039&amp;DEV_7012&amp;SUBSYS_0C98105B&amp;REV_A0\3&amp;B1BFB68&amp;0&amp;17 скачал прогу их много ну все...

немогу найти драйвера на PCI\VEN_1039&DEV_7012&SUBSYS_810D1043&REV_A0\3&61AAA01&0&17 - Звук, акустика
Мультимедиа аудиоконтроллер PCI\VEN_1039&amp;DEV_7012&amp;SUBSYS_810D1043&amp;REV_A0\3&amp;61AAA01&amp;0&amp;17

Драйвера на PCI\VEN_10B7&DEV_1700&SUBSYS_80EB1043&REV_12\4&2E98101C&0&28 F0 - Звук, акустика
помогите плиз - вот ID: PCI\VEN_10B7&amp;DEV_1700&amp;SUBSYS_80EB1043&amp;REV_12\4&amp;2E98101C&amp;0&amp;28F0 Заранее...

Кто встречался с таким, подскажите - (#206;&#225;&#250;&#229;&#234;&#242;) - C# ASP.NET
Если на asp-странице происходит ошибка, то сервер выдает сообщение вот примерно в таком виде: &lt;p&gt;Îáúåêò Response&lt;/font&gt; &lt;font...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
12
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2017, vBulletin Solutions, Inc.
Рейтинг@Mail.ru