I have just completely lost my cool in trying to get Microsoft's ComboBox control to work as I would *naturally* expect it to.
Background
This extends on from the work Rocky did about a year ago for the Microsoft ComboBox control of previous versions of Silverlight. Essentially it didn't support the concept of SelectedValue and SelectedValuePath which is essential for FK / Lookup scenarios that use only the Id / Key and so Rocky had to roll his own implementation (refer http://www.lhotka.net/weblog/SilverlightComboBoxControlAndDataBinding.aspx).
Fastforward to Silverlight 4 and Microsoft annouces they've fixed all the ComboBox issues and added SelectedValue and SelectedValuePath support in SL4.
"Yay... but wait, not so fast!"
Disclaimer:
When binding directly to SelectedIndex property, the ComboBox works like a dream, but forget about it when using the new SelectedValue and SelectedValuePath.
Issues:
Sure I can (and I have received repeated comments to) just not bother with the Microsoft ComboBox and use those from control suites like Telerik or Component one, but that immediately places dependencies of code, contract, funds, maintainability, etc. on a 3rd party vendor. Sometime I generally avoid, unless I absolutely have to.
Besides, I simply believe that something as rudimentary as a ComboBox should work right!
And so I have spent more time than I care to admit (or afford) trying to get to the bottom of this and make it work for me. I very nearly came to a solution where you simply have to change the control and do nothing else: E.g.
<ComboBox ... />
becomes
<custom:ComboBox ... />
But I just couldn't make it work... came close though, but in the end you have to not bind to SelectedValue (the root / axis of all this Evil) and instead bind to SelectedValueProper. E.g.
<ComboBox SelectedValue="{Binding Path=Id, Mode=TwoWay}" ... />
becomes
<custom:ComboBox SelectedValueProper="{Binding Path=Id, Mode=TwoWay}" ... />
Here's the full class listing - use as you wish, but for obvious reasons, only on the condition that I cannot be held liable.
Note while this takes concepts from Rocky's implementation for the previous Silverlight version (I think SL2) of the Combox this code below is for the Silverlight 4 version of the ComboBox.
Hope that helps someone else too (then I would feel vindicated!)
Jaans
public class ComboBoxEx : ComboBox
{
#region Fields
private bool _suppressSelectionChangedUpdatesRebind = false;
#endregion
#region Properties
public static readonly DependencyProperty SelectedValueProperProperty =
DependencyProperty.Register(
"SelectedValueProper",
typeof( object ),
typeof( ComboBoxEx ),
new PropertyMetadata( ( o, dp ) =>
{
var comboBoxEx = o as ComboBoxEx;
if ( comboBoxEx == null )
return;
comboBoxEx.SetSelectedValueSuppressingChangeEventProcessing( dp.NewValue );
} ) );
[Browsable( true ), EditorBrowsable( EditorBrowsableState.Always )]
public object SelectedValueProper
{
get { return (object)GetValue( SelectedValueProperProperty ); }
set { SetValue( SelectedValueProperProperty, value ); }
}
#endregion
#region Constructor and Overrides
public ComboBoxEx()
{
SelectionChanged += ComboBoxEx_SelectionChanged;
}
/// <summary>
/// Updates the current selected item when the <see cref="P:System.Windows.Controls.ItemsControl.Items"/> collection has changed.
/// </summary>
/// <param name="e">Contains data about changes in the items collection.</param>
protected override void OnItemsChanged( System.Collections.Specialized.NotifyCollectionChangedEventArgs e )
{
// Must re-apply value here because the combobox has a bug that
// despite the fact that the binding still exists, it doesn't
// re-evaluate and subsequently drops the binding on the change event
SetSelectedValueSuppressingChangeEventProcessing( SelectedValueProper );
}
#endregion
#region Events
private void ComboBoxEx_SelectionChanged( object sender, SelectionChangedEventArgs e )
{
// Avoid recursive stack overflow
if ( _suppressSelectionChangedUpdatesRebind )
return;
if ( e.AddedItems != null && e.AddedItems.Count > 0 )
{
//SelectedValueProper = GetMemberValue( e.AddedItems[0] );
SelectedValueProper = SelectedValue; // This is faster than GetMemberValue
}
// Do not apply the value if no items are selected (ie. the else)
// because that just passes on the null-value bug from the combobox
}
#endregion
#region Helpers
/// <summary>
/// Gets the member value based on the Selected Value Path
/// </summary>
/// <param name="item">The item.</param>
/// <returns></returns>
private object GetMemberValue( object item )
{
return item.GetType().GetProperty( SelectedValuePath ).GetValue( item, null );
}
/// <summary>
/// Sets the selected value suppressing change event processing.
/// </summary>
/// <param name="newSelectedValue">The new selected value.</param>
private void SetSelectedValueSuppressingChangeEventProcessing( object newSelectedValue )
{
try
{
_suppressSelectionChangedUpdatesRebind = true;
SelectedValue = newSelectedValue;
}
finally
{
_suppressSelectionChangedUpdatesRebind = false;
}
}
#endregion
}
Right now I'm experiencing similar issues!
I'm having problem with Combobox that is binded (ItremsSource, SelectedValue, SelectedValuePath) to a list (tried NVL and BLB), that is part of of Csla ViewModel<...>.Model property, but ONLY AFTER undo. I think my problem is this.
Quick solution is moving list from from Model to ViewModel, but that is writing much more code.
Btw, NotUndoable is not working in SL?
Btw, NotUndoable is not working in SL?
NotUndoable doesn't work with managed backing fields. If you want to use it, you need to use a private backing field and also override OnGetState/OnSetState.
I'm feeling your pain. I have a form that has a Country combobox and depending on the Country selected I populate the province combobox and depending on the province I populate the Town combobox etc. This turn out to be a nightmare. Why couldn't the silverlight team just do this right from the beginning and why is there no fix. There is lot of people complaining about the stupid behaviour of the SL4 combobox.
I have used your custom combobox and it is working like you would expect a combobox to work. Thanks Jaans for the code you have definitely save me a lot of time.
+1 for being vindicated
Ps: Hannes, lyk my die boere moet mekaar maar help.
Ps: Hannes, lyk my die boere moet mekaar maar help.
You got me there. At first I thought it was dutch but of course it's afrikaans
Hi Jaans,
I am using your cutom combobox and I got this error message
Cannot call StartAt when content generation is in progress.
at System.Windows.Controls.ItemContainerGenerator.System.Windows.Controls.Primitives.IItemContainerGenerator.StartAt(GeneratorPosition position, GeneratorDirection direction, Boolean allowStartAtRealizedItem)
at System.Windows.Controls.ComboBox.SetContentPresenter(Int32 index)
at System.Windows.Controls.ComboBox.PrepareContainerForItemOverride(DependencyObject element, Object item)
at Smac.Controls.CustomComboBox.PrepareContainerForItemOverride(DependencyObject element, Object item)
I have trying to find out what's going on, but I am stuck. Do have any idea about this error message?
Thanks, Kita
Hi Kita
Unfortunately I haven't seen that particular error message before.
From the error stack it would appear that the PrepareContainerForItemOverride is being called at an inappropriate time. The ComboBox solution at the start of this thread does not override it, so I'm assuming your code does directly (or indirectly) - likely ways that could cause it are:
Check your code and comment out any method overloads for the combobox, styles (explicit or implicit), maybe even temporarily disable themes (if you're using them) just to figure out what the source this error is.
Hope that helps some,
Jaans
Hi Jaans,
Thank you for your work on the breakage of what should be a basic function from a combo box. I started my first SL4 RIA project and hit the wall several days ago and I have been looking for a solution this problem. Your solution appears to be best one I have seen and I have see a few. I have not needed to make a custom control in the past so I am have a problem with implementing your custom control. My project complies with no errors and I have no issues using in my view.xaml that is using the custom combo box but there something missing becuase it breaks at InitializeComponents(). Here is the error:
Error: Unhandled Error in Silverlight Application
Code: 4004
Category: ManagedRuntimeError
Message: Set property 'System.Windows.FrameworkElement.Style' threw an exception.
Is it possible for you to post a link to your complete implementation of your custom combo box? I sure hope the MS fixes this in SL5!
Thank you, Ricker
Hi Ricker
Just a rough guess based on the above, but I suspect you may have had a style for your old "<ComboBox />" and you are still applying that same style to the <ComboBoxEx /> without updating the type of the style
Would you mind posting the following?
Jaans
Hi Ricker - Have you managed to resolve your issue?
Hello Jaans,
I have not resolved my issue of properly using the custom combo box. I moved on to other aspects of the project and the SL page on have been working on, but I am now at the point where this is the last part of the SL page that does not work. I will re-attempt tonight in making this work. I searched the entire project for any styles that target the combobox and found none. I am sure it is something small that I am missing. Thank you for checking up on me.
Ricker
That's great Jaans, thanks!
On a similar note, is anybody else annoyed with the fact that the ComboBox doesn't support key press events? I don't need anything fancy, but just for it to function like a ComboBox does in any browser. I have tried wiring up the KeyUp event, but these events don't fire when the ComboBox is open, only when it's closed. It looks like the Popup control or whatever is inside the ComboBox is stealing those events.
Has anybody else solved this problem? (PS I know about the AutoComplete control, but that's not what I want...I just want a working ComboBox)
YES! Thank you, this has saved the day for me.
Bedankt voor de moeite Jaans
Is there any chance of a VB version?
Many Thanks
Hi Tracy
Unfortunately no, not one that I wrote anyway, but you have a couple of alternative options:
Hope that helps
Jaans
Thank you!!!
SelectedValue worked in .NET 3.5, but broke when I upgraded to .NET 4. Your class fixes the problem very nicely. Thanks so much for posting it!
Hi Jaans,
I'd lik to add the ComboBoxEx class/control to CslaContrib.Xaml.Silverlight project to make it more easily available for Csla developers.
Would you agree to submit the extended control to cslaContrib?
Hi Jonny
Sure! No problem... let me know if I can help.
Happy 2011 !!
Hi all
If you would like to help put pressure on Microsoft to fix the ComboBox for Silverlight, please cast you votes here at user voice:
http://dotnet.uservoice.com/forums/4325/suggestions/1273023
Jaans
hallo, thanks for that implementation.
your combobox works great with selectedvalue but in my case i have two comboboxes. the first is the "master" combobox, the second the "slave" combobox. the slave combobox should populated (itemsource property) dependend on the selected item of master combobox. some master items has no sub items.
when i select one master item which should not has sub items the itemsource of slave combobox is updated, the converter is getting the selected item of master combobox and returning a collection with zero items. but the combobox is not updating correctly, i can see that the slave combox.itemsource.count is zero but the old selected item is shown ! also the selected index is wrong index...
any idea how i can fix it?
best regards
i have solved the problem by looking on your old post about the combobox :)
i have changed the code:
/// <summary>
/// Sets the selected value suppressing change event processing.
/// </summary>
/// <param name="newSelectedValue">The new selected value.</param>
private void SetSelectedValueSuppressingChangeEventProcessing(object newSelectedValue)
{
try
{
_suppressSelectionChangedUpdatesRebind = true;
SelectedValue = newSelectedValue;
var sel = (from item in this.Items
where GetMemberValue(item).Equals(newSelectedValue)
select item).SingleOrDefault();
this.SelectedItem = sel;
}
finally
{
_suppressSelectionChangedUpdatesRebind = false;
}
}
know it works fine...
here my xaml code:
<myctrl:ComboBoxEx x:Name="cmboxSubType"
DisplayMemberPath="DisplayMember"
SelectedValuePath="ValueMember"
SelectedValueProper="{Binding Path=SubType, Mode=TwoWay}"
ItemsSource="{Binding ElementName=cmboxType, Path=SelectedItem, Converter={StaticResource EnumToComboBoxItemsConverter}, ConverterParameter=SUBTYPE}"
>
</myctrl:ComboBoxEx>
You just saved me a lot of time. This bug is so annoying. Your fix worked like a charm.
Thanks!!
Hi there Jaans,
Wow, okay I'm a bit of a newbie, but your code has saved me a lot of arsing around. All I wanted to do was to be able to update the text that we can see when the combo box is not in drop down mode. Jesus H Christ was that like trying to pull teeth. I was on the Silverlight forums for days... but this fixes it.
I created an account just so I could tell you how grateful I am for this.
In my project I replaced the original combobox with the extended one and now binding to nullable foreign keys became easy. But I noticed that if ItemsSource of the combo is bound to ICollectionView, CurrentChanged event of the view doesn't get fired...
To handle this situation I modified the ComboBoxEx_SelectionChanged event handler:
private void ComboBoxEx_SelectionChanged(object sender, SelectionChangedEventArgs e) { // Avoid recursive stack overflow if (_suppressSelectionChangedUpdatesRebind) return; if (e.AddedItems != null && e.AddedItems.Count > 0) { //SelectedValueProper = GetMemberValue( e.AddedItems[0] ); SelectedValueProper = SelectedValue; // This is faster than GetMemberValue if (this.ItemsSource is ICollectionView) (this.ItemsSource as ICollectionView).MoveCurrentTo(this.SelectedItem); } // Do not apply the value if no items are selected (ie. the else) // because that just passes on the null-value bug from the combobox }
I wanted to provide an update on something I found related to Jaans' fix. While using his solution, I came across an issue in SL4/CSLA 4.1 where I would select a combobox item from the list but the item would not appear as the selected item. If I selected the same item a second time, it finally appeared as selected. In addition, binding was not working...it wouldn't save.
I tried Walk's modification, and it seemed to make the selection issue work correctly (i.e. the first selection made the item actually appear as selected), but two-way binding was still not working. I noticed the other combobox on my usercontrol was working fine with Jaans' code, so I began to look at what was different between the two. Turns out the problem had something to do with binding to a managed property that was defined as a byte. I changed this to an int and everything started working fine.
I can use this getting selected value is working fine but i need selectionChanged for my own use. i hae functionalities using selectionchanged in my project. And also when i use this im receiving an error
"Failed to assign to property 'System.Windows.FrameworkElement.Style'. [Line: 279 Position: 240]"
can u help me out...
A quick solution is, store the selected item in a tempure var, then restore the item from it.
Hello Jaans,
I though this could help me !! but not ... let me explain my issue in details:
I have two comboboxs; the first one is showing the Countries, based on its selection the second combo is showing list of Regions, this is how i define the Countries(Gov) combo in xaml:
<Infrastructure:ComboBoxEx x:Name="cbxMGov" Margin="0,0,0,5" Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="4"
ItemsSource="{Binding Source={StaticResource ContextProxy},Path=DataSource.Govs,NotifyOnValidationError=True,ValidatesOnExceptions=True}" MaxDropDownHeight ="120"
SelectedValuePath ="ID" SelectedValueProper="{Binding Path=CurrentItem.MgovID, Mode=TwoWay,NotifyOnValidationError=True,ValidatesOnExceptions =True}" DisplayMemberPath ="Name"
SelectedItem="{Binding Source={StaticResource ContextProxy},Path=DataSource.SelectedGov,Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName ="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChangedGovCommand}" CommandParameter="{Binding SelectedItem, ElementName=cbxMGov}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Infrastructure:ComboBoxEx>
Here is; when the user select or change his selection in the combobox, I use the SelectedGov to retrieve the Regions based on the selection, the SelectedValueProper should hold the current ID of the selected Country. and store it in the CurrentItem.MgovID.
This scenario works okay when I create a new record, but when I try to updated an existing record and change the country from the combobox, it doesn't accept any changes and keep the selection of the first selected country !!!
What could be the reason of this.
I hope I explained the issue, please advise if more information is needed
Best regards
At quick glance it could be many things.
One I'm wondering about it that you are using two way binding for both the SelectedItem and the SelectedValueProper. Usually you can bind to the one or the other, but if you bind to both you could get them affecting each other. Try using only the one.
Also, I'm unable to see the code for the item source (ContextProxy's DataSource.Govs) and the code for the target binding (CurrentItem.MgovID) as these could be part of the problem (especially the latter) if it's not implemented as a writable property on a dependency object (ultimately not raising the relevant INPC events).
If you are still stuck, please try and provide a complete sample / code showing the issue, so that I could look at it and see if I can find the issue.
Hth.
Dear Jaans,
Appreciate your prompt reply.
I tried and changed the SelectedItem to be Twoway and made the SelectedValueProper Oneway.
Well, I am almost there, It doesn't do the trick first time, but If I continue to change the Gov, Regions changes accordingly.
Here is the Gov item source, it is a telerik QueryableDomainServiceCollectionView here is how I define it in the VM:
private QueryableDomainServiceCollectionView<Government> _gov;
public QueryableDomainServiceCollectionView<Government> Govs
{
get { return _gov; }
set { _gov = value; RaisePropertyChanged(() => Govs); }
}
and I am populating it as follow:
SelectedGov = DataService.GetGovernments().Where(o=>o.ID == CurrentItem.MgovID).FirstOrDefault();
if (Govs!=null) { if (Govs.HasChanges) Govs.RejectChanges(); }
Govs = DataService.GetGovernments();
Govs.AutoLoad = true;
please note that the SelectedGov is binded to the SelectItem from the combo
CurrentItem is defined as follow:
private MasterFile _currentItem;
public MasterFile CurrentItem
{
get{return _currentItem ;}
set { _currentItem = value; RaisePropertyChanged(() => CurrentItem);
}
and I am poupating it using Loadoperation.
Note: If I follow the other solution and make the SelectedItem as Oneway and SelectedValueProper as Twoway and try to populate the region when the Gov changes in the CurrentItem ProperityChanged the value never changed to the newly selected Gov, as if it is stuck and doesn't comply. see the code below:
void CurrentItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var currRecord = (MasterFile)sender;
switch (e.PropertyName)
{
case "MgovID":
if (currRecord.MgovID == null) return;
if (Regions != null) if (Regions.HasChanges) Regions.RejectChanges();
Regions = DataService.GetRegionsByGovId((int)currRecord.MgovID);
Regions.AutoLoad = true;
break;
}
If this is not enough, tell me and I will try to submit you a repo.
Thanks again & best regards
Admittedly, I can't clearly see a culprit, but there are some possibilities.
Please note that binding to *only* the SelectedItem (not just changing the BindingMode of SelectedValueProper to OneWay) has always worked correctly for the standard combo box and does not require the workaround posted in this thread. This scenario of binding to SelectedItem is very effective when you need the full object of the item selected in the combo box, but you have to do extra work to use the value. I often use DependencyProperty for my SelectedItem binding targets and then utilise the PropertyChangedEvent of those to do the 'extra work'.
However, if you instead require binding of a property (on the object of the selected item), then this ComboBoxEx workaround might help by using SelectedValueProper.
From the above it would seem that you are mixing the two. You could consider using just the one or the other. From the above, you are binding both the CurrentItem.MgovID property to SelectedValueProper and the SelectedGov property to SelectedItem on the combobox, and both are trying to influence the combo box.
Furthermore, the code that sets SelectedGov is getting a different instance of the Goverment class. Remember, just because the MgovID's match doesn't mean it's the same object, just an equivalent one. If you are using the SelectedItem binding this will be a problem because you need to be using the same instance as that which is present in the combo box's ItemSource collection.
If you are using SelectedValueProper scenario, then you don't need the SelectedGov nor do you need to look it up against the data source again (possibly an expensive database hit). It (the ComboBoxEx) should be using the CurrentItem.MgovID value to lookup the matching Government instance, and 'preselect' the correct item for you.
PS: If this is urgent then a working repo would probably be the quickest way for me to help.
Hope that helps,
Jaans
Dear Jaans,
I was preparing a repo to send to you, when I suddenly descoverd the issue !!!
It was the Region combo, I was using telerik combo for Regions and not your modified one, when I replace the telerik combo with that one, everything worked fine as excpected ...
I now have simple issue, which is when the Govs changed the old selected value in Region combo is still there, but this is easy & can resolve.
Thanks to you, I managed to resolve the Main Big issue.
Best regards
Dear Jaans,
I wounder If you can help to apply this solution for telerik RadComboBox, I tried myself but didn't succeed
Best
Hi Jaans,
I have implemented the same, in my scenario based on combobox1 selection my other combobox2 itemsource is getting update. When I am clicking on combobox2 I am getting object reference null exception I am getting
Message: Unhandled Error in Silverlight Application
Code: 4004
Category: ManagedRuntimeError
Message: System.NullReferenceException: Object reference not set to an instance of an object.
at System.Windows.Controls.ItemContainerGenerator.System.Windows.Controls.Primitives.IItemContainerGenerator.PrepareItemContainer(DependencyObject container)
at System.Windows.Controls.ComboBox.SetContentPresenter(Int32 index)
at System.Windows.Controls.ComboBox.OnIsDropDownOpenChanged(Boolean isDropDownOpen)
at System.Windows.Controls.ComboBox.OnIsDropDownOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
at System.Windows.DependencyObject.RaisePropertyChangeNotifications(DependencyProperty dp, Object oldValue, Object newValue)
at System.Windows.DependencyObject.UpdateEffectiveValue(DependencyProperty property, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, ValueOperation operation)
at System.Windows.DependencyObject.SetValueInternal(DependencyProperty dp, Object value, Boolean allowReadOnlySet, Boolean isBindingInStyleSetter)
at System.Windows.DependencyObject.SetValue(DependencyProperty property, Boolean b)
at System.Windows.Controls.ComboBox.ElementDropDownToggle_Click(Object sender, RoutedEventArgs e)
at System.Windows.Controls.Primitives.ButtonBase.OnClick()
at System.Windows.Controls.Primitives.ToggleButton.OnClick()
at System.Windows.Controls.Primitives.ButtonBase.OnMouseLeftButtonUp(MouseButtonEventArgs e)
at System.Windows.Controls.Control.OnMouseLeftButtonUp(Control ctrl, EventArgs e)
at MS.Internal.JoltHelper.FireEvent(IntPtr unmanagedObj, IntPtr unmanagedObjArgs, Int32 argsTypeIndex, Int32 actualArgsTypeIndex, String eventName, UInt32 flags)
Can you please help me out what can be the issue for this exception.
Thanks in advance
@kondamca - Hi
Not much to go on here... could you attach a sample project showing the problem?
Copyright (c) Marimer LLC