Search code examples
c#wpfmvvmmenu-itemsmvvm-toolkit

Menu items with checkboxes dynamically created from collection in WPF


I'm new into WPF, and I'm unfortunately used to work only with WinForms. I'd like to ask for advice with binding.

I have MainView and also MainViewModel

I want to fill menu items for languages like you can see in the picture below

Language menu items

I have obtained dictionary with language name as a key and bool as value in my viewModel:

    internal Dictionary<string,bool> Languages { get; set; }

#endregion
public MainWindowViewModel()
{
    Loc = new Locale(CurrentLanguage);
    Languages = Loc.GetAvailableLanguages();
}

I have handled it by adding them in the code behind of the MainWindowView.xaml file:

public partial class MainWindow : Window
{

    MainWindowViewModel viewModel;
    public MainWindow()
    {
        viewModel = new MainWindowViewModel();
        DataContext = viewModel;
        InitializeComponent();
        PopulateMILanguages();
    }

private void PopulateMILanguages()
{
    foreach (var lng in viewModel.Languages)
    {
        MenuItem mi = new MenuItem();
        mi.IsCheckable = true;
        mi.Header = lng.Key;
        mi.Checked += Mi_Checked;
        MI_Lngs.Items.Add(mi);
        mi.IsChecked = lng.Value;
        if (lng.Key.ToLowerInvariant() == 
            Settings.Default.LastSelectedLanguage.ToLowerInvariant())
        {
            mi.IsChecked = true;
        }
    }
}

private void Mi_Checked(object sender, RoutedEventArgs e)
{
    MenuItem menuItem = sender as MenuItem;
    viewModel.CurrentLanguage = menuItem.Header.ToString()
        .ToLowerInvariant();
    Settings.Default.LastSelectedLanguage=viewModel.CurrentLanguage;
    foreach (MenuItem mi in MI_Lngs.Items)
    {
        if (mi.Header != menuItem.Header)
        {
            mi.IsChecked = false;
        }
    }
}

    private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        Settings.Default.Save();
    }
}

I really don't like this solution, but I was not able to handle it to fill collection in the xaml:

<!--    LANGUAGE    -->
                <MenuItem x:Name="MI_Lngs" Header="{Binding Loc[Lng]}"
                          Style="{StaticResource MenuItemToolbar}">
                    <MenuItem.Icon>
                        <Image Source="/Assets/languages.png"/>
                    </MenuItem.Icon>
                    <MenuItem.ToolTip>
                        <ToolTip Content="{Binding Loc[Lng_TT]}"/>
                    </MenuItem.ToolTip>
                </MenuItem>

Could anyone please give me an advice how to handle it to fill menu items from the collection and also be able to catch checked events?

Thanks in advance

Jiri


Solution

  • You can use this menu with the below viewmodel to get the desired functionality.

    <Menu>
        <MenuItem Header="File"/>
        <MenuItem Header="Settings">
            <MenuItem Header="Application"/>
            <MenuItem Header="Language" ItemsSource="{Binding Languages}">
                <MenuItem.ItemContainerStyle>
                    <Style>
                        <Setter Property="MenuItem.IsCheckable" Value="True"/>
                        <Setter Property="MenuItem.Header" Value="{Binding Header, Mode=OneWay}"/>
                        <Setter Property="MenuItem.IsChecked" Value="{Binding IsSelected, Mode=OneWay}"/>
                        <Setter Property="MenuItem.Command" Value="{Binding DataContext.SelectLanguageCommand, RelativeSource={RelativeSource AncestorType=MenuItem}}"/>
                        <Setter Property="MenuItem.CommandParameter" Value="{Binding}"/>
                    </Style>
                </MenuItem.ItemContainerStyle>
            </MenuItem>
        </MenuItem>
    </Menu>
    

    I used CommunityToolkit.Mvvm library to give you some idea here. You can use another library.

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using System.Collections.ObjectModel;
    
    namespace WpfApp1.ViewModel
    {
        public class MainWindowViewModel : ObservableObject
        {
            private ObservableCollection<Language> _languages = new();
            public ObservableCollection<Language> Languages
            {
                get => _languages;
                set => SetProperty(ref _languages, value);
            }
    
            public IRelayCommand SelectLanguageCommand { get; }
    
            public MainWindowViewModel()
            {
                Languages.Add(new Language { Header = "CZ", IsSelected = false });
                Languages.Add(new Language { Header = "EN", IsSelected = true });
    
                SelectLanguageCommand = new RelayCommand<Language>(l =>
                {
                    if (l != null)
                    {
                        foreach (Language item in Languages)
                        {
                            item.IsSelected = false;
                        }
    
                        l.IsSelected = true;
                    }
                });
            }
        }
    
        public class Language : ObservableObject
        {
            private string? _header;
            public string? Header
            {
                get => _header;
                set => SetProperty(ref _header, value);
            }
    
            private bool _isSelected;
            public bool IsSelected
            {
                get => _isSelected;
                set => SetProperty(ref _isSelected, value);
            }
        }
    }