Summary
This article lays out a solution to a bug with the Silverlight ComboBox bindings. You may notice that occasionally your data bindings on a ComboBox stop working and they do not update their source, or the ComboBox value can't be set. This may be due to the bug documented on MS Connect here: http://connect.microsoft.com/VisualStudio/feedback/details/597689/silverlight-ComboBox-binding-drops-after-null-binding.
UPDATE: The bug listing on MS Connect (the link above) is marked as fixed, but there is no information on how to obtain the fix or how if it is part of an update or patch. I am still experiencing the issue.
Problem
This bug occurs when you are using the SelectedValuePath
property on your ComboBox and at some point during runtime the property that the SelectedValue
is bound to is set to null
, thus the SelectedValue
is set to null
. Once the SelectedValue
is set to null
, the binding is dropped and your ComboBox will not update the bound property and the SelectedValue
will not be updated when the bound property is changed. In fact, you can inspect the bindings of the ComboBox before and after the SelectedValue
is set to null
and see that before there is a binding present, and then after the bindings are null
.
This is clearly not expected behavior. If you do not use the SelectedValuePath
property but instead set the SelectedValue
directly, this will not occur, even when you set the SelectedValue
to null
.
At the time of this writing, there is no official fix from Microsoft.
Solution
The source code for this solution with a sample implementation is attached to this project.
This problem was nearly a show stopper for an application I worked on recently and we came up with a solution that has served us well so far. This problem did not become evident until development was well under way, so the criteria that we had for a solution was:
- It had to work universally for all the ComboBoxes. (We could not fix it one way for one ComboBox and another way for a different one .. maintenance nightmare!)
- It had to require minimal code changes to implement. (We had a lot of code in place, and we wanted to refactor as little of it as possible.)
The solution that I have come up with for this problem is to keep a cache of the ComboBox bindings in memory and if the SelectedValue
is ever set to null
, pull the binding out of the cache and re-apply the binding to the ComboBox.
The cache is implemented as a static hash table with ComboBoxes identified by their hashcode.
private static Dictionary<int, Binding> _BindingsByHashCode = new Dictionary<int, Binding>();
private static void SaveBinding(int objectHashCode, Binding binding)
{
if (binding == null)
return;
lock (_SyncLock)
{
if (_BindingsByHashCode.ContainsKey(objectHashCode))
{
_BindingsByHashCode[objectHashCode] = binding;
}
else
{
_BindingsByHashCode.Add(objectHashCode, binding);
}
}
}
private static Binding GetSavedBinding(int objectHashCode)
{
Binding binding = null;
lock (_SyncLock)
{
if (_BindingsByHashCode.ContainsKey(objectHashCode))
{
binding = _BindingsByHashCode[objectHashCode];
}
}
return binding;
}
We subscribe to the SelectedValueChanged
event of the ComboBox and either store the current binding in the cache, or if the SelectedValue
is null
, check if we have a cached binding for this control and re-apply it.
private static void comboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ComboBox comboBox = (ComboBox)sender;
if (comboBox.SelectedValue == null)
{
Binding binding = GetSavedBinding(comboBox.GetHashCode());
if (binding != null)
{
comboBox.SetBinding(ComboBox.SelectedValueProperty, binding);
}
}
else
{
SaveBinding(comboBox);
}
}
The trickiest part was to find all of the ComboBoxes on the page without having to manually subscribe them (ugh!). To do this, we recursively traverse the Visual UI Tree of the page and return a list of all the ComboBoxes.
private static void GetAllComboBoxes(UIElement element, List<ComboBox> comboBoxes)
{
if (element == null)
return;
int childCount = VisualTreeHelper.GetChildrenCount(element);
for (int i = 0; i < childCount; i++)
{
UIElement child = (UIElement)VisualTreeHelper.GetChild(element,i);
if (child is ComboBox)
{
comboBoxes.Add((ComboBox)child);
}
else
{
GetAllComboBoxes(child, comboBoxes);
}
}
}
Because a lot of ComboBoxes are loaded dynamically or exist in data grid cells, we do not always know when they will be created, but we need them to participate in our solution no matter when they are created. In order to ensure that dynamically created controls were found, we execute our tree traversal every time the page layout is updated.
public partial class MainPage : UserControl
{
public MainPage()
{
this.LayoutUpdated += new EventHandler(MainPage_LayoutUpdated);
}
void MainPage_LayoutUpdated(object sender, EventArgs e)
{
ComboBoxHelper.FixSelectedValueBindings(this);
}
}
So, ultimately in order to apply this solution across our application, we only have to add the page layout updated event (like above) to every page that we need this solution on.
Issues
I do not love this solution. It gets the job done, but it adds a lot of unnecessary operations. Because of the need to support dynamically created controls, we perform the tree traversal and search every time the root page layout is updated. I found out during debugging that this event is fired a lot, basically every time a control on the page changes shape. I wish I had some sort of "control added" event to use instead of Layout Updated, because the vast majority of the time, the ComboBox search is performed unnecessarily.
Also, although this has not been an issue for us, if you dynamically change the ComboBox's bindings at runtime, it is possible that you could end up in a situation where you overwrite a new binding with an old one because it is re-applied from the cache.
Other Solutions
Another solution was suggested on the MS Connect bug page which implements an inherited ComboBox control with a fix for the bindings. I have not tried this solution although I trust that it works. I chose not to use this method as I did not want to have to change all of our existing ComboBoxes to the new control. I also thought that the subscription method we used was less intrusive and would be easier to back out of if need be.
Conclusion
I believe this is a "best of all evils" solution to this problem. It is not perfect, but it can be applied easily to an existing application to make the problem just go away. It has served us well, I hope someone else finds it useful too.
I also hope that Microsoft just fixes the bug, and I can throw all of this code away.