Complex Bindings
Complex Bindings in Eto
View Models
This section is not for Python, python cannot use View Models and bindings in the same way as C#.

Introduction

If you haven’t read the introductory page on bindings, start there.

Bindings in Eto are very powerful, very easy to chain together and as such can get complex. This page attempts to break all the exciting things that can be done with bindings and then use those knowledge chunks to build some of those complex bindings.

Knowledge Chunks

Conversions

Convert<T>

Convert<t> makes converting a ViewModel property into a more View appropriate value very simple with the added bonus of keeping the data in the View Model and View simple. In the example below Enums are used for all the value operations whilst the binding takes care of the messy text.

using Eto;
using Eto.Forms;
using Eto.Drawing;
using Rhino;
using Rhino.UI;
var lb = new Label() { Text = "Go Me" };
var prop = Binding.Property((ViewModel vm) => vm.Value)
.Convert((v) => {
var str = v switch
{
MyEnum.Brie => "Brie.. Mmmm..",
MyEnum.Cheddar => "Cheddar, my favourite!",
MyEnum.Edam => "Edam? No Thanks,",
MyEnum.Pecorino => "MMMMMMM",
_ => "err!"
};
return str;
});
lb.BindDataContext(l => l.Text, prop);
var viewModel = new ViewModel();
var dialog = new Dialog()
{
Padding = 8,
MinimumSize = new Size(200, 200),
Content = lb,
DataContext = viewModel,
};
dialog.KeyDown += (s, e) => {
if (e.Key == Keys.Up)
{
viewModel.MoveUp();
e.Handled = true;
}
if (e.Key == Keys.Down)
{
viewModel.MoveDown();
e.Handled = true;
}
};
public enum MyEnum { Brie = 0, Cheddar, Edam, Pecorino }
public class ViewModel : Rhino.UI.ViewModel
{
public MyEnum Value { get; set; }
public void MoveUp()
{
Value = ++Value;
if (Value > MyEnum.Pecorino)
Value = MyEnum.Pecorino;
RaisePropertyChanged(nameof(Value));
}
public void MoveDown()
{
Value = --Value;
if (Value < 0)
Value = 0;
RaisePropertyChanged(nameof(Value));
}
}
dialog.ShowModal();

OfType<T>

OfType conveniently allows for objects to be cast into other compatible types. The below example uses a list of enums which can be exchanged for ints, very convenient for SelectedIndex.

using System;
using System.Linq;
using System.Collections.ObjectModel;
using Eto;
using Eto.Forms;
using Eto.Drawing;
using Rhino;
using Rhino.UI;
var dd = new DropDown()
{
DataStore = new ObservableCollection<object>(Enum.GetValues<Days>().Cast<object>()),
};
var prop = Binding.Property((ViewModel vm) => vm.SelectedDay).OfType<int>();
dd.BindDataContext(l => l.SelectedIndex, prop);
var dialog = new Dialog()
{
Padding = 8,
MinimumSize = new Size(200, 200),
Content = dd,
DataContext = new ViewModel()
};
public enum Days { Monday = 0, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }
public class ViewModel : Rhino.UI.ViewModel
{
private Days _selectedDay { get; set; }
public Days SelectedDay
{
get => _selectedDay;
set
{
_selectedDay = value;
}
}
}
dialog.ShowModal();

ToBool

ToBool offers an easy way to convert bool? and bool bindings. It seems silly and small, but as many View Models will want to use bool and many toggleable controls use bool?, it’s super handy.

using Eto.Forms;
using Eto.Drawing;
using Rhino;
using Rhino.UI;
var cb = new CheckBox() { Text = "Click Me" };
// NOTE : Removing ToBool(...) will result in compilation errors
cb.BindDataContext(c => c.Checked, Binding.Property((MyViewModel vm) => vm.Checked).ToBool(true, false));
var dialog = new Dialog()
{
MinimumSize = new Size(200, 200),
DataContext = new MyViewModel(),
Content = cb,
};
public class MyViewModel : ViewModel
{
public bool Checked { get; set; }
}
dialog.ShowModal();

Child

Child lets you access nested properties of a View Model, possibly even from a ViewModel inside the main Model.

using Eto;
using Eto.Forms;
using Eto.Drawing;
using Rhino;
using Rhino.UI;
var cb = new CheckBox() { Text = "Click Me" };
cb.BindDataContext(cb => cb.Checked, Binding.Property((ViewModel vm) => vm.Model).Child(vc => vc.Checked).ToBool(true, false));
var dialog = new Dialog()
{
Padding = 8,
MinimumSize = new Size(200, 200),
Content = cb,
DataContext = new ViewModel(),
};
public class ViewModel : Rhino.UI.ViewModel
{
public InnerViewModel Model { get; set; } = new();
}
public class InnerViewModel : Rhino.UI.ViewModel
{
private bool _checked { get; set; } = false;
public bool Checked
{
get => _checked;
set
{
_checked = value;
RaiseInvalidPropertyValue(nameof(Checked));
RhinoApp.WriteLine("Checked!");
}
}
}
dialog.ShowModal();

