【问题标题】:Editable ComboBox binding and update source trigger可编辑的 ComboBox 绑定和更新源触发器
【发布时间】:2015-08-15 03:06:45
【问题描述】:

要求

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

}

此代码有 2 个问题:

  • 当从下拉菜单中选择项目时,源会更新为以前选择的值,为什么?
  • 当用户开始输入内容然后单击下拉按钮从列表中选择内容时 - 源再次更新(由于焦点丢失?),如何避免这种情况?

我有点害怕落入XY problem,这就是为什么我发布原始要求(也许我走错方向了?)而不是要求帮助我解决上述问题之一。

【问题讨论】:

    标签: c# wpf binding combobox


    【解决方案1】:

    您更新源以响应特定事件的方法是正确的,但是您必须考虑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" />
    

    我认为这会给你你正在寻找的行为。随意按原样使用它或根据自己的喜好对其进行修改。

    行为实现存在一个问题,如果您开始键入现有值(并且不按 Enter),然后从下拉列表中选择相同的值,在这种情况下源不会更新,直到您按输入、更改焦点或选择不同的值。我确信这可以解决,但我花时间在这方面还不够,因为这不是一个正常的工作流程。

    【讨论】:

      【解决方案2】:

      我建议保留UpdateSourceTrigger=PropertyChanged,并延迟组合框以帮助缓解昂贵的设置器/更新问题。延迟将导致PropertyChanged 事件在触发前等待您指定的毫秒数。

      更多关于延迟的信息:http://www.jonathanantoine.com/2011/09/21/wpf-4-5-part-4-the-new-bindings-delay-property/

      希望有人会为您提出更好的解决方案,但这至少应该让您暂时继续前进。

      【讨论】:

      • 感谢您的想法,很高兴了解Delay。如果按照我理解的方式工作,这看起来是一个非常好的解决方法。
      【解决方案3】:

      我遇到了类似的问题,我在 .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;
          }
      

      【讨论】:

        【解决方案4】:

        我遇到了同样的问题。我对 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();
        }
        

        【讨论】:

          猜你喜欢
          • 2011-06-13
          • 1970-01-01
          • 1970-01-01
          • 2015-05-22
          • 1970-01-01
          • 2010-10-05
          • 2021-02-20
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多