[recovery mode] Использование подхода MVC в WinForms для самых маленьких
В статье описаны общие принципы построения приложений, с использованием подхода MVC, на примере внедрения в приложение, использующее много лет Code Behind подход.Не будет: • Классического Unit Test; • Принижения влияния Code Behind; • Неожиданных открытий в MVC.
Будет: • Unit Test и Stub; • MVC; • Внедрение подхода на базе существующего продукта.
Перед тем, как начать использовать данных подход, я прошел большой путь обычного разработчика корпоративных систем. По пути мне встречались абсолютно шаблонные события и вещи, как простые, так и более сложные. Например, разработка закончена, функциональность отправлена на тестирование, кликается, далее валится масса ошибок, от простых недочетов, до грубых нарушений. Более сложный случай: функциональность принята, пользователи активно участвуют в обратной связи и приводят к необходимости менять функционал. Функционал поменяли, пошли по тому же пути и перестал работать старый функционал, да и новый оставляет желать лучшего. Кое как прищёлкивая быстрорастущую функциональность начинается постоянная правка багов. Сначала это были часы, потом недели, сейчас я знаю, что и месяцы не предел. Когда это занимало часы или дни, ошибки закрывались, все более — менее текло. Но стоило начать переоткрывать ошибки — начинался ад. Отдел тестирования постоянно ругается, разработчики постоянно правят ошибки и делают новые в разрабатываемом функционале. Все чаще приходит обратная связь, от реальных пользователей, полная негатива. А функционал рос и рос, я искал методы и подходы, как можно себе упростить жизнь. При этом меня давно посещала идея, задействовать при разработке пользовательского интерфейса тестирование на уровне разработчика. Желание заложить пользовательские сценарии привело меня к осознанию факта наличия проблемы: отсутствия хоть сколько-то объективных критериев готовности продукта от разработчика. И я начал мыслить несколько шире, а что, если постараться заложить все положительные сценарии использования в некоторые правила, постоянно проверять наборы правил, ведь это хоть какая-то гарантия того, что пользователь на следующем выпуске не будет ошарашен отвалившимся функционалом, а тестировщики будут задействованы по назначению. Именно этим правилам и предрекалось стать критерием окончания работы над функционалом для себя, для разработчика. Начались поиски инструментов, для формализации правил. Весь код логики взаимодействия с пользователем и данными находился в формах. Конечно же, были отделены слои работы с базой данных, сервисы, ремоутинги и многое, многое другое. Но пользовательский интерфейс оставался перенасыщен логикой. Логикой, никак не связанной с отображением. Первым, и вполне логичным шагом, мне показалось использовать средства для визуального тестирования форм. Набираем правило их кликов — вводов и преобразуем это в тест. Для примера это прекрасно умеет делать Visual Studio 2013 Ultimate, вернее один из её шаблонов. Я бодро создал несколько тестов и понял, что создавать таким образом тесты, а потом запускать, огромная беда. Требовалось кучу времени чтобы полностью загрузить всю систему, дойти до нужных диалогов и вызвать соответствующие события. Это требовало работу всех звеньев цепи, что местами было невозможно. Все действия я предпринимал абсолютно отделив для себя Unit Testing, при этом его постоянно использовал для сервисов и логики, которая вращалась на сервере, ведь у неё не было форм, соответственно Code Behind. Параллельно работая с ASP.NET MVC мне все больше нравился подход с отделением логики от представлений. Упрощенно, вызывая контроллер ASP.NET MVC мы просто дергаем его публичный метод, который возвращает представление с некоторыми изменениями. Я начал думать, как эту модель переложить на WinForms и пришел к выводу, что используя события на представлениях я буду вызывать методы контроллеров. Я принял правило, что у меня всегда есть класс-контроллер, который привязывается к событиям класса, реализующего интерфейс представления. Тем самым я убрал весь код логики из Code Behind в код контроллера, а контроллер подлежал тестированию с помощью обычных Unit Tests, реализуя Stubs для классов менеджеров данных и самих представлений. Позже я научился тестировать и сами представления, исключительно с помощью Unit Tests. Далее я хочу показать пример того, как я пришел к этому. Описание задачи Все описанное выше я рассмотрю на примере реализации формы изменения исключений при импорте данных. Исключения это просто сущность с тремя полями, необходимая для чтения текстовых файлов в системе. С точки зрения пользователя данная форма должна позволять добавлять, изменять, удалять исключения. После добавления нового исключения оно должно добавляться в конец списка. При удалении исключения фокус должен переходить на первый элемент списка. При этом форма вызывается из контекста никак не реализующего MVC.Формализация представления Воспитанный на огромном количестве форм, в корпоративных приложениях, я пришел к выводу, что тоже буду использовать стандартный подход к интерфейсу: панель управления с кнопками — действиями над исключениями, список исключений, в виде таблицы, и базовой кнопкой закрыть. Соответственно для отображения в интерфейсе мне понадобится: • Весь список исключений, для построения таблицы; • Исключение в фокусе; • Событие смены исключения в фокусе; • Событие добавления; • Событие удаления; • Событие нажатия кнопки закрыть; • Метод открытия; • Метод закрытия; • Метод обновления представления.В дальнейшем я понял, что для реализации более дружественного интерфейса мне понадобится еще несколько вещей: • Доступность создания; • Доступность удаления; • Список всех выделенных исключений, для массового удаления.Преобразовав все это в код, я получил интерфейс представления:
public interface IReplaceView { bool EnableCreate { get; set; }
bool EnableDelete { get; set; }
List
List
event EventHandler Closing;
event EventHandler Creating;
event EventHandler Deleting;
event EventHandler FocusedChanged;
void Open ();
void Close ();
void RefreshView (); } Формализация контроллера Вся формализация контроллера заключается в том, что он должен принимать менеджер данных и собственно само представление, которое поддерживает интерфейс, представленный выше. Преобразовав в код, получил: public class ReplaceController: IReplaceController { private IReplaceView _view = null;
private IReplaceStorage _replaceStorage = null;
public ReplaceController (IReplaceView view, IReplaceStorage replaceStorage) { Contract.Requires (view!= null, «View can«t be null»); Contract.Requires (replaceStorage!= null, «ReplaceStorage can«t be null»);
_view = view;
_replaceStorage = replaceStorage;
}
}
В данном контроллере для проверки параметров я использую контракты, но для статьи это не имеет никакого значения.Тестирование
Создавая и формализуя можно позабыть о том, зачем все это затевалось. Все ради одного, повысить качество работы за счет получения адекватного критерия оценки завершенности работ. Поэтому сразу же начал формализовать правила в тесты. Для реализации интерфейсов, принимаемых контроллером в конструктор реализуем просто стабы.Загрузить пустой список исключений
При этом должна быть доступна кнопка создания исключений, недоступна кнопка удаления исключений, элемент в фокусе должен быть null, а список выбранных элементов пустой, но не null. Преобразовав в код получил:
[TestMethod]
public void LoadEmptyReplaces ()
{
var emptyReplaces = new List
var storage = new StubIReplaceStorage () { ReplaceSymbolsGet = () => { return emptyReplaces; }, };
ReplaceSymbol focusedReplace = null;
var loadedReplaces = new List
var view = new StubIReplaceView () { FocusedReplaceGet = () => { return focusedReplace; }, FocusedReplaceSetReplaceSymbol = x => { focusedReplace = x;
if (x!= null) { selectedReplaces.Add (x); } }, ReplacesSetListOfReplaceSymbol = x => { loadedReplaces = x; }, EnableCreateSetBoolean = x => { enableCreate = x; }, EnableDeleteSetBoolean = x => { enableDelete = x; }, SelectedReplacesGet = () => { return selectedReplaces; }, };
var controller = new ReplaceController (view, storage); controller.Open (); view.FocusedChangedEvent (null, null);
Assert.IsNotNull (loadedReplaces, «После создания контроллераи и его открытия список замен не должен быть null»); Assert.IsTrue (loadedReplaces.Count == 0, «После создания контроллера и его открытия список замен не должен быть пустым, так как в него загружен пустой список»); Assert.IsNull (focusedReplace, «После создания контроллера и его открытия активной замены быть не должно»); Assert.IsNotNull (selectedReplaces, «После создания контроллера список замен не может быть null»); Assert.IsTrue (selectedReplaces.Count == 0, «После создания контроллера и его открытия в список выбраных замен должен быть пустой»); Assert.IsTrue (enableCreate, «После создания контроллера должна быть доступна возможность создавать замены»); Assert.IsFalse (enableDelete, «После создания контроллера не должно быть доступно удаление, так как список замен пустой»); } Другие правила После чего, формализуя остальные правила, я получил список тестов, который сразу же позволял оценить готовность функционала контроллера к работе. Ни разу не запустив самого клиента.Реализация представления После этого я создал само представление, реализующее вышеуказанный интерфейс. Для него я так же создал небольшие тесты, используя забавный класс PrivateObject, который позволяет вызывать приватные методы, реализованные в объекте. Например, тест на удаление я получил такой: [TestMethod] public void DeleteOneReplaceView () { var replaces = GetTestReplaces ();
var storage = new StubIReplaceStorage () { ReplaceSymbolsGet = () => { return replaces; }, };
var controller = new ReplaceController (_view, storage); Task.Run (() => { controller.Open (); }).Wait (WaitTaskComplited);
var privateObject = new PrivateObject (_view); privateObject.Invoke («RiseDeleting»);
Assert.IsTrue (replaces.Count == 2, «После удаления одной замены должны остаться еще две»); Assert.IsNotNull (_view.FocusedReplace, «После удаления замены фокус должен перейти на первую замену»); Assert.AreEqual (replaces.First ().Source, _view.FocusedReplace.Source, «После удаления замены фокус должен перейти на первую замену из оставшегося списка»); Assert.IsTrue (_view.SelectedReplaces.Count == 1, «После удаления замены должна быть выбрана первая из оставшегося списка»); Assert.AreEqual (_view.FocusedReplace.Source, _view.SelectedReplaces.First ().Source, «После удаления замены фокус должен перейти на первую замену из оставшегося списка»); Assert.IsTrue (_view.EnableCreate, «После удаления замены должна быть доступна возможность добавлять замены»); Assert.IsTrue (_view.EnableDelete, «После удаления замены должна быть возможность удалить первую из оставшегося списка»); } После чего формализуя остальные правила, я получил список тестов, который сразу же позволял оценить готовность функционала представления к работе. Опять же, ни разу не запустив самого клиента. Постепенно реализуя каждый из кусочков функционала я добился картины: После, сделал вызов в коде контроллера с уже реальным менеджером и представлением. _replaceView = new ReplaceView (); _replaceController = new ReplaceController (_replaceView, _importer); Запустил все приложение и увидел конечный результат работы. Все работало точно так, как я закладывал. Отдел тестирования доволен, клиенты довольны, положительная обратная связь получена. Что еще необходимо? Очень буду рад любым замечаниям или своим рассказам о том, как вы справляетесь с Windows Forms. Первые отзывы правильно замечают, что своим пыхтением я пришел к MVP, это абсолютно верно.Спасибо за внимание! Надеюсь, вы не зря потратили время на меня.