WPFNewbie
WPFNewbie

Reputation: 2534

How to cancel a ComboBox SelectionChanged event?

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.

  1. 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.

  2. 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?

Upvotes: 28

Views: 52830

Answers (8)

Mosca Pt
Mosca Pt

Reputation: 527

Haven't read all in this thread, but managed to cancel the ComboBox from showing the dropdown in this case where we instead needed to simply allow the user the to do ctrl-c/copy text:

        if (e.KeyCode == Keys.Control || e.KeyCode == Keys.C)
        {
            Clipboard.SetText(comboBoxPatternSearch.Text);
            e.Handled = true;
            e.SuppressKeyPress = true;
            return;
        }

I think e.Handled did nothing, it was SuppressKeyPress that made it. Sorry for not having more time, just wanted to contribute this as I had exactly this issue.

Upvotes: 0

Glen Herman
Glen Herman

Reputation: 156

Easiest solution I have found is to use the PreviewMouseLeftButtonDown ComboBox event. e.Handled works to bail out and not trigger the combobox change event

Upvotes: 1

mike
mike

Reputation: 1734

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.

Upvotes: 0

Subhash Saini
Subhash Saini

Reputation: 274

In WPF dynamically set the object with

    if (sender.IsMouseCaptured)
    {
      //perform operation
    }

Upvotes: 1

Ahmad
Ahmad

Reputation: 1524

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

Upvotes: 0

Rob
Rob

Reputation: 5286

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

Upvotes: 1

Jaider
Jaider

Reputation: 14874

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

Upvotes: 28

ispiro
ispiro

Reputation: 27633

Maybe create a class deriving from ComboBox, and override the OnSelectedItemChanged (Or OnSelectionChangeCommitted.)

Upvotes: 1

Related Questions