Search code examples
wpfwindowsxaml

WPF ComboBox - ItemTemplate vs. IsEditable


I created a combo box with an item template. The template includes a status icon and text:

enter image description here

Here is the XAML:

    <DataTemplate x:Key="ItemTemplate">
      <WrapPanel>
        <Image Width="24" Height="24" Stretch="Fill" Source="{Binding StateImage}" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0,0,15,0"/>
        <Label Content="{Binding Address}" VerticalAlignment="Center" HorizontalAlignment="Center" />
      </WrapPanel>
    </DataTemplate>

<ComboBox x:Name="CbxClients" HorizontalAlignment="Center" VerticalAlignment="Top" ItemTemplate="{StaticResource ItemTemplate}" Width="320" Height="24" IsEditable="False" />

Now I want to make the the combo box editable, so that the user can enter new strings, which hould then be added to the list. Thus I set "IsEditable" to true. This is the result:

enter image description here

This change causes a problem: the icon is now no longer shown in the combo box (only in the drop down area). While editing/entering a string this would be fine, but after the input has been committed, I would expect the new item being added to the list and an icon to be shown.

Is there any way I can achieve this behavior?


Solution

  • You are encountering two problems:

    1. Problem: The ComboBox is not containing simple string items. Instead you have defined a DataTemplate to render a more complex item model.
      Solution: The point is that the ComboBox has no chance to know how to use the input value to construct a new instance of the item model. You have to do it explicitly.

    2. Problem: Because your ComboBox is configured to be in edit mode, the TextBox will only contain a text representation of your item model (the ComboBox will call object.ToString on the model to obtain a text representation).
      Solution: There is no out-of-the-box solution. If ComboBox.IsEditable returns false the ComboBox will display the ComboBox.SelectedItem using a ContentPresenter. If ComboBox.IsEditable returns true the ComboBox will replace the ContentPresenter with a TextBox in order to allow the editing. You can now override the original ControlTemplate and toggle between both content sites. For example, on GotFocus you show the TextBox and on LostFocus you switch back to the ContentPresenter.
      You can use the XAML Designer to extract the ControlTemplate or use visit Microsoft Dos: ComboBox Styles and Templates and copy the template and references from there.

    To enable text search set ComboBox.IsTextSearchEnabled to true and set the attached property TextSearch.TextPath to the actual property on the item model.

    For performance reasons you should replace the Label with a TextBlock, especially when the displayed text is dynamic.

    Given is the following item model:

    class NetworkAddress : INotifyPropertyChanged
    {
      public ImageSource StateImage { get; set; }
      public string Address { get; set; }
    }
    

    To enable searching and item creation you must configure the ComboBox as follows:

    <ComboBox ItemsSource="{Binding NetworkAddresses}"
              IsEditable="True"
              IsTextSearchEnabled="True"
              TextSearch.TextPath="Address"
              PreviewKeyUp="ComboBox_PreviewKeyUp">
      <ComboBox.ItemTemplate>
        <DataTemplate DataType="{x:Type NetworkAddress}">
          <WrapPanel>
            <Image Source="{Binding StateImage}" />
            <TextBlock Text="{Binding Address}" />
          </WrapPanel>
        </DataTemplate>
      </ComboBox.ItemTemplate>
    </ComboBox>
    

    Then handle the the ComboBox.PreviewKeyUp event to add a new item:

    partial class MainWindow : Window
    {
      // TODO::Property must be a dependency property because MainWindow is a DependencyObject
      public ObservableCollection<NetworkAddress> NetworkAddresses { get; }
    
      public MainWindow()
      {
        InitializeComponent();
    
        this.DataContext = this;
        this.NetworkAddresses = new ObservableCollection<NetworkAddress>
        {
          new NetworkAddress() { ... },
          new NetworkAddress() { ... },
        };
      }
    
      private void ComboBox_PreviewKeyUp(object sender, KeyEventArgs e)
      {
        if (e.Key is not Key.Enter or not Key.Return)
        {
          return;
        }
    
        var comboBox = sender as ComboBox;
        string inputText = comboBox.Text;
        if (comboBox.IsTextSearchEnabled
            && comboBox.SelectedItem is null
          || !this.NetworkAddresse.Any(address => address.Address.Equals(inputText, StringComparison.OrdinalIgnoreCase)))
        {
          AddNewItemToCollection(inputText);;
          comboBox.SelectedIndex = comboBox.Items.Count - 1;
    
          // Refresh is only required for the ComboBox flyout to render properly
          comboBox.Items.Refresh();
        }
      }
    
      private void AddNewItemToCollection(string text) 
        => this.NetworksAddresses.Add(new NetworkAddress() { ... });