diff --git a/src/Bonsai.Gui/ComboBoxBuilder.cs b/src/Bonsai.Gui/ComboBoxBuilder.cs
new file mode 100644
index 0000000..d1dcf2c
--- /dev/null
+++ b/src/Bonsai.Gui/ComboBoxBuilder.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Represents an operator that interfaces with a combo box control and generates
+ /// a sequence of notifications whenever the selection changes.
+ ///
+ [TypeVisualizer(typeof(ComboBoxVisualizer))]
+ [Description("Interfaces with a combo box control and generates a sequence of notifications whenever the selection changes.")]
+ public class ComboBoxBuilder : ListControlBuilderBase
+ {
+ }
+}
diff --git a/src/Bonsai.Gui/ComboBoxDataSourceBuilder.cs b/src/Bonsai.Gui/ComboBoxDataSourceBuilder.cs
new file mode 100644
index 0000000..5a7e91e
--- /dev/null
+++ b/src/Bonsai.Gui/ComboBoxDataSourceBuilder.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Represents an operator that interfaces with a combo box control bound to each data
+ /// source in the sequence and generates notifications whenever the selection changes.
+ ///
+ [TypeVisualizer(typeof(ComboBoxDataSourceVisualizer))]
+ [Description("Interfaces with a combo box control bound to each data source in the sequence and generates notifications whenever the selection changes.")]
+ public class ComboBoxDataSourceBuilder : ListControlDataSourceBuilderBase
+ {
+ }
+}
diff --git a/src/Bonsai.Gui/ComboBoxDataSourceVisualizer.cs b/src/Bonsai.Gui/ComboBoxDataSourceVisualizer.cs
new file mode 100644
index 0000000..fd223c9
--- /dev/null
+++ b/src/Bonsai.Gui/ComboBoxDataSourceVisualizer.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Provides a type visualizer representing a combo box control bound
+ /// to an arbitrary data source.
+ ///
+ public class ComboBoxDataSourceVisualizer : ControlVisualizerBase
+ {
+ ///
+ protected override ComboBox CreateControl(IServiceProvider provider, ComboBoxDataSourceBuilder builder)
+ {
+ var comboBox = new ComboBox();
+ comboBox.Dock = DockStyle.Fill;
+ comboBox.Size = new Size(300, 150);
+ comboBox.SubscribeTo(builder._DisplayMember, value => comboBox.DisplayMember = value);
+ comboBox.SubscribeTo(builder._DataSource, value => comboBox.DataSource = value);
+ comboBox.SelectedIndexChanged += (sender, e) =>
+ {
+ var index = comboBox.SelectedIndex;
+ var selectedValue = index < 0 ? null : comboBox.Items[index];
+ builder._SelectedItem.OnNext(selectedValue);
+ };
+ return comboBox;
+ }
+ }
+}
diff --git a/src/Bonsai.Gui/ComboBoxVisualizer.cs b/src/Bonsai.Gui/ComboBoxVisualizer.cs
new file mode 100644
index 0000000..1fe8805
--- /dev/null
+++ b/src/Bonsai.Gui/ComboBoxVisualizer.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Provides a type visualizer representing a combo box control.
+ ///
+ public class ComboBoxVisualizer : ControlVisualizerBase
+ {
+ ///
+ protected override ComboBox CreateControl(IServiceProvider provider, ComboBoxBuilder builder)
+ {
+ var comboBox = new ComboBox();
+ comboBox.Dock = DockStyle.Fill;
+ comboBox.Size = new Size(300, 150);
+ comboBox.DataSource = builder._Items;
+ comboBox.SubscribeTo(builder._Items, values => comboBox.DataSource = values);
+ comboBox.SubscribeTo(builder._SelectedItem, value =>
+ {
+ var index = value == null ? -1 : comboBox.Items.IndexOf(value);
+ if (index < 0 && comboBox.Items.Count > 0) index = 0;
+ comboBox.SelectedIndex = index;
+ });
+ comboBox.SelectedIndexChanged += (sender, e) =>
+ {
+ var index = comboBox.SelectedIndex;
+ var selectedValue = index < 0 ? string.Empty : comboBox.Items[index];
+ builder._SelectedItem.OnNext((string)selectedValue);
+ };
+ return comboBox;
+ }
+ }
+}
diff --git a/src/Bonsai.Gui/DataMemberSelectorEditor.cs b/src/Bonsai.Gui/DataMemberSelectorEditor.cs
new file mode 100644
index 0000000..155a577
--- /dev/null
+++ b/src/Bonsai.Gui/DataMemberSelectorEditor.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using Bonsai.Design;
+
+namespace Bonsai.Gui
+{
+ internal class DataMemberSelectorEditor : MemberSelectorEditor
+ {
+ public DataMemberSelectorEditor()
+ : base(GetDataElementType, allowMultiSelection: false)
+ {
+ }
+
+ static Type GetDataElementType(Expression expression)
+ {
+ var parameterType = expression.Type.GetGenericArguments()[0];
+ return ExpressionHelper.GetGenericTypeBindings(typeof(IList<>), parameterType).FirstOrDefault() ?? parameterType;
+ }
+ }
+}
diff --git a/src/Bonsai.Gui/DataSourceControlBuilderBase.cs b/src/Bonsai.Gui/DataSourceControlBuilderBase.cs
new file mode 100644
index 0000000..b99feb6
--- /dev/null
+++ b/src/Bonsai.Gui/DataSourceControlBuilderBase.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Provides an abstract base class for UI controls which can be bound to each
+ /// data source from an observable sequence.
+ ///
+ public abstract class DataSourceControlBuilderBase : ControlBuilderBase
+ {
+ static readonly Range argumentRange = Range.Create(lowerBound: 1, upperBound: 1);
+
+ ///
+ /// Gets the range of input arguments that this expression builder accepts.
+ ///
+ public override Range ArgumentRange => argumentRange;
+
+ ///
+ /// Builds the expression tree for configuring and calling the UI control.
+ ///
+ ///
+ public override Expression Build(IEnumerable arguments)
+ {
+ var source = arguments.First();
+ var sourceType = source.Type.GetGenericArguments()[0];
+ var valueType = ExpressionHelper.GetGenericTypeBindings(typeof(IList<>), sourceType);
+ return Expression.Call(Expression.Constant(this), nameof(Generate), valueType, source);
+ }
+
+ ///
+ /// Generates an observable sequence of values containing the currently
+ /// selected item from the data source whenever the selection changes.
+ ///
+ ///
+ /// The type of the values in the data source.
+ ///
+ ///
+ /// A sequence of collections representing the data sources to bind to
+ /// the UI control. Only one collection is bound at any one time.
+ ///
+ ///
+ /// A sequence of values representing the currently selected item in
+ /// the UI control.
+ ///
+ protected abstract IObservable Generate(IObservable> source);
+ }
+}
diff --git a/src/Bonsai.Gui/ListBoxBuilder.cs b/src/Bonsai.Gui/ListBoxBuilder.cs
new file mode 100644
index 0000000..52cbc2c
--- /dev/null
+++ b/src/Bonsai.Gui/ListBoxBuilder.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Represents an operator that interfaces with a list box control and generates
+ /// a sequence of notifications whenever the selection changes.
+ ///
+ [TypeVisualizer(typeof(ListBoxVisualizer))]
+ [Description("Interfaces with a list box control and generates a sequence of notifications whenever the selection changes.")]
+ public class ListBoxBuilder : ListControlBuilderBase
+ {
+ }
+}
diff --git a/src/Bonsai.Gui/ListBoxDataSourceBuilder.cs b/src/Bonsai.Gui/ListBoxDataSourceBuilder.cs
new file mode 100644
index 0000000..4dae5ac
--- /dev/null
+++ b/src/Bonsai.Gui/ListBoxDataSourceBuilder.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Represents an operator that interfaces with a list box control bound to each data
+ /// source in the sequence and generates notifications whenever the selection changes.
+ ///
+ [TypeVisualizer(typeof(ListBoxDataSourceVisualizer))]
+ [Description("Interfaces with a list box control bound to each data source in the sequence and generates notifications whenever the selection changes.")]
+ public class ListBoxDataSourceBuilder : ListControlDataSourceBuilderBase
+ {
+ }
+}
diff --git a/src/Bonsai.Gui/ListBoxDataSourceVisualizer.cs b/src/Bonsai.Gui/ListBoxDataSourceVisualizer.cs
new file mode 100644
index 0000000..b72083e
--- /dev/null
+++ b/src/Bonsai.Gui/ListBoxDataSourceVisualizer.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Provides a type visualizer representing a list box control bound
+ /// to an arbitrary data source.
+ ///
+ public class ListBoxDataSourceVisualizer : ControlVisualizerBase
+ {
+ ///
+ protected override ListBox CreateControl(IServiceProvider provider, ListBoxDataSourceBuilder builder)
+ {
+ var listBox = new ListBox();
+ listBox.Dock = DockStyle.Fill;
+ listBox.Size = new Size(300, 150);
+ listBox.SubscribeTo(builder._DisplayMember, value => listBox.DisplayMember = value);
+ listBox.SubscribeTo(builder._DataSource, value => listBox.DataSource = value);
+ listBox.SelectedIndexChanged += (sender, e) =>
+ {
+ var index = listBox.SelectedIndex;
+ var selectedValue = index < 0 ? null : listBox.Items[index];
+ builder._SelectedItem.OnNext(selectedValue);
+ };
+ return listBox;
+ }
+ }
+}
diff --git a/src/Bonsai.Gui/ListBoxVisualizer.cs b/src/Bonsai.Gui/ListBoxVisualizer.cs
new file mode 100644
index 0000000..67442ff
--- /dev/null
+++ b/src/Bonsai.Gui/ListBoxVisualizer.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Provides a type visualizer representing a list box control.
+ ///
+ public class ListBoxVisualizer : ControlVisualizerBase
+ {
+ ///
+ protected override ListBox CreateControl(IServiceProvider provider, ListBoxBuilder builder)
+ {
+ var listBox = new ListBox();
+ listBox.Dock = DockStyle.Fill;
+ listBox.Size = new Size(300, 150);
+ listBox.DataSource = builder._Items;
+ listBox.SubscribeTo(builder._Items, values => listBox.DataSource = values);
+ listBox.SubscribeTo(builder._SelectedItem, value =>
+ {
+ var index = value == null ? -1 : listBox.Items.IndexOf(value);
+ if (index < 0 && listBox.Items.Count > 0) index = 0;
+ listBox.SelectedIndex = index;
+ });
+ listBox.SelectedIndexChanged += (sender, e) =>
+ {
+ var index = listBox.SelectedIndex;
+ var selectedValue = index < 0 ? string.Empty : listBox.Items[index];
+ builder._SelectedItem.OnNext((string)selectedValue);
+ };
+ return listBox;
+ }
+ }
+}
diff --git a/src/Bonsai.Gui/ListControlBuilderBase.cs b/src/Bonsai.Gui/ListControlBuilderBase.cs
new file mode 100644
index 0000000..eeeb2f1
--- /dev/null
+++ b/src/Bonsai.Gui/ListControlBuilderBase.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Reactive.Subjects;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Provides an abstract base class for interfacing with combo box and list box controls.
+ ///
+ public abstract class ListControlBuilderBase : ControlBuilderBase
+ {
+ internal readonly ObservableCollection _Items = new();
+ internal readonly BehaviorSubject _SelectedItem = new(string.Empty);
+
+ ///
+ /// Gets the collection of items contained in the list control.
+ ///
+ [Description("The collection of items contained in the list control.")]
+ public Collection Items
+ {
+ get => _Items;
+ }
+
+ ///
+ /// Gets or sets the currently selected item in the list control.
+ ///
+ [Description("The currently selected item in the list control.")]
+ public string SelectedItem
+ {
+ get => _SelectedItem.Value;
+ set => _SelectedItem.OnNext(value);
+ }
+
+ ///
+ /// Generates an observable sequence of values containing the currently
+ /// selected item in the list control whenever the selection changes.
+ ///
+ ///
+ /// A sequence of values representing the currently
+ /// selected item in the list control.
+ ///
+ protected override IObservable Generate()
+ {
+ return _SelectedItem;
+ }
+ }
+}
diff --git a/src/Bonsai.Gui/ListControlDataSourceBuilderBase.cs b/src/Bonsai.Gui/ListControlDataSourceBuilderBase.cs
new file mode 100644
index 0000000..1fd3622
--- /dev/null
+++ b/src/Bonsai.Gui/ListControlDataSourceBuilderBase.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing.Design;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+
+namespace Bonsai.Gui
+{
+ ///
+ /// Provides an abstract base class for interfacing with combo box and list box controls
+ /// bound to each data source in an observable sequence.
+ ///
+ public class ListControlDataSourceBuilderBase : DataSourceControlBuilderBase
+ {
+ internal readonly BehaviorSubject _DisplayMember = new(string.Empty);
+ internal readonly BehaviorSubject