Pull to refresh

Использование подхода MVC в WinForms для самых маленьких

Reading time 8 min
Views 36K
В статье описаны общие принципы построения приложений, с использованием подхода 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<ReplaceSymbol> Replaces
        {
            set;
        }
        ReplaceSymbol FocusedReplace
        {
            get;
            set;
        }

        List<ReplaceSymbol> SelectedReplaces
        {
            get;
            set;
        }

        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<ReplaceSymbol>();

      var storage = new StubIReplaceStorage()
      {
           ReplaceSymbolsGet =
           () =>
           {
               return emptyReplaces;
           },
      };

      ReplaceSymbol focusedReplace = null;
      var loadedReplaces = new List<ReplaceSymbol>();
      var selectedReplaces = new List<ReplaceSymbol>();
      var enableCreate = false;
      var enableDelete = false;

      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, это абсолютно верно.
Спасибо за внимание! Надеюсь, вы не зря потратили время на меня.
Tags:
Hubs:
+8
Comments 10
Comments Comments 10

Articles