可编辑组合框绑定和更新源触发器
Editable ComboBox binding and update source trigger
要求
我想要 ComboBox
用户可以在其中输入一些文本或从下拉列表中选择文本。当用户在键入后按 Enter 或当项目只是从下拉列表中 selected 时,应该更新绑定源(在我的情况下最好的视图行为)。
问题
- 当设置
UpdateSourceTrigger=PropertyChange
(默认)时,源更新将在每个字符后触发,这不是很好,因为属性 setter 调用是昂贵的;
- 设置
UpdateSourceTrigger=LostFocus
后,select从下拉列表中选择项目将需要更多操作才能真正失去焦点,这对用户来说不是很友好(点击后需要额外点击到 select 项)。
我尝试使用 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 个问题:
- 当从下拉菜单中 selected 项目时,源会更新为 之前 selected 值,为什么?
- 当用户开始输入内容然后单击下拉按钮从列表中选择内容时 - 源再次更新(由于失去焦点?),如何避免这种情况?
我有点害怕陷入困境 XY problem 所以我发布了原始需求(也许我走错方向了?)而不是要求帮助我解决上述问题之一。
我建议保留 UpdateSourceTrigger=PropertyChanged
,并延迟组合框以帮助缓解昂贵的 setter/update 问题。延迟将导致 PropertyChanged
事件在触发前等待您指定的毫秒数。
更多关于延迟的信息:http://www.jonathanantoine.com/2011/09/21/wpf-4-5-part-4-the-new-bindings-delay-property/
希望有人会为您提出更好的解决方案,但这至少应该让您暂时继续前进。
您根据特定事件更新源代码的方法是正确的,但是 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、更改焦点或选择不同的值。我确信这可以解决,但我花时间解决这个问题还不够,因为它不是正常的工作流程。
我有一个类似的问题,我在 .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;
}
我遇到了同样的问题。我对 ComboBox.Text 属性 进行了绑定,包括 ValidationRules。如果在从列表中选择某些内容时立即更新源,这似乎是更好的用户体验,但如果在框中输入内容,那么我不希望在输入完成之前进行验证。
通过让绑定的 UpdateSourceTrigger="LostFocus",我得到了一个令人满意的解决方案。我创建了一个附加行为,它在发布 SelectionChanged 事件时强制更新绑定源(在 TextBox 中键入时不发布)。如果您愿意,可以将此事件处理程序放入代码隐藏而不是附加行为或附加 属性 class.
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();
}
要求
我想要 ComboBox
用户可以在其中输入一些文本或从下拉列表中选择文本。当用户在键入后按 Enter 或当项目只是从下拉列表中 selected 时,应该更新绑定源(在我的情况下最好的视图行为)。
问题
- 当设置
UpdateSourceTrigger=PropertyChange
(默认)时,源更新将在每个字符后触发,这不是很好,因为属性 setter 调用是昂贵的; - 设置
UpdateSourceTrigger=LostFocus
后,select从下拉列表中选择项目将需要更多操作才能真正失去焦点,这对用户来说不是很友好(点击后需要额外点击到 select 项)。
我尝试使用 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 个问题:
- 当从下拉菜单中 selected 项目时,源会更新为 之前 selected 值,为什么?
- 当用户开始输入内容然后单击下拉按钮从列表中选择内容时 - 源再次更新(由于失去焦点?),如何避免这种情况?
我有点害怕陷入困境 XY problem 所以我发布了原始需求(也许我走错方向了?)而不是要求帮助我解决上述问题之一。
我建议保留 UpdateSourceTrigger=PropertyChanged
,并延迟组合框以帮助缓解昂贵的 setter/update 问题。延迟将导致 PropertyChanged
事件在触发前等待您指定的毫秒数。
更多关于延迟的信息:http://www.jonathanantoine.com/2011/09/21/wpf-4-5-part-4-the-new-bindings-delay-property/
希望有人会为您提出更好的解决方案,但这至少应该让您暂时继续前进。
您根据特定事件更新源代码的方法是正确的,但是 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、更改焦点或选择不同的值。我确信这可以解决,但我花时间解决这个问题还不够,因为它不是正常的工作流程。
我有一个类似的问题,我在 .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;
}
我遇到了同样的问题。我对 ComboBox.Text 属性 进行了绑定,包括 ValidationRules。如果在从列表中选择某些内容时立即更新源,这似乎是更好的用户体验,但如果在框中输入内容,那么我不希望在输入完成之前进行验证。
通过让绑定的 UpdateSourceTrigger="LostFocus",我得到了一个令人满意的解决方案。我创建了一个附加行为,它在发布 SelectionChanged 事件时强制更新绑定源(在 TextBox 中键入时不发布)。如果您愿意,可以将此事件处理程序放入代码隐藏而不是附加行为或附加 属性 class.
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();
}