I want every time my itemssource of my combobox is changed, that the SelectedIndex is set.
I have this so far:
<Style TargetType="ComboBox">
<Setter Property="SelectedIndex"
Value="0"></Setter>
</Style>
It sets the selectedIndex, but only the first time the itemSource is set. When the whole itemSource is changed, it is loaded in the combobox but not selected anymore. At this moment it should select the first item. Every ItemSource has a binding and I do not want to set the SelectedIndex by code behind. I want a solution in xaml with maybe a trigger.
I have to disagree with Sheridan - the developer is of course allowed to set the SelectedIndex
property. However, it is not possible to achieve your goal only within XAML, i.e. by using styles, control templates and triggers. That is because dependency properties have a certain value precedence, e.g. locally set values (by a binding or by the control itself) have greater priority than values that come from a WPF style (that's why your Setter only works once). You can learn more about that at http://msdn.microsoft.com/en-us/library/ms743230(v=vs.110).aspx
However, besides the possibility to control the selected index from a view model (as Sheridan proposed), there's also the possibility to create e.g. an attached dependency property which is set on the combo box and that looks for changes on the combo boxes' ItemsSource
property. Your XAML would then look somewhat like this:
<ComboBox x:Name="ComboBox" l:ComboBoxExtensions.InitialIndexOnItemsSourceChanged="0" />
Notice that I set the attached dependency property ComboBoxExtensions.InitialIndexOnItemsSourceChanged
to 0
, which means that every time the ItemsSource
changes, the SelectedIndex
will be set to 0. The l:
refers to the local XML namespace (xmlns) that I need to reference to use my custom attached property.
I've implemented the Dependency Property this way:
public class ComboBoxExtensions
{
public static readonly DependencyProperty InitialIndexOnItemsSourceChangedProperty;
private static readonly IDictionary<ComboBox, BindingSpy<ComboBox, IEnumerable>> ComboBoxToBindingSpiesMapping = new Dictionary<ComboBox, BindingSpy<ComboBox, IEnumerable>>();
static ComboBoxExtensions()
{
InitialIndexOnItemsSourceChangedProperty = DependencyProperty.RegisterAttached("InitialIndexOnItemsSourceChanged",
typeof (int?),
typeof (ComboBoxExtensions),
new FrameworkPropertyMetadata(null, OnInitialIndexOnItemsSourceChanged));
}
public static void SetInitialIndexOnItemsSourceChanged(ComboBox targetComboBox, int? value)
{
if (targetComboBox == null) throw new ArgumentNullException("targetComboBox");
targetComboBox.SetValue(InitialIndexOnItemsSourceChangedProperty, value);
}
public static int? GetInitialIndexOnItemsSourceChanged(ComboBox targetComboBox)
{
if (targetComboBox == null) throw new ArgumentNullException("targetComboBox");
return (int?) targetComboBox.GetValue(InitialIndexOnItemsSourceChangedProperty);
}
private static void OnInitialIndexOnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var targetComboBox = d as ComboBox;
if (targetComboBox == null)
return;
if ((int?) e.NewValue != null)
{
SetInitialIndexIfPossible(targetComboBox);
EstablishBindingSpy(targetComboBox);
return;
}
ReleaseBindingSpy(targetComboBox);
}
private static void EstablishBindingSpy(ComboBox targetComboBox)
{
if (ComboBoxToBindingSpiesMapping.ContainsKey(targetComboBox))
return;
var bindingSpy = new BindingSpy<ComboBox, IEnumerable>(targetComboBox, ItemsControl.ItemsSourceProperty);
bindingSpy.TargetValueChanged += OnItemsSourceChanged;
ComboBoxToBindingSpiesMapping.Add(targetComboBox, bindingSpy);
}
private static void ReleaseBindingSpy(ComboBox targetComboBox)
{
if (ComboBoxToBindingSpiesMapping.ContainsKey(targetComboBox) == false)
return;
var bindingSpy = ComboBoxToBindingSpiesMapping[targetComboBox];
bindingSpy.ReleaseBinding();
ComboBoxToBindingSpiesMapping.Remove(targetComboBox);
}
private static void OnItemsSourceChanged(BindingSpy<ComboBox, IEnumerable> bindingSpy)
{
SetInitialIndexIfPossible(bindingSpy.TargetObject);
}
private static void SetInitialIndexIfPossible(ComboBox targetComboBox)
{
var initialIndexOnItemsSourceChanged = GetInitialIndexOnItemsSourceChanged(targetComboBox);
if (targetComboBox.ItemsSource != null && initialIndexOnItemsSourceChanged.HasValue)
{
targetComboBox.SelectedIndex = initialIndexOnItemsSourceChanged.Value;
}
}
}
In this class I define the former mentioned attached property. The other important part is the OnInitialIndexOnItemsSourceChanged
method, which will be called when the attached property is set on a combo box. In there I just determine whether I have to observe the ItemsSource
property of the combo box or not.
To achieve observation of ItemsSource
, I use another custom class called BindingSpy
which establishes a data binding between ItemsSource
and a custom dependency property of the binding spy because that is the only way that I can recognize if the ItemsSource
property has changed. Unfortunately, ComboBox
(respectively ItemsControl
) does not provide an event that indicates that the source collection has changed. BindingSpy
is implemented as follows:
public class BindingSpy<TSource, TValue> : DependencyObject where TSource : DependencyObject
{
private readonly TSource _targetObject;
private readonly DependencyProperty _targetProperty;
public static readonly DependencyProperty TargetValueProperty = DependencyProperty.Register("TargetValue",
typeof (TValue),
typeof (BindingSpy<TSource, TValue>),
new FrameworkPropertyMetadata(null, OnTargetValueChanged));
public BindingSpy(TSource targetObject, DependencyProperty targetProperty)
{
if (targetObject == null) throw new ArgumentNullException("targetObject");
if (targetProperty == null) throw new ArgumentNullException("targetProperty");
_targetObject = targetObject;
_targetProperty = targetProperty;
var binding = new Binding
{
Source = targetObject,
Path = new PropertyPath(targetProperty),
Mode = BindingMode.OneWay
};
BindingOperations.SetBinding(this, TargetValueProperty, binding);
}
public TValue TargetValue
{
get { return (TValue) GetValue(TargetValueProperty); }
set { SetValue(TargetValueProperty, value); }
}
public TSource TargetObject
{
get { return _targetObject; }
}
public DependencyProperty TargetProperty
{
get { return _targetProperty; }
}
public void ReleaseBinding()
{
BindingOperations.ClearBinding(this, TargetValueProperty);
}
private static void OnTargetValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var bindingSpy = d as BindingSpy<TSource, TValue>;
if (bindingSpy == null)
return;
if (bindingSpy.TargetValueChanged != null)
bindingSpy.TargetValueChanged(bindingSpy);
}
public event Action<BindingSpy<TSource, TValue>> TargetValueChanged;
}
As I said before, this class establishes a binding and notifies clients via the TargetValueChanged
event.
Another way to achieve this is to create a WPF behavior that is part of the Blend SDK. You can find a tutorial for that here: http://wpftutorial.net/Behaviors.html. Basically it's the same pattern as with attached properties.
If you use MVVM, I would definitely suggest that you use Sheridan's solution - but if not, my solution might be more appropriate.
If you have any questions, please feel free to ask.