|
|
|
Архитектура ПО в WinForms (FAQ & HowTo)21.10.2015, 17:24. Показов 178033. Ответов 12
Метки нет (Все метки)
Архитектура ПО в WinForms (FAQ & HowTo)
В этом FAQ в основном обсуждаются вопросы проектирования пользовательского интерфейса (GUI) и взаимодействия интерфейса с моделью данных. Приводятся также некоторые типовые решения проектирования интерфейса для WinForms. Это не FAQ по паттернам проектирования. Несмотря на то, что архитектура GUI имеет отношение к паттернам MVC, MVP, MVVC и другим, здесь эти паттерны не рассматриваются. Данное руководство нацелено на соблюдение элементарных правил составления программ, что-то вроде гигиены кода. Соблюдение правил описанных ниже позволит вам разрабатывать простой, красивый, расширяемый и эффективный код для ваших приложений. Приведенные решения не являются догмой, которые всегда и везде нужно использовать. Всегда бывают исключения, всегда есть альтернативные подходы. Тем не менее, помните: наличие хоть какой-нибудь архитектуры лучше, чем полное ее отсутствие. Просьба все замечания и вопросы постить в отдельной теме: https://www.cyberforum.ru/faq/thread1558546.html
55
|
|
| 21.10.2015, 17:24 | |
|
Ответы с готовыми решениями:
12
query='SELECT * FROM resume WHERE ' & ''' & RecSet('place')& ''' & '=' & '''& s_loc &''' & - что не так? MVVM & WinForms error '80020009' Îøèáêà. /lalala/profile.asp, line 28 |
|
|
|
| 21.10.2015, 17:29 [ТС] | |
|
Разделение данных и интерфейса
Главное что вы должны знать об архитектуре ПО – это то, что пользовательский интерфейс и данные должны быть разделены. Самое плохое что вы можете сделать – это разместить весь код программы в Form1.cs. Ответьте себе на вопрос – зачем нужна ваша программа? Каков ее функционал? Функция любой программы – это хранение и обработка данных. Именно так вы должны смотреть на свою программу – как на обработчик данных. Интерфейс же хоть и большая, но не самая главная часть программы. Наверняка функция вашей программы – не нажатие на кнопки. Начните разработку вашей программы с составления модели данных – классов, который будут хранить и обрабатывать ваши данные. Но не думайте о программе как о наборе формочек! Формочки зависят от данных а не наоборот. Разработайте модель данных, оформите ее в отдельные классы, а затем уже подумайте об интерфейсе. Программа в представлении новичка выглядит так: А должна программа выглядеть вот так: Модель данных должна быть независима от GUI. Никогда не передавайте контролы в модель данных, и не возвращайте контролы из нее. Модель вообще не должна ничего знать об интерфейсе. Не забывайте, что интерфейсов может быть много, а модель всегда одна. Интерфейса вообще может не быть, и ваша модель может работать как сервис. Более того, сегодня ваша программа работает локально, в связке с интерфейсом, а завтра вы захотите сделать серверный вариант вашей программы. Отдельная и независимая модель данных позволит вам это сделать. Сильно привязанная к GUI – нет. Процессы в программе с точки зрения новичка: А должны процессы выглядеть так: Антрипримеры (так делать нельзя!)
Ниже приведены довольно типичные посты, в которых нет разделения данных и интерфейса, из-за чего у авторов возникают проблемы: Крестики нолики - автор делает крестики-нолики. Данные хранит прямо в пикчербоксах ![]() Подсчет количества чисел в 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) Проверка корректности данных – может быть сложной задачей, и эта задача является частью модели предметной области. Роль интерфейса – пассивна и проста. Получил данные – показал данные – принял данные – передал в модель данных. Все. Сложные манипуляции с данными и их проверку – вне компетенции интерфейса.
62
|
|
|
|
||||||||||||||||||||||||||
| 21.10.2015, 17:33 [ТС] | ||||||||||||||||||||||||||
|
С чего нужно начинать разработку приложения?
С модели данных. Не думайте о приложении как о наборе кнопок или гридов. Не начинайте разработку с 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 доменная модель будет выглядеть следующим образом:
А в модели Anemic решение уравнения выносится в отдельный класс:
Если объекты доменной модели – сложные, а алгоритмы обработки – простые, используйте Rich модель. Если объекты модели данных – простые, а алгоритмы обработки – сложные, используйте Anemic модель. Замечания
1) Некоторые классики теории программирования выступают категорически против модели Anemic (например Мартин Фаулер) 2) В реальных программах редко встречается чистый Rich или чистый Anemic. Зачастую есть некая смесь с преобладанием одного из подходов. 3) Не стоит считать что Rich модель подразумевает что все вычисления должны быть только внутри объекта доменной модели. Rich также может создавать сервисные классы. Но Rich модель разрешает реализацию методов внутри доменного объекта, а Anemic этого всячески избегает. Упражнение 1
Создайте модель данных для следующей задачи: Разработать систему денежных переводов банка. У каждого клиента банка есть один или несколько счетов. Необходимо разработать систему, позволяющую делать денежные переводы с одного счета на другой. Упражнение 2
Сделайте предыдущее упражнение, но с учетом того, что счета могут быть в разных валютах. Упражнение 3
Сделайте Упражнение 2, но при этом в банке должна храниться история всех транзакций (переводов) с возможностью отмены уже выполненного перевода. Практические советы по реализации классов доменной модели Не ленитесь делать классы – контейнеры. Например, если ваша модель содержит список студентов, то лучше разработать отдельный класс
Используйте те контейнерные классы, которые наиболее подходят вашей модели. Не забывайте, что 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. Аналогично – номер телефона. Обратите внимание на то, что те идентификаторы, которые сейчас выглядят как числа, завтра могут быть представлены с буквенным префиксом. Если у вас они хранились как числа – возникнут проблемы. Другой случай – длина числового идентификатора выросла и теперь она у вас не помещается в числовой тип – снова проблема. Как определить, что это идентификатор, а не число? Просто задайте себе вопрос – имеет ли смысл складывать или вычитать данные числа? Если смысла нет – значит это не числа, а идентификаторы. Например, может ли нам понадобиться складывать или вычитать номера банковских карт? Скорее всего нет. Значит это – не числа.
66
|
||||||||||||||||||||||||||
|
|
||||||||||||||||
| 21.10.2015, 17:47 [ТС] | ||||||||||||||||
|
Использование UserControl
UserControl – лучшее решение для реализации интерфейсов в WinForms. Пользовательский контрол позволяет разбить GUI на независимые части, где каждый UserControl отвечает только за отображение одного объекта модели данных. Код в таком случае становится более простым, лаконичным и однородным. Также:
Начинающие программисты редко используют UserControl, видимо считая их сложными. На самом деле создание UserControl ничем не отличается от создания новой формы. Разница лишь в том, что форма может быть открыта в виде отдельного окна, а контрол – нужно положить на другую форму. Практическая реализация Пусть нам нужно отобразить объект данных класса Data. Наиболее простой вариант пользовательского контрола может быть таким: Пример
Код содержит свойство Data, хранящее редактируемый объект, и два метода: Build(Data data) и UpdateData(). Первый из них – заносит данные из Data в контролы. Второй – наоборот – заносит данные из контролов в объект Data. Оба метода, и свойство – публичные, и могут вызываться извне. Для использования этого UserControl, нужно разместить его на форме и в определенный момент вызвать метод Build(), передав ему объект для отображения (например после того, как пользователь кликнул на элемент в списке объектов). На событие Validating контрола – создать обработчик такого вида: Пример
Этот код будет заносить изменения в объект при выходе пользователя из UserControl. Возможен другой вариант – с кнопкой Apply/Save/Ok. Тогда нужно вызвать метод UpdateData из обработчика нажатия кнопки. Примеры UserControl смотрите также в главе "Как сделать панель свойств" и в упражнениях к этой главе. Замечания
1) Обрабатывать ошибки ввода (try/catch) можно и непосредственно внутри метода UpdateData(). Но в таком случае, метод должен возвращать bool сигнализирующий о том, что метод отработан удачно. 2) Для оповещения об изменениях в объекте в UserControl можно сделать событие DataChanged. 3) Обратите внимание, что созданный UserControl появляется в Toolbox VisualStudio только после того как приложение было скомпилировано. 4) Не передавайте объект данных в конструктор UserControl. Во-первых потому что один контрол может быть использован для редактирования нескольких разных объектов, а во-вторых, обновление контрола может быть вызвано несколько раз даже для одного и того же объекта. Кроме того, если ваш конструктор будет иметь параметры, вы не сможете редактировать его в дизайнере форм. Advanced features
Часто бывает нужно изменять свойства объекта сразу после внесения изменений (например, менять свойство объекта непосредственно в процессе набора текста в TextBox). Тогда можно реализовать следующую схему: Пример
Счетчик updating используется для предотвращения зацикливания программы при обновлении данных в контролах. Упражнение 1
Создайте приложение, которое считывает все картинки из папки Изображения (используйте метод Environment.GetFolderPath(Environment.Sp ecialFolder.MyPictures)), отображает на форме все картинки, рядом с каждой картинкой выводится имя файла, и размер файла. Имя файла можно менять, соответствующий файл должен переименовываться. Общий вид приложения: Решение: Example8.zip Упражнение 2
Создайте приложение для расчета напряжения по известным значениям сопротивления и силы тока (Закон Ома: U = IR). Интерфейс программы должен позволять производить одновременный расчет для трех наборов параметров. Также программа должна вычислять и отображать суммарное напряжение для всех наборов данных (U = U1 + U2 + U3). Запретить ввод сопротивления меньше 0. Общий вид приложения: Решение: Example9.zip Упражнение 3
Если вы реализовали Упражнение 2 с моделью Rich, то переделайте его на модель Anemic. Если у вас была модель Anemic (ну а вдруг)), то переделайте на модель Rich. (Что такое Rich и Anemic смотрите в главе "Как разработать модель данных?") Решение для Anemic: Example10.zip Решение для Rich преведено в предыдущем упражнении.
45
|
||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| 21.10.2015, 17:57 [ТС] | ||||||||||||||||||||||||||||||||||||||||||||||
|
Именование контролов
Давайте контролам осмысленные названия, отражающие суть действия или имя объекта, которые они хранят. Не оставляйте названия типа 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 – для форм. Например, если вам нужно создать и открыть форму логина, пишите так:
Пояснения
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 контрола. Это имя не нужно для работы контрола. Оно может потребоваться только если вы хотите делать поиск контролов на форме по имени заданному как строка. Но это плохой подход к работе с контролами. Если вам нужно создать контрол динамичеки и затем вы хотите обрщаться к нему, то создайте поле в вашей форме и обращайтесь к нему:
Где и как обрабатывать исключения Пишите блок try/catch только в интерфейсе программы - в обработчиках нажатия кнопок или других контролов. Не перехватывайте исключения в теле методов модели данных или бизнес-логики. Не оставляйте блок catch пустым – выводите сообщение пользователю в виде MessageBox.Show(ex.Message). Пояснения
1) Если класс не может самостоятельно решить проблему с исключением, он не должен его обрабатывать. Обработка такого исключения – является просто замалчиванием проблемы. Например, если уровень DAL не может отправить данные в БД и просто отловил исключение, то вышестоящие уровни (в том числе GUI и пользователь) будут уверены, что данные отправились. Что недопустимо. Если же класс не перехватил исключение, оно автоматически будет пробрасываться вверх, вышестоящему слою. А самым вышестоящим слоем является GUI. Здесь исключение нужно перехватить и сообщать о нем пользователю. Неперехват исключения в GUI означает аварийное закрытие приложения. 2) Если у вас серверное приложение – исключение может быть отловлено самым вышестоящим слоем и отправлено пользователю по сети. 3) Перехват внутри классов модели возможен, если класс, который его перехватывает, может исправить ситуацию, либо исключение является частью логики программы. Заметьте также, что механизм исключений в C# является очень медленным и использовать исключения для логики программы – не рекомендуется. Пример
Антипример (так делать нельзя!)
Упражнение
Напишите правильную реализацию метода из антипримера. Решение
Все исключения пробрасываются в вызывающий уровень:
Advanced features
1) Довольно утомительно в каждом обработчике писать однотипный код try{…}catch(Exception ex){ MessageBox.Show(ex.Message);}. Для того что бы облегчить себе жизнь, можно обрабатывать событие Application.ThreadException, в котором показывать пользователю сообщение об ошибке. После обработки данного события, главный поток вернет управление в форму и приложение продолжит работу. Для срабатывания этого события не забывайте предварительно вызвать Application.SetUnhandledExceptionMode(Un handledExceptionMode.CatchException); в методе Main() из Program.cs. 2) Часто исключения обернуты друг в друга и содержатся в свойстве Exception.InnerException. В таком случае имеет смысл показывать пользователю самый глубокий Exception, именно тот, который содержит изначальную причину:
4) Не используйте генерацию исключения для выхода из приложения (например при неправильно введенном пароле). Исключение не всегда приводит к завершению приложения: может быть выдано окно в котором будет кнопка "Продолжить" и пользователь сможет продолжить работу в программе. А как же обрабатывать исключения в потоках? Отловите исключение с помощью try/catch в главном методе потока. В обработчике catch вызовите некий метод формы для информирования пользователя о проблеме. Пояснения
1) Не UI потоки не могут пробросить исключение в главный GUI поток приложения. Поэтому вы должны отловить исключение в главном методе потока и вызвать метод формы, который сообщит пользователю о проблеме. Поскольку исключение происходит не в главном потоке, то метод формы должен проверять InvokeRequired и делать перевызов себя через Invoke. 2) Вы должны обрабатывать все исключения, кроме ThreadAbortException и AppDomainUnloadedException. Первое возникает при остановке потока через метод Abort() и обрабатывается самим объектом Thread. Второе исключение возникает при выгрузке домена и его тоже обрабатывать не следует. Пример
Advanced features
1) Необработанные исключения в потоках обычно приводят к завершению приложения. Но это поведение можно изменить, если прописать в app.config:
Так делать не рекомендуется, но если очень хочется...
42
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 21.10.2015, 18:05 [ТС] | ||||||||||||||||||||||||||
|
Где нужно парсить строки
Парсинг строк, введенных пользователем - делайте только в интерфейсе программы (в формах или UserControl). Не передавайте нераспарсенные строки в объекты доменной модели. Передавайте туда только распарсенные данные. Не делайте преобразования точек в запятые или наоборот при парсинге дробных чисел. Парсинг текстовых файлов делайте в модели данных. Используйте инвариантную культуру CultureInfo.InvariantCulture при парсинге. Пояснения
1) Парсинг введенных данных – не функция доменной модели или бизнес-логики. Модель работает с уже распарсенными объектами. Парсинг – функция GUI. 2) В процессе парсинга возможны ошибки ввода. Эти ошибки легче отлавливать и контролировать в слое GUI. 3) Сам парсинг зависит от настроек системы и пользовательского интерфейса. Например, разделителем дробной части числа может быть и точка и запятая. От этих настроек зависит правильное преобразование строки в число. Но это совсем не забота модели данных. Модель данных никак не должна зависеть от настроек пользовательского интерфейса. Да это и не всегда возможно – например если модель данных находится на удаленном сервере, который не знает настроек локальной машины и не может правильно распарсить строки в принципе. 4) Почему не нужно делать преобразования точек в запятые или наоборот: Во-первых пользователь обязан вводить числа в том формате, который указан в настройках его системы. Во-вторых, замена запятых на точки не гарантирует правильность чисел и может привести к трудноуловимым ошибкам. Например, в американском стандарте точка является разделителем дробной части числа, а запятая - разделителем разрядов числа. То есть такое число вполне корректно: 10,000.00. Если же вы принудительно замените точки на запятые - число станет некорректным ни в одной культуре.5) Исключение составляет парсинг файлов (например CSV). Это не является частью GUI, поэтому парсингом файлов может заниматься модель данных. При этом, парсинг не должен зависеть от настроек системы или GUI. Ведь файлы могут быть получены с другой машины, с другими настройками. Да и сама модель данных – может находиться на сервере. При этом конечно подразумевается, что данные файла сохранены в InvariantCulture (как и должно быть). Пример (парсинг пользовательского ввода дробного числа)
Пример (парсинг файла данных - делается в доменной модели)
Добавлено через 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, которые будет вызываться когда пользователь будет пытаться покинуть пределы контрола. Пример
Антипример (так делать не нужно!)
Упражнение
Укажите потенциальные проблемы кода, приведенного в антипримере. Ответ
1) Невозможность ввести отрицательное значение, а также значение в экспоненциальном формате (1e-02). 2) Невозможно использовать в поле ввода клавиши Backspace, Ctrl-A, Ctrl-V, Ctrl-C и т.д. 3) Код позволяет вводить дробные числа только если разделитель дробной части – запятая. Если в настройках Windows стоит иной разделитель – например точка – код работать не будет. 4) В методе btSave_Click не обрабатываются исключения (а они возможны, например если пользователь оставил поле ввода пустым и нажал Save). Advanced features
1) Если у вас несколько полей на форме, и вы хотите выдавать в сообщении имя поля, где произошла ошибка, то вам придется писать отдельные блоки try/catch для каждого поля. Это неудобно. Простым, но немного костальным решением может быть такое:
32
|
||||||||||||||||||||||||||
|
|
|||||||||||
| 21.10.2015, 18:16 [ТС] | |||||||||||
|
Виртуальный режим работы контролов
В виртуальном режиме контролы не хранят данные а запрашивают их из модели данных непосредственно в момент прорисовки. Это наиболее простой и эффективный способ отображения данных модели в контрол. Виртуальный режим имеет следующие преимущества:
Пример
Отображение списка 1 млн точек типа Point в DataGridView в виртуальном режиме:
Упражнение
В приведенном примере сделайте кнопки добавления и удаления записи. Решение
Добавьте обработчики для кнопок:
30
|
|||||||||||
|
|
||||||||||||||||
| 21.10.2015, 18:33 [ТС] | ||||||||||||||||
|
HowTo: Форма добавления/редактирования записи таблицы
В форме редактирования создайте метод Build() который будет заносить значения из DataRow в контролы формы:
В форме редактирования, если пользователь нажимает OK, то пропарсите данные и сделайте DialogResult = DialogResult.OK. В первой форме проверяете, что ShowDialog вернул DialogResult.OK, и если это так - добавляйте созданный DataRow в таблицу. Аналогично поступайте, если ваш источник данных – типизированная коллекция классов модели данных. Тогда вместо DataRow указывайте ваш тип данных. Замечание
Есть также альтернативный (хотя и очень похожий, но более универсальный) способ реализации формы редактирования - через UserControl. Смотрите главу "Как сделать панель свойств" Пояснения
1) Не передавайте в форму другие контролы или формы в качестве параметров. Передавайте в форму только данные. Этим вы уменьшаете зависимость контролов друг от друга. 2) Не передавайте данные в конструктор формы или контрола. Если вы будете это делать, то во-первых форма или контрол не откроются в дизайнере форм. А во-вторых вы не сможете перестроить контролы формы редактирования после того, как форма уже создана (а это часто бывает нужно). 3) Использование метода Build() позволяет вам не пересоздавать форму каждый раз. Вы можете создать форму один раз, а затем показывать ее, предварительно вызвав метод Build() для перестройки ее контролов под новые данные. 4) Рассмотренное решение позволяет использовать одну и ту же форму как для добавления данных, так и для их редактирования. Кроме того, форму редактирования можно вызывать из различных форм. 5) Рассмотренное решение поддерживает отмену операции, если вы нажимаете Cancel на форме (или закрываете ее крестиком). Даже если вы изменили значения в текстбоксах, но нажали Cancel, то введенные значения не попадут в исходные данные. 6) Если вы нажали OK, но данные были введены неправильно и форма ввода выдала сообщение об ошибке, то часть данных может все же попасть в DataRow, даже если вы потом нажмете Cancel. Для того, что бы избежать этой ситуации – необходимо делать клон объекта данных и передавать в форму редактирования его, вместо исходного объекта. Пример
Главная форма
Упражнение
Переделайте приведенный выше пример так, что бы в качестве источника данных выступал список объектов типа List<PointF>, а DataGridView работал в виртуальном режиме. Решение: Example2.zip
30
|
||||||||||||||||
|
|
|||||||||||
| 21.10.2015, 18:37 [ТС] | |||||||||||
|
HowTo: Как сделать панель свойств
Если вы хотите редактировать свойства объектов непосредственно в форме (без открытия диалогового окна), нужно сделать следующее:
Пояснения
Реализация редакторов свойств через UserControl позволяет: 1) Разделить логику приложения на независимые части. Код отображения, парсинга и верификации объекта будет сосредоточен в одном отдельном контроле. 2) Реализуя редакторы свойств в отдельных контролах мы разгружаем код главной формы, делаем его более простым и легко модифицируемым. В идеале - главная форма вообще не должна содержать кода. Весь функционал должен быть сосредоточен в UserControls, лежащих на форме. 3) Разработанный единожды контрол можно использовать во многих местах, где вам потребуется редактирование объектов данного типа. 4) Если класс модели данных меняется, вам нужно будет изменить код только соответствующего UserControl. Код главной формы, остальных форм и контролов - останется прежним. 5) UserControl позволяют распределить разработку GUI между несколькими программистами. Пример
Главная форма:
Упражнение 1
Измените приведенный выше пример так, что бы кнопка Apply была активна только если пользователь изменил поля редактируемого объекта. Упражнение 2
Измените приведенный выше пример так, что бы кнопки Apply не было, а измененные поля автоматически сохранялись в модель данных. Подсказка 1
Используйте событие Validating для UserControl. Подсказка 2
Используйте публичный метод Apply у UserControl. Создайте интерфейс IPropertyPanel. Решение: Example5.zip Упражнение 3
Сделайте так, что бы в редакторе свойств CareditCard можно было редактировать свойства держателя карты (класс Person). Редактирование свойств должно быть реализовано через диалоговое окно. Убедитесь, что UserControl для Person может быть использован как внутри главной формы, так и в диалоговых окнах.
28
|
|||||||||||
|
|
||||||||||||||||
| 21.10.2015, 18:43 [ТС] | ||||||||||||||||
|
HowTo: Как сделать фильтрацию и сортировку грида в виртуальном режиме
Создайте класс-обертку над списком из модели данных, который будет выполнять функции источника данных с фильтрацией и сортировкой. Замечание
Если вы используете грид в режиме Data Binding c DataTable (то есть не виртуальный режим), то фильтрация, сортировка и поиск легко реализуются с помощью методов класса DataView. Пояснения
1) Виртуальный режим позволяет удобно отображать большие объемы информации, связывая непосредственно модель данных и контролы. Однако, если связывать данные напрямую, то будет невозможным режим фильтрации данных. Для этого создается класс-обертка, которая содержит те же объекты, что и исходная модель данных, но отдает эти объекты в другом порядке и не все а только подходящие по фильтру. Пример 1
Главная форма:
Пример 2 (альтернативная реализация view)
Данная реализация PersonsView более эффективна: промежуточный список не используется. Сортируется и фильтруется исходный список.
Упражнение 1
Используя один из примеров приведенных выше, реализуйте отображение Persons с возможностью фильтрации и сортировки, но не в DataGridView, а в ListView в виртуальном режиме. Упражнение 2
Используя один из примеров приведенных выше, реализуйте поиск Person по имени. Найденная запись должна стать выделенной, а грид должен прокрутиться к выделенной записи.
28
|
||||||||||||||||
|
|
||||||
| 21.10.2015, 18:46 [ТС] | ||||||
|
HowTo: Как реализовать Save, SaveAs, Open, New
Просто делайте как описано в примере :o) Пояснение
1) Приведенный в примере код контролирует имя текущего файла, состояние документа (изменен/не изменен) и в зависимости от этого управляет активностью пунктов меню save, saveAs, диалогов сохранения, открытия файла и т.д. 2) Код запрашивает сохранение документа при попытке заменить его новым, или при выходе из программы. При этом учитывается был ли в документ изменен. Также выдается повторный запрос на сохранение, если при сохранении возникли ошибки. Пример
Упражнение
Пользуясь приведенным примером переделайте его так, что бы программа поддерживала редактирование нескольких документов в нескольких вкладках TabControl.
27
|
||||||
|
|
||||||||||||||||
| 26.10.2015, 12:48 [ТС] | ||||||||||||||||
|
HowTo: Как сделать Undo, Redo
Для реализации стека отмены, нужно выполнить следующие пункты: 1) Создать интерфейс ICommand следующего вида: ICommand
2) Создать команды, реализующие ICommand для каждого действия, изменяющего модель данных. Любые изменения в модель можно будет вносить только(!) через команды. Пример команды, меняющей цвет прямоугольника в графическом редакторе: ChangeColorCommand
Каждая команда должна уметь выполнять определенное действие над моделью данных и должна уметь отменять свое действие - таким образом приводя модель в исходное состояние. 3) Создать менеджер Undo/Redo, который будет содержать стек команд для Undo, и стек команд для Redo: UndoRedoManager
Когда пользователь хочет изменить некое свойство модели, пользовательский интерфейс должен создать соответствующую команду и выполнить метод 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.
46
|
||||||||||||||||
|
|
|
| 31.10.2018, 00:05 [ТС] | |
|
9
|
|
| 31.10.2018, 00:05 | |
|
Помогаю со студенческими работами здесь
13
Помогите найти драйвера для pci\ven_8086&DEV_266E&SUBSYS_A002145&REV_05\3&13C0B0C5&0&F2 немогу найти драйвера на PCI\VEN_1039&DEV_7012&SUBSYS_810D1043&REV_A0\3&61AAA01&0&17 Нужен драйвера, код PCI\VEN_1039&DEV_7012&SUBSYS_0C98105B&REV_A0\3&B1BFB68&0&17 Мультимедиа контролер PCI\VEN_14F1&DEV_8800&SUBSYS_EA3D14F1&REV_05\4&25700A26&0&3020 Драйвера на PCI\VEN_10B7&DEV_1700&SUBSYS_80EB1043&REV_12\4&2E98101C&0&28 F0 Искать еще темы с ответами Или воспользуйтесь поиском по форуму: |
|
Новые блоги и статьи
|
||||
|
Новый ноутбук
volvo 07.12.2025
Всем привет.
По скидке в "черную пятницу" взял себе новый ноутбук Lenovo ThinkBook 16 G7 на Амазоне:
Ryzen 5 7533HS
64 Gb DDR5
1Tb NVMe
16" Full HD Display
Win11 Pro
|
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
|
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
|
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов
На странице:
https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/
нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
|
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов.
. . .
|
|
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
|
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
|
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут.
В век Веб все очень привыкли к дизайну Single-Page-Application .
Быстренько разберем подход "на фреймах".
Мы делаем одну. . .
|
Фото: Daniel Greenwood
kumehtar 13.11.2025
|
Расскажи мне о Мире, бродяга
kumehtar 12.11.2025
— Расскажи мне о Мире, бродяга,
Ты же видел моря и метели.
Как сменялись короны и стяги,
Как эпохи стрелою летели.
- Этот мир — это крылья и горы,
Снег и пламя, любовь и тревоги,
И бескрайние. . .
|