Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Особенности реализации MVP для Windows Forms

Anna | 17.06.2014 | нет комментариев
Доброго времени суток!
Model-View-Presenter — достаточно знаменитый образец проектирования. С первого взора все выглядит легко: есть Модель (Model), которая содержит всю бизнес-логику экрана; Вид/Представление (View), тот, что знает, как отобразить те либо иные данные; Поверенный (Presenter), тот, что является связующий звеном — реагирует на действия пользователя во View, изменяя Model, и напротив.
Трудность начинается, когда число форм в плане становится больше одной.
В данной статье рассматривается:
— немного теории;
— всеобщие задачи реализации MVP (а именно Passive View) под Windows Forms;
— особенности реализации переходов между формами и передача параметров, модальные окна;
— применение IoC-контейнера и образца Dependency Injection — DI (а именно Сonstructor Injection);
— некоторые особенности тестирования MVP приложения (с применением NUnit и NSubstitute);
— все это будет протекать на примере мини-плана и постарается быть наглядным.
В статье затрагивается:
— использование образца Адаптер (Adapter);
— простенькая реализация образца Контроллер приложения (Application Controller).
Для кого эта статья?
Основным образом для начинающих разработчиков на Windows Forms, которые слышали, но не пробовали, либо пробовали, но не получилось. Правда уверен, что некоторые приемы применимы и для WPF, и даже для веб-разработки.

Постановка задачи

Придумаем примитивную задачу — реализовать 3 экрана:
1) экран авторизации;
2) основной экран;
3) модальный экран метаморфозы имени пользователя.
Должно получиться что-то как бы этого:

Немножко теории

MVP, как и его родитель, MVC (Model-View-Controller) придуман для комфорта распределения бизнес-логики от метода ее отображения.

На просторах интернета дозволено встретить целое уйма реализаций MVP. По методу доставки данных в представление их дозволено поделить на 3 категории:
— Passive View: View содержит минимальную логику отображения простых данных (строки, числа), остальным занимается Presenter;
— Presentation Model: во View могут передаваться не только простые данные, но и бизнес-объекты;
— Supervising Controller: View знает о наличии модели и сам забирает из нее данные.

Дальше будет рассматриваться модификация Passive View. Опишем основные черты:
— интерфейс Представления (IView), тот, что предоставляет некоторый контракт для отображения данных;
— Представление — определенная реализация IView, которая может отображать саму себя в определенном интерфейсе (будь то Windows Forms, WPF либо даже консоль) и ничего не знает о том, кто ей управляет. В нашем случае это формы;
— Модель — предоставляет некоторую бизнес-логику (примеры: доступ к базе данных, репозитории, сервисы). Может быть представлена в виде класса либо вновь же, интерфейса и реализации;
— Поверенный содержит ссылку на Представление через интерфейс (IView), управляет им, подписывается на его события, изготавливает примитивную валидацию (проверку) введенных данных; также содержит ссылку на модель либо на ее интерфейс, передавая в нее данные из View и запрашивая обновления.

Нормальная реализация Поверенного

public class Presenter
{
    private readonly IView _view;
    private readonly IService _service;

    public Presenter(IView view, IService service)
    {
        _view = view;
        _service = service;

        _view.UserIdChanged  = () => UpdateUserInfo();
    }

    private void UpdateUserInfo()
    {
        var user = _service.GetUser(_view.UserId);
        _view.Username = user.Username;
        _view.Age = user.Age;
    }
}

Какие плюсы нам дает малая связанность классов (применение интерфейсов, событий)?
1. Разрешает касательно вольно менять логику всякого компонента, не ломая остального.
2. Крупные вероятности при unit-тестировании. Поклонники TDD обязаны быть в фуроре.
Начнем!

Как организовать планы?

Условимся, чторешение будет состоять из 4х планов:
— DomainModel — содержит сервисы и всевозможные репозитории, одним словом — модель;
— Presentation — содержит логику приложения, не зависящую от визуального представления, т.е. все Представители, интерфейсы Представлений и остальные базовые классы;
— UI — Windows Forms приложение, содержит только лишь формы (реализацию интерфейсов Представлений) и логику запуска;
— Tests — unit-тесты.

Что писать в Main()?

Стандартная реализация запуска Windows Forms приложения выглядит так:

private static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new MainForm()); // непринужденный запуск формы (представления)
}

Но мы условились, что Представители будут руководить Представлениями, следственно хотелось бы, Дабы код выглядел как-то так:

private static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    var presenter = new LoginPresenter(new LoginForm(), new LoginService()); // Dependency Injection
    presenter.Run();
}

Испробуем реализовать 1-й экран:

Базовые интерфейсы

// всеобщие способы для всех представлений
public interface IView
{
    void Show();
    void Close();
}
// контракт, по которому поверенный будет взаимодействовать с формой
public interface ILoginView : IView
{
    string Username { get; }
    string Password { get; }
    event Action Login;      // событие "пользователь пытается авторизоваться"
    void ShowError(string errorMessage);
}
public interface IPresenter
{
    void Run();
}
// тупейший сервис авторизации
public interface ILoginService
{
    bool Login(User user); // true - удачная авторизация, напротив false
}
Представление

public class LoginPresenter : IPresenter
{
    private readonly ILoginView _view;
    private readonly ILoginService _service;

    public LoginPresenter(ILoginView view, ILoginService service)
    {
        _view = view;
        _service = service;

        _view.Login  = () => Login(_view.Username, _view.Password);
    }

    public void Run()
    {
        _view.Show();
    }

