Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Custom Templated ComboBox Control in Gtk# using MonoDevelop and Gtk3

0.00/5 (No votes)
28 Mar 2020GPL38 min read 6.2K   88  
C# ComboBox control
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.

Image 1

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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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

C#
void LoadItems( Ilist<IListable> lst )

loads the list of items into the ComboBox.

C#
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...

C#
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...

C#
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 TreeViews, ListStores and Renderers, 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:

C#
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:

C#
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:

C#
IListable dt = ( model.GetValue( iter, nRecordCol ) as IListable );
// this is where the IEquatable implementation is applied.
if( dt == item )  // found!
    return iter ; 

Having found the item, FindDetail() returns the Iterator which we then use to set the selected item...

C#
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:

C#
public class WeekDay
{
public int    Day  { get; set; }
public String Name { get; set; } 
}

and which here morphs into this:

C#
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:

C#
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:

C#
public class PersonTitle : IListable
{
public long  ID   { get; set; }          // assume this is a domain Primary Key
public int   Key  { get; set; }
public String Description  { get; set; }
public String Abbreviation { get; set; } // additional properties...

public IListable Record   { get { return this; } }
}

The MainWindow class has three TComboBoxes 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:

C#
// notice how Linq lets us cast the PersonTitles to IListable on the fly
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).

C#
// cast IListable to the actual class
PersonTitle sel = ( PersonTitle ) m_cbxTitles.GetSelection();
//
// apply PersonTitle-specific business logic here...
//
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

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)