Search code examples
c#wpfxaml

Apply style to a MenuItem populated from an Observable Collection


I'm trying to populate a MenuItem from my menu item list, in doing so I'm able to get the data properly but the styling is getting messed up, it's as if there is an overlay with my current style and the default menu item style

Please have a look at the images:

Working
(https://i.sstatic.net/zWIPI.png)

Not Working
enter image description here

This is my list of sample items in the Menu

public IEnumerable<String> MenuItems { get; set; } = new List<string>() { "ItemA", "ItemB" };*

This is the main code

<Border Background="#fff">
     <Menu>
         <MenuItem Style="{StaticResource MenuItemDropDownBaseStyle}" 
                   Header="Hello"
                   ItemsSource="{Binding MenuItems}">
             <MenuItem.ItemTemplate>
                  <DataTemplate>
                       <!-- doesn't work -->
                       <MenuItem Header="{Binding }" Style="{StaticResource MenuItemBaseStyle}" />
                   </DataTemplate>
             </MenuItem.ItemTemplate>

             <!-- works -->
             <!-- <MenuItem Header="ItemA" Style="{StaticResource MenuItemBaseStyle}" /> -->
             <!-- <MenuItem Header="ItemB" Style="{StaticResource MenuItemBaseStyle}" /> -->
         </MenuItem>
      </Menu>
  </Border>

This is my styling for MenuItem dropdown and each container item within

<Window.Resources>

        <Color x:Key="White">#ffffff</Color>
        <SolidColorBrush x:Key="BackgroundBrushWhite" Color="{StaticResource White}" />

        <Color x:Key="Black">#000000</Color>
        <SolidColorBrush x:Key="BackgroundBrushBlack" Color="{StaticResource Black}" />

        <Color x:Key="Viking">#67cfdf</Color>
        <SolidColorBrush x:Key="BackgroundBrushViking" Color="{StaticResource Viking}" />

        <Color x:Key="MidnightBlue">#003966</Color>
        <SolidColorBrush x:Key="BackgroundBrushMidnightBlue" Color="{StaticResource MidnightBlue}" />

        <Color x:Key="CongressBlue">#014984</Color>
        <SolidColorBrush x:Key="BackgroundBrushCongressBlue" Color="{StaticResource CongressBlue}" />

        <Color x:Key="DullLavender">#8ca5e0</Color>
        <SolidColorBrush x:Key="BackgroundBrushDullLavender" Color="{StaticResource DullLavender}" />

        <Style x:Key="MenuItemDropDownBaseStyle" TargetType="MenuItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type MenuItem}">
                        <Grid>
                            <Border x:Name="ContentBorder" Padding="12, 7, 12, 7" CornerRadius="5" Margin="5">
                                <ContentPresenter ContentSource="Header"
                                              TextBlock.Foreground="{StaticResource BackgroundBrushBlack}"
                                              HorizontalAlignment="Center"
                                              VerticalAlignment="Center"/>
                            </Border>

                            <Popup x:Name="PART_Popup"
                               AllowsTransparency="True"
                               IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
                               PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}">

                                <Border CornerRadius="5" 
                                    Width="190"
                                    Padding="0, 5, 0, 5"
                                    Background="{StaticResource BackgroundBrushViking}">
                                    <!--<ScrollViewer Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}">-->

                                    <Grid RenderOptions.ClearTypeHint="Enabled">
                                        <!--<Canvas HorizontalAlignment="Center" VerticalAlignment="Center" />-->
                                        <ItemsPresenter x:Name="ItemsPresenter" />
                                    </Grid>
                                    <!--</ScrollViewer>-->
                                </Border>

                            </Popup>
                        </Grid>

                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSuspendingPopupAnimation" Value="True">
                                <Setter Property="PopupAnimation" TargetName="PART_Popup" Value="None" />
                            </Trigger>

                            <Trigger Property="IsHighlighted" Value="True">
                                <Setter Property="Background" TargetName="ContentBorder" Value="{StaticResource BackgroundBrushViking}" />
                                <Setter Property="BorderBrush" TargetName="ContentBorder" Value="{StaticResource BackgroundBrushViking}" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style x:Key="MenuItemBaseStyle" TargetType="MenuItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type MenuItem}">
                        <Border x:Name="ItemBorder" Height="32">
                            <Grid>
                                <ContentPresenter x:Name="ContentP"
                                              ContentSource="Header"
                                              TextBlock.Foreground="{StaticResource BackgroundBrushWhite}"
                                              HorizontalAlignment="Center"
                                              VerticalAlignment="Center" />
                                <Border x:Name="SideHighlight"
                                    Height="{Binding RelativeSource={RelativeSource AncestorType=Border}, Path=Height}"
                                    Width="4"
                                    Background="{StaticResource BackgroundBrushCongressBlue}"
                                    CornerRadius="0, 50, 50, 0"
                                    HorizontalAlignment="Left"
                                    VerticalAlignment="Center"
                                    Visibility="Collapsed" />
                            </Grid>
                        </Border>

                        <ControlTemplate.Triggers>
                            <Trigger Property="IsHighlighted" Value="True">
                                <Setter Property="TextBlock.Foreground" TargetName="ContentP" Value="{StaticResource BackgroundBrushMidnightBlue}" />
                                <Setter Property="Visibility" TargetName="SideHighlight" Value="Visible" />
                            </Trigger>

                            <Trigger Property="IsChecked" Value="True">
                                <Setter Property="Background" TargetName="ItemBorder" Value="blue" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

I tried applying in the ItemContainerStyle my custom style but doesn't work anyhow, I know I've missed something here but not able to figure out


