tags:

views:

189

answers:

3

Is there any way to tell how the ComboBox_SelectionChanged event was raised in WPF.

That is, was the event raised as the result of a user interaction, or as the result of a change in the property it is bound to?

+1  A: 

Short answer: no. There shouldn't be a difference, in both cases the selection has changed, that is all that matters. To determine if it was a user interaction, you will have to monitor a combination of other events, like DropDownOpened/Closed and KeyDown/Up, and the Stylus* ones.

slugster
The sender is always the ComboBox because it's the logical control firing the event.
Drew Marsh
+1 don't know why someone downvoted this answer. It was correct and contained useful information, even though it didn't solve the original question. Drew is correct that the sender is always the ComboBox whose selection was changed.
Ray Burns
+3  A: 

In the ComboBox.SelectionChanged event, the sender is always the ComboBox and there is nothing in SelectionChangedEventArgs that will help you.

Two solutions to this occur to me. You could use a converter on the binding, or you could check the stack trace to see if System.Windows.Controls.Primitives.Selector.OnSelectedItemsCollectionChanged(object, NotifyCollectionChangedArgs) is in the stack. The stack check is extremely ugly, a bad practice, and won't work in partial trust environments. So I'll only describe the other one.

Using a converter on the binding to detect change sources

This solution is relatively clean, but requires a change to the binding. It also sometimes notifies you when things haven't changed.

Step 1: Create a converter that does no conversion but has a "Converted" event and a "ConvertedBack" event:

public EventingConverter : IValueConverter
{
  public event EventHandler Converted;
  public event EventHandler ConvertedBack;

  public object Convert(object value, ...)
  {
    if(Converted!=null) Converted(this, EventArgs.Empty);
    return value;
  }
  public object ConvertBack(object value, ...)
  {
    if(ConvertedBack!=null) ConvertedBack(this, EventArgs.Empty);
    return value;
  }
}

Step 2: Set your binding to use a new instance of this converter (don't share converter instances using a resource dictionary or a static property as is normally done)

<ComboBox ...>
  <ComboBox.SelectedValue>
    <Binding Path="..." ...>
      <Binding.Converter>
        <local:EventingConverter
          Converted="ComboBoxSelectedValue_Converted"
          ConvertedBack="ComboBoxSelectedValue_ConvertedBack" />
      </Binding.Converter>
    </Binding>
  </ComboBox.SelectedValue>
</ComboBox>

Now your ComboBoxSelectedValue_Converted and ComboBoxSelectedValue_ConvertedBack methods will be called from within the binding process.

Warning: If you throw an exception in these events you'll break the binding.

If you can't modify the XAML that does the binding

If you have no control over the XAML that creates the binding (for example you are using attached properties) you can still come in and add the converter after the fact. In this case your converter class will need to chain to the previously declared converter, you'll have to clone the Binding and install the new one (they are immutable once they have been used), and you'll also have to deal with MultiBindings (if you want to support them).

Final note

The need to determine whether the change was made by the user or the property may in fact be a symptom of poor UI design, usually resulting from users who don't really understand their own requirements.

I've had several projects I've worked on where the end user specified that such-and-such was to happen "when I change this ComboBox". In almost every case it has turned out that the application would have behaved unexpectedly in certain use cases and we found a better way to accomplish the goal. In many cases what the user really wanted was "when this value first differs from the one in the database" or "when this value is no longer default" or "whenever this value is 5".

Ray Burns
Beautifully answered. Thank you so much for taking the time to put this together.
mattdlong
Great answer :)
slugster
+1  A: 

I also met this kind of problem and solved it using a boolean bInternalChange variable.

Imagine an interface converting °C to °F and back with two ComboBoxes. Selecting a value in the first updates the selection of the second, and selecting a value in the second updates the first. It creates an infinite loop if you don't distinguish UI changes from internal changes.

bool bInternalChange = false;
private void ComboBoxF_SelectionChanged(...)
{
    if (!bInternalChange)
    {
        bInternalChange = true;
        ComboBoxC.SelectedValue = ConvertFtoC(...);
        bInternalChange = false;
    }
}
private void ComboBoxC_SelectionChanged(...)
{
    if (!bInternalChange)
    {
        bInternalChange = true;
        ComboBoxF.SelectedValue = ConvertCtoF(...);
        bInternalChange = false;
    }
}
Mart
I have used this solution too. It is more reliable if you use try..finally for bInternalChange, otherwise an exception anywhere in the conversion code or the value update will cause your synchronization to stop working. I often encapsulate this in an object for example: using(var change = ChangeTracker.StartChange()) if(change) ComboBoxC.SelectedValue= ...; StartChange() returns an IDisposable that is implicitly convertable to bool.
Ray Burns