Mutually exclusive checkable menu items?

Viewed 34654

Given the following code:

<MenuItem x:Name="MenuItem_Root" Header="Root">
    <MenuItem x:Name="MenuItem_Item1" IsCheckable="True" Header="item1" />
    <MenuItem x:Name="MenuItem_Item2" IsCheckable="True" Header="item2"/>
    <MenuItem x:Name="MenuItem_Item3" IsCheckable="True" Header="item3"/>
</MenuItem>

In XAML, is there a way to create checkable menuitem's that are mutually exclusive? Where is the user checks item2, item's 1 and 3 are automatically unchecked.

I can accomplish this in the code behind by monitoring the click events on the menu, determining which item was checked, and unchecking the other menuitems. I'm thinking there is an easier way.

Any ideas?

18 Answers

Here's a simple, MVVM-based solution that leverages a simple IValueConverter and CommandParameter per MenuItem.

No need to re-style any MenuItem as a different type of control. MenuItems will automatically be deselected when the bound value doesn't match the CommandParameter.

Bind to an int property (MenuSelection) on the DataContext (ViewModel).

<MenuItem x:Name="MenuItem_Root" Header="Root">
    <MenuItem x:Name="MenuItem_Item1" IsCheckable="True" Header="item1" IsChecked="{Binding MenuSelection, ConverterParameter=1, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
    <MenuItem x:Name="MenuItem_Item2" IsCheckable="True" Header="item2" IsChecked="{Binding MenuSelection, ConverterParameter=2, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
    <MenuItem x:Name="MenuItem_Item3" IsCheckable="True" Header="item3" IsChecked="{Binding MenuSelection, ConverterParameter=3, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
</MenuItem>

Define your value converter. This will check the bound value against the command parameter and vice versa.

public class MatchingIntToBooleanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var paramVal = parameter as string;
        var objVal = ((int)value).ToString();

        return paramVal == objVal;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool)
        {
            var i = System.Convert.ToInt32((parameter ?? "0") as string);

            return ((bool)value)
                ? System.Convert.ChangeType(i, targetType)
                : 0;
        }

        return 0; // Returning a zero provides a case where none of the menuitems appear checked
    }
}

Add your resource

<Window.Resources>
    <ResourceDictionary>
        <local:MatchingIntToBooleanConverter x:Key="MatchingIntToBooleanConverter"/>
    </ResourceDictionary>
</Window.Resources>

Good luck!

I achieved this using a couple of lines of code:

First declare a variable:

MenuItem LastBrightnessMenuItem =null;

When we are considering a group of menuitems, there is a probability of using a single event handler. In this case we can use this logic:

    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


                }

Several years after i see this post with the keywords i wrote... i thought there was an easy solution, in wpf... Perhaps it's me, but i think it's a bit special to have a such massive arsenal for a so little thing as accepted solution. I don't even talk about the solution with 6likes i didn't understood where to click to have this options.

So perhaps it's really no elegant at all... But here a simple solution. What it do is simple.. a loop to all elements contained by the parent, to put it at false. The most of time people split this part from the others parts, of course it's only correct in this case.

private void MenuItem_Click_1(object sender, RoutedEventArgs e)
{
    MenuItem itemChecked = (MenuItem)sender;
    MenuItem itemParent = (MenuItem)itemChecked.Parent;

    foreach (MenuItem item in itemParent.Items)
    {
        if (item == itemChecked)continue;

        item.IsChecked = false;
    }
}

that's all and easy, xaml is a classic code with absolutaly nothing particular

<MenuItem Header="test">
    <MenuItem Header="1"  Click="MenuItem_Click_1" IsCheckable="True" StaysOpenOnClick="True"/>
    <MenuItem Header="2"  Click="MenuItem_Click_1" IsCheckable="True"  StaysOpenOnClick="True"/>
</MenuItem>

Of course you could have a need of the click method, it's not a problem, you can make a method that accept an object sender and each of your click method will use this method. It's old, it's ugly but for the while it works. And i have some problems to imagine so much code line for a so little thing, it's probably me that have a problem with xaml, but it seems incredible to have to do this to obtains to just have only one menuitem selected.

A small addition to the @Patrick answer.

As @MK10 mentioned, this solution allows user to deselect all items in a group. But the changes he suggested doesn't work for me now. Maybe, the WPF model was changed since that time, but now Checked event doesn't fired when an item is unchecked.

To avoid it, I would suggest to process the Unchecked event for MenuItem.

I changed these procedures:

        private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!(d is MenuItem menuItem))
                return;

            var newGroupName = e.NewValue.ToString();
            var oldGroupName = e.OldValue.ToString();
            if (string.IsNullOrEmpty(newGroupName))
            {
                RemoveCheckboxFromGrouping(menuItem);
            }
            else
            {
                if (newGroupName != oldGroupName)
                {
                    if (!string.IsNullOrEmpty(oldGroupName))
                    {
                        RemoveCheckboxFromGrouping(menuItem);
                    }
                    ElementToGroupNames.Add(menuItem, e.NewValue.ToString());
                    menuItem.Checked += MenuItemChecked;
                    menuItem.Unchecked += MenuItemUnchecked; // <-- ADDED
                }
            }
        }

        private static void RemoveCheckboxFromGrouping(MenuItem checkBox)
        {
            ElementToGroupNames.Remove(checkBox);
            checkBox.Checked -= MenuItemChecked;
            checkBox.Unchecked -= MenuItemUnchecked;   // <-- ADDED
        }

