Реализация MVP на основе ApplicationController и IoC в WinForms приложении

    Добрый день!

    В этой статье я расскажу о том как я внедрял паттерн MVP в своём Windows Forms приложении и опишу практические ситуации и особенности использования IoC и ApplicationController. Переход от codebehind к MVP мне позволил:

    • улучшить читатемость за счёт лучшего разделения кода (SRP) — отделить BL от View;
    • выработать методику дальнейшего расширения функциональности приложения;
    • избавиться от singleton, который я использовал для работы с настройками приложения.

    О приложении


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

    С ростом его функциональности накопилось достаточно много codebehind, который стал смущать тем, что форма стала очень загружена и содержала всё в себе. Планируя дальнейшее развитие проекта я не мог представлять, что дальше можно продолжать в том же духе и вот пришёл момент, когда главной задачей стало не наращивание функционала, а рефакторинг. И я стал искать решение, которое бы помогло оптимизировать и разделить код и в целом улучшить архитектуру приложения, чтобы с ним было приятнее работать.

    Исходный код

    Решение по рефакторингу


    Решение было — внедрение паттерна MVP. За основу я взял статью Особенности реализации MVP для Windows Forms.

    В статье разбирается расширенный пример простого приложения с 3мя формами: 2мя основными и 1й модальной. В статье рассматривается очень расширенный подход:

    • помимо ApplicationController и IoC там используется ещё и Adapter, позволяющий использовать разные IoC;
    • 3 вида форм: с параметрами, без параметров и модальная;
    • широко применяется принцип DIP.

    В своём проекте я использую только одну форму без аргументов, отказался от адаптера (следуя принципу YAGNI), так как мне достаточно будет IoC Lightinject и в меньшей мере применяю DIP, чтобы упростить проект.

    Реализация MVP


    MVP (Model-View-Presenter) — паттерн проектирования, придуманный для удобства разделения бизнес-логики от способа её отображения. Подробнее о теории можно прочитать в статье выше. Опишу составные части в моей реализации:

    • Model — это структура данных передаваемая между View и Presenter и содержит данные как для отображения так и для исполнения логики. В моём случае модель — это Settings. При старте проекта Settings загружаются во MainFormView, а при запуске загрузки MainFormView проверяет и передаёт Settigns в Presenter, для того чтобы Presenter выполнил логику на своей стороне.
    • View — это форма, в которой отображаются данные для пользователя. В моем случае это данные модели Settings, а так же View предоставляет события, для того, чтобы Presenter связал View с BL.

    MainFormView реализует общий интерфейс IView, характерный для всех View

        public interface IView
        {
            void Show();
    
            void Close();
        }
    

    а так же частный интерфейс IMainFormView, характерный только для данного View. В начале я думал от него отказаться, но если связать Presenter непосредственно с формой, то при работе с таким View, будет доступен весь набор методов, характерных для Form, что не удобно.

        public interface IMainFormView: IView
        {
            void LoadSettings(Settings settings);
    
            void UpdateSettings(Settings settings);
    
            void ShowMessage(string message);
    
            void LoadGroups(List<Group> groups);
    
            void EnableVKUploadGroupBox();
    
            bool Check();
    
            event Action Login;
    
            new event Action Close;
    
            event Action VKUpload;
        }
    

    Ещё одно нововведение MVP в том, что у формы заменён метод Show и через конструктор в форму передаётся ApplicationContext, таким образом, чтобы при переключении от формы к форме и закрытии — переназначалась главная форма.

            protected ApplicationContext _context;
    
            public MainForm(ApplicationContext context)
            {
                _context = context;
                InitializeComponent();
    
                dateTimePickerBeginDate.Format = DateTimePickerFormat.Custom;
                dateTimePickerBeginDate.CustomFormat = "MM/dd/yyyy hh:mm:ss";
    
                buttonAuth.Click += (sender, args) => Invoke(Login);
                this.FormClosing += (sender, args) => Invoke(Close);
                buttonLoad.Click += (sender, args) => Invoke(VKUpload);
            }
    
            public new void Show()
            {
                _context.MainForm = this;
                Application.Run(_context);
            }
    

    Presenter — это класс, который инкапсулирует в себе View, Services и бизнес-логику (BL), с помощью которой организует взаимодествие между View и Services. BL реализована в основном в обработчиках событий View. В отличии от ранее используемого CodeBehind, в MVP обработчики событий, выполняющих BL выведены в Presenter, а так же для простоты события у View выведены в виде Action без аргументов. Все необходимые данные для выполнения обработчики получают через модель, полученную из формы через публичный метод.

    Presenter содержит метод Run, который вызывается ApplicationController-ом и который запускает форму:

        public interface IPresenter
        {
            void Run();
        }
    

    ApplicationController — единая точка управления и выполнения всего приложения. Инкапсулирует в себе всю логику: IoC, Presenters, View, Services.

    Управление происходит через метод Run, который вызывает соответствующий Presenter. Все Presenter-ы соединены друг с другом через ApplicationController, который Presenter получают в конструкторе. Таким образом Presenter может вызвать другой Presenter вызвав метод Run, который внутри себя обращается к IoC Container для получения нужного Presenter и его запуска.

        public class ApplicationController
        {
            ServiceContainer _container;
    
            public ApplicationController(ServiceContainer serviceContainer)
            {
                _container = serviceContainer;
                _container.RegisterInstance<ApplicationController>(this);
            }
    
            public void Run<TPresenter>() where TPresenter:class, IPresenter
            {
                var presenter = _container.GetInstance<TPresenter>();
                presenter.Run();
            }
        }
    

    IoC container — это агрегатор всех «зависимостей» используемых в логике работы приложение. Он содержит в себе:

    • конструкторы View
    • конструкторы Presenters
    • инстансы сервисов
    • контекст приложения
    • ApplicationController

    Все зависимости добавляются в контейнер во время запуска, это можно видеть в файле Program.cs:

             static void Main()
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
    
                ulong appid = ulong.Parse(ConfigurationManager.AppSettings["AppIdForTest"]);
                VKGroupHelperWorker vk = new VKGroupHelperWorker(appid);
    
    
                ServiceContainer container = new ServiceContainer();
                container.RegisterInstance<VKGroupHelperWorker>(vk);
                container.RegisterInstance<Settings>(Globals.Settings);
                container.RegisterInstance<ApplicationContext>(Context);
                container.Register<IMainFormView,MainForm>();
                container.Register<MainFormPresenter>();
    
                ApplicationController controller = new ApplicationController(container);
                controller.Run<MainFormPresenter>();
            }
    

    Для IoC я использовал компонент Lightinject, который перед использованием необходимо установить через NPM.

    Таким образом контейнер может содержать как конструкторы объектов, так и сами объекты, как это сделано с Settings и VKGroupHelperWorker (клиент ВК API), образуя множество всех используемых ресурсов приложения. Полезной особенностью контейнера является то, что все эти внедрённые ресурсы, классы могут получить через аргументы конструктора. Например
    ApplicationController, IMainFormView, VKGroupHelperWorker — ранее внедрённые зависимости, которые могут быть как конструкторами объектов так и инстансами. В случае если был внедрён инстанс, то все образуемые объекты будут работать с одним и тем же инстансом, что позволяет избавиться от паттерна синглтон, если он использовался.

    public MainFormPresenter(ApplicationController applicationController, IMainFormView mainForm, Settings settings, VKGroupHelperWorker vk)
            {
                _view = mainForm;
                _settings = settings;
                _vk = vk;
    
                _view.Login += () => Login();
                _view.Close += () => Close();
                _view.VKUpload += () => VKUpload();
            }
    

    Внедрение MVP мне позволило:

    • частично избавиться от Singleton, который я использовал для работы с настройками приложения;
    • отделить BL от View, тем самым улучшив разделение кода (SRP);
    • выработать подход к дальнейшему расширению приложения, не загромождая View.

    Подробнее о том что было сделано можно посмотреть в репозитории проекта.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое