Search code examples
c#wpfxamlcombobox

How to bind Multiple Buttons in Combobox


enter image description here

I want to bind a list of buttons in combo boxes. Each Combobox will contains buttons of one category and so on. As referred in attached image.

Below is my code:

<ItemsControl x:Name="iNumbersList">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal" MaxWidth="930"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button  Click="ItemButtonClick"
                 Tag="{Binding ItemTag}"
                 HorizontalContentAlignment="Center"
                 VerticalContentAlignment="Center"
                 Height="100" Width="300">
                <TextBlock TextAlignment="Center" Foreground="Red"
                       HorizontalAlignment="Center" FontWeight="SemiBold"
                       FontSize="25" TextWrapping="Wrap"
                       Text="{Binding ItemDisplayMember}"/>
            </Button>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
public class NumberModel
{
    public string ItemDisplayMember { get; set; }
    public object ItemTag { get; set; }
    public string ItemCategory { get; set; }
}

How can I group by ItemCategory property and bind it on GUI, a ComboBox for each ItemCategory and then multiple buttons in it?


Solution

  • Probably you don't need a ComboBox but Expander because the objective is reachable using it. ComboBox is needed when you have to filter something inside it or use dropdown displayed above Windows content.

    I wrote a simple example using MVVM programming pattern. There will be many new classes but most of it you need add to the project only once. Let's go from the scratch!

    1) Create class NotifyPropertyChanged to implement INotifyPropertyChanged interface. It needed to make able Binding to update the layout dynamically in Runtime.

    NotifyPropertyChanged.cs

    public class NotifyPropertyChanged : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    

    2) Create MainViewModel class derived from NotifyPropertyChanged. It will be used for Binding target Properties.

    MainViewModel.cs

    public class MainViewModel : NotifyPropertyChanged
    {
        public MainViewModel()
        {
    
        }
    }
    

    3) Attach MainViewModel to MainWindow's DataContext. One of ways - doing it in xaml.

    MainWindow.xaml

    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    

    4) Derive the data class NumberModel from NotifyPropertyChanged and add OnPropertyChanged call for each Property. This will give wonderful effect: when you change any Property in runtime, You'll immediately see the changes in UI. MVVM Magic called Binding :)

    NumberModel.cs

    public class NumberModel : NotifyPropertyChanged
    {
        private string _itemDisplayMember;
        private object _itemTag;
        private string _itemCategory;
    
        public string ItemDisplayMember
        { 
            get => _itemDisplayMember;
            set 
            {
                _itemDisplayMember = value;
                OnPropertyChanged();
            }
        }
        public object ItemTag
        {
            get => _itemTag;
            set
            {
                _itemTag = value;
                OnPropertyChanged();
            }
        }
        public string ItemCategory
        {
            get => _itemCategory;
            set
            {
                _itemCategory = value;
                OnPropertyChanged();
            }
        }
    }
    

    5) When button is clicked I will not handle the Click event but call a Command. For easy use of Commands I suggest this relaying its logic class (grabbed here).

    RelayCommand.cs

    public class RelayCommand : ICommand
    {
        private readonly Action<object> _execute;
        private readonly Func<object, bool> _canExecute;
    
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    
        public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
        {
            _execute = execute;
            _canExecute = canExecute;
        }
    
        public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
        public void Execute(object parameter) => _execute(parameter);
    }
    

    6) All ready to fill the MainViewModel with code. I've added there a command and some items to collection for test.

    MainViewModel.cs

    public class MainViewModel : NotifyPropertyChanged
    {
        private ObservableCollection<NumberModel> _itemsList;
        private ICommand _myCommand;
    
        public ObservableCollection<NumberModel> ItemsList
        {
            get => _itemsList;
            set
            {
                _itemsList = value;
                OnPropertyChanged();
            }
        }
    
        public ICommand MyCommand => _myCommand ?? (_myCommand = new RelayCommand(parameter =>
        {
            if (parameter is NumberModel number) 
                MessageBox.Show("ItemDisplayMember: " + number.ItemDisplayMember + "\r\nItemTag: " + number.ItemTag.ToString() + "\r\nItemCategory: " + number.ItemCategory);
        }));
    
        public MainViewModel()
        {
            ItemsList = new ObservableCollection<NumberModel>
            {
                new NumberModel { ItemDisplayMember = "Button1", ItemTag="Tag1", ItemCategory = "Category1" },
                new NumberModel { ItemDisplayMember = "Button2", ItemTag="Tag2", ItemCategory = "Category1" },
                new NumberModel { ItemDisplayMember = "Button3", ItemTag="Tag3", ItemCategory = "Category1" },
                new NumberModel { ItemDisplayMember = "Button4", ItemTag="Tag4", ItemCategory = "Category2" },
                new NumberModel { ItemDisplayMember = "Button5", ItemTag="Tag5", ItemCategory = "Category2" },
                new NumberModel { ItemDisplayMember = "Button6", ItemTag="Tag6", ItemCategory = "Category2" },
                new NumberModel { ItemDisplayMember = "Button7", ItemTag="Tag7", ItemCategory = "Category3" },
                new NumberModel { ItemDisplayMember = "Button8", ItemTag="Tag8", ItemCategory = "Category4" },
                new NumberModel { ItemDisplayMember = "Button9", ItemTag="Tag9", ItemCategory = "Category4" }
            };
        }
    }
    

    7) The main answer to your main question is: use IValueConverter to filter the list with requred criteria. I wrote 2 converters. First for Categories, second for Buttons.

    Converters.cs

    public class CategoryConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is ObservableCollection<NumberModel> collection)
            {
                List<NumberModel> result = new List<NumberModel>();
                foreach (NumberModel item in collection)
                {
                    if (!result.Any(x => x.ItemCategory == item.ItemCategory))
                        result.Add(item);
                }
                return result;
            }
            return null;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    
    public class ItemGroupConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values[0] is ObservableCollection<NumberModel> collection && values[1] is string categoryName)
            {
                List<NumberModel> result = new List<NumberModel>();
                foreach (NumberModel item in collection)
                {
                    if (item.ItemCategory == categoryName)
                        result.Add(item);
                }
                return result;
            }
            return null;
        }
    
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

    8) Now everything is ready to fill the markup. I post full markup here to make everything clear.

    Note: I faced Visual Studio 2019 16.5.4 crash while setting a MultiBinding in ItemsSource and applied the workaround.

    MainWindow.xaml

    <Window x:Class="WpfApp1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:WpfApp1"
            Title="MainWindow" Height="600" Width="1000" WindowStartupLocation="CenterScreen">
        <Window.DataContext>
            <local:MainViewModel/>
        </Window.DataContext>
        <Window.Resources>
            <local:CategoryConverter x:Key="CategoryConverter"/>
            <local:ItemGroupConverter x:Key="ItemGroupConverter"/>
        </Window.Resources>
        <Grid>
            <ItemsControl ItemsSource="{Binding ItemsList, Converter={StaticResource CategoryConverter}}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Vertical"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate DataType="{x:Type local:NumberModel}">
                        <Expander Header="{Binding ItemCategory}">
                            <ItemsControl DataContext="{Binding DataContext,RelativeSource={RelativeSource AncestorType=Window}}">
                                <ItemsControl.Style>
                                    <Style TargetType="ItemsControl">
                                        <Setter Property="ItemsSource">
                                            <Setter.Value>
                                                <MultiBinding Converter="{StaticResource ItemGroupConverter}">
                                                    <Binding Path="ItemsList"/>
                                                    <Binding Path="Header" RelativeSource="{RelativeSource AncestorType=Expander}"/>
                                                </MultiBinding>
                                            </Setter.Value>
                                        </Setter>
                                    </Style>
                                </ItemsControl.Style>
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <WrapPanel HorizontalAlignment="Left" VerticalAlignment="Center" Orientation="Horizontal" MaxWidth="930"/>
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate DataType="{x:Type local:NumberModel}">
                                        <Button Tag="{Binding ItemTag}"
                                                HorizontalContentAlignment="Center"
                                                VerticalContentAlignment="Center"
                                                Height="100" Width="300"
                                                Command="{Binding DataContext.MyCommand,RelativeSource={RelativeSource AncestorType=Window}}"
                                                CommandParameter="{Binding}">
                                            <TextBlock TextAlignment="Center" Foreground="Red"
                                                       HorizontalAlignment="Center" FontWeight="SemiBold"
                                                       FontSize="25" TextWrapping="Wrap"
                                                       Text="{Binding ItemDisplayMember}"/>
                                        </Button>
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                        </Expander>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Grid>
    </Window>
    

    Job is done with MVVM. Enjoy. :)

    Screenshot

    P.S. Ah, yes, I forgot to show you the code-behind class. Here it is!

    MainWindow.xaml.cs

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }