How can I set IsThreeState = True on a WPF MenuItem checkbox?

I am looking to have a MenuItem whose inner CheckBox has the IsThreeState property set to true. I want to bind it to bool? in my ViewModel.

After researching a bit, I found IsChecked to be a plain old bool.

My first instinct is to add an anchored bool? The IsCheckedThreeState property on the MenuItem, but I still can't figure out how to get around the fact that the inner CheckBox is bound to the non-nullable IsChecked.

If there is no easier way, I could create a new control template and modify the CheckBox directly, but I think it will also require a ClassItem subclass to set IsChecked to NULL.

So, I need to subclass / customize the MenuItem template to get the functionality I want, or is there an easier way that I don't think of. Thanks for any help you could provide.

+3


source to share


3 answers


Ok, so I did some more digging.

The control template for MenuItem doesn't even have an internal CheckBox control. MenuItem uses its own IsChecked property and shows or hides the inner Path control to indicate its state.

So, I changed the default control template. I replaced Path with CheckBox and I reworked the triggers correctly:

<ControlTemplate x:Key="CustomMenuItemControlTemplate" TargetType="{x:Type MenuItem}">
    <Grid SnapsToDevicePixels="True" MinWidth="225" MinHeight="26">
        <Rectangle x:Name="OuterBorder" RadiusY="2" RadiusX="2"/>
        <Rectangle x:Name="Bg" Fill="{TemplateBinding Background}" Margin="1" RadiusY="1" RadiusX="1" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="1"/>
        <Rectangle x:Name="InnerBorder" Margin="2"/>
        <DockPanel>
            <ContentPresenter x:Name="Icon" Content="{TemplateBinding Icon}" ContentSource="Icon" Margin="4,0,6,0" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
            <CheckBox x:Name="MenuCheckBox" Margin="7,4" Visibility="Hidden" VerticalAlignment="Center" IsThreeState="True" />
            <ContentPresenter ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" ContentStringFormat="{TemplateBinding HeaderStringFormat}" ContentSource="Header" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center" />
        </DockPanel>
        <Popup x:Name="PART_Popup" AllowsTransparency="True" Focusable="False" HorizontalOffset="1" IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}" PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}" Placement="Bottom" VerticalOffset="-1">
            <Themes:SystemDropShadowChrome x:Name="Shdw" Color="Transparent">
                <Border x:Name="SubMenuBorder" BorderBrush="#FF959595" BorderThickness="1" Background="WhiteSmoke">
                    <ScrollViewer x:Name="SubMenuScrollViewer" Margin="1,0" Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}">
                        <Grid RenderOptions.ClearTypeHint="Enabled">
                            <Canvas HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0">
                                <Rectangle x:Name="OpaqueRect" Fill="WhiteSmoke" Height="{Binding ActualHeight, ElementName=SubMenuBorder}" Width="{Binding ActualWidth, ElementName=SubMenuBorder}"/>
                            </Canvas>
                            <Rectangle Fill="#FFF1F1F1" HorizontalAlignment="Left" Margin="1,2" RadiusY="2" RadiusX="2" Width="28"/>
                            <Rectangle Fill="#FFE2E3E3" HorizontalAlignment="Left" Margin="29,2,0,2" Width="1"/>
                            <Rectangle Fill="White" HorizontalAlignment="Left" Margin="30,2,0,2" Width="1"/>
                            <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Cycle" Grid.IsSharedSizeScope="True" Margin="2" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" KeyboardNavigation.TabNavigation="Cycle"/>
                        </Grid>
                    </ScrollViewer>
                </Border>
            </Themes:SystemDropShadowChrome>
        </Popup>
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="IsSuspendingPopupAnimation" Value="True">
            <Setter Property="PopupAnimation" TargetName="PART_Popup" Value="None"/>
        </Trigger>
        <Trigger Property="Icon" Value="{x:Null}">
            <Setter Property="Visibility" TargetName="Icon" Value="Collapsed"/>
        </Trigger>
        <Trigger Property="IsCheckable" Value="True">
            <Setter Property="Visibility" TargetName="MenuCheckBox" Value="Visible"/>
            <Setter Property="Visibility" TargetName="Icon" Value="Collapsed"/>
        </Trigger>
        <Trigger Property="HasDropShadow" SourceName="PART_Popup" Value="True">
            <Setter Property="Margin" TargetName="Shdw" Value="0,0,5,5"/>
            <Setter Property="Color" TargetName="Shdw" Value="#71000000"/>
        </Trigger>
        <Trigger Property="IsKeyboardFocused" Value="True">

            <Setter Property="Fill" TargetName="Bg">
                <Setter.Value>
                    <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                        <GradientStop Color="#0462c9f5" Offset="0"/>
                        <GradientStop Color="#1C62c9f5" Offset="0.75"/>
                        <GradientStop Color="#3062c9f5" Offset="1"/>
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
            <Setter Property="Stroke" TargetName="OuterBorder" Value="#C062c9f5"/>
        </Trigger>
        <Trigger Property="IsEnabled" Value="False">
            <Setter Property="Foreground" Value="#FF9A9A9A"/>
        </Trigger>
        <Trigger Property="CanContentScroll" SourceName="SubMenuScrollViewer" Value="False">
            <Setter Property="Canvas.Top" TargetName="OpaqueRect" Value="{Binding VerticalOffset, ElementName=SubMenuScrollViewer}"/>
            <Setter Property="Canvas.Left" TargetName="OpaqueRect" Value="{Binding HorizontalOffset, ElementName=SubMenuScrollViewer}"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

      