    private void Login(string username, string password)
    {
        if (username == null)
            throw new ArgumentNullException("username");
        if (password == null)
            throw new ArgumentNullException("password");

        var user = new User {Name = username, Password = password};
        if (!_service.Login(user))
        {
            _view.ShowError("Invalid username or password");
        }
        else
        {
            // удачная авторизация, запуск основного экрана (?)
        }
    }
}

Сделать форму и реализовать в ней интерфейс ILoginView не составит труда, как и написать реализацию ILoginService. Следует только подметить одну специфика:

public partial class LoginForm : Form, ILoginView
{
    // ...
    public new void Show()
    {
        Application.Run(this);
    }
}

Это заклинание дозволит нашему приложению запуститься, отобразить форму, а по закрытии формы правильно закончить приложение. Но к этому мы еще возвратимся.

А тесты будут?

С момента написания поверенного (LoginPresenter), возникает вероятность сразу же его от-unit-тестировать, не реализуя ни формы, ни сервисы.
Для написания тестов я применял библиотеки NUnit и NSubstitute (библиотека создания классов-заглушек по их интерфейсам, mock).

Тесты для LoginPresenter

[TestFixture]
public class LoginPresenterTests
{
    private ILoginView _view;

    [SetUp]
    public void SetUp()
    {
        _view = Substitute.For<ILoginView>();          // заглушка для представления
        var service = Substitute.For<ILoginService>(); // заглушка для обслуживания
        service.Login(Arg.Any<User>())                 // авторизуется только пользователь admin/password
            .Returns(info => info.Arg<User>().Name == "admin" && info.Arg<User>().Password == "password");
        var presenter = new LoginPresenter(_view, service);
        presenter.Run();
    }

    [Test]
    public void InvalidUser()
    {
        _view.Username.Returns("Vladimir");
        _view.Password.Returns("VladimirPass");
        _view.Login  = Raise.Event<Action>();
        _view.Received().ShowError(Arg.Any<string>()); // данный способ должен вызваться с текстом ошибки
    }

    [Test]
    public void ValidUser()
    {
        _view.Username.Returns("admin");
        _view.Password.Returns("password");
        _view.Login  = Raise.Event<Action>();
        _view.DidNotReceive().ShowError(Arg.Any<string>()); // а в этом случае все ОК
    }
}

Тесты достаточно тупые, как пока и само приложение. Но так либо напротив, они удачно пройдены.

Кто и как запустит 2-й экран с параметром?

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

// LoginPresenter: удачная авторизация
var mainPresenter = new MainPresenter(new MainForm());
mainPresenter.Run(user);

Но мы условились, что представители ничего не знают о представлениях помимо их интерфейсов. Что же делать?
На поддержка приходит паттерн Application Controller (реализован упрощенно), внутри которого содержится IoC-контейнер, ведающий, как по интерфейсу получить объект реализации.
Контроллер передается всякому Поверенному параметром конструктора (вновь DI) и реализует приблизительно следующие способы:

public interface IApplicationController
{
    IApplicationController RegisterView<TView, TImplementation>()
        where TImplementation : class, TView
        where TView : IView;
    IApplicationController RegisterService<TService, TImplementation>()
        where TImplementation : class, TService;
    void Run<TPresenter>()
        where TPresenter : class, IPresenter;
}

Позже небольшого рефакторинга запуск приложения стал выглядеть так:

private static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    // все зависимости сейчас регистрируются в одном месте:
    var controller = new ApplicationController(new LightInjectAdapder())
        .RegisterView<ILoginView, LoginForm>()
        .RegisterService<ILoginService, StupidLoginService>()
        .RegisterView<IMainView, MainForm>();

    controller.Run<LoginPresenter>();
}

Пару слов о new ApplicationController(new LightInjectAdapder()). В качестве IoC-контейнера я применял библиотеку LightInject, но не напрямую, а через адаптер (паттерн Adapter), Дабы в случае, если потребуется сменить контейнер на иной, я сумел написать иной адаптер и не менять логику контроллера. Все используемые способы есть в большинстве IoC-библиотек, трудностей появиться не должно.
Реализуем добавочный интерфейс IPresenter<TArg>, отличающийся только тем, что способ Run принимает параметр. После этого унаследуемся от него подобно первому экрану.
Сейчас, не без гордости, запускаем 2-й экран, передавая туда авторизованного пользователя:

Controller.Run<MainPresener, User>(user);
View.Close();

Невозможно легко так взять и закрыть форму…

Один из подводных камней связан со строчкой View.Close(), позже которой закрывалась первая форма, а совместно с ней и приложение. Дело в том, что Application.Run(Form) запускает типовой цикл обработки сообщений Windows и рассматривает переданную форму как основную форму приложения. Это выражается в том, что приложение вешает ExitThread на событие Form.Closed, что и вызывает закрытие приложения позже закрытия формы.
Обойти данную задачу дозволено несколькими методами, один из них — применять иной вариант способа:Application.Run(ApplicationContext), после этого своевременно подменяя качествоApplicationContext.MainForm. Передача контекста формам реализована с поддержкой Контроллера приложения, в котором регистрируется объект (instance) ApplicationContext и после этого подставляется в конструктор формы (вновь DI) во время запуска Поверенного. Способы отображения первых 2-х экранов сейчас выглядят так:

// LoginForm
public new void Show()
{
    _context.MainForm = this;
    Application.Run(_context);
}

// MainForm
public new void Show()
{
    _context.MainForm = this;
    base.Show();
}

Модальное окно

Реализация модального окна не вызывает сложностей. По кнопке «Сменить имя» выполняетсяController.Run<ChangeUsernamePresenter, User>(user). Исключительное различие этой формы от остальных — она не основная, писатель

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB