User interface. Мысли вслух.
Категории: Разработка ПО
Тэги: C#, programming, UI, visitor pattern
Позволю себе немного порассуждать на тему реализации 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().
Как мне кажется, описаный подход наиболее действенный и срабатывал неоднократно. Прекрасная масштабируемость в плане добавления котнролв, измемения поведения контролов, ну вообще одни плюсы!





.png)