Когда я был начинающим программистом, то программировал с использованием библиотеки MFC. Мне очень нравилась предлагаемая MFC модель использования команд. Команда – это некоторый идентификатор. С идентификатором команды просто и естественно связываются строка подсказки, метод обработки доступности команды, метод для выполнения действия команды и т.п. Команды объявляются централизовано, обработчики команд не зависят от того, как была вызвана команда – программно, нажатием на кнопку в панели инструментов или через контекстное меню. Все было понятно и логично.
Команды в WinForms
Впервые столкнувшись с WinForms, я был неприятно удивлен отсутствием аналогичных возможностей по работе с командами. Вместо этого требовалось вручную связывать каждый обработчик с соответствующим интерфейсным элементом – кнопкой или пунктом меню. Для обновления доступности интерфейсных элементов в зависимости от состояния программы нет вообще никакой инфраструктуры.Такой подход даже удобен для небольших программок – когда не хочется городить большой огород ради трех кнопок. Но для сложных программных продуктов, которые постоянно изменяются и развиваются, необходимость связывать обработчики непосредственно с интерфейсными элементами ужасно мешает! Добавление дублирующего кнопку пункта меню или поиск всех обработчиков нажатия кнопки может стать проблемой.
Команды в WPF
В WPF я удовольствием снова обнаружил возможность объявлять команды централизовано и независимо от интерфейсных элементов. Команда в WPF – это экземпляр класса RoutedUICommand. К каждому экземпляру команды можно присоединить обработчики выполнения команды или проверки её доступности. В процессе выполнения программы можно легко и просто создавать новые команды – это существенное отличие от возможностей MFC.
Мне не понравились два момента –
- Необходимость вручную (в коде или XAML) добавлять привязки к командам в CommandBindings. Т.е. для каждой необходимой мне команды я должен написать процедуру создания, методы-обработчики и добавление в коллекцию привязок. В случае MFC всем этим занимался ClassWizard, а здесь все приходится делать руками – это увеличивает простор для ошибок.
- Обработчик команды прозрачно и легко (через CommandBinding) добавляется только в интерфейсные элементы. Имеющаяся инфраструктура плохо пригодна для добавления обработчиков команд, например, в класс приложения. Хотя для многих команд это более естественно и логично, чем обработка в интерфейсных объектах.
Что получилось
Когда-то я уже реализовывал концепцию команд для Windows Forms. Теперь я с энтузиазмом принялся сочинять расширение для модели команд WPF. В итоге у меня получились несколько классов + правила их применения, которые позволяют:
- Объявить все нужные мне команды в виде перечисления. Текстовое значение элемента перечисления я использую в качестве имени команды, эта же строка используется в качестве идентификатора строкового ресурса, содержащего описание команды (см. пост про управление строками).
- Размещать обработчики команд я разместил в нужных мне классах (в моем случае это были класс приложения, страницы и некоторые элементах управления). Связь между командой и её обработчиком декларируется с помощью атрибутов метода-обработчика.
Для меня главное достоинство получившегося решения – понятность кода и простота его сопровождения. Все команды конструируются в одном месте. Видны их названия, акселераторы и т.п. Найти обработчики команды можно простым поиском использования поля перечисления.
Технические детали
Создание команд
А теперь несколько слов о том, как именно это было реализовано.
Как я уже говорил, все команды приложения оформлены в виде перечисления
(в пример я включил не все команды):
public enum CommandName
{
Activate,
NewWorker,
NewProject,
NewWork,
NewMilestone
}
Для создания экземпляров команд и управления ими предназначен класс Commands. В статическом конструкторе этого класса для каждого элемента перечисления создается команда и добавляется в словарь. После этого к командам добавляются горячие клавиши.
//словарь команд
static Dictionary<Names, RoutedUICommand> _commands = new Dictionary<Names, RoutedUICommand>();
//статический конструктор
static Commands ()
{
Enum.GetValues(typeof(CommandName)).Cast<CommandName>()
.ToList()
.ForEach(x => _commands.Add(x, CreateCommand(x)));
AddHotKeys();
}
CreateCommand – фабричный метод для создания команды для каждого элемента перечисления:
private static RoutedUICommand CreateCommand(CommandName cmdName)
{
string cmdString = cmdName.ToString();
return new RoutedUICommand(App.RString(cmdString), cmdString, typeof(Commands));
}
Для получения перечисления всех созданных команд в классе Commands есть метод GetAllCommands
public static IEnumerable<RoutedUICommand> GetAllCommands()
{
return _commands.Values;
}
Вызов обработчиков команд
Для декларации связи метода-обработчика с командой создан я объявил атрибут:
[System.AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
class CommandHandlerAttribute : System.Attribute
{
Type _myType;
string _name;
public CommandHandlerAttribute(CommandName cmdn)
: this(typeof(Commands), cmdn.ToString())
{}
private CommandHandlerAttribute(Type tType, string name)
{
_name = name;
_myType = tType;
}
public bool IsMyCommand(object cmd)
{
RoutedUICommand rc = cmd as RoutedUICommand;
if (rc == null)
return false;
return (_name == rc.Name && _myType.Equals(rc.OwnerType));
}
}
В конструктор атрибута передается элемент перечисления, соответствующий команде. Этот атрибут предназначен для обозначения методов в классе, которые нужно вызвать для обработки команды. Метод без возвращаемого значения – для обработки команды; метод, возвращающий bool – для определения доступности команды.
[CommandHandler(CommandName.NewProject)]
public void NewProject(object Param)
{
...
}
[CommandHandler(CommandName.NewProject)]
[CommandHandler(CommandName.NewWorker)]
public bool CanCreateNew(object Param)
{
...
}
Возникает закономерный вопрос: а кто же вызовет эти методы? А вызовет эти методы экземпляр класса для маршрутизации команд CommandsRouter, который должен быть создан в каждом классе, использующем для своих методов атрибут CommandHandler. Приведу пример его создания в классе страницы:
private CommandsRouter _cmdRouter;
internal CommandsRouter CmdRouter
{
get
{
return _cmdRouter ?? (_cmdRouter = new CommandsRouter(this, App.Me.CmdRouter));
}
}
В этом примере видно, что в конструктор маршрутизатора команд страницы передается маршрутизатор команд приложения. Это приводит к тому, что команды, не обработанные страницей передаются для обработки приложению.
Чтобы страница (и любой другой элемент управления) получала команды и могла их обработать, необходимо добавить привязки к командам в коллекцию CommandBinding. Использование централизованного набора команд позволяет выполнить эту операцию одной строкой в коде:
//связываем команды приложения c обработчиками
CommandBindings.AddRange(Commands.GetAllCommands().Select(x => CmdRouter.CreateBinding(x)).ToList());
Все привязки к командам конструирует маршрутизатор команд, подставляя свои обработчики для выполнения и обработки доступности команд:
public CommandBinding CreateBinding(RoutedUICommand rc)
{
return new CommandBinding(rc, CommandExecuted, CanExecuteCommand);
}
private void CommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
ExecuteCommand(e.Command, e.Parameter);
}
private void CanExecuteCommand(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = CanExecuteCommand(e.Command, e.Parameter);
}
Заключение
Кому-то такой способ управления командами и вызовами может показаться громоздким неудобным. Согласен, первоначальное внедрение всей описанной инфраструктуры требует некоторого времени. НО! после внедрений этой инфраструктуры добавление новых команд становится делом простым и требующим минимума деятельности:
- Добавить необходимый элемент перечисления
- Добавить в ресурсы строку с описанием команды
- Добавить методы обработки доступности и выполнения команды.
И все!
P.S.
Формат блога не очень удобен для размещения полных исходных программ, поэтому я привожу примеры только самых ключевых моментов. Если Вам интересен такой подход к управлению командами, но не все понятно как делать – задайте свой вопрос в комментариях, я на него отвечу как только смогу.
Привет.
ОтветитьУдалитькак реализован CommandsRouter?
Доброго дня.
ОтветитьУдалитьCommandsRouter просто ищет среди методов объекта подходящие по сигнатуре и помеченные атрибутами, а затем вызывает их через Reflection.
private void ExecuteCommand(ICommand cmd, object Parameter)
{
///ищем void метод помеченный атрибутом
var method = _me.GetType().GetMethods().FirstOrDefault(m => m
.GetCustomAttributes(typeof(CommandHandlerAttribute), true)
.Cast()
.Any(a => a.IsMyCommand(cmd)) && m.ReturnType.Equals(typeof(void)));
if (method == null)
{
if (_other == null)
throw new NotSupportedException();
_other.ExecuteCommand(cmd, Parameter);
return;
}
method.Invoke(_me, new object[] { Parameter });
}
Очень интересная идея, но многие детали к сожалению не раскрыты. Было бы интересно увидеть пример целиком.
ОтветитьУдалитьВ частности интересно каким способом происходит привязка конкретной команды к контролу в xaml'е (конвертер, аттачед проперти или ещё что то). Так же возникает вопрос - перечисление с командами одно на приложение или же есть возможность сделать их несколько (например для отдельных модулей).
Для привязки команда в XAML я использовал статические свойства в классе. Например, объявил
Удалитьclass MyCommands {
public static RoutedUICommand Save;
...
В классе MyCommand я завел метод Initalize(), в котором создаю все свойства методом CreateCommand.
void Initalize()
{
Save = CreateCommand(CommandName.Save);
...
далее создаются остальные свойства
}
Сначала я писал код метода вручную, потом стал использовать генерацию кода непосредственно по перечислению (с помощью шаблонов T4).
А что касается количества перечислений с командами - я использовал одно, но это не догма. Перечисление нужно лишь как перечень именованных констант. Метод несложно доработать и для использования нескольких перечислений - нужно лишь свести названия команд в общий список, который будет обрабатываться маршрутизатором.
Удалить