Как в XAML создать checkable-элементы меню, которые поддерживают только один нажатый элемент (как RadioButton)
Допустим, мы хотим в XAML создать меню, содержащее список элементов, в котором одновременно может быть выбран только один (поведение наподобие RadioButton).
В первую очередь, создадим вспомогательный класс, который наследуется от DependencyObject и содержит базовую логику.
Namespace Helpers
''' <summary>
''' Позволяет создать checkable-элементы меню,
''' которые поддерживают только один нажатый элемент меню одновременно (как RadioButton).
''' </summary>
Public Class MenuItemExtensions
Inherits DependencyObject
Public Shared ElementToGroupNames As New Dictionary(Of MenuItem, String)()
''' <summary>
''' Хранит состояние выделенных пунктов меню.
''' </summary>
Private Shared ReadOnly ElementToGroupValues As New Dictionary(Of MenuItem, Boolean)()
Public Shared ReadOnly GroupNameProperty As DependencyProperty = DependencyProperty.RegisterAttached("GroupName", GetType(String), GetType(MenuItemExtensions), New PropertyMetadata(String.Empty, AddressOf OnGroupNameChanged))
Public Shared Sub SetGroupName(element As MenuItem, value As String)
element.SetValue(GroupNameProperty, value)
End Sub
Public Shared Function GetGroupName(element As MenuItem) As String
Return element.GetValue(GroupNameProperty).ToString()
End Function
Private Shared Sub OnGroupNameChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim menuItem As MenuItem = TryCast(d, MenuItem)
If (menuItem IsNot Nothing) Then
Dim newGroupName As String = e.NewValue.ToString()
If (String.IsNullOrEmpty(newGroupName)) Then
RemoveCheckboxFromGrouping(menuItem) 'удаляем из группы чекбокс
Else
'Переключаемся на новую группу:
Dim oldGroupName As String = e.OldValue.ToString()
If (newGroupName <> oldGroupName) Then
If (Not String.IsNullOrEmpty(oldGroupName)) Then 'удаляем привязку старой группы
RemoveCheckboxFromGrouping(menuItem)
End If
ElementToGroupNames.Add(menuItem, e.NewValue.ToString())
AddHandler menuItem.Checked, AddressOf MenuItemChecked
AddHandler menuItem.Unchecked, AddressOf MenuItemUnchecked
End If
End If
End If
End Sub
Private Shared Sub RemoveCheckboxFromGrouping(itm As MenuItem)
ElementToGroupNames.Remove(itm)
ElementToGroupValues.Remove(itm)
RemoveHandler itm.Checked, AddressOf MenuItemChecked
RemoveHandler itm.Unchecked, AddressOf MenuItemUnchecked
End Sub
Private Shared Sub MenuItemChecked(sender As Object, e As RoutedEventArgs)
Dim menuItem As MenuItem = CType(e.OriginalSource, MenuItem)
For Each item In ElementToGroupNames
If (item.Key IsNot menuItem) AndAlso (item.Value = GetGroupName(menuItem)) Then
item.Key.IsChecked = False
End If
ElementToGroupValues(item.Key) = item.Key.IsChecked 'запоминаем выделенный пункт меню
Next
End Sub
''' <summary>
''' Проверяет, что есть выделенный пункт меню. Если нет, то восстанавливает выделение.
''' </summary>
''' <remarks>
''' Если нажать на выделенный пункт меню, то по умолчанию выделение снимается.
''' Чтобы этого избежать, проверяем, что есть один выделенный пункт, и если его нет, то восстанавливаем выделение.
''' </remarks>
Private Shared Sub MenuItemUnchecked(sender As Object, e As RoutedEventArgs)
Dim mi As MenuItem = CType(sender, MenuItem)
Dim hasCheckedItem As Boolean = False
Dim sameGroupItems = ElementToGroupNames.Where(Function(x) GetGroupName(x.Key) = GetGroupName(mi))
'Проверяем, есть ли выделенные элементы:
For Each item As KeyValuePair(Of MenuItem, String) In sameGroupItems
If item.Key.IsChecked Then
hasCheckedItem = True
Exit For
End If
Next
'Если нет выделенных элементов, то восстанавливаем выделение:
If (Not hasCheckedItem) Then
Dim selectedGroupItems = ElementToGroupValues.Where(Function(x) GetGroupName(x.Key) = GetGroupName(mi))
For Each item In selectedGroupItems
If item.Value Then
item.Key.IsChecked = True
Exit For
End If
Next
End If
End Sub
End Class
End Namespace
Конечно же нам понадобится ссылка на пространство имён Helpers в XAML файле:
xmlns:help="clr-namespace:Helpers"
Рассмотрим использование класса MenuItemExtensions на таком примере. Пусть у нас имеется несколько значений толщин линий, которыми мы хотим рисовать некий график. Эти толщины будут представлены в виде списка дочерних элементов меню. Естественно, в один момент времени выбрать можно только одну толщину.
В XAML файле в разделе ресурсов создадим массив толщин:
<x:Array Type="{x:Type sys:Int32}" x:Key="lineWidthItems">
<sys:Int32>1</sys:Int32>
<sys:Int32>2<sys:Int32>
<sys:Int32>3</sys:Int32>
<sys:Int32>4</sys:Int32>
<sys:Int32>5</sys:Int32>
</x:Array>
Здесь "sys" – это псевдоним пространства имён System, который мы должны импортировать и указать в атрибутах класса XAML файла:
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Теперь в XAML разметке добавим меню, элементами которого будут толщины линий:
<Menu>
<MenuItem Header="Толщина линий" ItemsSource="{StaticResource lineWidthItems} Style="{StaticResource mnuLineWidthStyle}" />
</Menu>
Указанный стиль mnuLineWidthStyle необходимо разместить в секции ресурсов XAML файла. Опишем стиль так:
<Style TargetType="MenuItem" x:Key="mnuLineWidthStyle">
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding .}" />
<Setter Property="help:MenuItemExtensions.GroupName" Value="linewidths" />
<Setter Property="IsCheckable" Value="True" />
<Setter Property="IsChecked">
<Setter.Value>
<MultiBinding Converter="{StaticResource myConv}">
<Binding Path="PlotLineWidth" />
<Binding RelativeSource="{RelativeSource Mode=Self}" Path="Header" />
</MultiBinding>
</Setter.Value>
</Setter>
<EventSetter Event="Checked" Handler="SetLineWidth" />
</Style>
</Setter.Value>
</Setter>
</Style>
Добавим в застраничный код обработчик выбора элемента:
Private Sub SetLineWidth(sender As Object, e As RoutedEventArgs)
Dim linewidth As String = CType(sender, MenuItem).Header.ToString()
PlotLineWidth = Integer.Parse(linewidth)
'PlotLineWidth – это свойство, значение которого содержит ту самую толщину линий
'Или реализуете здесь какую-то свою логику.
End Sub
Также нужно создать конвертер "myConv", который сравнивает два полученных значения (значение текстового свойства Header элемента меню и свойства PlotLineWidth) и возвращает результат сравнения в вышеупомянутом MultiBinding:
Namespace Converters
Public Class MyConverter
Implements IMultiValueConverter
Public Function Convert(values() As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IMultiValueConverter.Convert
Dim cm1 As String = values(0).ToString()
Dim cm2 As String = values(1).ToString()
Return (cm1 = cm2)
End Function
Public Function ConvertBack(value As Object, targetTypes() As Type, parameter As Object, culture As CultureInfo) As Object() Implements IMultiValueConverter.ConvertBack
Throw New NotImplementedException()
End Function
End Class
End Namespace
И объявить этот конвертер в ресурсах XAML файла:
<conv:MyConverter x:Key="myConv" />
Само собой, что пространство имён Conveters также необходимо импортировать
xmlns:conv="clr-namespace:Converters"
Теперь всё будет работать, как мы и хотели.
