¿Artículos de menú seleccionables mutuamente exclusivos?

Dado el siguiente código:

     

En XAML, ¿hay alguna forma de crear elementos de menú seleccionables que sean mutuamente exclusivos? ¿Dónde está el usuario revisa el elemento 2? Los elementos 1 y 3 se desactivan automáticamente.

Puedo lograr esto en el código subyacente controlando los eventos de clic en el menú, determinando qué elemento se verificó y desmarcando los demás elementos de menú. Estoy pensando que hay una manera más fácil.

¿Algunas ideas?

Puede que esto no sea lo que estás buscando, pero podrías escribir una extensión para la clase MenuItem que te permite usar algo como la propiedad GroupName de la clase RadioButton . ToggleButton ligeramente este útil ejemplo para extender de manera ToggleButton controles ToggleButton y lo ToggleButton un poco para su situación y se me ocurrió esto:

 using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; namespace WpfTest { public class MenuItemExtensions : DependencyObject { public static Dictionary ElementToGroupNames = new Dictionary(); public static readonly DependencyProperty GroupNameProperty = DependencyProperty.RegisterAttached("GroupName", typeof(String), typeof(MenuItemExtensions), new PropertyMetadata(String.Empty, OnGroupNameChanged)); public static void SetGroupName(MenuItem element, String value) { element.SetValue(GroupNameProperty, value); } public static String GetGroupName(MenuItem element) { return element.GetValue(GroupNameProperty).ToString(); } private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { //Add an entry to the group name collection var menuItem = d as MenuItem; if (menuItem != null) { String newGroupName = e.NewValue.ToString(); String oldGroupName = e.OldValue.ToString(); if (String.IsNullOrEmpty(newGroupName)) { //Removing the toggle button from grouping RemoveCheckboxFromGrouping(menuItem); } else { //Switching to a new group if (newGroupName != oldGroupName) { if (!String.IsNullOrEmpty(oldGroupName)) { //Remove the old group mapping RemoveCheckboxFromGrouping(menuItem); } ElementToGroupNames.Add(menuItem, e.NewValue.ToString()); menuItem.Checked += MenuItemChecked; } } } } private static void RemoveCheckboxFromGrouping(MenuItem checkBox) { ElementToGroupNames.Remove(checkBox); checkBox.Checked -= MenuItemChecked; } static void MenuItemChecked(object sender, RoutedEventArgs e) { var menuItem = e.OriginalSource as MenuItem; foreach (var item in ElementToGroupNames) { if (item.Key != menuItem && item.Value == GetGroupName(menuItem)) { item.Key.IsChecked = false; } } } } } 

Luego, en el XAML, escribirías:

       

Es un poco molesto, pero ofrece la ventaja de no forzarte a escribir ningún código de procedimiento adicional (aparte de la clase de extensión, por supuesto) para implementarlo.

El mérito recae en Brad Cunningham, autor de la solución ToggleButton original.

También puedes usar un Comportamiento. Como éste:

         public class MenuItemButtonGroupBehavior : Behavior { protected override void OnAttached() { base.OnAttached(); GetCheckableSubMenuItems(AssociatedObject) .ToList() .ForEach(item => item.Click += OnClick); } protected override void OnDetaching() { base.OnDetaching(); GetCheckableSubMenuItems(AssociatedObject) .ToList() .ForEach(item => item.Click -= OnClick); } private static IEnumerable GetCheckableSubMenuItems(ItemsControl menuItem) { var itemCollection = menuItem.Items; return itemCollection.OfType().Where(menuItemCandidate => menuItemCandidate.IsCheckable); } private void OnClick(object sender, RoutedEventArgs routedEventArgs) { var menuItem = (MenuItem)sender; if (!menuItem.IsChecked) { menuItem.IsChecked = true; return; } GetCheckableSubMenuItems(AssociatedObject) .Where(item => item != menuItem) .ToList() .ForEach(item => item.IsChecked = false); } } 

Sí, esto se puede hacer fácilmente haciendo que cada MenuItem sea RadioButton. Esto puede hacerse editando la Plantilla de MenuItem.

  1. Haga clic con el botón derecho en MenuItem en el panel izquierdo del esquema de documento> EditTemplate> EditCopy. Esto agregará el código para editar en Window.Resources.

  2. Ahora, tienes que hacer solo dos cambios que son muy simples.

    Artículos de menú mutuamente exclusivos a. Agregue RadioButton con algunos recursos para ocultar su porción de círculo.

    segundo. Cambiar BorderThickness = 0 para la parte del borde de MenuItem.

    Estos cambios se muestran a continuación como comentarios, el rest del estilo generado se debe usar como está:

          M 0,5.1 L 1.7,5.2 L 3.4,7.1 L 8,0.4 L 9.2,0 L 3.3,10.8 Z                               ...  
  3. Aplicar el estilo,

      

Agregando esto en la parte inferior ya que aún no tengo la reputación …

Tan útil como la respuesta de Patrick es que no garantiza que los elementos no se puedan desmarcar. Para hacer eso, el manejador Controlado debe cambiarse a un manejador de Clic, y debe cambiarse a lo siguiente:

 static void MenuItemClicked(object sender, RoutedEventArgs e) { var menuItem = e.OriginalSource as MenuItem; if (menuItem.IsChecked) { foreach (var item in ElementToGroupNames) { if (item.Key != menuItem && item.Value == GetGroupName(menuItem)) { item.Key.IsChecked = false; } } } else // it's not possible for the user to deselect an item { menuItem.IsChecked = true; } } 

No existe una forma integrada de hacer esto en XAML, necesitará rodar su propia solución u obtener una solución existente si está disponible.

Solo pensé en incluir mi solución, ya que ninguna de las respuestas satisfacía mis necesidades. Mi solución completa está aquí …

WPF MenuItem como RadioButton

Sin embargo, la idea básica es usar ItemContainerStyle.

    

Y se debe agregar el siguiente clic de evento para que se active RadioButton cuando se hace clic en MenuItem (de lo contrario, debe hacer clic exactamente en RadioButton):

 private void MenuItemWithRadioButtons_Click(object sender, System.Windows.RoutedEventArgs e) { MenuItem mi = sender as MenuItem; if (mi != null) { RadioButton rb = mi.Icon as RadioButton; if (rb != null) { rb.IsChecked = true; } } } 

Como no hay una respuesta similar, publico mi solución aquí:

 public class RadioMenuItem : MenuItem { public string GroupName { get; set; } protected override void OnClick() { var ic = Parent as ItemsControl; if (null != ic) { var rmi = ic.Items.OfType().FirstOrDefault(i => i.GroupName == GroupName && i.IsChecked); if (null != rmi) rmi.IsChecked = false; IsChecked = true; } base.OnClick(); } } 

En XAML simplemente utilízalo como un MenuItem habitual:

                

Muy simple y limpio. Y, por supuesto, puede hacer que GroupName una propiedad de dependencia mediante algunos códigos adicionales, eso es lo mismo que otros.

Por cierto, si no te gusta la marca de verificación, puedes cambiarla a lo que quieras:

 public override void OnApplyTemplate() { base.OnApplyTemplate(); var p = GetTemplateChild("Glyph") as Path; if (null == p) return; var x = p.Width/2; var y = p.Height/2; var r = Math.Min(x, y) - 1; var e = new EllipseGeometry(new Point(x,y), r, r); // this is just a flattened dot, of course you can draw // something else, eg a star? ;) p.Data = e.GetFlattenedPathGeometry(); } 

Si utilizó mucho de este RadioMenuItem en su progtwig, hay otra versión más eficiente que se muestra a continuación. Los datos literales se e.GetFlattenedPathGeometry().ToString() de e.GetFlattenedPathGeometry().ToString() en el fragmento de código anterior.

 private static readonly Geometry RadioDot = Geometry.Parse("M9,5.5L8.7,7.1 7.8,8.3 6.6,9.2L5,9.5L3.4,9.2 2.2,8.3 1.3,7.1L1,5.5L1.3,3.9 2.2,2.7 3.4,1.8L5,1.5L6.6,1.8 7.8,2.7 8.7,3.9L9,5.5z"); public override void OnApplyTemplate() { base.OnApplyTemplate(); var p = GetTemplateChild("Glyph") as Path; if (null == p) return; p.Data = RadioDot; } 

Y, por último, si planea envolverlo para usarlo en su proyecto, debe ocultar la propiedad IsCheckable de la clase base, ya que la comprobación automática de machenismo de la clase MenuItem hará que el estado de verificación de radio marque un comportamiento incorrecto.

 private new bool IsCheckable { get; } 

Por lo tanto, VS dará un error si un novato intenta comstackr XAML de la siguiente manera:

// ¡tenga en cuenta que este es un uso incorrecto!

// ¡tenga en cuenta que este es un uso incorrecto!

Lo logré usando un par de líneas de código:

Primero declara una variable:

 MenuItem LastBrightnessMenuItem =null; 

Cuando consideramos un grupo de elementos de menú, hay una probabilidad de usar un único controlador de eventos. En este caso, podemos usar esta lógica:

  private void BrightnessMenuClick(object sender, RoutedEventArgs e) { if (LastBrightnessMenuItem != null) { LastBrightnessMenuItem.IsChecked = false; } MenuItem m = sender as MenuItem; LastBrightnessMenuItem = m; //Handle the rest of the logic here } 

Encuentro que obtengo elementos de menú mutuamente excluyentes al vincular MenuItem.IsChecked a una variable.

Pero tiene una peculiaridad: si hace clic en el elemento del menú seleccionado, se vuelve inválido, que se muestra en el rectángulo rojo habitual. Lo resolví agregando un controlador para MenuItem. Haga clic en el botón que impide deseleccionar simplemente ajustando IsChecked en verdadero.

El código … me estoy vinculando a un tipo de enumeración, por lo que utilizo un convertidor enum que devuelve verdadero si la propiedad enlazada es igual al parámetro proporcionado. Aquí está el XAML:

    

Y aquí está el código detrás:

  private void MenuItem_OnClickDisallowUnselect(object sender, RoutedEventArgs e) { var menuItem = e.OriginalSource as MenuItem; if (menuItem == null) return; if (! menuItem.IsChecked) { menuItem.IsChecked = true; } } 

Simplemente cree una Plantilla para MenuItem que contendrá un RadioButton con un GroupName configurado en algún valor. También puede cambiar la plantilla de RadioButtons para que se parezca al glifo de comprobación predeterminado de MenuItem (que se puede extraer fácilmente con Expression Blend).

¡Eso es!

Podrías hacer algo como esto:

                    

Tiene un extraño efecto secundario visualmente (lo verás cuando lo usas), pero funciona de todos modos

Aquí hay otro enfoque que usa RoutedUICommands, una propiedad enum pública y DataTriggers. Esta es una solución bastante detallada. Desafortunadamente, no veo ninguna forma de hacer que Style.Triggers sea más pequeño, porque no sé cómo decir que el valor de enlace es lo único diferente. (Por cierto, para MVVMers este es un ejemplo terrible. Puse todo en la clase MainWindow solo para mantener las cosas simples).

MainWindow.xaml:

                                       

MainWindow.xaml.cs:

 using System.Windows; using System.Windows.Input; using System.ComponentModel; namespace MutuallyExclusiveMenuItems { public partial class MainWindow : Window, INotifyPropertyChanged { public MainWindow() { InitializeComponent(); DataContext = this; } #region Enum Property public enum CurrentItemEnum { EnumItem1, EnumItem2, EnumItem3 }; private CurrentItemEnum _currentMenuItem; public CurrentItemEnum CurrentMenuItem { get { return _currentMenuItem; } set { _currentMenuItem = value; OnPropertyChanged("CurrentMenuItem"); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } #endregion Enum Property #region Commands public static RoutedUICommand MenuItem1Cmd = new RoutedUICommand("Item_1", "Item1cmd", typeof(MainWindow)); public void MenuItem1Execute(object sender, ExecutedRoutedEventArgs e) { CurrentMenuItem = CurrentItemEnum.EnumItem1; } public static RoutedUICommand MenuItem2Cmd = new RoutedUICommand("Item_2", "Item2cmd", typeof(MainWindow)); public void MenuItem2Execute(object sender, ExecutedRoutedEventArgs e) { CurrentMenuItem = CurrentItemEnum.EnumItem2; } public static RoutedUICommand MenuItem3Cmd = new RoutedUICommand("Item_3", "Item3cmd", typeof(MainWindow)); public void MenuItem3Execute(object sender, ExecutedRoutedEventArgs e) { CurrentMenuItem = CurrentItemEnum.EnumItem3; } public void CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } #endregion Commands } } 

Aquí hay otra manera: no es fácil de ningún modo, pero es compatible con MVVM, combinable y altamente comprobable por unidad. Si tiene la libertad de agregar un convertidor a su proyecto y no le importa un poco de basura en la forma de una nueva lista de elementos cada vez que se abre el menú contextual, esto funciona realmente bien. Cumple con la pregunta original de cómo proporcionar un conjunto mutuamente exclusivo de elementos marcados en un menú contextual.

Creo que si desea extraer todo esto en un control de usuario, podría convertirlo en un componente de biblioteca reutilizable para volver a utilizarlo en su aplicación. Los componentes utilizados son Type3.Xaml con una grilla simple, un bloque de texto y el menú contextual. Haga clic derecho en cualquier lugar de la grilla para que aparezca el menú.

Un convertidor de valor denominado AllValuesEqualToBooleanConverter se usa para comparar el valor de cada elemento de menú con el valor actual del grupo y mostrar la marca de verificación junto al elemento de menú que está seleccionado actualmente.

Una clase simple que representa sus opciones de menú se usa para ilustración. El contenedor de muestra utiliza Tuple con las propiedades String y Entero que hacen que sea bastante fácil tener un fragmento de texto legible por humanos estrechamente emparejado con un valor amigable para la máquina. Puede usar cadenas solo o String y un Enum para realizar un seguimiento del valor para tomar decisiones sobre lo que es actual. Type3VM.cs es el ViewModel asignado a DataContext para Type3.Xaml. Sin embargo, puede asignar su contexto de datos en su marco de aplicación existente, use el mismo mecanismo aquí. El marco de aplicación en uso se basa en INotifyPropertyChanged para comunicar los valores modificados a WPF y su enlace goo. Si tiene propiedades de dependencia, puede necesitar modificar un poco el código.

El inconveniente de esta implementación, además del convertidor y su longitud, es que se crea una lista de elementos no utilizados cada vez que se abre el menú contextual. Para aplicaciones de usuario único, esto probablemente sea correcto, pero debe tenerlo en cuenta.

La aplicación utiliza una implementación de RelayCommand que está disponible en el sitio web de Haacked o en cualquier otra clase de ayuda compatible con ICommand disponible en cualquier marco que esté utilizando.

clase pública Type3VM: INotifyPropertyChanged {private List menuData = new List (new [] {new MenuData (“Zero”, 0), new MenuData (“One”, 1), new MenuData (“Two”, 2), new MenuData ( “Tres”, 3),});

  public IEnumerable MenuData { get { return menuData.ToList(); } } private int selected; public int Selected { get { return selected; } set { selected = value; OnPropertyChanged(); } } private ICommand contextMenuClickedCommand; public ICommand ContextMenuClickedCommand { get { return contextMenuClickedCommand; } } private void ContextMenuClickedAction(object clicked) { var data = clicked as MenuData; Selected = data.Item2; OnPropertyChanged("MenuData"); } public Type3VM() { contextMenuClickedCommand = new RelayCommand(ContextMenuClickedAction); } private void OnPropertyChanged([CallerMemberName]string propertyName = null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged; } public class MenuData : Tuple { public MenuData(String DisplayValue, int value) : base(DisplayValue, value) { } } 

” />

 public class AreAllValuesEqualConverter : IMultiValueConverter { public T EqualValue { get; set; } public T NotEqualValue { get; set; } public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) { T returnValue; if (values.Length < 2) { returnValue = EqualValue; } // Need to use .Equals() instead of == so that string comparison works, but must check for null first. else if (values[0] == null) { returnValue = (values.All(v => v == null)) ? EqualValue : NotEqualValue; } else { returnValue = (values.All(v => values[0].Equals(v))) ? EqualValue : NotEqualValue; } return returnValue; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } [ValueConversion(typeof(object), typeof(Boolean))] public class AllValuesEqualToBooleanConverter : AreAllValuesEqualConverter { }