Eto gridView binding to observableCollection (including a Rhino-crasher piece of sh.. code)

Hi @curtisw, all, I’m struggling to bind some custom class -CustomObj- properties to gridView DataCells in IronPython.

CustomObj class looks like this:

class CustomObj(System.Object):
    def __init__(self,name=None,obj=None,active=False,type=None):
        self._name = name
        self._obj = obj
        self._active = active
        self.type = type
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self,value):
        self._name = value
    @property
    def obj(self):
        return self._obj
    @obj.setter
    def obj(self,value):
        self._obj = value
    @property
    def active(self):
        return self._active
    @active.setter
    def active(self,value):
        self._active = value

CustomObjs are stored in an instance of MyModel class, in various properties that are .net BindableCollections.

class MyModel():
    def __init__(self):
        self.views = System.Collections.ObjectModel.ObservableCollection[type(CustomObj())]()
        self.versions = System.Collections.ObjectModel.ObservableCollection[type(CustomObj())]()

    def add(self,customObj):
        if customObj.type == 'view':
            self.views.Add(customObj)
        elif customObj.type == 'version':
            self.versions.Add(customObj)

I finally create a dialogClass with a gridView, and assign one of the MyModels observableCollections as gridView’s DataStore. Now, I’m struggling to figure out how I can bind a property of each item of the observableCollection to DataCells.
I tried with Binding.Property as follows:

class MyDialog(forms.Dialog[bool]):
    def __init__(self,myModel):
        self.cm = myModel
        self.gridView = forms.GridView()
        self.gridView.ShowHeader = True
        self.gridView.DataStore = self.cm.views #set a bindableCollection as gridView DataStore

        col_01 = forms.GridColumn()
        col_01.HeaderText = 'view'
        col_01.Editable = False
        col_01.DataCell = forms.TextBoxCell(Binding = forms.Binding.Property[type(CustomObj()),System.String]('name')) ##NOT WORKING CODE!!

        col_02 = forms.GridColumn()
        col_02.Editable = True
        col_02.DataCell = forms.CheckBoxCell(Binding=forms.Binding.Property[type(CustomObj()),System.Boolean]('active'))  ##NOT WORKING CODE!!

        #[code follows...]

image

Alternative, I tried using delegates as the example shared by @dale here . With this tecnique, it seems that TextBoxCell is correctly binding to ‘name’ property, but it fails to bing to the other DataCell of type CheckBoxCell.

class MyDialog(forms.Dialog[bool]):
    def __init__(self,myModel):
        self.cm = myModel
        self.gridView = forms.GridView()
        self.gridView.ShowHeader = True
        self.gridView.DataStore = self.cm.views #set a bindableCollection as gridView DataStore

        col_01 = forms.GridColumn()
        col_01.HeaderText = 'view'
        col_01.Editable = False
        col_01.DataCell = forms.TextBoxCell(Binding = forms.Binding.Delegate[CustomObj,System.String] (self.myNameDelegate)) #THIS BINDING WORKS!!!

        col_02 = forms.GridColumn()
        col_02.Editable = True
        col_02.DataCell = forms.CheckBoxCell(Binding = forms.Binding.Delegate[CustomObj,System.Boolean](self.myActiveDelegate)) #THIS MISERABLY FAILS!!! WHY. THE OTHER BINDING WORKS SO NICELY...
        #[code follows... complete code at the end of the post]
    def myNameDelegate(self,customObj):
        return customObj.name
    def myActiveDelegate(self,customObj):
        nullableBool = clr.Convert(customObj.active,System.Nullable)
        return nullableBool

I really can’t figure out how is done this with IronPython. All examples found out there were for C# and I can’t figure out how to translate them to IronPython.

Any help would be appreciated. Let me share the full code. Thanks!

import System
import Eto.Forms as forms
import Eto.Drawing as drawing
import clr




