Сообщений 11    Оценка 440 [+1/-1]         Оценить  
Система Orphus

Использование паттерна “Команда”

Автор: Андрей Глизнецов
Источник: RSDN Magazine #4-2004
Опубликовано: 08.03.2005
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Класс AbstractCommand
Класс MenuItemEx
Работа с ресурсами
Создаем команды
Реестр команд
Создаем меню
Динамическое конфигурирование команд
Заключение

Исходные коды

Введение

Современные среды разработки, такие, как Visual Studio или Delphi, позволяют легко и просто создать Windows-приложение – набросать на форму control-ы и сформировать главное меню. Такой способ разработки подходит для небольших программ. Но при достижении программы определенного размера выявляются и недостатки этого способа.

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

Класс AbstractCommand

Паттерн "Команда" отделяет объект, инициирующий действие (главная форма), от объекта, который знает, как это действие выполнить. У команды есть заголовок (Caption), описание (Description), картинка (Icon), а также «горячая кнопка» (Shortcut). Все эти свойства могут быть использованы в меню. Кроме того, команда может быть временно недоступной (Disabled) или вообще скрытой от пользователя. Ну и конечно, команду можно выполнить (метод Execute). Исходя из вышеизложенного, общий предок всех команд в системе будет выглядеть следующим образом:

      public
      abstract
      class AbstractCommand
{
  protectedstring _name;
  protectedstring _caption;
  protectedstring _description;
  protectedbool _enabled;
  protectedbool _visible;
  protected Image _icon;
  protected Shortcut _shortcut;

  public AbstractCommand()
  {
    _visible = true;
    _enabled = true;
    _shortcut = Shortcut.None;
  }
  
  publicstring Name
  {
    get
    {
      return _name;
    }
  }

  publicstring Caption
  {
    get
    {
      return _caption;
    }
  }

  publicstring Description
  {
    get
    {
      return _description;
    }
  }

  public Image Icon
  {
    get
    {
      return _icon;
    }
  }

  publicbool Enabled
  {
    get
    {
      return _enabled;
    }
    set
    {
      _enabled = value;
      OnChange();
    }
  }

  publicbool Visible
  {
    get
    {
      return _visible;
    }
    set
    {
      _visible = value;
      OnChange();
    }
  }

  public Shortcut Shortcut
  {
    get
    {
      return _shortcut;
    }
    set
    {
      _shortcut = value;
    }
  }

  publicevent EventHandler Changed;

  protectedvoid OnChange()
  {
    if (Changed != null)
      Changed(this, EventArgs.Empty);
  }

  publicabstractvoid Execute();
  
  publicvirtualvoid Execute(object argument)
  {
    Execute();
  }
}

Класс MenuItemEx

К сожалению, стандартное .NET-меню не позволяет отображать картинки. Для исправления этого недочета можно воспользоваться одной из готовых библиотек наподобие DevExpress или написать свой наследник класса MenuItem. Выберем последний вариант и создадим класс MenuItemEx. Кроме возможности отображать картинку, класс MenuItemEx обладает еще несколькими особенностями. В параметрах конструктора необходимо задать экземпляр класса AbstractCommand - то есть назначить элементу меню действие, которое этот элемент будет выполнять. Поскольку состояние команды может измениться, необходимо отслеживать эти изменения и соответствующим образом менять внешний вид элемента меню:

      public MenuItemEx(AbstractCommand command)
{
  _command = command;
  Text = command.Caption;
  _icon = command.Icon;
  OwnerDraw = true;
  Update();
  command.Changed += new System.EventHandler(command_Changed);
}

privatevoid Update()
{
  Enabled = _command.Enabled;
  Visible = _command.Visible;
  Shortcut = _command.Shortcut;
}

privatevoid command_Changed(object sender, System.EventArgs e)
{
  Update();
}

В обработчике события Click происходит выполнение команды:

      protected
      override
      void OnClick(System.EventArgs e)
{
  base.OnClick(e);
  if (_command != null)
  _command.Execute();
}

Работа с ресурсами

Для отображения иконок в .NET-control-ах обычно применяется класс ImageList и свойство ImageIndex. Но мы будем использовать другой способ для хранения картинок. Создадим класс ResourceHolder, ответственный за хранение картинок. У него есть статический метод GetBitmap, который возвращает картинку по имени. У класса есть внутренний кэш картинок. Когда картинка запрашивается впервые, она читается из ресурсов и добавляется в кэш. При следующем запросе вернется ссылка на уже загруженную в память картинку. Кроме этого, для красивого отображения иконок сделаем картинку прозрачной. Прозрачным цветом будет цвет верхнего левого пиксела.

        public
        static Bitmap GetBitmap(string key, bool transparent)
{
  if (!_cache.ContainsKey(key))
  {
    try
    {
      Bitmap myBitmap = new Bitmap(GetStream(key));
      if (transparent)
      {
        Color backColor = myBitmap.GetPixel(0, 0);
        myBitmap.MakeTransparent(backColor);
      }
      _cache.Add(key, myBitmap);
    }
    catch(Exception)
    {
      returnnull;
    }
  }
  return (Bitmap)_cache[key];
}

