Search code examples
c#wpfxamldata-bindingwpf-controls

Binding to SelectedItem of ComboBox in UserControl


I have a UserControl consisting of a ComboBox with a Label. I am looking to update a screen with an instance of this ComboBox and dynamically create UserControls in a StackPanel, based on the SelectedItem value.

I currently have a screen with an instance of this ComboBox and have it binding the following way:

Pseudocode Example (removing unrelated code):

<!-- MyComboBoxExample.xaml -->    
<ComboBox x:Name="myComboBox" SelectedValuePath="Key" DisplayMemberPath="Value" ItemsSource="{Binding MyBoxItems}/>
/* MyComboBoxExample.xaml.cs */

public static readonly DependencyProperty MyBoxItemsProperty = DependencyProperty.Register("MyBoxItems", typeof(Dictionary<string, string>),
    typeof(MyComboBoxExample), new PropertyMetadata(null));
<!-- MyScreen.xaml -->    
<local:MyComboBoxExample x:Name="MyComboBoxExampleInstance" MyBoxItems="{Binding Descriptions}"/>

I am new to WPF and databinding, so not sure the best way to implement this. Basically, on the screen: when MyComboBoxExampleInstance selection changes, dynamically set the controls of a StackPanel on the screen. I am not sure how to properly hook in to the SelectionChanged event of a child object of a UserControl.

Any thoughts, corrections, and (constructive) criticism is appreciated. Thanks for any help in advance.


