This is a ComboBox control written in C# which wraps a Gtk 3 ComboBox control to simplify its usage while taking advantage of the features it offers, such as the ListStore.
Introduction
In this article, I present a ComboBox
control written in C# which wraps a Gtk 3 ComboBox
control so as to simplify its usage, while taking advantage of the features it offers, such as the ListStore
which it contains. This control builds on the experience gained from developing the HComboBox
I presented in a previous article [1].
Goals
- Whereas the
HComboBox
is intended to be as lean and simple as possible, the purpose of the TComboBox
is to be able to handle more complex and varied objects in a consistent manner. - Encourage other programmers to use the Mono / MonoDevelop / Gtk# combination by sharing this example.
- Make it as simple to use as a basic Gtk widget, while adding as little overhead as possible.
- Make it more accessible to Windows / Forms / WPF programmers by hiding the Gtk-specific implementation details.
Credits
Thanks are due to the Mono and MonoDevelop teams for making it possible to run C# programs on Linux. Thanks also to the Gtk# team for their framework in general and, for this article, for their TreeView
/ ListView
implementation in particular. Also, the GtkSharp tutorials [2] have been a huge help.
Pre-requisites
While not exactly required reading, I suggest you take a look at the Remarks section of my HComboBox
article [3] where I introduce the concept of the IListable
interface and the reasons for its existence.
Background
I am in the process of porting a bunch of my Windows C# WPF / MVVM applications to Linux / Mono / Gtk3. I am learning the Gtk toolkit as I go along, and I have been trying to craft tools that will allow me to use as much of my existing code as possible. The first Gtk ComboBox
I learned to use was the ComboBoxText
. At the time, the Gtk ComboBox
seemed a bit daunting to use, whereas the ComboBoxText
was much simpler, so the HComboBox
was built around the ComboBoxText
.
As I gained familiarity with the Gtk widgets, and in particular, the TreeView
and the Listore
, the Gtk ComboBox
became much more accessible to me because it is actually a TreeView
in disguise!
Concept
If you reduce the function of the ComboBox
to its simplest form, you come to the conclusion that essentially, for it to do its job, it needs just:
- an integer “
key
” or index to be able to keep track of each item, - and an associated string to display as a description for that item.
This is the thinking behind the HComboBox
control, which manipulates a list of KeyValuePair<int,String>
items. For very simple items, this approach works just fine, but for more complex applications, it has one very serious limitation: retrieval of an item is possible only via the ComboBox
’s Selected Item key, since this is all we’ve got to work with. In many scenarios though, this is not enough. We need the actual object. Somehow we need to be able to associate this key with an underlying object.
The HComboBox
could conceivably work with something like this:
public class ComboItem
{
public int Key { get; set; }
public String Description { get; set; }
}
and based on it, we came up with the IListable
interface, thus:
public interface IListable
{
int Key { get; }
String Description { get; }
}
which has the shortcoming we just mentioned.
What we actually need is something more like this:
public class ComboItem
{
public int Key { get; set; }
public String Description { get; set; }
public object Tag { get; set; }
}
which yields an IListable
interface that looks like this:
public interface IListable
{
int Key { get; }
String Description { get; }
IListable Record { get; }
}
We need to add one more detail to be where we want to be, the ability of the object to locate itself, so finally we have this:
public interface IListable : IEquatable<IListable>
{
int Key { get; }
String Description { get; }
IListable Record { get; }
}
The reason I have devoted so much time to the IListable
interface is because it allows us to use Templates. And the Gtk ComboBox
control can work with Templates.
Solution
All of the above leads us to the TComboBox
control presented here, which contains a ComboBox
, that uses Templates, hence the name.
Since it is related to (but not derived from) the HComboBox
, the exposed properties and methods are similar.
Properties
- Set the
Height
/ Width
via the Constructor
Methods
void LoadItems( Ilist<IListable> lst )
loads the list of items into the ComboBox
.
IListable GetSelection()
gets the selected item. The importance of the IListable
becomes clear now, because this way we can access the underlying object via the “Record
” property.
To set the selected item, we now have two methods available.
The...
void SetSelectionByKey( int nKey )
...works the same as the HcomboBox
’s SetSelection(nKey)
method, i.e., by the index of the item in the list, whereas...
void SetSelectedItem( IListable item )
...sets the selected item via the actual object, something akin to the SelectedItem
property of WPF.
Events
It exposes the Changed
event which the clients can use to know when the selected item has changed.
Implementation Details
When I said that the Gtk ComboBox
is actually a TreeView
in disguise, I was not joking. It is.
All the nifty things you can do with TreeView
s, ListStore
s and Renderer
s, you can apply to the ComboBox
as well. This was a revelation to me.
You can configure a ListStore
and assign it to the Combo’s Model
property. You can define a Renderer
and attach it to a CellRenderer
, which you can then add to the Combo
. This is powerful stuff!
Our TComboBox
control, in addition to the ComboBox
member also has a ListStore
member.
We initialize the ListStore
member to hold IListable
objects:
private void InitializeDetailList()
{
m_dtlStore = new ListStore( typeof(IListable) );
}
We define a method RenderDescription()
which renders the Text
that will be displayed and tie everything together in the InitializeComponent()
method:
private void InitializeComponent()
{
…
m_cbo.Model = m_dtlStore;
var crt = new CellRendererText();
m_cbo.PackStart( crt, true );
m_cbo.SetCellDataFunc( crt,
new CellLayoutDataFunc( RenderDescription ) );
...
}
These four lines of code are the heart of this ComboBox
control, but for all we know, they could be initializing a TreeView
.
LoadItems()
is very simple, since we are now loading the ListStore
.
The SetSelectionByKey()
method is just as simple, as it just sets the Active
property of the ComboBox
member.
The IListable GetSelection()
method is where things start to get interesting. It uses an iterator, and it retrieves the actual selected object, which is what we wanted to achieve.
The final piece of the puzzle is the SetSelectedItem(IListable item)
method where the reason for the IEquatable
becomes clear: to be able to set an item as selected, we first have to find it. This is done by the TreeIter FindDetail(IListable item)
method, which at its heart has these lines of code:
IListable dt = ( model.GetValue( iter, nRecordCol ) as IListable );
if( dt == item )
return iter ;
Having found the item, FindDetail()
returns the Iterator
which we then use to set the selected item...
IListable rec = ( m_cbo.Model.GetValue( iter, 0 ) as IListable ) ;
if( rec != null )
m_cbo.Active = rec.Key ;
...and that’s about it.
Sample Usage
See the MainWindow
class of the attached sample program as an example of how to use this control.
To demonstrate how the IListable
interface allows us to use the TComboBox
with disparate classes, in the attached sample application, I have supplied three simple classes which we use.
In the HComboBox
sample application, we used a simple WeekDay
class (turns out DayOfWeek
is used by DateTime
) which looked like this:
public class WeekDay
{
public int Day { get; set; }
public String Name { get; set; }
}
and which here morphs into this:
public class WeekDay : IListable
{
public int Day { get; set; }
public String Name { get; set; }
public int Key { get { return Day ; } }
public String Description { get { return Name; } }
public IListable Record { get { return this; } }
}
Similarly, we define a MonthOfYear
class:
public class MonthOfYear : IListable
{
public int Month { get; set; }
public String Name { get; set; }
public int Key { get { return Month ; } }
public String Description { get { return Name; } }
public IListable Record { get { return this; } }
}
and finally a class that more closely resembles a real-world domain object:
public class PersonTitle : IListable
{
public long ID { get; set; }
public int Key { get; set; }
public String Description { get; set; }
public String Abbreviation { get; set; }
public IListable Record { get { return this; } }
}
The MainWindow
class has three TComboBox
es which it uses to display collections of the above three classes.
See how the InitializeLookupLists()
method initializes the combo boxes. Notice that now there is no need for a ConvertToComboList()
method to transform our domain objects. If we have legacy methods returning the actual objects, e.g., IList<PersonTitle> GetPersonTitles()
, LinQ comes to our resQ and does the packing for us:
m_cbxTitles.LoadItems(
( from x in GetPersonTitles() select ( x as IListable ) ).ToList() );
Finally, see the OnTitleSelectionChanged()
method for the most important part:
How to retrieve the actual domain object from the selected item (i.e., unpacking).
PersonTitle sel = ( PersonTitle ) m_cbxTitles.GetSelection();
if( sel.Abbreviation.CompareTo( sel.Description ) != 0 ) ...
Remarks
I must emphasize again the power of the TreeView
/ ListView
pattern. The TComboBox
as implemented here is actually a TreeView
with only one Column
, which serves to display the Description
of each item. It has “only one” column because I wanted to emulate the “classic” ComboBox
look.
If we wanted to add more columns, e.g., to display an image along with the Description
, it can be done very easily in the same way you would add columns to any other TreeView
/ ListView
.
Exercises for the Reader
Searching the ListView
If you look at my implementation of the TreeIter FindDetail(IListable item)
function, you will notice I use the brute-force method – I start iterating the list until I find the item I am looking for. I am not happy with that. My only consolation is that a ComboBox
usually has a few items, so the impact is minimal, but still...
I understand that the ListStore
is implemented as a linked-list and from what I have found on the ‘net so far, apparently this is the only way to go. I would dearly like to find a more efficient way to do that. If anyone has something better to suggest, please share your wisdom.
Requirements
- Mono (currently I have version 6.6.0.166)
- MonoDevelop (current version 7.8.4)
- Gtk3 libraries (currently at version 3.22.25.56)
The supplied sample application is a MonoDevelop Solution. It requires the Gtk 3 packages.
Since these are DLLs, they are not included in the sample project, but the packages.config file is included. After opening the solution in MonoDevelop, if it is not done automatically, select restore / update Packages and NuGet will fetch them.
References / See Also
GtkSharp TreeView Tutorial for a very nice description of the Model, View, Controller pattern implemented by the TreeView
/ ListView
, plus some very nice examples demonstrating their capabilities.
History
- 27th March, 2020: Initial version