Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Editable ComboBox binding and update source trigger

Requirement

I want to have ComboBox where user may enter some text or choose text from drop-down list. Binding source should be updated when user press Enter after typing or when item is simply selected from drop-down list (best View behavior in my case).

Problem

  • when UpdateSourceTrigger=PropertyChange (default) is set, then source update will trigger after every character, which is not good, because property setter call is expensive;
  • when UpdateSourceTrigger=LostFocus is set, then selecting item from drop-down list will require one more action to actually lose focus, which is not very user-friendly (required additional click after click to select item).

I tried to use UpdateSourceTrigger=Explicit, but it doesn't go well:

<ComboBox IsEditable="True" VerticalAlignment="Top" ItemsSource="{Binding List}"
          Text="{Binding Text, UpdateSourceTrigger=LostFocus}"
          SelectionChanged="ComboBox_SelectionChanged"
          PreviewKeyDown="ComboBox_PreviewKeyDown" LostFocus="ComboBox_LostFocus"/>

public partial class MainWindow : Window
{
    private string _text = "Test";
    public string Text
    {
        get { return _text; }
        set
        {
            if (_text != value)
            {
                _text = value;
                MessageBox.Show(value);
            }
        }
    }

    public string[] List
    {
        get { return new[] { "Test", "AnotherTest" }; }
    }

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
    }

    private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (e.AddedItems.Count > 0)
            ((ComboBox)sender).GetBindingExpression(ComboBox.TextProperty).UpdateSource();
    }

    private void ComboBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        if(e.Key == Key.Enter)
            ((ComboBox)sender).GetBindingExpression(ComboBox.TextProperty).UpdateSource();
    }

    private void ComboBox_LostFocus(object sender, RoutedEventArgs e)
    {
        ((ComboBox)sender).GetBindingExpression(ComboBox.TextProperty).UpdateSource();
    }

}

This code has 2 issues:

  • when item is selected from drop-down menu, then source is updated with previously selected value, why?
  • when user start typing something and then click drop-down button to choose something from the list - source is updated again (due to focus lost?), how to avoid that?

I am a bit afraid to fall under XY problem that's why I posted original requirement (perhaps I went wrong direction?) instead of asking to help me to fix one of above problems.

like image 482
Sinatr Avatar asked Jan 09 '23 06:01

Sinatr


2 Answers

Your approach of updating the source in response to specific events is on the right track, but there is more you have to take into account with the way ComboBox updates things. Also, you probably want to leave UpdateSourceTrigger set to LostFocus so that you do not have as many update cases to deal with.

You should also consider moving the code to a reusable attached property so you can apply it to combo boxes elsewhere in the future. It so happens that I have created such a property in the past.

/// <summary>
/// Attached properties for use with combo boxes
/// </summary>
public static class ComboBoxBehaviors
{
    private static bool sInSelectionChange;

    /// <summary>
    /// Whether the combo box should commit changes to its Text property when the Enter key is pressed
    /// </summary>
    public static readonly DependencyProperty CommitOnEnterProperty = DependencyProperty.RegisterAttached("CommitOnEnter", typeof(bool), typeof(ComboBoxBehaviors),
        new PropertyMetadata(false, OnCommitOnEnterChanged));

    /// <summary>
    /// Returns the value of the CommitOnEnter property for the specified ComboBox
    /// </summary>
    public static bool GetCommitOnEnter(ComboBox control)
    {
        return (bool)control.GetValue(CommitOnEnterProperty);
    }

    /// <summary>
    /// Sets the value of the CommitOnEnterProperty for the specified ComboBox
    /// </summary>
    public static void SetCommitOnEnter(ComboBox control, bool value)
    {
        control.SetValue(CommitOnEnterProperty, value);
    }

    /// <summary>
    /// Called when the value of the CommitOnEnter property changes for a given ComboBox
    /// </summary>
    private static void OnCommitOnEnterChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        ComboBox control = sender as ComboBox;
        if (control != null)
        {
            if ((bool)e.OldValue)
            {
                control.KeyUp -= ComboBox_KeyUp;
                control.SelectionChanged -= ComboBox_SelectionChanged;
            }
            if ((bool)e.NewValue)
            {
                control.KeyUp += ComboBox_KeyUp;
                control.SelectionChanged += ComboBox_SelectionChanged;
            }
        }
    }

    /// <summary>
    /// Handler for the KeyUp event attached to a ComboBox that has CommitOnEnter set to true
    /// </summary>
    private static void ComboBox_KeyUp(object sender, KeyEventArgs e)
    {
        ComboBox control = sender as ComboBox;
        if (control != null && e.Key == Key.Enter)
        {
            BindingExpression expression = control.GetBindingExpression(ComboBox.TextProperty);
            if (expression != null)
            {
                expression.UpdateSource();
            }
            e.Handled = true;
        }
    }

    /// <summary>
    /// Handler for the SelectionChanged event attached to a ComboBox that has CommitOnEnter set to true
    /// </summary>
    private static void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (!sInSelectionChange)
        {
            var descriptor = DependencyPropertyDescriptor.FromProperty(ComboBox.TextProperty, typeof(ComboBox));
            descriptor.AddValueChanged(sender, ComboBox_TextChanged);
            sInSelectionChange = true;
        }
    }

    /// <summary>
    /// Handler for the Text property changing as a result of selection changing in a ComboBox that has CommitOnEnter set to true
    /// </summary>
    private static void ComboBox_TextChanged(object sender, EventArgs e)
    {
        var descriptor = DependencyPropertyDescriptor.FromProperty(ComboBox.TextProperty, typeof(ComboBox));
        descriptor.RemoveValueChanged(sender, ComboBox_TextChanged);

        ComboBox control = sender as ComboBox;
        if (control != null && sInSelectionChange)
        {
            sInSelectionChange = false;

            if (control.IsDropDownOpen)
            {
                BindingExpression expression = control.GetBindingExpression(ComboBox.TextProperty);
                if (expression != null)
                {
                    expression.UpdateSource();
                }
            }
        }
    }
}

Here is an example of setting the property in xaml:

<ComboBox IsEditable="True" ItemsSource="{Binding Items}" Text="{Binding SelectedItem, UpdateSourceTrigger=LostFocus}" local:ComboBoxBehaviors.CommitOnEnter="true" />

I think this will give you the behavior you are looking for. Feel free to use it as-is or modify it to your liking.

There is one issue with the behavior implementation where if you start to type an existing value (and don't press enter), then choose that same value from the dropdown, the source does not get updated in that case until you press enter, change focus, or choose a different value. I am sure that could be worked out, but it was not enough of an issue for me to spend time on it since it is not a normal workflow.

like image 80
Xavier Avatar answered Jan 20 '23 08:01

Xavier


I would recommend keeping UpdateSourceTrigger=PropertyChanged, and also putting a delay on the combobox to help mitigate your expensive setter/update problem. The delay will cause the PropertyChanged event to wait the amount of milliseconds you specify before firing.

More on Delay here: http://www.jonathanantoine.com/2011/09/21/wpf-4-5-part-4-the-new-bindings-delay-property/

Hopefully somebody will come up with a better solution for you, but this should at least get you moving along for now.

like image 37
jdnew18 Avatar answered Jan 20 '23 08:01

jdnew18