and added the next handler:

    private static void MenuItemUnchecked(object sender, RoutedEventArgs e)
    {
        if (!(e.OriginalSource is MenuItem menuItem))
            return;

        var isAnyItemChecked = ElementToGroupNames.Any(item => item.Value == GetGroupName(menuItem) && item.Key.IsChecked);
        if (!isAnyItemChecked)
            menuItem.IsChecked = true;
    }

Now the checked item remains checked when user clicks it second time.

You can hook both check and uncheck event for the MenuItem and inside the event you can call a common method like below:

        private void MenuItem_Unchecked(object sender, RoutedEventArgs e)
        {
            this.UpdateCheckeditem(sender as MenuItem);
        }

        private void MenuItem_Checked(object sender, RoutedEventArgs e)
        {
            this.UpdateCheckeditem(sender as MenuItem);
        }

        private void UpdateCheckedstatus(MenuItem item)
        {
             MenuItem itemChecked = (MenuItem)sender;
            MenuItem itemParent = (MenuItem)itemChecked.Parent;

            foreach (MenuItem item in itemParent.Items)
            {

                if (item != itemChecked && item.IsChecked)
                {
                    item.IsChecked = false;
                    break;
                }
            }
        }

I think this will give you the expected behavior.

Here is a custom control that i've created for this purpose. It handles correctly checking, unchecking, clicks events and group name changes.

If you want you can override the style of the menu item and change the checkmark to a radiomark, but is not necessary:

public class RadioMenuItem : MenuItem
{
    private bool abortCheckChange = false;

    [DefaultValue("")]
    public string GroupName
    {
        get => (string)GetValue(GroupNameProperty);
        set => SetValue(GroupNameProperty, value);
    }

    public static readonly DependencyProperty GroupNameProperty =
        DependencyProperty.Register(nameof(GroupName), typeof(string), typeof(RadioMenuItem),
            new PropertyMetadata("", (d, e) => ((RadioMenuItem)d).OnGroupNameChanged((string)e.OldValue, (string)e.NewValue)));


    static RadioMenuItem()
    {
        IsCheckedProperty.OverrideMetadata(typeof(RadioMenuItem),
            new FrameworkPropertyMetadata(null, (d, o) => ((RadioMenuItem)d).abortCheckChange ? d.GetValue(IsCheckedProperty) : o));
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new RadioMenuItem();
    }

    protected override void OnClick()
    {
        //This will handle correctly the click, but prevents the unchecking.
        //So the menu item acts that is correctly clicked (e.g. the menu disappears
        //but the user can only check, not uncheck the item.
        if (IsCheckable && IsChecked) abortCheckChange = true;
        base.OnClick();
        abortCheckChange = false;
    }

    protected override void OnChecked(RoutedEventArgs e)
    {
        base.OnChecked(e);
        //If the menu item is checked, other items of the same group will be unchecked.
        if (IsChecked) UncheckOtherGroupItems();
    }

    protected virtual void OnGroupNameChanged(string oldGroupName, string newGroupName)
    {
        //If the menu item enters on another group and is checked, other items will be unchecked.
        if (IsChecked) UncheckOtherGroupItems();
    }

    private void UncheckOtherGroupItems()
    {
        if (IsCheckable)
        {
            IEnumerable<RadioMenuItem> radioItems = Parent is ItemsControl parent ? parent.Items.OfType<RadioMenuItem>()
                .Where((item) => item.IsCheckable && (item.DataContext == parent.DataContext || item.DataContext != DataContext)) : null;

            if (radioItems != null)
            {
                foreach (RadioMenuItem item in radioItems)
                {
                    if (item != this && item.GroupName == GroupName)
                    {
                        //This will uncheck all other items on the same group.
                        item.IsChecked = false;
                    }
                }
            }
        }
    }
}

Example:

<Grid Background="Red" HorizontalAlignment="Left" Height="125" Margin="139,120,0,0" VerticalAlignment="Top" Width="120">
    <Grid.ContextMenu>
        <ContextMenu>
            <MenuItem IsCheckable="True" Header="Normal check 1"/>
            <MenuItem IsCheckable="True" Header="Normal check 2"/>
            <Separator/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 1" GroupName="Group1"/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 2" GroupName="Group1"/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 3" GroupName="Group1"/>
            <Separator/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 4" GroupName="Group2"/>
            <local:RadioMenuItem IsCheckable="True" Header="Radio check 5" GroupName="Group2"/>
        </ContextMenu>
    </Grid.ContextMenu>
</Grid>
Related