Combining bindings together

Here is a more fully formed UI project with some of the bindings we used above, all brought together.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Eto.Forms;
using Eto.Drawing;
using Rhino.UI;
using Rhino.UI.Controls;
public enum Geometry { None = 0, Point, Line, Curve, Brep, Mesh, Sphere, SubD }
// A Main View Model
public class MyViewModel : ViewModel
{
public ChoicesModel Top { get; set; } = new ChoicesModel(Enum.GetValues<Geometry>().Except(new Geometry[] { Geometry.None }).Cast<object>());
public ChoicesModel Bottom { get; set; } = new ChoicesModel( new object[] {});
}
// A nested ViewModel
public class ChoicesModel : ViewModel
{
private int _selectedIndex { get; set; } = 0;
public bool Enabled => Choices.Count > 0;
public ObservableCollection<object> Choices { get; set; }
public int SelectedIndex {
get => _selectedIndex;
set
{
_selectedIndex = value;
RaisePropertyChanged(nameof(SelectedIndex));
}
}
public ChoicesModel(IEnumerable<object> data)
{
Choices = new(data);
Choices.CollectionChanged += (s, e) => {
// The update must be updated Async as when the UI updates the VM
// RaisePropertyChanged is ignored to prevent loops
Application.Instance.AsyncInvoke(() => {
// SelectedIndex will be -1 when the selected item is removed
if (SelectedIndex < 0)
SelectedIndex = 0;
RaisePropertyChanged(nameof(SelectedIndex));
RaisePropertyChanged(nameof(Enabled));
});
};
}
}
// We inherit from Dialog
class DropDownDialog : Dialog
{
// Helper property to make accessing DataContext easier
private MyViewModel Model => DataContext as MyViewModel;
// Handle on Control Elements
private Button MoveUp { get; set; } = new Button() { Text = "↑" };
private Button MoveDown { get; set; } = new Button() { Text = "↓" };
private DropDown TopChoices { get; set; } = new DropDown() { Width = 200 };
private DropDown BottomChoices { get; set; } = new DropDown() { Width = 200 };
public DropDownDialog()
{
Width = 300;
Padding = 8;
DataContext = new MyViewModel();
InitLayout();
InitBindings();
}
private void InitLayout()
{
Content = new TableLayout() {
Spacing = new Size(8, 8),
Rows = {
new TableRow(new Label() { Text = "Top" }),
new TableRow(TopChoices, MoveDown),
new TableRow(new Divider(), new Divider()),
new TableRow(new Label() { Text = "Bottom" }),
new TableRow(BottomChoices, MoveUp)
},
};
}
private void InitBindings()
{
TopChoices.BindDataContext(tc => tc.DataStore, Binding.Property((MyViewModel vm) => vm.Top).Child(t => t.Choices).Cast<IEnumerable<object>>());
TopChoices.SelectedIndexBinding.BindDataContext(Binding.Property((MyViewModel vm) => vm.Top).Child(t => t.SelectedIndex));
BottomChoices.BindDataContext(tc => tc.DataStore, Binding.Property((MyViewModel vm) => vm.Bottom).Child(t => t.Choices).Cast<IEnumerable<object>>());
BottomChoices.SelectedIndexBinding.BindDataContext(Binding.Property((MyViewModel vm) => vm.Bottom).Child(t => t.SelectedIndex));
// Disabling controls that will do nothing is a nice visual indicator for the user
MoveDown.BindDataContext(md => md.Enabled, Binding.Property((MyViewModel vm) => vm.Top).Child(t => t.Enabled));
MoveUp.BindDataContext(md => md.Enabled, Binding.Property((MyViewModel vm) => vm.Bottom).Child(b => b.Enabled));
TopChoices.BindDataContext(md => md.Enabled, Binding.Property((MyViewModel vm) => vm.Top).Child(t => t.Enabled));
BottomChoices.BindDataContext(md => md.Enabled, Binding.Property((MyViewModel vm) => vm.Bottom).Child(b => b.Enabled));
MoveDown.Click += (s,e) => {
var toMove = (Geometry)Model.Top.Choices.ElementAtOrDefault(Model.Top.SelectedIndex);
if (toMove == Geometry.None) return;
Model.Top.Choices.Remove(toMove);
Model.Bottom.Choices.Add(toMove);
};
MoveUp.Click += (s,e) => {
var toMove = (Geometry)Model.Bottom.Choices.ElementAtOrDefault(Model.Bottom.SelectedIndex);
if (toMove == Geometry.None) return;
Model.Bottom.Choices.Remove(toMove);
Model.Top.Choices.Add(toMove);
};
}
}
var dialog = new DropDownDialog();
var parent = RhinoEtoApp.MainWindowForDocument(__rhino_doc__);
dialog.ShowModal(parent);

More Reading