Создаем команды

Теперь можно приступить к разработке функциональности нашего приложения, т.е. к созданию команд. Вот так будет выглядеть команда закрытия приложения:

      public
      class ExitApplication: AbstractCommand
{
  private Form _form;

  public ExitApplication(Form form)
  {
    _caption = "Exit";
    _form = form;
  }
  
  publicoverridevoid Execute()
  {
    if (MessageBox.Show("Are you really want to exit?", "Exit", MessageBoxButtons.OKCancel, 
          MessageBoxIcon.Warning) == DialogResult.OK)
      _form.Close();
  }
}

Поведение более сложных команд может изменяться в зависимости от контекста. Аргументы для выполнения таких команд необходимо менять динамически через свойства. Например, команда удаления во время редактирования текста удаляет выделенные символы, а если активным окном является ProjectExplorer - удаляет элемент проекта. Введем еще один абстрактный класс для таких случаев. Класс EditCommand содержит свойство типа IEditable, которому команда будет делегировать полномочия на выполнение своих действий.

      public
      abstract
      class EditCommand: AbstractCommand
{
  protected IEditable _subject;

  public IEditable Subject
  {
    get
    {
      return _subject;
    }
    set
    {
      _subject = value;
    }
  }
}

Команда удаления будет выглядеть следующим образом:

      public
      class Delete: EditCommand
{
  public Delete()
  {
    _caption = "Delete";
    _name = CommandNames.Delete;
    _icon = ResourceHolder.GetBitmap("delete.bmp");
    _shortcut = Shortcut.Del;
    _enabled = false;
  }
  
  publicoverridevoid Execute()
  {
    if (_subject != null)
      _subject.Delete();
  }
}

Еще один способ сконфигурировать команду – передать аргумент в вызов метода Execute. Таким образом ведет себя команда редактирования элемента проекта (EditProjectItem). Ее текст можно посмотреть в исходных кодах, которые прилагаются к данной статье.

Реестр команд

Поскольку некоторые команды используются в разных частях приложения, необходимо обеспечить свободный доступ к ним из любого места программы. Для этого служит класс CommandManager. Он реализован как сиглтон и содержит два метода – зарегистрировать команду и получить зарегистрированную команду по строковому ключу.

      public
      void Register(AbstractCommand command)
{
  _commands[command.Name] = command;
}

public AbstractCommand GetCommand(string name)
{
  return (AbstractCommand)_commands[name];
}
СОВЕТ

Отметим, что CommandManager может в некоторых случаях играть роль фабрики по производству команд. Поскольку вызов функций по регистрации и получению команды может выполняться независимо, возможно гибкое изменение стратегии поведения приложения. Так, например, представим, что у нас есть команда повернуть изображение с ключом “RotateImage”. Редактор изображений при создании своего меню обращается к CommandManager чтобы получить экземпляр команды для поворота изображения. Пользователь может при помощи окна с настройками зарегистрировать под ключом “RotateImage” различные варианты команды (поворот по часовой или против часовой стрелки).

Создаем меню

В качестве поясняющего примера будем использовать простое MDI-приложение, интерфейс которого весьма отдаленно напоминает среду VisualStudio. Задача программы – создание некого проекта, состоящего из текстовых и графических файлов. В проект можно добавлять новые элементы, удалять старые, а также редактировать содержимое текстовых элементов. Для отображения списка элементов в проекте используется окно “Project Explorer”. У приложения есть главное меню и панель инструментов (см. рисунок 1).


Рисунок 1.

Вот часть кода основной формы, отвечающая за работу меню:

      private
      void CreateMenu()
{
  this.Menu = new MainMenu();

  MenuItem file = Menu.MenuItems.Add("File");
  AddMenuItem(file, new NewProject(), true, false);
  AddMenuItem(file, new OpenProject(), true, false);
  AddMenuItem(file, new SaveProject(), true, false);
  file.MenuItems.Add( new MenuItem("-") );
  AddMenuItem(file, new ShowExplorer(), false, false);
  file.MenuItems.Add( new MenuItem("-") );
  AddMenuItem(file, new ExitApplication(this), false, false);

  MenuItem edit = Menu.MenuItems.Add("Edit");
  AddMenuItem(edit, new Delete(), true, true);
  AddMenuItem(edit, new Cut(), true, true);
  AddMenuItem(edit, new Copy(), true, true);
  AddMenuItem(edit, new Paste(), true, true);

  MenuItem project = Menu.MenuItems.Add("Project");
  AddMenuItem(project, new AddTextItem(), false, true);
  AddMenuItem(project, new AddImageItem(), false, true);
}