I have this in my own resource dictionary. You need to add a link to your project in "PresentationFramework.Aero" and the following xmlns:

xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"

      

Then I hook it all up in style:

<Style TargetType="MenuItem">
    <Style.Resources>
        <Style TargetType="{x:Type CheckBox}">
            <Setter Property="IsChecked" Value="{Binding IsSelected}" />
        </Style>
    </Style.Resources>
    <Setter Property="Template" Value="{StaticResource CustomMenuItemControlTemplate}" />
    <Setter Property="IsCheckable" Value="True" />
    <Setter Property="IsChecked" Value="{Binding ToggleIsSelected}" />
    <Setter Property="StaysOpenOnClick" Value="True" />
</Style>

      

The last piece of the puzzle is the bindings. Now I have two properties:

1) IsSelected - bool? which I bind to the internal CheckBox IsChecked property

2) ToggleIsSelected - a bool that I bind to the MenuItem's IsChecked property



Having these two bindings allows the user to click anywhere on the MenuItem to toggle the CheckBox without any binding errors if IsSelected is null. Properties are defined as:

public bool? IsSelected
{
    get
    {
        return _isSelected;
    }
    set
    {
        if (value == _isSelected)
            return;
        _isSelected = value ?? false;
        OnPropertyChanged();
        OnPropertyChanged("ToggleIsSelected");
    }
}

      

and

public bool ToggleIsSelected
{
    get
    {
        return IsSelected ?? false;
    }
    set
    {
        if (value == IsSelected)
            return;
        IsSelected = value;
    }
}

      

One potential drawback of this method is that users cannot switch the CheckBox to it with a null state. In my case, I only want to program it to zero, so it works for me. If you need users to toggle null, remove the ToggleIsSlected property from the ViewModel and its associated installer:

<Setter Property="IsChecked" Value="{Binding ToggleIsSelected}" />

      

... and change the IsSelected property to:

public bool? IsSelected
{
    get
    {
        return _isSelected;
    }
    set
    {
        if (value == _isSelected)
            return;
        _isSelected = value;
    OnPropertyChanged();
    }
}

      

This will also remove the ability to click anywhere to switch, so your users will have to check the CheckBox directly ... unless you think of another way to implement it.

I know this is a long answer ... but I thought that someone might need this. I might as well have saved them if I can. If you see any room for improvement, please let me know.

+1


source


The easiest option is probably to check the box via a property Icon

.

<MenuItem Header="..." StaysOpenOnClick="True" Click="MenuItem_ToggleCheckBox">
    <MenuItem.Icon>
        <CheckBox HorizontalAlignment="Center" VerticalAlignment="Center" IsThreeState="True" />
    </MenuItem.Icon>
</MenuItem>

      



In your event handler, Click

you need to manually update the checkbox (or some property the checkbox is bound to).

+1


source


If you want to keep the original MenuItem validation style and only want to display null values ​​without letting the user set them, this can be done using converters:

  1. A two-way ThreeStateToBooleanConverter that changes false / null / true to false / false / true (and false / true to false / true otherwise) that is used when binding MenuItem to bool? the property.
  2. IsNullToVisibilityConverter unidirectional converter, which changes null to Visible and everything else to Collapsed, which can be used to bind Visibility to what you want to display in the MenuItem when the value is NULL.
0


source







All Articles