class CustomObj(System.Object):
    def __init__(self,name=None,obj=None,active=False,type=None):
        self._name = name
        self._obj = obj
        self._active = active
        self.type = type
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self,value):
        self._name = value
    @property
    def obj(self):
        return self._obj
    @obj.setter
    def obj(self,value):
        self._obj = value
    @property
    def active(self):
        return self._active
    @active.setter
    def active(self,value):
        self._active = value
  
class MyModel():
    def __init__(self):
        self.views = System.Collections.ObjectModel.ObservableCollection[type(CustomObj())]()
        self.versions = System.Collections.ObjectModel.ObservableCollection[type(CustomObj())]()

    def add(self,customObj):
        if customObj.type == 'view':
            self.views.Add(customObj)
        elif customObj.type == 'version':
            self.versions.Add(customObj)

class MyDialog(forms.Dialog[bool]):
    def __init__(self,myModel):
        self.cm = myModel
        self.gridView = forms.GridView()
        self.gridView.ShowHeader = True
        self.gridView.DataStore = self.cm.views #set a bindableCollection as gridView DataStore

        col_01 = forms.GridColumn()
        col_01.HeaderText = 'view'
        col_01.Editable = False
             ###binding to property trial...###
        #col_01.DataCell = forms.TextBoxCell(Binding = forms.Binding.Property[type(CustomObj()),System.String]('name'))

             ###binding to delegate trial...###
        col_01.DataCell = forms.TextBoxCell(Binding = forms.Binding.Delegate[CustomObj,System.String](self.myNameDelegate))  ## THIS IS WORKING CODE!!

        col_02 = forms.GridColumn()
        col_02.Editable = True
             ###binding to property trial...###
        #col_02.DataCell = forms.CheckBoxCell(Binding=forms.Binding.Property[type(CustomObj()),System.Boolean]('active'))  ##NOT WORKING CODE!!

            ###binding to delegate trial...###
        #col_02.DataCell = forms.CheckBoxCell(Binding = forms.Binding.Delegate[CustomObj,System.Boolean](self.myActiveDelegate))

        
        
        self.gridView.Columns.Add(col_01)
        self.gridView.Columns.Add(col_02)
        
        addButton = forms.Button(Text = 'test Add Element')

        def onAddButtonClick(sender,e):
            try:
                self.cm.add(CustomObj(name = 'testName',active = True,type='view'))
                self.veriLabel.Text = 'number of elements: {0}'.format(str(self.cm.views.Count))
            except Exception as es:
                Rhino.RhinoApp.WriteLine(str(e))
        addButton.Click += onAddButtonClick
        
        self.veriLabel = forms.Label(Text = 'number of elements: {0}'.format(str(self.cm.views.Count)))
        
        self.layout = forms.TableLayout(
                    Padding=drawing.Padding(5),
                    Size = drawing.Size(500,500)
                    )
        self.layout.Rows.Add(forms.TableRow(self.gridView))
        self.layout.Rows.Add(forms.TableRow(addButton))
        self.layout.Rows.Add(forms.TableRow(self.veriLabel))
        
        self.Content = self.layout

    def myNameDelegate(self,customObj):
        return customObj.name
    def myActiveDelegate(self,customObj):
        nullableBool = clr.Convert(customObj.active,System.Nullable)
        return nullableBool
 
#CREATE ANY MODEL
myModel = MyModel()
for i in range(10):
    name = 'name_{0}'.format(i)
    if i>5:
        active = False
    else:
        active = True
    myModel.add(CustomObj(name=name, active=active,type='view'))