Solution

  • There are several ways to go about this. Here's one way. It's not necessarily the best way but it's easy to understand.

    First, the user control xaml. Note the binding of the ItemsSource property on the user control, which specifies MyComboBoxItems as the items source. More on where that comes from in a bit.

     <UserControl x:Class="WpfApp1.MyUserControl"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:local="clr-namespace:WpfApp1"
                 mc:Ignorable="d" 
                 d:DesignHeight="450" d:DesignWidth="800">
            <Grid>
                <ComboBox Height="Auto" ItemsSource="{Binding MyComboBoxItems}" SelectionChanged="OnSelectionChanged">
                   <ComboBox.ItemTemplate>
                       <DataTemplate>
                           <TextBlock Text="{Binding Text}"/>
                       </DataTemplate>
                   </ComboBox.ItemTemplate>
               </ComboBox>
            </Grid>
        </UserControl>
    

    Now the code-behind, MyUserControl.xaml.cs. We provide an combobox selection changed event handler that in turn raises a custom event, MyComboBoxSelectionChanged, which is defined by the event argument class and delegate handler at the bottom of the code. Our OnSelectionChanged method simply forwards the selection change event via the custom event we've defined.

    using System;
    using System.Windows.Controls;
    
    namespace WpfApp1
    {
        /// <summary>
        /// Interaction logic for MyUserControl.xaml
        /// </summary>
        public partial class MyUserControl : UserControl
        {
            public event MyComboBoxSelectionChangedEventHandler MyComboBoxSelectionChanged;
            public MyUserControl()
            {
                InitializeComponent();
            }
    
            private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
            {
    
                if (e.AddedItems.Count > 0)
                {
                    MyComboBoxSelectionChanged?.Invoke(this,
                        new MyComboBoxSelectionChangedEventArgs() {MyComboBoxItem = e.AddedItems[0]});
                }
            }
        }
    
        public class MyComboBoxSelectionChangedEventArgs : EventArgs
        {
            public object MyComboBoxItem { get; set; }
        }
    
        public delegate void MyComboBoxSelectionChangedEventHandler(object sender, MyComboBoxSelectionChangedEventArgs e);
    
    }
    

    Now we go to our MainWindow.xaml, where we define an instance of MyUserControl and set a handler for the custom event we defined. We also provide a StackPanel to host the items that will be created on a selection changed event.

    <Window x:Class="WpfApp1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            mc:Ignorable="d"
            Title="MainWindow"
            Height="450"
            Width="800">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
    
            <local:MyUserControl Width="140" Height="32" DataContext="{Binding}"  Grid.Row="0" MyComboBoxSelectionChanged="OnSelectionChanged"></local:MyUserControl>
    
            <StackPanel Grid.Row="1" x:Name="MyUserControls"/>
        </Grid>
    
    </Window>
    

    Now the code-behind for MainWindow.xaml. Here we define a public property containing a list of objects of type MyComboBoxItem (defined at the bottom of the file), and we initialize the array with some values.

    Recall that we set the ItemsSource property of the ComboBox inside MyUserControl to "{Binding MyComboBoxItems}", so the question is, how does the property defined in the MainWindow magically become available in MyUserControl?

    In WPF, DataContext values are inherited from parent controls if they aren't explicitly set, and since we did not specify a data context for the control, the instance of MyUserControl inherits the DataContext of the parent window. In the constructor we set the MainWindow data context to refer to itself, so the MyComboBoxItems list is available to any child controls (and their children, and so on.)

    Typically we'd go ahead and add a dependency property for the user control called ItemsSource and in the user control we'd bind the ComboBox's ItemsSource property to the dependency property rather than to MyComboxItems. MainWindow.xaml would then bind it's collection directly to the dependency property on the user control. This helps make the user control more re-usable since it wouldn't depend on specific properties defined in an inherited data context.

    Finally, in the event handler for the user control's custom event we obtain the value selected by the user and create a UserControl populated with a text box (all with various properties set to make the items interesting visually) and we directly add them to the Children property of the StackPanel.

    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    
    namespace WpfApp1
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            public List<MyComboBoxItem> MyComboBoxItems { get; set; } = new List<MyComboBoxItem>()
            {
                new MyComboBoxItem() {Text = "Item1"},
                new MyComboBoxItem() {Text = "Item2"},
                new MyComboBoxItem() {Text = "Item3"},
    
            };
            public MainWindow()
            {
                InitializeComponent();
                DataContext = this;
            }
    
            private void OnSelectionChanged(object sender, MyComboBoxSelectionChangedEventArgs e)
            {
                if (e.MyComboBoxItem is MyComboBoxItem item)
                {
                    MyUserControls.Children.Add(
                    new UserControl()
                    {
                        Margin = new Thickness(2),
                        Background = new SolidColorBrush(Colors.LightGray),
                        Content = new TextBlock()
                        {
                            Margin = new Thickness(4),
                            VerticalAlignment = VerticalAlignment.Center,
                            HorizontalAlignment = HorizontalAlignment.Center,
                            FontSize = 48,
                            FontWeight = FontWeights.Bold,
                            Foreground = new SolidColorBrush(Colors.DarkGreen),
                            Text = item.Text
                        }
                    });
                }
            }
        }
    
        public class MyComboBoxItem
        {
            public string Text { get; set; }
        }
    }
    

    Finally, I'd consider using an ItemsControl or a ListBox bound to an ObservableCollection rather than sticking things into a StackPanel. You could define a nice data template for the user control to display and maybe a DataTemplateSelector to use different user controls based on settings in the data item. This would allow me to simply add the reference to the MyComboBoxItem obtained in the selection changed handler to that collection, and the binding machinery would automatically generate a new item using the data template I defined and create the necessary visual elements to display it.

    So given all that, here are the changes to do all that.

    First, we modify our data item to add a color property. We'll use that property to determine how we display the selected item:

    public class MyComboBoxItem
    {
        public string Color { get; set; }
        public string Text { get; set; }
    }
    

    Now we implement INotifyPropertyChanged in MainWindow.xaml.cs to let the WPF binding engine update the UI when we change properties. This is the event handler and a helper method, OnPropertyChanged.

    We also modify the combo box initializer to add a value for the Color property. We'll leave on blank for fun.

    We then add a new ObservableCollect, "ActiveUserControls" to store the MyComboBoxItem received in the combo box selection changed event. We do that instead of creating user controls on the fly in code.

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        public List<MyComboBoxItem> MyComboBoxItems { get; set; } = new List<MyComboBoxItem>()
        {
            new MyComboBoxItem() {Text = "Item1", Color = "Red"},
            new MyComboBoxItem() {Text = "Item2", Color = "Green"},
            new MyComboBoxItem() {Text = "Item3"},
        };
    
        private ObservableCollection<MyComboBoxItem> _activeUserControls;
        public ObservableCollection<MyComboBoxItem> ActiveUserControls
        {
            get => _activeUserControls;
            set { _activeUserControls = value; OnPropertyChanged(); }
        }
    
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }
    
        private void OnSelectionChanged(object sender, MyComboBoxSelectionChangedEventArgs e)
        {
            if (e.MyComboBoxItem is MyComboBoxItem item)
            {
                if (ActiveUserControls == null)
                {
                    ActiveUserControls = new ObservableCollection<MyComboBoxItem>();
                }
    
                ActiveUserControls.Add(item);
            }
        }
    }
    

    Now let's look at some changes we made to MyUserControl. We've modified the combo box ItemsSource to point at a property, ItemsSource defined in MyUserControl, and we also map the ItemTemplate to an ItemTemplate property in MyUserControl.

    <UserControl x:Class="WpfApp1.MyUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d"
             d:DesignHeight="450"
             d:DesignWidth="800">
        <Grid>
            <ComboBox Height="Auto"
                  ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}}"
                  ItemTemplate="{Binding ItemTemplate, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}}"
                  SelectionChanged="OnSelectionChanged">
    
            </ComboBox>
        </Grid>
    </UserControl>
    

    Here's were we define those new properties in MyUserControl.cs.

    public partial class MyUserControl : UserControl
    {
        public event MyComboBoxSelectionChangedEventHandler MyComboBoxSelectionChanged;
        public MyUserControl()
        {
            InitializeComponent();
        }
    
        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource",
                typeof(System.Collections.IEnumerable),
                typeof(MyUserControl),
                new PropertyMetadata(null));
    
        public System.Collections.IEnumerable ItemsSource
        {
            get => GetValue(ItemsSourceProperty) as IEnumerable;
            set => SetValue(ItemsSourceProperty, (IEnumerable)value);
        }
    
        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate",
                typeof(DataTemplate),
                typeof(MyUserControl),
                new PropertyMetadata(null));
    
        public DataTemplate ItemTemplate
        {
            get => GetValue(ItemTemplateProperty) as DataTemplate;
            set => SetValue(ItemTemplateProperty, (DataTemplate)value);
        }
    
        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
    
            if (e.AddedItems.Count > 0)
            {
                MyComboBoxSelectionChanged?.Invoke(this,
                    new MyComboBoxSelectionChangedEventArgs() {MyComboBoxItem = e.AddedItems[0]});
            }
        }
    }
    

    Let's look at how we bind to those in MainWindow.xaml:

    <local:MyUserControl Width="140"
                             Height="32"
                             Grid.Row="0"
                             MyComboBoxSelectionChanged="OnSelectionChanged"
                             ItemsSource="{Binding MyComboBoxItems}"
                             ItemTemplate="{StaticResource ComboBoxItemDataTemplate}" />
    

    So now we can bind our items directly and provide our own data template to specify how the combobox should display the item.

    Finally, I want to replace the StackPanel with an ItemsControl. This is like a ListBox without scrolling or item selection support. In fact, ListBox is derived from ItemsControl. I also want to use a different user control in the list based on the value of the Color property. To do that, we define some data templates for each value in MainWindow.Xaml:

        <DataTemplate x:Key="ComboBoxItemDataTemplate"
                      DataType="local:MyComboBoxItem">
            <StackPanel Orientation="Horizontal">
                <TextBlock Margin="4"
                           Text="{Binding Text}" />
                <TextBlock Margin="4"
                           Text="{Binding Color}" />
            </StackPanel>
        </DataTemplate>
    
        <DataTemplate x:Key="GreenUserControlDataTemplate"
                      DataType="local:MyComboBoxItem">
            <local:GreenUserControl DataContext="{Binding}" />
        </DataTemplate>
    
        <DataTemplate x:Key="RedUserControlDataTemplate"
                      DataType="local:MyComboBoxItem">
            <local:RedUserControl DataContext="{Binding}" />
        </DataTemplate>
    
        <DataTemplate x:Key="UnspecifiedUserControlDataTemplate"
                      DataType="local:MyComboBoxItem">
            <TextBlock Margin="4"
                       Text="{Binding Text}" />
        </DataTemplate>
    

    Here's RedUserControl. Green is the same with a different foreground color.

    <UserControl x:Class="WpfApp1.RedUserControl"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                 xmlns:local="clr-namespace:WpfApp1"
                 mc:Ignorable="d"
                 d:DesignHeight="450"
                 d:DesignWidth="800">
        <Grid Background="LightGray"
              Margin="2">
            <TextBlock Margin="4"
                       Foreground="DarkRed"
                       TextWrapping="Wrap"
                       Text="{Binding Text}"
                       FontSize="24"
                       FontWeight="Bold" />
        </Grid>
    </UserControl>
    

    Now the trick is to use the right data template based on the color value. For that we create a DataTemplateSelector. This is called by WPF for each item to be displayed. We can examine the data context object a choose which data template to use:

    public class UserControlDataTemplateSelector : DataTemplateSelector
    {
    
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            if (container is FrameworkElement fe)
            {
                if (item is MyComboBoxItem cbItem)
                {
                    if (cbItem.Color == "Red")
                    {
                        return fe.FindResource("RedUserControlDataTemplate") as DataTemplate;
                    }
                    if (cbItem.Color == "Green")
                    {
                        return fe.FindResource("GreenUserControlDataTemplate") as DataTemplate;
                    }
                    return fe.FindResource("UnspecifiedUserControlDataTemplate") as DataTemplate;
                }
            }
            return null;
        }
    }
    

    We create an instance of our data template selector in xaml in MainWindow.xaml:

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

    Finally we replace our stack panel with an Items control:

       <ItemsControl Grid.Row="1"
                      x:Name="MyUserControls"
                      ItemsSource="{Binding ActiveUserControls}"
                      ItemTemplateSelector="{StaticResource UserControlDataTemplateSelector}" />