Улучшаем Silverlight приложения: стандартное контекстное меню для TextBox

    • Silverlight
    • XAML
    • Silverlight 4
    • Sample
    • Behavior
    • Toolkit
  • modified:
  • reading: 4 minutes

Как часто у меня бывает такое, что в одной руке у меня кружка чая или пряник, а другой рукой я печатаю и вожу мышкой. И вот не могу я одной рукой сразу же нажать Ctrl+C или Ctrl+V (могу конечно, но не удобно, не привычно). Во всех программах, на всех сайтах, у меня есть возможность выделить мышкой текст и скопировать его, а дальше вставить в другое место, а вот TextBox по умолчанию в Silverlight не предоставляет мне такой возможности, и это очень плохо. В особенности для бизнес-приложений. Люди привыкают к стандартным функциям, нельзя их лишать этого. Я говорю об этом меню:

sample

В 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.

Get Microsoft Silverlight

Конечно же, этот Behavior еще можно доработать. Так можно добавить возможность выбора, как отображать меню: в отдельном контекстном меню, выше или ниже определения, сделанного разработчиком. Сейчас, всегда меню добавляется ниже определения из XAML.

Скачать полностью все исходники можно из моего репозитория на assembly.com.

See Also