outcoldman
outcoldman Denis Gladkikh

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

Silverlight, XAML, Silverlight 4, Sample, Behavior, and Toolkit

Как часто у меня бывает такое, что в одной руке у меня кружка чая или пряник, а другой рукой я печатаю и вожу мышкой. И вот не могу я одной рукой сразу же нажать 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.

Have feedback or questions? Looking for consultation?

My expertise: MongoDB, ElasticSearch, Splunk, and other databases. Docker, Kubernetes. Logging, Metrics. Performance, memory leaks.

Send me an email to public@denis.gladkikh.email.

The content on this site represents my own personal opinions and thoughts at the time of posting.

Content licensed under the Creative Commons CC BY 4.0.