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.
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.
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.
I had a similar issue, I handled it in the .cs code. It isn't the XAML way, but it gets it done. First I broke the binding, then manually propagated the value both ways.
<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;
}
I had this same problem. I had a binding on the ComboBox.Text property, including ValidationRules. It seems like a better user experience if the source is updated immediately when something is selected from the list, but if something is being typed into the box, then I didn't want validation to occur until the entry is complete.
I got a satisfying solution by letting the binding's UpdateSourceTrigger="LostFocus". I created an attached behavior that forces update to binding source when the SelectionChanged event is published (it is not published when typing into the TextBox). If you prefer, you can put this event handler into the code-behind instead of an attached behavior or attached property 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();
}
Related
This is my first foray into WPF and I've been trying to follow MVVM closely to get things right. The context here is that I've got a view that should display different sets of messages, all of which are stored in ObservableCollection<T>.
This is the code in my View (it's a UserControl that's hosted by a different view so I can navigate between different views at runtime)
The "i" namespace is xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
<ListBox ItemsSource="{Binding Messages}">
<i:Interaction.Behaviors>
<behaviours:ScrollOnNewItemBehaviour />
</i:Interaction.Behaviors>
<ListBox.ItemTemplate>
<DataTemplate DataType="entities:DisplayedUserMessage">
<!-- Removed for brevity -->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This is the code for the behaviour (pieced together from other SO questions I was browsing while getting to grips with the concept):
public sealed class ScrollOnNewItemBehaviour : Behavior<ListBox>
{
protected override void OnAttached()
{
AssociatedObject.Loaded += OnLoaded;
AssociatedObject.Unloaded += OnUnLoaded;
}
protected override void OnDetaching()
{
AssociatedObject.Loaded -= OnLoaded;
AssociatedObject.Unloaded -= OnUnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged += OnCollectionChanged;
}
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
var border = (Border)VisualTreeHelper.GetChild(AssociatedObject, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
// Only scroll when we're scrolled to the bottom of the listbox
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
{
scrollViewer.ScrollToBottom();
}
}
}
}
So here is where my specific issue comes up- the binding works just fine. When I change _selectedChannel (I've removed irrelevant code from the ViewModel below) the view updates with the new messages (_messages is a dictionary that holds the various ObservableCollection instances) and when I add new messages to them, the UI updates as well.
The problem is that at no point does the behaviour I've registered to the ListBox get triggered, which is a bit of an issue since I'm relying on it to keep things scrolled. My best guess was that maybe it doesn't support a bound ItemSource and the fact that the ItemSource is initially null (the dictionary will be populated asynchronously so there is no default set) means that it doesn't get registered properly/needs to be re-registered every time the binding updates?
public MessagesViewModel : ViewModelBase
{
private ObservableCollection<DisplayedUserMessage> _displayedMessages;
private Channel _selectedChannel;
public IList<DisplayedUserMessage> Messages
{
get
{
return _displayedMessages;
}
set
{
if (_displayedMessages == value)
{
return;
}
_displayedMessages = value;
NotifyPropertyChanged();
}
}
public Channel SelectedChannel
{
get
{
return _selectedChannel;
}
set
{
if (_selectedChannel == value)
{
return;
}
_selectedChannel = value;
Messages = _messages[_selectedChannel.Id];
NotifyPropertyChanged();
}
}
}
The behaviour works if it gets executed (I've verified that it doesn't with breakpoints), so if anyone has an idea regarding what I should change to make this work with changing ItemSources, please let me know!
You can subscribe to the PropertyChanged event of AssociatedObject.DataContext and wait for the Messages property to change to a valid value.
Or you initialize Messages with an empty collection and once the items are created, add them to Messages (this would raise the CollectionChanged for each item added).
A third solution is to subscribe to the ItemsControl.ItemContainerGenerator.ItemsChanged event. It is raised whenever the ItemsControl.Items property or the collection changes.
You can use this event instead of relying on the ItemsSource binding source to implement INotifyCollectionChanged. You also don't need to require the DataContext to implement INotifyPropertyChanged.
This will make your behavior more generic and reusable as it can now work with any ItemsControl.
This means listening to ItemsControl.ItemContainerGenerator.ItemsChanged is equal to INotifyCollectionChanged.CollectionChanged:
private void OnLoaded(object sender, RoutedEventArgs e)
{
var itemsControl = AssociatedObject as ItemsControl;
if (itemsControl == null) return;
itemsControl.ItemContainerGenerator.ItemsChanged += OnCollectionChanged;
}
private void OnCollectionChanged(object sender, ItemsChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
var border = (Border)VisualTreeHelper.GetChild(AssociatedObject, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
// Only scroll when we're scrolled to the bottom of the listbox
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
{
scrollViewer.ScrollToBottom();
}
}
}
In wpf I setup a tab control that binds to a collection of objects each object has a data template with a data grid presenting the data. If I select a particular cell and put it into edit mode, leaving the grid by going to another tab this will cause the exception below to be thrown on returning the datagrid:
'DeferRefresh' is not allowed during an AddNew or EditItem transaction.
It appears that the cell never left edit mode. Is there an easy way to take the cell out of edit mode, or is something else going on here?
Update: It looks like if I do not bind the tab control to the data source, but instead explicitly define each tab and then bind each item in the data source to a content control this problem goes away. This is not really a great solution, so I would still like to know how to bind the collection directly to the tab control.
Update: So what I have actually done for my own solution is to use a ListView and a content control in place of a tab control. I use a style to make the list view look tab like. The view model exposes a set of child view models and allows the user to select one via the list view. The content control then presents the selected view model and each view model has an associated data template which contains the data grid. With this setup switching between view models while in edit mode on the grid will properly end edit mode and save the data.
Here is the xaml for setting this up:
<ListView ItemTemplate="{StaticResource MakeItemsLookLikeTabs}"
ItemsSource="{Binding ViewModels}"
SelectedItem="{Binding Selected}"
Style="{StaticResource MakeItLookLikeATabControl}"/>
<ContentControl Content="{Binding Selected}">
I'll accept Phil's answer as that should work also, but for me the solution above seems like it will be more portable between projects.
I implemented a behavior for the DataGrid based on code I found in this thread.
Usage:<DataGrid local:DataGridCommitEditBehavior.CommitOnLostFocus="True" />
Code:
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
/// <summary>
/// Provides an ugly hack to prevent a bug in the data grid.
/// https://connect.microsoft.com/VisualStudio/feedback/details/532494/wpf-datagrid-and-tabcontrol-deferrefresh-exception
/// </summary>
public class DataGridCommitEditBehavior
{
public static readonly DependencyProperty CommitOnLostFocusProperty =
DependencyProperty.RegisterAttached(
"CommitOnLostFocus",
typeof(bool),
typeof(DataGridCommitEditBehavior),
new UIPropertyMetadata(false, OnCommitOnLostFocusChanged));
/// <summary>
/// A hack to find the data grid in the event handler of the tab control.
/// </summary>
private static readonly Dictionary<TabPanel, DataGrid> ControlMap = new Dictionary<TabPanel, DataGrid>();
public static bool GetCommitOnLostFocus(DataGrid datagrid)
{
return (bool)datagrid.GetValue(CommitOnLostFocusProperty);
}
public static void SetCommitOnLostFocus(DataGrid datagrid, bool value)
{
datagrid.SetValue(CommitOnLostFocusProperty, value);
}
private static void CommitEdit(DataGrid dataGrid)
{
dataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
dataGrid.CommitEdit(DataGridEditingUnit.Row, true);
}
private static DataGrid GetParentDatagrid(UIElement element)
{
UIElement childElement; // element from which to start the tree navigation, looking for a Datagrid parent
if (element is ComboBoxItem)
{
// Since ComboBoxItem.Parent is null, we must pass through ItemsPresenter in order to get the parent ComboBox
var parentItemsPresenter = VisualTreeFinder.FindParentControl<ItemsPresenter>(element as ComboBoxItem);
var combobox = parentItemsPresenter.TemplatedParent as ComboBox;
childElement = combobox;
}
else
{
childElement = element;
}
var parentDatagrid = VisualTreeFinder.FindParentControl<DataGrid>(childElement);
return parentDatagrid;
}
private static TabPanel GetTabPanel(TabControl tabControl)
{
return
(TabPanel)
tabControl.GetType().InvokeMember(
"ItemsHost",
BindingFlags.NonPublic | BindingFlags.GetProperty | BindingFlags.Instance,
null,
tabControl,
null);
}
private static void OnCommitOnLostFocusChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var dataGrid = depObj as DataGrid;
if (dataGrid == null)
{
return;
}
if (e.NewValue is bool == false)
{
return;
}
var parentTabControl = VisualTreeFinder.FindParentControl<TabControl>(dataGrid);
var tabPanel = GetTabPanel(parentTabControl);
if (tabPanel != null)
{
ControlMap[tabPanel] = dataGrid;
}
if ((bool)e.NewValue)
{
// Attach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown += OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus += OnDataGridLostFocus;
dataGrid.DataContextChanged += OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged += OnDataGridIsVisibleChanged;
}
else
{
// Detach event handlers
if (parentTabControl != null)
{
tabPanel.PreviewMouseLeftButtonDown -= OnParentTabControlPreviewMouseLeftButtonDown;
}
dataGrid.LostKeyboardFocus -= OnDataGridLostFocus;
dataGrid.DataContextChanged -= OnDataGridDataContextChanged;
dataGrid.IsVisibleChanged -= OnDataGridIsVisibleChanged;
}
}
private static void OnDataGridDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
CommitEdit(dataGrid);
}
private static void OnDataGridIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var senderDatagrid = (DataGrid)sender;
if ((bool)e.NewValue == false)
{
CommitEdit(senderDatagrid);
}
}
private static void OnDataGridLostFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var dataGrid = (DataGrid)sender;
var focusedElement = Keyboard.FocusedElement as UIElement;
if (focusedElement == null)
{
return;
}
var focusedDatagrid = GetParentDatagrid(focusedElement);
// Let's see if the new focused element is inside a datagrid
if (focusedDatagrid == dataGrid)
{
// If the new focused element is inside the same datagrid, then we don't need to do anything;
// this happens, for instance, when we enter in edit-mode: the DataGrid element loses keyboard-focus,
// which passes to the selected DataGridCell child
return;
}
CommitEdit(dataGrid);
}
private static void OnParentTabControlPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var dataGrid = ControlMap[(TabPanel)sender];
CommitEdit(dataGrid);
}
}
public static class VisualTreeFinder
{
/// <summary>
/// Find a specific parent object type in the visual tree
/// </summary>
public static T FindParentControl<T>(DependencyObject outerDepObj) where T : DependencyObject
{
var dObj = VisualTreeHelper.GetParent(outerDepObj);
if (dObj == null)
{
return null;
}
if (dObj is T)
{
return dObj as T;
}
while ((dObj = VisualTreeHelper.GetParent(dObj)) != null)
{
if (dObj is T)
{
return dObj as T;
}
}
return null;
}
}
I have managed to work around this issue by detecting when the user clicks on a TabItem and then committing edits on visible DataGrid in the TabControl. I'm assuming the user will expect their changes to still be there when they click back.
Code snippet:
// PreviewMouseDown event handler on the TabControl
private void TabControl_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (IsUnderTabHeader(e.OriginalSource as DependencyObject))
CommitTables(yourTabControl);
}
private bool IsUnderTabHeader(DependencyObject control)
{
if (control is TabItem)
return true;
DependencyObject parent = VisualTreeHelper.GetParent(control);
if (parent == null)
return false;
return IsUnderTabHeader(parent);
}
private void CommitTables(DependencyObject control)
{
if (control is DataGrid)
{
DataGrid grid = control as DataGrid;
grid.CommitEdit(DataGridEditingUnit.Row, true);
return;
}
int childrenCount = VisualTreeHelper.GetChildrenCount(control);
for (int childIndex = 0; childIndex < childrenCount; childIndex++)
CommitTables(VisualTreeHelper.GetChild(control, childIndex));
}
This is in the code behind.
This bug is solved in the .NET Framework 4.5. You can download it at this link.
What I think you should do is pretty close to what #myermian said.
There is an event called CellEditEnding end this event would allow you to intercept and make the decision to drop the unwanted row.
private void dataGrid1_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
DataGrid grid = (DataGrid)sender;
TextBox cell = (TextBox)e.EditingElement;
if(String.IsNullOrEmpty(cell.Text) && e.EditAction == DataGridEditAction.Commit)
{
grid.CancelEdit(DataGridEditingUnit.Row);
e.Cancel = true;
}
}
Is there an easy method to prompt the user to confirm a combo box selection change and not process the change if the user selected no?
We have a combo box where changing the selection will cause loss of data. Basically the user selects a type, then they are able to enter attributes of that type. If they change the type we clear all of the attributes as they may no longer apply. The problem is that to under the selection you raise the SelectionChanged event again.
Here is a snippet:
if (e.RemovedItems.Count > 0)
{
result = MessageBox.Show("Do you wish to continue?",
"Warning", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (result == MessageBoxResult.No)
{
if (e.RemovedItems.Count > 0)
((ComboBox)sender).SelectedItem = e.RemovedItems[0];
else
((ComboBox)sender).SelectedItem = null;
}
}
I have two solutions, neither of which I like.
After the user selects 'No', remove the SelectionChanged event handler, change the selected item and then register the SelectionChanged event handler again. This means you have to hold onto a reference of the event handler in the class so that you can add and remove it.
Create a ProcessSelectionChanged boolean as part of the class. Always check it at the start of the event handler. Set it to false before we change the selection back and then reset it to true afterwards. This will work, but I don't like using flags to basically nullify an event handler.
Anyone have an alternative solution or an improvement on the ones I mention?
I found this good implementation.
private bool handleSelection=true;
private void ComboBox_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if (handleSelection)
{
MessageBoxResult result = MessageBox.Show
("Continue change?", MessageBoxButton.YesNo);
if (result == MessageBoxResult.No)
{
ComboBox combo = (ComboBox)sender;
handleSelection = false;
combo.SelectedItem = e.RemovedItems[0];
return;
}
}
handleSelection = true;
}
source: http://www.amazedsaint.com/2008/06/wpf-combo-box-cancelling-selection.html
Maybe create a class deriving from ComboBox, and override the OnSelectedItemChanged (Or OnSelectionChangeCommitted.)
Validating within the SelectionChanged event handler allows you to cancel your logic if the selection is invalid, but I don't know of an easy way to cancel the event or item selection.
My solution was to sub-class the WPF combo-box and add an internal handler for the SelectionChanged event. Whenever the event fires, my private internal handler raises a custom SelectionChanging event instead.
If the Cancel property is set on the corresponding SelectionChangingEventArgs, the event isn't raised and the SelectedIndex is reverted to its previous value. Otherwise a new SelectionChanged is raised that shadows the base event. Hopefully this helps!
EventArgs and handler delegate for SelectionChanging event:
public class SelectionChangingEventArgs : RoutedEventArgs
{
public bool Cancel { get; set; }
}
public delegate void
SelectionChangingEventHandler(Object sender, SelectionChangingEventArgs e);
ChangingComboBox class implementation:
public class ChangingComboBox : ComboBox
{
private int _index;
private int _lastIndex;
private bool _suppress;
public event SelectionChangingEventHandler SelectionChanging;
public new event SelectionChangedEventHandler SelectionChanged;
public ChangingComboBox()
{
_index = -1;
_lastIndex = 0;
_suppress = false;
base.SelectionChanged += InternalSelectionChanged;
}
private void InternalSelectionChanged(Object s, SelectionChangedEventArgs e)
{
var args = new SelectionChangingEventArgs();
OnSelectionChanging(args);
if(args.Cancel)
{
return;
}
OnSelectionChanged(e);
}
public new void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (_suppress) return;
// The selection has changed, so _index must be updated
_index = SelectedIndex;
if (SelectionChanged != null)
{
SelectionChanged(this, e);
}
}
public void OnSelectionChanging(SelectionChangingEventArgs e)
{
if (_suppress) return;
// Recall the last SelectedIndex before raising SelectionChanging
_lastIndex = (_index >= 0) ? _index : SelectedIndex;
if(SelectionChanging == null) return;
// Invoke user event handler and revert to last
// selected index if user cancels the change
SelectionChanging(this, e);
if (e.Cancel)
{
_suppress = true;
SelectedIndex = _lastIndex;
_suppress = false;
}
}
}
In WPF dynamically set the object with
if (sender.IsMouseCaptured)
{
//perform operation
}
I do not believe using the dispatcher to post (or delay) a property update is a good solution, it is more of a workaround that is not really needed. The following solution i fully mvvm and it does not require a dispatcher.
First Bind the SelectedItem with an Explicit binding Mode. //this enables us to decide whether to Commit using the UpdateSource() method the changes to the VM or to Revert using the UpdateTarget() method in the UI.
Next, add a method to the VM that confirms if the change is allowed (This method can contain a service that prompts for user confirmation and returns a bool).
In the view code behind hook to the SelectionChanged event and update the Source (i.e., the VM) or the Target (i.e. the V) in accordance to whether the VM.ConfirmChange(...) method returned value as follows:
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(e.AddedItems.Count != 0)
{
var selectedItem = e.AddedItems[0];
if (e.AddedItems[0] != _ViewModel.SelectedFormatType)
{
var comboBoxSelectedItemBinder = _TypesComboBox.GetBindingExpression(Selector.SelectedItemProperty); //_TypesComboBox is the name of the ComboBox control
if (_ViewModel.ConfirmChange(selectedItem))
{
// Update the VM.SelectedItem property if the user confirms the change.
comboBoxSelectedItemBinder.UpdateSource();
}
else
{
//otherwise update the view in accordance to the VM.SelectedItem property
comboBoxSelectedItemBinder.UpdateTarget();
}
}
}
}
This is an old question, but after struggling with the issue time and again I came up with this solution:
ComboBoxHelper.cs:
public class ComboBoxHelper
{
private readonly ComboBox _control;
public ComboBoxHelper(ComboBox control)
{
_control = control;
_control.PreviewMouseLeftButtonDown += _control_PreviewMouseLeftButtonDown; ;
_control.PreviewMouseLeftButtonUp += _control_PreviewMouseLeftButtonUp; ;
}
public Func<bool> IsEditingAllowed { get; set; }
public Func<object, bool> IsValidSelection { get; set; }
public Action<object> OnItemSelected { get; set; }
public bool CloseDropDownOnInvalidSelection { get; set; } = true;
private bool _handledMouseDown = false;
private void _control_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
var isEditingAllowed = IsEditingAllowed?.Invoke() ?? true;
if (!isEditingAllowed)
{
e.Handled = true;
return;
}
_handledMouseDown = true;
}
private void _control_PreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (!_handledMouseDown) return;
_handledMouseDown = false;
var fe = (FrameworkElement)e.OriginalSource;
if (fe.DataContext != _control.DataContext)
{
//ASSUMPTION: Click was on an item and not the ComboBox itself (to open it)
var item = fe.DataContext;
var isValidSelection = IsValidSelection?.Invoke(item) ?? true;
if (isValidSelection)
{
OnItemSelected?.Invoke(item);
_control.IsDropDownOpen = false;
}
else if(CloseDropDownOnInvalidSelection)
{
_control.IsDropDownOpen = false;
}
e.Handled = true;
}
}
}
It can be used in a custom UserControl like this:
public class MyControl : UserControl
{
public MyControl()
{
InitializeComponent();
var helper = new ComboBoxHelper(MyComboBox); //MyComboBox is x:Name of the ComboBox in Xaml
helper.IsEditingAllowed = () => return Keyboard.Modifiers != Modifiers.Shift; //example
helper.IsValidSelection = (item) => return item.ToString() != "Invalid example.";
helper.OnItemSelected = (item) =>
{
System.Console.WriteLine(item);
};
}
}
This is independent of the SelectionChanged event, there are no side effects of the event firing more often than required. So others can safely listen to the event, e.g. to update their UI. Also avoided: "recursive" calls caused by resetting the selection from within the event handler to a valid item.
The assumptions made above regarding DataContext may not be a perfect fit for all scenarios, but can be easily adapted. A possible alternative would be to check, if the ComboBox is a visual parent of e.OriginalSource, which it isn't when an item is selected.
Not really sure how to tackle this issue:
I have a "Save" button that has access keys attached to it... but, if I type something into a textbox and press the access keys to save, the textbox doesn't update my viewmodel because it never lost focus. Any way to solve this outside of changing the UpdateSourceTrigger to PropertyChanged?
Your problem is the UpdateSourceTrigger="LostFocus"
This is default for TextBoxes, and means that the TextBox will only update its bound value when it loses focus
One way to force it to update without setting UpdateSourceTrigger="PropertyChanged" is to hook into the KeyPress event and if the key combination is something that would trigger a save, call UpdateSource() first
Here's an Attached Property I like using when the Enter key should update the source.
It is used like this:
<TextBox Text="{Binding Name}"
local:TextBoxProperties.EnterUpdatesTextSource="True" />
and the Attached Property definition looks like this:
public class TextBoxProperties
{
public static readonly DependencyProperty EnterUpdatesTextSourceProperty =
DependencyProperty.RegisterAttached("EnterUpdatesTextSource", typeof(bool), typeof(TextBoxProperties),
new PropertyMetadata(false, EnterUpdatesTextSourcePropertyChanged));
// Get
public static bool GetEnterUpdatesTextSource(DependencyObject obj)
{
return (bool)obj.GetValue(EnterUpdatesTextSourceProperty);
}
// Set
public static void SetEnterUpdatesTextSource(DependencyObject obj, bool value)
{
obj.SetValue(EnterUpdatesTextSourceProperty, value);
}
// Changed Event - Attach PreviewKeyDown handler
private static void EnterUpdatesTextSourcePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var sender = obj as UIElement;
if (obj != null)
{
if ((bool)e.NewValue)
{
sender.PreviewKeyDown += OnPreviewKeyDown_UpdateSourceIfEnter;
}
else
{
sender.PreviewKeyDown -= OnPreviewKeyDown_UpdateSourceIfEnter;
}
}
}
// If key being pressed is the Enter key, and EnterUpdatesTextSource is set to true, then update source for Text property
private static void OnPreviewKeyDown_UpdateSourceIfEnter(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
if (GetEnterUpdatesTextSource((DependencyObject)sender))
{
var obj = sender as UIElement;
BindingExpression textBinding = BindingOperations.GetBindingExpression(
obj, TextBox.TextProperty);
if (textBinding != null)
textBinding.UpdateSource();
}
}
}
}
I have some code to set the focused property of a text box, but what i'm actually after is finding out if the text box currently has the keyboard focus, I need to determine this from my view model
public static class FocusExtension
{
public static bool GetIsFocused(DependencyObject obj)
{
return (bool)obj.GetValue(IsFocusedProperty);
}
public static void SetIsFocused(DependencyObject obj, bool value)
{
obj.SetValue(IsFocusedProperty, value);
}
public static readonly DependencyProperty IsFocusedProperty = DependencyProperty.RegisterAttached
(
"IsFocused",
typeof(bool),
typeof(FocusExtension),
new UIPropertyMetadata(false, OnIsFocusedPropertyChanged)
);
public static void OnIsFocusedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uie = (UIElement)d;
if ((bool)e.NewValue)
{
uie.Focus();
}
}
}
And the xaml is
<TextBox Text="{Binding Path=ClientCode}" c:FocusExtension.IsFocused="{Binding IsClientCodeFocused}" />
source of code
have you seen the FocusManager? you can get/set focus using this object.
Edit
Based on the comments below, here's an example of an attached property that hooks up an event and updates the source of a binding. I'll add comments where I know you'll need to make modifications. Hopefully it will point you in the right direction
public class TextBoxHelper
{
// I excluded the generic stuff, but the property is called
// EnterUpdatesSource and it makes a TextBox update it's source
// whenever the Enter key is pressed
// Property Changed Event - You have this in your class above
private static void EnterUpdatesTextSourcePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
UIElement sender = obj as UIElement;
if (obj != null)
{
// In my case, the True/False value just determined a behavior,
// so toggling true/false added/removed an event.
// Since you want your events to be on at all times, you'll either
// want to have two AttachedProperties (one to tell the control
// that it should be tracking the current focused state, and
// another for binding the actual focused state), or you'll want
// to find a way to only add the EventHandler when the
// AttachedProperty is first added and not toggle it on/off as focus
// changes or add it repeatedly whenever this value is set to true
// You can use the GotFocus and LostFocus Events
if ((bool)e.NewValue == true)
{
sender.PreviewKeyDown += new KeyEventHandler(OnPreviewKeyDownUpdateSourceIfEnter);
}
else
{
sender.PreviewKeyDown -= OnPreviewKeyDownUpdateSourceIfEnter;
}
}
}
// This is the EventHandler
static void OnPreviewKeyDownUpdateSourceIfEnter(object sender, KeyEventArgs e)
{
// You won't need this
if (e.Key == Key.Enter)
{
// or this
if (GetEnterUpdatesTextSource((DependencyObject)sender))
{
// But you'll want to take this bit and modify it so it actually
// provides a value to the Source based on UIElement.IsFocused
UIElement obj = sender as UIElement;
// If you go with two AttachedProperties, this binding should
// point to the property that contains the IsFocused value
BindingExpression textBinding = BindingOperations.GetBindingExpression(
obj, TextBox.TextProperty);
// I know you can specify a value for a binding source, but
// I can't remember the exact syntax for it right now
if (textBinding != null)
textBinding.UpdateSource();
}
}
}
There might be a better way of accomplishing what you're trying to do, but if not then I hope this provides a good starting point :)
in your OnIsFocusedPropertyChanged handler, you need to get a reference to the control that it is being set on and subscribe to its FocusChanged event, where you can re-set the dependency pproperty. Make sure in your XAML you set the binding mode to TwoWay