User interface. Мысли вслух.

Категории: Разработка ПО
Тэги: , , ,

Позволю себе немного порассуждать на тему реализации UI обычных десктопных приложений.

Во-первых, разговоров о том, что UI правильнее всего описывать с помощью конечных автоматов была тьма-тьмущая. В целом это очень верно – логика работы заранее продумана и отточена. В каждый момент времени презентационная часть программы может находится в одном из состояний и, посредством манипуляции пользователем доступных контролов, переходить в одно из возможных целевых состояний. При грамотном построении автомата на этапе проектирования, сбоев в работе быть не должно. Потому автомат как паттерн поведения для “витрины” приложения наиболее гуманный выбор.

Во-вторых, при реализации GUI я предпочитаю пользоваться парадигмой MVP (Model-View-Presenter). Почему не MVC? Потому, что лично мне MVC (Model-View-Control) нравится гораздо меньше. Одно из основных различий этих подходов как раз и позволяет мне сделать однозначный выбор. Я считаю, что выводить половину функциональности презентационной части из диалога с пользователем стратегически неверно. Т.е. ответ от приложения приходит в окно, а весь feedback от пользователя идет контроллеру, тем самым заставляя его решать задачи, с которыми бы отлично справилось окно. Но самое интересное о применении MVP я опишу в далее.

В-третьих, есть мнение, что контроллеру (в случае MVC) или презентору (в случае MVP) совершенно неважно нажал ли пользователь кнопку, поменял ли значение комбобокса или повазюкал слайдер, ему должно все приходить в терминах некоторой внутренней структуры. Т.е., по-хорошему, между котроллером/презентором и view должен быть некоторый метауровень предназначенный для преобразования сущностей из одного архитектурного слоя в другой. Вот тут нам и приходит на помощь подход MVP, изолированность пользователя от презентера не вынуждает “рассказывать” ему об особенностях работы пользователя. Его не надо знакомить с интерфейсам в целом к его работе не имеющим отношения. Пользователю в целом предоставлена свобода действий в рамках интерактивного окна (или набора окон), все его действия ограничиваются манипуляциями с контролами на форме.

В-четвертых, при реализации автомата, одним из самых применимых шаблонов является шаблон Visitor. Потому при создании слоя для преобразования хэндлеров контролов в термины модели приложения необходимо четко продумать интерфейсы, о них я напишу ниже.

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

Подводя итог всему вышеперечисленному, позволю себе привести пример реализации небольшого приложения.

Вот так для презентора должна выглядеть логика работы UI:

Для начала определим делегаты и интерфейс с ними согласующийся. Вообще говоря часто можно обойтись двумя типами с параметром и без:

delegate void Action_WO_EventArgumentHandler();
delegate void Actoin_W_EventArgumentHandler<T> (T ea);
 
interface IUserActions
{
    event Action_WO_EventArgumentHandler OnUserWantsSomething;
    event Actoin_W_EventArgumentHandler<DateTime> OnUserWantsDateTime;
}

Использование generic добавляет известную гибкость при определении типа параметра.

Далее добавляем интерфейс для управления текущим состоянием контрола:

interface IStateSetters
{
    void SetContentForStateOne();
    void SetContentForStateTwo();
}

Теперь в теле класса нашего контрола реализуем оба интерфейса:

Далее со всеми необходимыми контролами поступаем схожим образом.

class FEControl1 : Control, IStateSetters, IUserActions
{
    public FEControl1()
        : base()
    {
        // Конструктор-шманструктор
    }
 
    public void SetContentForStateOne()
    {
        // Делаем вское c содержимым контрола для перевода его в StateOne
    }
 
    public void SetContentForStateTwo()
    {
        // Делаем вское c содержимым контрола для перевода его в StateTwo
    }
 
    public event Action_WO_EventArgumentHandler OnUserWantsSomething;
    public void OnClickAction()
    {
        if (OnUserWantsSomething != null)
            OnUserWantsSomething();
        // Обрабатываем вское, вызываем события контрола и т.п.
    }
 
    public event Actoin_W_EventArgumentHandler<DateTime> OnUserWantsDateTime;
    public void OnOtherClickAction()
    {
        if (OnUserWantsDateTime != null)
            OnUserWantsDateTime(DateTime.Now);
        // Обрабатываем вское, вызываем события контрола и т.п.
    }
}

Теперь наша задача реализовать Visitor

class FEControlVisitor
{
    List<IStateSetters> m_lstStateChangerList = new List<IStateSetters>();
 
    public void SetState()
    {
        foreach (IStateSetters st in m_lstStateChangerList)
        {
            if(ProgramData.State == States.StateOne)
                st.SetContentForStateOne();
            else if(ProgramData.State == States.StateTwo)
                st.SetContentForStateTwo();
        }
    }
}

Тут полет фантазии практически неограниченный. ProgramData, например, может быть Singleton, States – enum. ProgramData можно передавать в качестве параметра функции. Для опередления состояния можно устроить каскадный if, можно устроить switch, в зависимости от иерархии состояний. А можно вообще для каждого состояния держать свою функцию, в которой обходится лист подписанных на Visitor контролов.

Для случая подписки на события все происходит по схожему сценарию. Визитор обходит подписчиков и добавляет в хэндлер эвента метод, который будет его обрабатывать. Процедура в целом схожая с SetState().

Как мне кажется, описаный подход наиболее действенный и срабатывал неоднократно. Прекрасная масштабируемость в плане добавления котнролв, измемения поведения контролов, ну вообще одни плюсы!