There is a nasty sore, a diseased abomination that is the blight of WPF, yes, it is the ComboBox. It is my nemesis and will fight me at each turn, it fiercely beating me off as I struggle to coerce it in to working with my thought patterns.
Out of .Net and the WPF framework, the ComboBox is the only control that I have to work with that really just defies my expectations…continuously.
Its behaviour seems inconsistent and outright frustrating at times – who am I kidding – frustrating all the time. DisplayMemberPath, why oh why?
Now someone at Microsoft no doubt put a lot of effort in to it was probably pretty proud of the ComboBox – it is a staple of UI design and I’m sure they nailed it perfectly and it is a pretty kewl control. However it just doesn’t interface well with…well…me.
This becomes even more notable when using JScripts.
Some of you may have a brain <-> ComboBox compatible interface – if so, you’re lucky 🙂 I don’t, so this post is an attempt to get the behaviours clear in my own mind. To reinforce that when using a ComboBox I need to hold my tongue in a certain way.
As we all know, one of the really really nice things about WPF is Binding; using reflection so you can add list of your really kewl class to a control, provide the name of your really funky property and the framework goes out and magically sorts all those annoying little details out. No more of you have to go out and create a new object for each of your classes entries in your list, and then populate a property on that object and then add it to say the ListViews Items list.
It is a thing of beauty, and something I have been most impressed with for a long time.
The ComboBox does that, sort of…
DisplayMemberPath and SelectedValuePath
Ok, with most of the WPF controls you set up a Binding object, in that binding object you specify the path to your really kewl classes property. Whereas with the ComboBox we have two properties for the path directly – DisplayMemberPath and SelectedValuePath
SelectedValue – why don’t you work?
If we have a SelectedValuePath set to “MyReallyKewlValue” and we populate the ItemsSource with objects that contain MyReallyKewlValue, then if we set the SelectedValue property, the ComboBox should search through the ItemsSource objects, inspecting each MyReallyKewlValue property searching for a match between it and the SetValue value you supplied.
This does work, however, it needs to be an exact match and if we are populating things from databases then there may be whitespace padding shenanigans going on.
We Can Only Bind To Properties
Now admittedly, I know this, and I know this very very well, however the issue comes down to Jscript and properties. It’s not hard adding properties, it’s just a little different than you’d expect.
Declaring your data as public in your classes and then wondering why you’re getting black ComboBox entries can provide hours of entertainment for the entire family. We need to declare our variable and then we need to create a function with the get keyword to create our property – for example
public function get MyReallyKewlProperty() : String
and then treat it like any other function that returns a value.
Undefined????
When using the SelectedValue property – be aware that in Jscript when you null an object it sets it to ‘undefined’. I had one we instance where I was adding values with the String.Format command and it dutifully wrote ‘undefined’ instead of a blank (which is what I expect and get in C#). Granted, this one is hardly the fault of the ComboBox 🙂
A Demonstration Script
So, what I have done is I’ve thrown together a little wee script which demonstrates how to create your own class and bind it to the ComboBox. I’ve tested it against OIS300 from a position perspective. And output is to the debug console.
Have a read through the code and its comments to see how it all comes together.
And finally the script:
import System; import System.Windows; import System.Windows.Controls; import MForms; package MForms.JScript { class ComboTest { var gcmbComboBox : ComboBox = new ComboBox(); // this is where we shall store our array of var gArray1 = null; var gController = null; var gDebug = null; public function Init(element: Object, args: Object, controller : Object, debug : Object) { var content : Object = controller.RenderEngine.Content; // save these variables to so we can use them in other functions gController = controller; gDebug = debug; // I'm connecting Grid.SetColumnSpan(gcmbComboBox, 15); Grid.SetColumn(gcmbComboBox, 30); gcmbComboBox.Height = Configuration.ControlHeight; content.Children.Add(gcmbComboBox); // we will display the value in the MyReallyKewlProperty property gcmbComboBox.DisplayMemberPath = "MyReallyKewlProperty"; // and the value that we get will be the value stored in MyReallyKewlValue property gcmbComboBox.SelectedValuePath = "MyReallyKewlValue"; // create an array of objects that we will use for the ItemsSource gArray1 = new MyReallyKewlClass[2]; // create our first object and populate the values gArray1[0] = new MyReallyKewlClass("PotatoIT", "Smart Office Rocks!"); // create our second object and populate the values gArray1[1] = new MyReallyKewlClass("M3", "Is a nifty product"); // here we are demonstrating the jscript nulls will end up // being 'undefined' if we try to print them. if(null == gcmbComboBox.SelectedValue) { // and we get our nasty 'undefined' to the console instead of just nothing being output gDebug.WriteLine("Selected Value is null '" + gcmbComboBox.SelectedValue + "'"); } else { gDebug.WriteLine("Selected Value = '" + gcmbComboBox.SelectedValue + "'"); } // add our array to the ComboBox gcmbComboBox.ItemsSource = gArray1; // this demonstrates that if we ise the SelectedValue, that the ComboBox will // look for objects that have a MyReallyKewlValue property that matches // and then it will set the combobox selecteditem to the object that contains the match gcmbComboBox.SelectedValue = "Is a nifty product"; // we shall demonstrate here that we have infact set the SelectedValue // and then, we check the SelectedItem to ensure that we have an item // before we output the MyReallyKewlProperty proving that the combobox // has done the hard yards and found out object gDebug.WriteLine("Selected Value = '" + gcmbComboBox.SelectedValue + "'"); if(null != gcmbComboBox.SelectedItem) { var currentSelectedItem = gcmbComboBox.SelectedItem; gDebug.WriteLine("Selected Property = '" + currentSelectedItem.MyReallyKewlProperty + "'"); } // subscribe to the selection change events so we can see what's going on gcmbComboBox.add_SelectionChanged(OnComboSelectionChanged); // do this so we can clean up gController.add_Requested(OnRequested); } // our combobox has changed, output the new values public function OnComboSelectionChanged(sender : Object, e : SelectionChangedEventArgs) { try { if(null != gcmbComboBox.SelectedItem) { gDebug.WriteLine("Selected Name: '" + gcmbComboBox.SelectedItem.MyReallyKewlProperty + "', Value: '" + gcmbComboBox.SelectedItem.MyReallyKewlValue + "'"); } else { gDebug.WriteLine("Nothing selected"); } } catch(ex) { gDebug.WriteLine("Exception: " + ex); } } public function OnRequested(sender: Object, e: RequestEventArgs) { // clean up gController.remove_SelectionChanged(OnComboSelectionChanged); gController.remove_Requested(OnRequested); } } // a custom class for us to mess with bindings public class MyReallyKewlClass { var pri_MyReallyKewlName : String; var pri_MyReallyKewlValue : String; public function MyReallyKewlClass(astrName : String, astrValue : String) { pri_MyReallyKewlName = astrName; pri_MyReallyKewlValue = astrValue; } // even though we are declaring a function, this effectively // acts as if it was a property public function get MyReallyKewlProperty() : String { return(pri_MyReallyKewlName); } public function get MyReallyKewlValue() : String { return(pri_MyReallyKewlValue); } } }
Thanks, this will solve one of my ComboBox questions. Taking this a little step further. Do you have an example (or thoughts) on how you would fill the list with values retrieved from an API list function. My objective is to replicate the F4 function on one field. For example If I have values in CUGEX1 and wish to use the LstFieldValue API.
Hi Jim,
if you are retrieving a list from the APIs then you can just create an array of your objects based on the size of the number of records returned from the API.
Then iterate through the API results – create a new object, populate that object from the current API result and then add it to your array.
Finally set your ComboBox.ItemsSource to the array.
If you really only after a single field you can just create an array of strings rather than your own object based on your own class.
Does that answer your question?
Be aware that I have had some oddities occur when deploying updated JScripts that have additional classes – you can end up in situations where the old ‘scripts’ compiled class doesn’t appear to get cleaned up correctly and causes a conflict – in this instance a mforms://jscript?clearcache clears everything up until the next time you update and deploy the script.
One of these days I’ll get around to documenting the scenario properly so it can be replicated and hopefully fixed.
Cheers,
Scott