Solution

  • MenuItem is a an ItemsControl (a HeaderdItemsControl). As such it automatically generates an item container to host the content. The content is the data item.
    This container is itself of type MenuItem. MenuItem is an item container that can host child item containers.

    Your properly behaving example omits the automatic item container generation because you were adding those containers explicitly:

    <MenuItem>
      <MenuItem /> <!-- Explicit child container -->
    </MenuItem>
    

    The "non-working" example defines an DataTemplate and the child elements are added implicitly via binding items to the MenuItem.ItemsSource: the parent MenuItem will generate an item container of type MenuItem and sets the data item as its content. Then the content's visuals are defined by the DataTemplate which in your case defines another MenuItem.

    The result is a MenuItem (item container) that has an MenuItem (from DataTemplate) as content. This leads to the undesired layout.
    In other words, the problem is not the Style but the DataTemplate.

    To fix this, remove the MenuItem from the DataTemplate. Instead provide an element that hosts the representation of the data item for example a TextBlock:

    <DataTemplate>
      <TextBlock Text="{Binding}" />
    </DataTemplate>
    

    Although this will make it work, the DataTemplate itself is pretty much redundant. It only displays a string value (the source collection is a collection of string items).
    The HeaderedItemsControl will create a similar content template automatically by default.
    Therefore, the following code is sufficient to show a list of menu items as child items of a MenuItem:

    <!-- Will automatically show "Item A" and "Item B" as child menu items -->
    <MenuItem Style="{StaticResource MenuItemDropDownBaseStyle}" 
              Header="Hello"
              ItemsSource="{Binding MenuItems}" /> 
    

    You only have to define a DataTemplate if the content layout is complex.

    To specify a Style for the chld items explicitly, simply set the ItemsControl.ItemContainer property:

    <!-- Will automatically show "Item A" and "Item B" as child menu items -->
    <MenuItem Style="{StaticResource MenuItemDropDownBaseStyle}" 
              Header="Hello"
              ItemsSource="{Binding MenuItems}"
              ItemContainerStyle="{StaticResource MenuItemBaseStyle}" /> 
    

    In case of a more complex data item, you can control the property which should be displayed as the header by setting the ItemsControl.DispalyMemberPath:

    <!-- Will automatically show "Item A" and "Item B" as child menu items -->
    <MenuItem Style="{StaticResource MenuItemDropDownBaseStyle}" 
              Header="Hello"
              ItemsSource="{Binding MenuItems}"
              ItemContainerStyle="{StaticResource MenuItemBaseStyle}"
              DispalyMemberPath="NameOfSomePropertyOnTheItemThatShouldBeTheHeader" /> 
    

    If your MenuItem is more complex than showing a simple text header then you have to define a DataTemplate or define a MenuItem.Header object using the XAML object notation (<MenuItem.Header> ... </MenuItem.Header>):

    ItemModel.cs

    // TODO::Implement INotifyPropertyChanged even if properties won't change!
    class ItemModel : INotifyPropertyChanged
    {
      // Value for the menu item's header
      public string Title { get; }
    
      // Value that binds to a CheckBox in the menu item
      public bool IsSelected { get; set; }
    }
    
    <!-- 
         Dynamic MenuItem generation using data binding (using ItemsSource).
    -->
    <Menu>
    
      <!-- 
           The Style assigned to ItemContainerStyle property is 
           automatically applied to all generated child containers (nested MenuItem elements). 
    
           Bind ItemsSource to an ObservableCollection<ItemModel> 
      -->
      <MenuItem Header="Parent of Generated Checkable Entries" 
                ItemsSource="{Binding ItemModels}" 
                ItemContainerStyle="{StaticResource MenuItemBaseStyle}"> 
        <MenuItem.ItemTemplate>
          <DataTemplate DataType="local:ItemModel">
            <CheckBox Content="{Binding Title}"
                      IsChecked="{Binding IsSelected}" />
          </DataTemplate>
        </MenuItem.ItemTemplate>
      </MenuItem>
    </Menu>
    
    <!-- Static (inline) MenuItem generation by adding MenuItem elements explicitly  -->
    <Menu>
    
      <!-- 
           The Style assigned to ItemContainerStyle property is 
           automatically applied to all children (nested MenuItem elements). 
           If a child MenuItem element has a value assigned to its Style property, 
           then this local value is used instead of the value from the ItemContainerStyle property.
      -->
      <MenuItem Header="Parent of Checkable Entries"
                ItemContainerStyle="{StaticResource MenuItemBaseStyle}"> 
    
        <!-- 
             The style assigned to the Style property is 
             exclusively applied to the current MenuItem. 
             In addition, this local value overrides the Style value
             assigned to the parent's ItemContainerStyle property.
        -->
        <MenuItem Style="{StaticResource MenuItemBaseStyle}">
          <MenuItem.Header>
            <CheckBox Content="Explicit menu item"
                      IsChecked="True" />
          </MenuItem.Header>
        </MenuItem>
    
        <!-- Alternative declaration using an item model -->
        <MenuItem Style="{StaticResource MenuItemBaseStyle}">
          <MenuItem.Header>
            <local:ItemModel Title="Explicit menu item"
                             IsSelected="True" />
          </MenuItem.Header>
          <MenuItem.ItemTemplate>
            <DataTemplate DataType="local:ItemModel">
              <CheckBox Content="{Binding Title}"
                        IsChecked="{Binding IsSelected}" />
            </DataTemplate>
          </MenuItem.ItemTemplate>
        </MenuItem>
      </MenuItem>
    </Menu>