可编辑组合框绑定和更新源触发器

6

要求

我想要一个ComboBox,用户可以输入一些文本或从下拉列表中选择文本。当用户在键入后按Enter或仅从下拉列表中选择项目时,绑定源应更新(在我的情况下最佳的显示行为)。

问题

  • 当设置UpdateSourceTrigger=PropertyChange(默认值)时,每输入一个字符便会触发源更新,这不好,因为调用属性设置器是昂贵的;
  • 当设置UpdateSourceTrigger=LostFocus时,选择下拉列表中的项将需要更多操作才能实际失去焦点,这对用户不太友好(需要点击两次才能选择项)。

我尝试使用UpdateSourceTrigger=Explicit,但结果不尽如人意:

<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();
    }

}

这段代码存在两个问题:
  • 当从下拉菜单中选择项目时,源会更新为以前选择的值,为什么?
  • 当用户开始输入内容,然后点击下拉按钮从列表中选择内容时,源再次更新(由于失去焦点?),如何避免这种情况?
我有些担心会陷入XY问题,因此我发布了原始要求(也许我走错了方向?)而不是要求帮助我解决上述问题之一。
4个回答

8

你的响应特定事件更新源代码的方法是正确的,但在更新 ComboBox 时还有其他需要考虑的因素。此外,您可能希望将 UpdateSourceTrigger 设置为 LostFocus,这样您就不必处理太多的更新情况。

您还应考虑将代码移动到可重用的附加属性中,以便将来可以将其应用于其他组合框。碰巧我过去创建了这样的属性。

/// <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();
                }
            }
        }
    }
}

这是在 XAML 中设置属性的示例:

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

我认为这会给您想要的行为,可以直接使用它或根据自己的喜好进行修改。
但是,该行为的实现存在一个问题,即如果您开始键入现有值(并且不按回车键),然后从下拉列表中选择相同的值,则在这种情况下源不会更新,直到您按回车键、更改焦点或选择其他值。我确信可以解决这个问题,但由于这不是正常的工作流程,所以我没有花时间去解决这个问题。

2

1
谢谢你的想法,了解到Delay很好。如果它按照我理解的方式工作,这看起来是一个非常好的解决方法。 - Sinatr

0

我曾经遇到过类似的问题,我在 .cs 代码中处理了它。虽然这不是 XAML 的方式,但它可以解决问题。首先我断开了绑定,然后手动双向传递值。

<ComboBox x:Name="Combo_MyValue" 
                      ItemsSource="{Binding Source={StaticResource ListData}, XPath=MyContextType/MyValueType}"
                      DisplayMemberPath="@Description"
                      SelectedValuePath="@Value"
                      IsEditable="True"
                      Loaded="Combo_MyValue_Loaded"
                      SelectionChanged = "Combo_MyValue_SelectionChanged"
                      LostFocus="Combo_MyValue_LostFocus"
                      />


    private void Combo_MyValue_Loaded(object sender, RoutedEventArgs e)
    {
        if (DataContext != null)
        {
            Combo_MyValue.SelectedValue = ((MyContextType)DataContext).MyValue;
        }
    }

    private void Combo_MyValue_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if( e.AddedItems.Count == 0)
        {
            // this is a custom value, we'll set it in the lost focus event
            return;
        }
        // this is a picklist value, get the value from itemsource
        XmlElement selectedItem = (XmlElement)e.AddedItems[0];
        string selectedValue = selectedItem.GetAttribute("Value");
        ((PumpParameters)DataContext).MyValue = selectedValue;
    }

    private void Combo_MyValue_LostFocus(object sender, RoutedEventArgs e)
    {
        if( Combo_MyValue.IsDropDownOpen || Combo_MyValue.SelectedIndex != -1)
        {
            // not a custom value
            return; 
        }
        // custom value
        ((MyContextType)DataContext).MyValue = Combo_MyValue.Text;
    }

0

我曾经遇到过同样的问题。我的ComboBox.Text属性上有一个绑定,包括ValidationRules。如果从列表中选择了某些内容,立即更新源似乎是更好的用户体验,但如果正在输入内容,则不希望在输入完成之前进行验证。

我通过让绑定的UpdateSourceTrigger="LostFocus"来得到令人满意的解决方案。我创建了一个附加行为,在SelectionChanged事件发布时强制更新绑定源(在TextBox中输入时不会发布此事件)。如果您愿意,您可以将此事件处理程序放入代码后台而不是附加行为或附加属性类中。

protected void ComboBox_SelectionChanged(Object sender, SelectionChangedEventArgs e)
{
    // Get the BindingExpression object for the ComboBox.Text property.
    // We'll use this to force the value of ComboBox.Text to update to the binding source
    var be = BindingOperations.GetBindingExpression(comboBox, ComboBox.TextProperty);
    if (be == null) return;
    // Unfortunately, the code of the ComboBox class publishes the SelectionChanged event
    // immediately *before* it transfers the value of the SelectedItem to its Text property.
    // Therefore, the ComboBox.Text property does not yet have the value
    // that we want to transfer to the binding source.  We use reflection to invoke method
    // ComboBox.SelectedItemUpdated to force the update to the Text property just a bit early.
    // Method SelectedItemUpdated encapsulates everything that we need--it is exactly what
    // happens from method ComboBox.OnSelectionChanged.
    var method = typeof(ComboBox).GetMethod("SelectedItemUpdated",
                                    BindingFlags.NonPublic | BindingFlags.Instance);
    if (method == null) return;
    method.Invoke(comboBox, new Object[] { });
    // Now that ComboBox.Text has the proper value, we let the binding object update
    // its source.
    be.UpdateSource();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接