#LAUNCH DIALOG
myDlg = MyDialog(myModel)
myDlg.ShowModal()
```
col_02.DataCell = forms.CheckBoxCell(Binding = forms.Binding.Delegate[CustomObj,System.Boolean](self.myActiveDelegate))

In short: What is wrong with this binding?

ok, so i found that error was related with not properly assigning types to the binding method, so writing this, no more exceptions are raised as before.

col_02.DataCell = forms.CheckBoxCell(Binding = forms.Binding.Delegate[CustomObj,System.Nullable[System.Boolean]](self.myActiveDelegate))

but now Rhino Crashes when I added this bounded columnGrid. In the video I first test the code without adding the second column to the gridView, and we can see that the first column is correctly binded. Then, in a second trial, I add the second column and Rhino Crashes:

import System
import Eto.Forms as forms
import Eto.Drawing as drawing
import clr




class CustomObj(System.Object):
    def __init__(self,name=None,obj=None,active=False,type=None):
        self._name = name
        self._obj = obj
        self._active = active
        self.type = type
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self,value):
        self._name = value
    @property
    def obj(self):
        return self._obj
    @obj.setter
    def obj(self,value):
        self._obj = value
    @property
    def active(self):
        return self._active
    @active.setter
    def active(self,value):
        self._active = value
  
class MyModel():
    def __init__(self):
        self.views = System.Collections.ObjectModel.ObservableCollection[type(CustomObj())]()
        self.versions = System.Collections.ObjectModel.ObservableCollection[type(CustomObj())]()

    def add(self,customObj):
        if customObj.type == 'view':
            self.views.Add(customObj)
        elif customObj.type == 'version':
            self.versions.Add(customObj)

class MyDialog(forms.Dialog[bool]):
    def __init__(self,myModel):
        self.cm = myModel
        self.gridView = forms.GridView()
        self.gridView.ShowHeader = True
        self.gridView.DataStore = self.cm.views #set a bindableCollection as gridView DataStore

        col_01 = forms.GridColumn()
        col_01.HeaderText = 'view'
        col_01.Editable = False
             ###binding to property trial...###
        #col_01.DataCell = forms.TextBoxCell(Binding = forms.Binding.Property[type(CustomObj()),System.String]('name'))

             ###binding to delegate trial...###
        col_01.DataCell = forms.TextBoxCell(Binding = forms.Binding.Delegate[CustomObj,System.String](self.myNameDelegate))  ## THIS IS WORKING CODE!!

        col_02 = forms.GridColumn()
        col_02.Editable = True
             ###binding to property trial...###
        #col_02.DataCell = forms.CheckBoxCell(Binding=forms.Binding.Property[type(CustomObj()),System.Boolean]('active'))  ##NOT WORKING CODE!!

            ###binding to delegate trial...###
        col_02.DataCell = forms.CheckBoxCell(Binding = forms.Binding.Delegate[CustomObj,System.Nullable[System.Boolean]](self.myActiveDelegate))

        
        
        self.gridView.Columns.Add(col_01)
        self.gridView.Columns.Add(col_02)
        
        addButton = forms.Button(Text = 'test Add Element')

        def onAddButtonClick(sender,e):
            try:
                self.cm.add(CustomObj(name = 'testName',active = True,type='view'))
                self.veriLabel.Text = 'number of elements: {0}'.format(str(self.cm.views.Count))
            except Exception as es:
                Rhino.RhinoApp.WriteLine(str(e))
        addButton.Click += onAddButtonClick
        
        self.veriLabel = forms.Label(Text = 'number of elements: {0}'.format(str(self.cm.views.Count)))
        
        self.layout = forms.TableLayout(
                    Padding=drawing.Padding(5),
                    Size = drawing.Size(500,500)
                    )
        self.layout.Rows.Add(forms.TableRow(self.gridView))
        self.layout.Rows.Add(forms.TableRow(addButton))
        self.layout.Rows.Add(forms.TableRow(self.veriLabel))
        
        self.Content = self.layout

    def myNameDelegate(self,customObj):
        return customObj.name
    def myActiveDelegate(self,customObj):
        nullableBool = clr.Convert(customObj.active,System.Nullable)
        return nullableBool
 
#CREATE ANY MODEL
myModel = MyModel()
for i in range(10):
    name = 'name_{0}'.format(i)
    if i>5:
        active = False
    else:
        active = True
    myModel.add(CustomObj(name=name, active=active,type='view'))

#LAUNCH DIALOG
myDlg = MyDialog(myModel)
myDlg.ShowModal()

Would it be possible to have a sample on how to use in ironpython a gridView binding to an observableCollection, preferably using forms.Binding.Property instead of Delegates?

My main problem with the bindings is to figure out how to use lambda functions with explicit type declarations. All the samples out there are written in C# and using lambdas is being particularly tricky (for me) to be translated into ironPython. I found this sample by @menno written in c#, and my problem is related to this sentence, again, to the lambda part in particular

var validBinding = Binding.Property((PropertyViewModel  pvm) => pvm.IsStringRepresentationValid);

thanks

how to use lambda functions with explicit type declarations.

And here I was, thinking that Python doesn’t need explicit types :confused:

Hi @aitorleceta,

Before anyone jumps in on writing a sample, can you provide more details on what this dialog box is supposed to do? Please provide as much detail as you can, as this will determine how simple or complicated the sample might end up being.

Thanks,

– Dale

@menno That’s true for regular python, but here we are talking about IronPython and a library, ETO, designed with strongly typed language in mind, so later or sooner, one found itself writing “anti-pythonic” chunks of code. It happens even when working with rhinocommon.

@dale I sent you a private message with further explanations. Thanks.

Hi @aitorleceta,

This block of your code:

col_02.DataCell = forms.CheckBoxCell(Binding=forms.Binding.Property[type(CustomObj()),System.Boolean]('active')) 

because the bindable data type for a CheckBoxCell is not a bool but rather a “nullable” bool. In C#, you’d use bool?. But you should be able to use System.Nullable[bool] in IronPython.

Traditionally, check boxes on Windows are three-state controls (check, unchecked, indeterminate).

More on Eto data binding:

– Dale

Sorry this link is totally confusing. DataBinding at least in WPF, is used to separate the GUI logic (=View) from its interaction with the Model Logic (Model) by having a intermediate layer which is debuggable and (unit) testable (=View Model). The view is usually XAML a markup language, more related to HTML,CSS then to C#.

Now if you have no MVVM pattern, no ViewModel at all, no markup language for the GUI, it doesn’t make sense to use a databinding concept at all. If GUI and Model logic is mixed anyway you can directly add things.

Calling a ViewModel a Model alone, like shown in the example link is a grave mistake.

Thanks for taking the time to see this @dale , but this is still not working. There must be wrong something else in my code but I definitively can’t figure out what is it.
If i try to used delegates, Rhino Crashes when running the, even inside try-except, so debugging has converted in a painful trial and error.

        #col_02.DataCell = forms.CheckBoxCell(Binding = forms.Binding.Delegate[type(CustomObj()),System.Nullable[System.Boolean]](self.myActiveDelegate))## THIS CODE CRASHES RHINO!!

using binding to properties instead of delegates throws an error:

col_02.DataCell = forms.CheckBoxCell(Binding=forms.Binding.Property[type(CustomObj),System.Nullable[System.Boolean]]('active'))  ##THIS CODE RAISES AN ERROR!!

image
At this point, I’m giving up using bindings with ETO + IronPython, this already has taken too many hours. I already studied your link before my first post; I still suspect that this is related to how c# lambdas equivalents are written in IronPython. I can’t figure this out myself, and because of that, I was looking for a working sample.

for documentation reasons, let me share my last attempt trying to implement nullable booleans
thanks

import System
import Eto.Forms as forms
import Eto.Drawing as drawing
import clr




class CustomObj(System.Object):
    def __init__(self,name=None,obj=None,active=False,type=None):
        self._name = name
        self._obj = obj
        self._active = active
        self.type = type
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self,value):
        self._name = value
    @property
    def obj(self):
        return self._obj
    @obj.setter
    def obj(self,value):
        self._obj = value
    @property
    def active(self):
        return self._active
    @active.setter
    def active(self,value):
        self._active = value
  
class MyModel():
    def __init__(self):
        self.views = System.Collections.ObjectModel.ObservableCollection[type(CustomObj())]()
        self.versions = System.Collections.ObjectModel.ObservableCollection[type(CustomObj())]()

    def add(self,customObj):
        if customObj.type == 'view':
            self.views.Add(customObj)
        elif customObj.type == 'version':
            self.versions.Add(customObj)

class MyDialog(forms.Dialog[bool]):
    def __init__(self,myModel):
        self.cm = myModel
        self.gridView = forms.GridView()
        self.gridView.ShowHeader = True
        self.gridView.DataStore = self.cm.views #set a bindableCollection as gridView DataStore

        col_01 = forms.GridColumn()
        col_01.HeaderText = 'view'
        col_01.Editable = False

        col_01.DataCell = forms.TextBoxCell(Binding = forms.Binding.Delegate[CustomObj,System.String](self.myNameDelegate))

        col_02 = forms.GridColumn()
        col_02.Editable = True
             ###binding to property trial...###
        col_02.DataCell = forms.CheckBoxCell(Binding=forms.Binding.Property[type(CustomObj),System.Nullable[System.Boolean]]('active'))  ##THIS CODE RAISES AN ERROR!!

            ###binding to delegate trial...###
        #col_02.DataCell = forms.CheckBoxCell(Binding = forms.Binding.Delegate[type(CustomObj()),System.Nullable[System.Boolean]](self.myActiveDelegate))## THIS CODE CRASHES RHINO!!

        
        
        self.gridView.Columns.Add(col_01)
        #self.gridView.Columns.Add(col_02)
        
        addButton = forms.Button(Text = 'test Add Element')

        def onAddButtonClick(sender,e):
            try:
                self.cm.add(CustomObj(name = 'testName',active = True,type='view'))
                self.veriLabel.Text = 'number of elements: {0}'.format(str(self.cm.views.Count))
            except Exception as es:
                Rhino.RhinoApp.WriteLine(str(e))
        addButton.Click += onAddButtonClick
        
        self.veriLabel = forms.Label(Text = 'number of elements: {0}'.format(str(self.cm.views.Count)))
        
        self.layout = forms.TableLayout(
                    Padding=drawing.Padding(5),
                    Size = drawing.Size(500,500)
                    )
        self.layout.Rows.Add(forms.TableRow(self.gridView))
        self.layout.Rows.Add(forms.TableRow(addButton))
        self.layout.Rows.Add(forms.TableRow(self.veriLabel))
        
        self.Content = self.layout

    def myNameDelegate(self,customObj):
        return customObj.name
    def myActiveDelegate(self,customObj):
        nullableBool = clr.Convert(customObj.active,System.Nullable)
        return nullableBool
 
#CREATE ANY MODEL
myModel = MyModel()
for i in range(10):
    name = 'name_{0}'.format(i)
    if i>5:
        active = System.Nullable[System.Boolean](False)
    else:
        active = System.Nullable[System.Boolean](True)
    myModel.add(CustomObj(name=name, active=active,type='view'))

#LAUNCH DIALOG
myDlg = MyDialog(myModel)
myDlg.ShowModal()

image

Hi @aitorleceta, no idea about the forms.Binding.Property but writing the first one (which does not crash over here) without a function using a lambda expression instead would look like this:

Binding = forms.Binding.Delegate[CustomObj, str](lambda obj: obj.name)

I guess you can omit type(CustomObj) as CustomObj returns the type without parenthesis.

I’ve got similar feelings but think that they are required in many situations. It would be helpful to see more IronPython examples from the developers in the future and i was able to learn something from yours, so thanks anyway.
_
c.

1 Like

Thats a wrong feeling. Databinding is an event-based mechanism to update data from UI to Model and vice versa. All it does is notifying a GUI element that it has to fetch data from viewmodel, when something in the viewmodel has changed. Its a mechanism to implement a software architecture. Its not a GUI related feature.

The viewmodel is not the model. The purpose of the viewmodel is to control the model logic (what you are doing in Rhino) .

the UI reacts to this event based. The Model doesn‘t know the View Model, and the View Model doesn‘t know the GUI. The advantage of this so called MVVM pattern is that in theory you can replace the GUI, you can separate the GUI markup from the interaction with model and you can unit test. I know I repeat myself, but whats been happening here totally shows the lack of understanding this fundamental principle. Otherwise it wouldn‘t be stored in a single script file.

I understand that you are referring to the sample shown at ETO official wiki, right?

I see what you are referring to MVVM pattern, but binding also give us “for free” some functionalities that otherwise we should implement for having a coherence between the data in the Model and the data displayed at the View…

Small programs shouldn‘t use a MVVM pattern at all. It is useful on medium to large sized apps. You can add data directly to any user control. They all derive from the same bases.

I cant show you this in IronPython. But if you look for a solution, always google for WPF and „Codebehind“. Translate it to IronPython and Eto,

The Wiki is confusing in this regard. But in the comment they call the model a view model. I believe Eto works better if you understand WPF

this is exactly what i tried to do before asking for a sample here at discourse… I’m not able to translate the c# statements for binding to ETO controls into IronPython.

I must recognize I’m a novice in regards to MVVM pattern, but I think my problem here is “idiomatic”

After all you, people that I consider literates in this software ecosystem, after all this post, the problem is still not solved. I particularly doesn’t need it anymore in the short therm, I figure out an alternative solution, but I would say that a sample of databinding to properties for ETO written in Iron Python would be well deserved learning material to add to the already excellent resources that McNeel is offering.

I’ve learned it only by doing it on a daily base. I currently develop software using MVVM/WPF/C#. The learning curve is pretty steep. Eto is a lot more difficult, because its such a niche technology. And IronPython makes it even more difficult. I’m not so sure if this is McNeels domain, I don’t think that building GUI’s like this (via scripts) should be done at all, feasable or not.

Hi @aitorleceta,

I’ve uploaded a simple GridView samples to the Rhino dev repo on GitHub.

SampleGridViewDialog.py

This sample does not use a MVVM pattern. Rather, it just uses a simple list as the data store.

I tend to agree with @TomTom in that just the framework is designed for MVVM, sometimes it doesn’t make sense to use it. For simple forms, such as a modal dialog box, its often quicker and less complicated to just get and set values from controls directly than to try to wire up a view model. I would think anyone writing scripts in Python would want to use Eto in this manner.

Learning any new framework and it’s nuances takes time, and I don’t think Eto is any more difficult to learn than anything else. The lack of Python samples does make learning Eto a bit more challenging.

Cheers,

– Dale

Thanks @dale, as I said, I already solved my problem the same way you resolved in your sample, accessing to datastore indices.

It seems that binding definitevely recommended to be use. This is certainly a pitty. I find it equally usefull even not implementing MVVM, but ok…

About using python for big projects, I always like to remember the case of LadyBug for Grasshopper, which is a quite a big project, still done in python.

Still, I must recognize IronPython, as a pseudo-language, faces, from time to time, some limitation/complications when dealing with .net libraries idiosyncracies…

Finally, I recognize I’ve learned from this lesson (even if it has not been about binding observable collection to properties in ETO using IronPython :stuck_out_tongue_winking_eye:) Thanks!

You always get a problem if documentation is missing. I think regarding IronPython and Eto, it just makes sense to understand the technology below their wrapping, which would be .Net/C# and WPF/Cocoa(?).
This is also true for CPython and QT or any other wrapping. But at least Pyside is much better documented and has a much greater user base, so you get much better help.

Ladybug as you mentioned is great and complex regarding the computational aspect, but the coding complexity is rather simple, meaning it does not (need to) exploit the framework nor the language to its fullest. This is why it works well with IronPython. Its rather a bunch of very well made script components.
The criticism from my point of view is that GUI development does not work well on a scripting base because it heavily relies on the framework and even on the IDE (Visual Studio). Especially GUI development is async and event driven. Event in Scripts can be really tough to handle, creating a couple of nasty bugs or unwanted behavior if you do things wrong.