Улучшаем Silverlight приложения: стандартное контекстное меню для TextBox
- modified:
- reading: 4 minutes
Как часто у меня бывает такое, что в одной руке у меня кружка чая или пряник, а другой рукой я печатаю и вожу мышкой. И вот не могу я одной рукой сразу же нажать Ctrl+C или Ctrl+V (могу конечно, но не удобно, не привычно). Во всех программах, на всех сайтах, у меня есть возможность выделить мышкой текст и скопировать его, а дальше вставить в другое место, а вот TextBox по умолчанию в Silverlight не предоставляет мне такой возможности, и это очень плохо. В особенности для бизнес-приложений. Люди привыкают к стандартным функциям, нельзя их лишать этого. Я говорю об этом меню:
В Silverlight 4 появилась возможность обрабатывать нажатие правой кнопки мыши, и так же с ним появился контрол ContextMenu (в Silverlight Toolkit). Следовательно, мы теперь можем обогатить наш интерфейс.
Первое, с чего можно начать, это погуглить и найти что-то вроде такой статьи Silverlight 4 textbox right click context menu with cut, copy and paste behavior, которая приведет нас к более доработанному варианту TextBoxCutCopyPasteBehavior. Его, как показала практика, мне тоже пришлось немного доработать.
На самом деле, мне много чего не понравилось в приведенной реализации, и я столкнулся со многими проблемами, используя эту реализацию. Перечислю некоторые из них.
В приведенной реализации зачем-то обрабатывается самостоятельно нажатие правой клавиши мыши для отображения меню, когда все это уже давно реализовано в ContextMenuService. То же самое с привязкой ContextMenu к текущему TextBox, все можно сделать при помощи ContextMenuService. Поэтому создание ContextMenu я изменил на (в методе CreateMenu):
_contextMenu = ContextMenuService.GetContextMenu(AssociatedObject);if (_contextMenu == null)
{ _contextMenu = new ContextMenu();
ContextMenuService.SetContextMenu(AssociatedObject, _contextMenu);
}
Более того, это мне позволило так же в XAML описывать как обычно ContextMenu, а данный Behavior будет просто добавлять в конец свои пункты по работе с текстом. Единственное, создание меню мне пришлось перенести из метода OnAttached в свой метод AssociatedObjectLoaded, который вызывается, соответственно, при вызове Loaded event у TextBox. Сделано это для того, чтобы не зависеть от того, когда было объявлено в XAML ContextMenu до моего Bahavior или после него.
Следующая проблема, в реализации TextBoxCutCopyPasteBehavior – это то, что они сами делают неактивными пункты меню при помощи выставления IsEnabled для каждого MenuItem на время получения фокуса контекстным меню. При этом, там есть ссылка на какой-то баг в Silverlight, поэтому они там еще кучу кода понаписали, чтобы этот баг исправить. В целом молодцы, конечно же: и баг нашли, и решение. Более того, по ссылкам даже можно найти патч для Silverlight Toolkit, который исправляет этот баг. Я же пошел другим путем, просто реализовал все на командах и у меня все заработало без каких либо хаков. Для этого я реализовал несколько базовых команд CommandBase и ClipboardCommandBase. Вторая просто наследуется от первой и добавляет один метод для обработки исключений SecurityException, который можно получить, если пользователь не разрешил доступ к буферу обмена. В результате команда на вставку из буфера обмена выглядит следующим образом:
internal class PasteCommand : ClipboardCommandBase
{ public PasteCommand(TextBox textBox, IClipboardSecurityExceptionNotify clipboardSecurityExceptionNotify)
: base(textBox, clipboardSecurityExceptionNotify)
{
}
public override void Execute()
{ try
{
TextBox.SelectedText = Clipboard.GetText();
} catch (SecurityException ex)
{
OnClipboardSecurityException(ex, StdCommandActionType.Paste);
}
TextBox.Focus();
}
public override bool CanExecute()
{ return Clipboard.ContainsText();
}
}
Намного проще и читабельнее, чем было.
При создании самого меню, я подписываюсь на событие Opened, при вызове которого я дергаю у всех команд элементов меню, которые создал в этом Behavior, событие CanExecuteChanged. По другому я не смог отлеживать возможность выполнения некоторых команд. Так, например, я не могу отслеживать, менялось ли значение, возвращаемое методом ContainsText у Clipboard, поэтому приходится проверять на каждое открытие меню. Ничего в этом страшного я не вижу, операции не такие уж и затратные.
В результате, чтобы добавить стандартные команды для работы с текстом, нужно просто в XAML для определенного TextBox указать этот самый Behavior:
<TextBox>
<i:Interaction.Behaviors>
<Behaviors:TextBoxStdCommandsBehavior />
</i:Interaction.Behaviors>
</TextBox>
Более того, как я и говорил, можно так же добавлять свои пункты меню, расширяя те, которые добавляются при помощи этого Behavior, например, так:
<TextBox>
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu>
<toolkit:MenuItem Header="Do..." Click="MenuItem_Click" />
<toolkit:Separator />
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
<i:Interaction.Behaviors>
<Behaviors:TextBoxStdCommandsBehavior />
</i:Interaction.Behaviors>
</TextBox>
Ну и, конечно же, я не мог не украсить все это в итоге иконками. Воспользовавшись Image Library, которая поставляется вместе с Visual Studio (в моем случае 2010, находится архив с картинками в c:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\VS2010ImageLibrary\), я нашел несколько подходящих иконок и приделал их к контекстному меню.
Результат ниже. У обоих TextBox элементов я добавил Behavior, который описывал выше. Так же я добавил свои пункты меню, слева обрабатывая нажатие при помощи события Click, справа при помощи команды, так же справа я могу управлять тем, можно ли вызывать команду “Do…”. XAML описание этого примера можно посмотреть тут – MainPage.xaml.
Конечно же, этот Behavior еще можно доработать. Так можно добавить возможность выбора, как отображать меню: в отдельном контекстном меню, выше или ниже определения, сделанного разработчиком. Сейчас, всегда меню добавляется ниже определения из XAML.
Скачать полностью все исходники можно из моего репозитория на assembly.com.