privatevoid AddMenuItem(
  MenuItem parent, AbstractCommand command, bool toolBar, bool register)
{
  parent.MenuItems.Add(new MenuItemEx(command));
  if (toolBar)
    AddToolBarButton(command);

  if (register)
    CommandManager.Instance.Register(command);
}

privatevoid AddToolBarButton(AbstractCommand command)
{
  ToolBarButton button = new ToolBarButton();
  button.Tag = command;
  button.ToolTipText = command.Description;
  button.Enabled = command.Enabled;
  button.Visible = command.Visible;
  if (command.Icon != null)
  {
    _toolBarImages.Images.Add(command.Icon);
    button.ImageIndex = _toolBarImages.Images.Count-1;
  }
  else
    button.Text = command.Caption;

  _toolBar.Buttons.Add( button );
  command.Changed += new EventHandler(command_Changed);
}

privatevoid _toolBar_ButtonClick(
  object sender, ToolBarButtonClickEventArgs e)
{
  AbstractCommand command = e.Button.Tag as AbstractCommand;
  if (command != null)
    command.Execute();
}

privatevoid command_Changed(object sender, EventArgs e)
{
  foreach(ToolBarButton button in _toolBar.Buttons)
    if (button.Tag == sender)
    {
      button.Enabled = ((AbstractCommand)sender).Enabled;
      button.Visible = ((AbstractCommand)sender).Visible;
    }
}

Здесь используется стандартный control ToolBar, оперирующий индексами картинок. Поэтому приходится копировать картинку команды в ImageList панели инструментов.

Динамическое конфигурирование команд

Состояние команды может меняться по двум причинам – при смене активного окна и при изменении его состояния. В качестве примера рассмотрим работу формы ProjectExplorer. Пользователь может удалить выделенный элемент проекта, выбрав пункт основного меню или при помощи всплывающего меню. Если ни одного элемента не выбрано, то команду удаления необходимо запретить. При двойном щелчке по элементу проекта необходимо открыть его для редактирования.

Вот как это работает. При создании формы создадим всплывающие меню:

      public ExplorerForm()
{
  InitializeComponent();
  
  _contextMenu.MenuItems.Add( new MenuItemEx(CommandManager.Instance.
    GetCommand(CommandNames.AddText)) );
  _contextMenu.MenuItems.Add( new MenuItemEx(CommandManager.Instance.
    GetCommand(CommandNames.AddImage)) );
  _contextMenu.MenuItems.Add( new MenuItem("-") );
  _contextMenu.MenuItems.Add( new MenuItemEx(CommandManager.Instance.
    GetCommand(CommandNames.Delete)) );
}

Метод для конфигурации команды удаления:

      private
      void UpdateCommands(IEditable subject)
{
  EditCommand com = (EditCommand)CommandManager.Instance.
    GetCommand(CommandNames.Delete);
  com.Subject = subject;
  com.Enabled = subject != null;
}

При активизации формы и при смене выделенного элемента дерева необходимо изменить статус команды:

      private
      void ExplorerForm_Enter(object sender, System.EventArgs e)
{
  if (SelectedItem != null)
    UpdateCommands(this);
  else
    UpdateCommands(null);
}

privatevoid ExplorerForm_Leave(object sender, System.EventArgs e)
{
  UpdateCommands(null);
}

privatevoid _treeView_AfterSelect(object sender, System.Windows.Forms.TreeViewEventArgs e)
{
  if (SelectedItem != null)
    UpdateCommands(this);
  else
    UpdateCommands(null);
}

Вот так происходит редактирование элемента проекта:

      private
      void _treeView_DoubleClick(object sender, System.EventArgs e)
{
  if (SelectedItem != null)
  {
    AbstractCommand com = CommandManager.Instance.
      GetCommand(CommandNames.EditProjectItem);
    com.Execute(SelectedItem);
  }
}

Заключение

Применение паттерна "Команда" позволяет вызывать одну и ту же функцию из различных меню. Можно быть уверенным, что независимо от того, где на ходится ссылка на команду, ее состояние и выполняемые действия будут одинаковы. Локализация приложения также не представляет трудностей, поскольку все локализуемые строки находятся в одном месте – в описании команды. Стоит отметить, что паттерн "Команда" позволяет реализовать и другие возможности, например, ведение журнала действий пользователя или Undo/Redo. Но это уже темы для отдельной статьи.


Эта статья опубликована в журнале RSDN Magazine #4-2004. Информацию о журнале можно найти здесь
    Сообщений 11    Оценка 440 [+1/-1]         Оценить