Looking for Comprehensive, In-Depth Eto / MVVM Reference

Hello friends,
I’ve been working with RhinoCommon and Eto for a couple of years now, and I find myself running into the limits of the documentation that I’ve been able to find for Eto. It seems to me that Eto itself is based on concepts established in other UI frameworks (like Windows Forms or ??? I don’t know much about them, except that Eto is intended to be a cross-platform system so we don’t have to re-write for Windows vs MacOS). It seems like Eto uses certain concepts that are used elsewhere (like data contexts and data bindings) but I’ve not yet found explanations/examples/documentation of those concepts specifically for Eto.

Does anyone have any suggestions for a detailed reference work on a framework similar to Eto, perhaps, that would help guide me on these advanced issues?

One specific example where my conceptual understanding is weak (in addition to the general concept of how data contexts and bindings work) is building a TreeGridView, that can assign true/false properties (i.e. using check boxes) to an object. Some implementation details:

  • For each object, its property data is stored in Attributes.UserDictionary, in a string[], (e.g. {"main", "all"}, or {"balcony", "all"})
  • The plugin class has a Dictionary<string, HashSet<Guid>> that collects all of the string names of the property, as well as all of the Guids of the objects that have that property.
  • Right now, this property is set on a per-Layer basis from another command, with the following code (excerpted from the specific command):
            var hitLayers = from layer in doc.Layers
                            where doc.Objects.GetObjectList(AR_Helpers.ARAllSrfObjTypesEnumSett(layer)).AllObjectsHit(editlist)
                            select layer.Index;
            if (Rhino.UI.Dialogs.ShowSelectMultipleLayersDialog(hitLayers, $"Layers to show Hits on list {editlist}", false, out int[] layerIndices))
            {
                foreach(int layer in hitLayers)
                    foreach (RhinoObject obj in doc.Objects.GetObjectList(AR_Helpers.ARAllSrfObjTypesEnumSett(doc.Layers[layer])))
                    {
                        ObjectAttributes newattr = obj.Attributes.Duplicate();
                        if(newattr.UserDictionary.ContainsKey(ARHitObjectID))
                        {
                            string[] hitlists = newattr.UserDictionary[ARHitObjectID] as string[];
                            string[] newlist = hitlists.Except(new[] { editlist }).ToArray();
                            if (newlist.Length > 0)
                                newattr.UserDictionary.Set(ARHitObjectID, newlist);
                            else
                                newattr.UserDictionary.Remove(ARHitObjectID);
                            doc.Objects.ModifyAttributes(obj, newattr, true);
                        }
                    }

                foreach (int layer in layerIndices)
                    foreach(RhinoObject obj in doc.Objects.GetObjectList(AR_Helpers.ARAllSrfObjTypesEnumSett(doc.Layers[layer])))
                    {
                        ObjectAttributes newattr = obj.Attributes.Duplicate();
                        if (newattr.UserDictionary.ContainsKey(ARHitObjectID))
                        {
                            string[] hitlists = newattr.UserDictionary[ARHitObjectID] as string[];
                            string[] newlist = hitlists.Append(editlist).ToArray();
                            newattr.UserDictionary.Set(ARHitObjectID, newlist);
                        }
                        else newattr.UserDictionary.Set(ARHitObjectID, new[] { editlist });
                        doc.Objects.ModifyAttributes(obj, newattr, true);
                    }
                return Result.Success;
            }
            else return Result.Cancel;
  • The previous code sets the list on each object in the layer, then the following code is triggered by the RhinoDoc.ModifyObjectAttributes event (excerpted), to add or remove those modified surfaces from the Dictionary in the plugin class (described above, which here is represented by parent.ARSim_HitLayers:
                    if (rhobj.Attributes.UserDictionary.ContainsKey(ARHitObjectID))
                    {
                        string[] lists = rhobj.Attributes.UserDictionary[ARHitObjectID] as string[];
                        foreach (string list in lists)
                        {
                            if (!parent.ARSim_HitLayers.ContainsKey(list)) parent.ARSim_HitLayers.Add(list, new HashSet<Guid>());
                            parent.ARSim_HitLayers[list].Add(rhobj.Id);
                        }
                    }

The point of the panel I’m writing is to be able to modify this attribute (i.e. the members of the string[] stored in the Attributes.UserDictionary) on a per-object basis, rather than a per-layer basis. (Please don’t worry about the Absorptive/Reflective/Transparent property; that part works just fine.)

The code for the entire panel follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Rhino;
using Rhino.UI;
using Rhino.DocObjects;
using static AcousticRays.AR_Helpers;
using static AcousticRays.Simulation.AR_Helpers_Simulation;
using Eto.Forms;
using Eto.Drawing;
using System.Security.Cryptography.X509Certificates;

namespace AR
{
    public class AR_PropPage_ObjectMaterial : ObjectPropertiesPage
    {
        public override string EnglishPageTitle => "AR Material";

        private AR_Panel_ObjectMaterial panel;

        public override object PageControl => panel ?? (panel = new AR_Panel_ObjectMaterial(p));

        public AR_PropPage_ObjectMaterial(ARPlugin p) 
        {
            this.p = p;
        }

        private ARPlugin p;

        public override bool ShouldDisplay(ObjectPropertiesPageEventArgs e)
        {
            //DebugLine($"arobjmatl shoulddisplay; {e.ObjectCount} objects, includes relevant: {e.IncludesObjectsType(ARSurfaceObjectTypes)}");
            return e.IncludesObjectsType(ARSurfaceObjectTypes);
        }

        public override void UpdatePage(ObjectPropertiesPageEventArgs e)
        {
            RhinoObject[] selectedObjects = e.GetObjects(ARSurfaceObjectTypes);
            panel.SelObjCount = selectedObjects.Length;
            panel.CountAbsorptive = 0;
            panel.CountReflective = 0;
            panel.CountTransparent = 0;
            panel.AnyLocked = false;
            foreach (RhinoObject obj in selectedObjects)
            {
                if (obj.Attributes.UserDictionary.TryGetEnumValue(ARMatID, out ARMaterial t))
                    switch (t)
                    {
                        case ARMaterial.Absorptive:
                            panel.CountAbsorptive++;
                            break;
                        case ARMaterial.Reflective:
                            panel.CountReflective++;
                            break;
                        case ARMaterial.Transparent:
                            panel.CountTransparent++;
                            break;
                    }
                if (obj.Attributes.UserDictionary.GetBool(ARLockObject, false))
                    panel.AnyLocked = true;
            }

            panel.ActiveOnly = false;
            panel.TransparentOnly = false;
            if (panel.CountReflective == panel.SelObjCount)
            {
                panel.ActiveOnly = true;
                panel.Status = ARMaterial.Reflective;
            }
            else if (panel.CountAbsorptive == panel.SelObjCount)
            {
                panel.ActiveOnly = true;
                panel.Status = ARMaterial.Absorptive;
            }
            else if (panel.CountTransparent == panel.SelObjCount)
            {
                panel.TransparentOnly = true;
                panel.Status = ARMaterial.Transparent;
            }
            else
            {
                panel.Status = ARMaterial.Unset;
                if (panel.CountReflective == 0 && panel.CountAbsorptive == 0)
                    panel.TransparentOnly = true;
                else if ((panel.CountAbsorptive + panel.CountReflective) == panel.SelObjCount)
                    panel.ActiveOnly = true;
            }

            panel.doc = e.Document;
            panel.RhinoObjects = selectedObjects;
            panel.UpdatePanelData();
            //DebugLine($"Updatepage SelObjCount: {panel.SelObjCount}, active: {panel.CountActive}, absorptive: {panel.CountAbsorptive}, transparent: {panel.CountTransparent}");
            return;
        }

        public override ObjectType SupportedTypes => ARSurfaceObjectTypes;
    }
    
    public class AR_Panel_ObjectMaterial : Panel
    {
        public ARMaterial Status { get; set; }
        public bool ActiveOnly { get; set; }
        public bool TransparentOnly { get; set; }
        public bool AnyLocked { get; set; }
        private RadioButton Reflective;
        private RadioButton Absorptive;
        private RadioButton Transparent;
        private Button clear;
        private GridColumn chkColumn, listColumn;
        private TreeGridView hitlayerlist;
        private bool inUpdate;
        internal RhinoDoc doc { private get; set; }
        public int CountReflective , CountAbsorptive , CountTransparent , SelObjCount ;
        public RhinoObject[] RhinoObjects;
        private ARPlugin p;

        public AR_Panel_ObjectMaterial(AcousticRays p)
        {
            DebugLine("material panel constructor");
            this.p = p;
            Reflective = new RadioButton();
            Absorptive = new RadioButton(Reflective);
            Transparent = new RadioButton(Reflective);
            clear = new Button { Text = "Clear Material", Size = new Size(-1, -1) };
            inUpdate = true;

            Reflective.CheckedChanged += Radio_CheckedChanged;
            Absorptive.CheckedChanged += Radio_CheckedChanged;
            Transparent.CheckedChanged += Radio_CheckedChanged;
            clear.Click += (sender, e) => { UpdateMaterial(clear: true); };

            CustomCell checkboxcells = new CustomCell
            {
                CreateCell = r =>
                {
                    TreeGridItem item = r.Item as TreeGridItem;
                    string listname = item.Values[0] as string;
                    var checkbox = new CheckBox();
                    checkbox.Checked = (bool)item.Values[0];
                    checkbox.CheckedChanged += (sender, args) =>
                    {
                        CheckBox box = sender as CheckBox;
                        bool enable = box.Checked ?? false;
                        foreach (var obj in RhinoObjects)
                        {
                            var newattr = obj.Attributes.Duplicate();
                            if (enable)
                            {
                                if (newattr.UserDictionary.ContainsKey(ARHitObjectID))
                                {
                                    string[] hitlists = newattr.UserDictionary[ARHitObjectID] as string[];
                                    string[] newlist = hitlists.Append(listname).ToArray();
                                    newattr.UserDictionary.Set(ARHitObjectID, newlist);
                                }
                                else
                                    newattr.UserDictionary.Set(ARHitObjectID, new[] { listname });
                            }
                            else if (newattr.UserDictionary.ContainsKey(ARHitObjectID))
                            {
                                string[] hitlists = newattr.UserDictionary[ARHitObjectID] as string[];
                                string[] newlist = hitlists.Except(new[] { listname }).ToArray();
                                newattr.UserDictionary.Set(ARHitObjectID, newlist);
                            }
                            doc.Objects.ModifyAttributes(obj, newattr, true);
                        }
                    };
                    return checkbox;
                }
            };

            chkColumn = new GridColumn
            {
                Editable = true,
                HeaderText = "",
                Resizable = false,
                Width = 20,
                DataCell = checkboxcells
            };

            listColumn = new GridColumn
            {
                Editable = false,
                HeaderText = "Hit List",
                Resizable = true,
                Width = 100,
                DataCell = new TextBoxCell(1)
            }; 
            hitlayerlist = new TreeGridView
            {
                Border = BorderType.Line,
                ShowHeader = true,
                AllowColumnReordering = false,
                GridLines = GridLines.None,
                Size = new Size(-1, -1),
                AllowMultipleSelection = true,
                Columns = { chkColumn, listColumn }
            };


            Padding = new Padding(5);

            var layout = new DynamicLayout()
            {
                Spacing = new Size(5, 5),
                Padding = new Padding(5),
                Size = new Size(-1, -1),
            };
            layout.AddRow(Reflective, Absorptive, Transparent, null);
            layout.AddRow(clear, null);
            layout.Add(null);

            var table = new TableLayout
            {
                Padding = new Padding(5),
                Size = new Size(-1, -1),
                Rows = { new TableRow(layout), new TableRow(hitlayerlist) }
            };
            Content = table;
        }

        private void Radio_CheckedChanged(object sender, EventArgs e)
        {
            if ((sender as RadioButton).Checked && !inUpdate) UpdateMaterial();
        }

        private void UpdatePanelText()
        {
            Reflective.Text = $"Reflective: {CountReflective}";
            Absorptive.Text = $"Absorptive: {CountAbsorptive}";
            Transparent.Text = $"Transparent: {CountTransparent}";
        }

        public void UpdatePanelData() // there's probably a better way to do this with events and bindings.
            // it doesn't update the item counts always, need extra click somewhere? Radios are fine tho...
        {
            //DebugLine("updatepanel");
            inUpdate = true;
            Content.Enabled = !AnyLocked;

            UpdatePanelText();
            switch (Status)
            {
                case ARMaterial.Reflective:
                    Reflective.Checked = true;
                    break;
                case ARMaterial.Absorptive:
                    Absorptive.Checked = true;
                    break;
                case ARMaterial.Transparent:
                    Transparent.Checked = true;
                    break;
                case ARMaterial.Unset:
                    Reflective.Checked = false;
                    Absorptive.Checked = false;
                    Transparent.Checked = false;
                    break;
            }
            HashSet<Guid> objectids = new HashSet<Guid>(RhinoObjects.Select(x => x.Id));
            TreeGridItemCollection hitlayers = new TreeGridItemCollection();
            foreach (KeyValuePair<string, HashSet<Guid>> hitlist in p.ARSim_HitLayers)
            {
                hitlayers.Add(new TreeGridItem(hitlist.Value.IsSupersetOf(objectids), hitlist.Key));
            }
            hitlayerlist.DataStore = hitlayers;
            inUpdate = false;
        }

        private void UpdateMaterial(bool clear=false)
        {
            uint undo = doc.BeginUndoRecord("PanelMaterialChange");
            if (clear) Status = ARMaterial.Unset;
            else if (Reflective.Checked) Status = ARMaterial.Reflective;
            else if (Absorptive.Checked) Status = ARMaterial.Absorptive;
            else if (Transparent.Checked) Status = ARMaterial.Transparent;

            //DebugLine($"updatematerial status: {Status}, obj count: {objects.Length}, objects: {objects}");

            CountAbsorptive = 0;
            CountReflective = 0;
            CountTransparent = 0;
            
            foreach (var obj in RhinoObjects)
            {
                var newattr = obj.Attributes.Duplicate();
                newattr.UserDictionary.SetEnumValue(ARMatID, Status);
                doc.Objects.ModifyAttributes(obj, newattr, true);
            }

            switch (Status)
            {
                case ARMaterial.Reflective:
                    CountReflective = SelObjCount;
                    break;
                case ARMaterial.Absorptive:
                    CountAbsorptive = SelObjCount;
                    break;
                case ARMaterial.Transparent:
                    CountTransparent = SelObjCount;
                    break;
                case ARMaterial.Unset:
                    break;
            }

            if (clear) UpdatePanelData();
            else UpdatePanelText();
            doc.EndUndoRecord(undo);
            return;
        }
    }
}

This code produces the following panel:

Amazingly, all this works. But I don’t entirely understand why. (I pasted the whole code so that others might benefit from my own struggle, and have a working example to start with.) This StackOverflow topic is the best example of how to use the CustomCell class I’ve found, but there’s not much meat there.

If I were to initialize the chkColumn with generic CheckBoxCell as such:

            chkColumn = new GridColumn
            {
                Editable = true,
                HeaderText = "",
                Resizable = false,
                Width = 20,
                DataCell = new CheckBoxCell(0)
            };

I don’t understand how I might get access to each CheckBoxCell to be able to confirm or modify its state (un/checked). I suppose the question is "Is CustomCell required, or can it be done with CheckBoxCell? (In this condition, nothing happens.)

I did have some success in the past with the TreeGridView to make an ersatz-Layer panel that operates as a command dialog (based on the Python code here: Eto.TreeGridView - Keyboard Cell Focus - #2 by spineribjoint1 ):

                int levels = 0;
                TreeGridItemCollection layerCollection = new TreeGridItemCollection();

                foreach (Layer layer in doc.Layers)
                {
                    //need to think about layer.IsReference? Could be for worksessions?
                    //EXCELLENT It does capture worksession layers, and correctly, too...

                    string[] names = layer.FullPath.Split(new string[] { Layer.PathSeparator }, StringSplitOptions.None);
                    int level = names.Length - 1;
                    if (level > levels) levels = level;
                    TreeGridItem item = new TreeGridItem(names[level], layer.UserDictionary.GetBool("AR_SWSD_Hit",false), layer.Index);
                    item.Expanded = layer.IsExpanded;
                    nodes.Add(new layerlistnodes { level = level, layer = layer, treeItem = item, layerIdx=layer.Index });
                }
                for (int i = 0; i <= levels; i++)
                {
                    foreach (layerlistnodes node in nodes)
                    {
                        if (node.level == i)
                        {
                            Guid parent = node.layer.ParentLayerId;
                            if (parent != Guid.Empty)
                            {
                                foreach (layerlistnodes ld in nodes)
                                {
                                    if (ld.layer.Id == parent)
                                    {
                                        ld.treeItem.Children.Add(node.treeItem);
                                    }
                                }
                            }
                            else layerCollection.Add(node.treeItem);
                        }
                    }
                }
                TreeGridView layerList = new TreeGridView
                {
                    Border = BorderType.Line,
                    ShowHeader = true,
                    AllowColumnReordering = false,
                    GridLines = GridLines.Horizontal,
                    DataStore = layerCollection,
                    Size = new Size(-1, -1),
                    AllowMultipleSelection = true,
                    Columns = {layColumn,chkColumn}
                };

However in this case the condition of each check box is read when the semi-modal panel is closed and the relevant process is started - no need to subscribe to any CheckBox.CheckedChanged events. Here’s the list box:

So, thanks to everyone who made it this far, and if you don’t have any suggestions on the code that’s fine, I’m really looking for an Eto reference.

Dan

1 Like

Hi,

I’m not coding MVVM in Eto, but I do so professionally for WPF, Avalonia and other UI framewoks.

Foremost, sorry for not deep-diving into your code base. But from what I see there, I can’t spot any MVVM pattern inside. Usually, MVVM separates the UI from the core functionality. It uses a mediator in between both worlds called the ViewModel. Most UI elements can be created declarative, so one of the most important differences between the View and the ViewModel is the fact, that the View handles how the UI looks a like, while the ViewModel processes the interaction/logic between the User and the code.

Simple example: A “materials view” creates a material preview and some sliders and buttons. The “view model” then handles what happens if the user presses one of the buttons, and the “model” does a computation such as changing the material properties, or applying it to a geometry. If the model then answers that the look of the material has changed, the “view model” will then inform the “view” and providing all the necessary data to perform a partial re-render.

There is an important thing to know. The view doesn’t know the view model and vice versa. They are both linked by a classic observer pattern using data-binding. Also, the model doesn’t know any “viewmodel”. However, usually the viewmodel knows the model. Usually also by an interface e.g. to mock-out the model for certain test scenarios.

The main advantages of MVVM is that you can better test UI’s, that you can replace views without any change on how things work and that have a higher encapsulation and separation of concerns.

All the magic of MVVM is just about the observer pattern and INotifyPropertyChanged and ICommand interface. In C# this is quite the same for WPF, Avalonia or Eto.

In other words, if you know how to create MVVM in one of the other (better documented) technologies, the transition to Eto will be quite straightforward.

However, in C#, MVVM is not very intuitive at first. Also, there are many flavors and many level on how strict MVVM is supposed to be. Highly reactive UI’s are much harder to implement with strict MVVM.
So at some point you either break the pattern or over-engineer solutions.

However, the essence of this post is. If you want to know Eto and MVVM, just don’t use it for learning. Use Wpf or Avalonia. Besides this, if you write your own plugins, you can always utilize other UI frameworks. This is doable, you just need to include all the dependencies.

1 Like

Hi Tom
Thanks for writing, and the straightforward explanation of MVVM concepts.
You’re right, I am definitely not following that pattern in my code examples. I can imagine that switching to a MVVM would be a major re-think for those items. Interesting to hear that it is often not so strictly adhered-to, and also that in C# it is not clear. There are other ideas percolating where it might benefit.
For learning MVVM on a different framework, do you have any literature references that you like?
Dan

Not sure if you are German,

So for me the best book about WPF and also MVVM is “WPF 4.5 and XAML”
ISBN: 978-3-446-43467-7
https://www.hanser-elibrary.com/doi/book/10.3139/9783446435414

It’s a bit outdated, but still explains WPF in a very comprehensive way. MVVM is a bit short, but it has everything you need to understand it. Without any extra dependencies…

You can also read this, but I find it not very easy to understand. It’s a bit overcomplicated:

Also, there are small libraries for MVVM like Prismn or ReactiveUI. I even think there is finally also a Microsoft MVVM library. I do have my own, because It’s not really complicated. It all boils down to write a ViewModelBase and a custom RelayCommand. And to implement some synchronization-helper methods like “InvokeOnUIThread()”.

1 Like

Hello Tom, I was thinking about using Avalonia in Rhino Windows/mac plugins for some time, but I did not had time to investigate this yet. Have you encountered any specific problems with integration of Avalonia in Rhino?

Thank you for any info.

Best
Łukasz

I haven’t tested it with Rhino, only standalone. Avalonia is a nice improvement of WPF. With some differences, but in general, those differences are rather addressing issues in WPF. On top of that, it’s cross-platform. I could imagine that the .Net version may conflict with the current Rhino one. But in general, you can always design the system to run in a separate process. Only having a small middleware doing communication with your GUI. Usually I like to do this extra work, because it gives you much more freedom in developing your own things, but of course it always depends on a couple of things. E.g. if you depend on Rhinocommon, then it might be a showstopper.
Anyway, for the purpose of learning, it’s a nice solution. And I guess once you understand how MVVM works, you can also better deal with Eto.

Hey @TomTom, do you know any good repo or resources for learning Avalonia? Or even examples of how to make middleware between your app and rhino?

I wasn’t sure if you might have any open projects or resources you use for learning.

I am trying to learn how to make nicer GUI in and out of rhino. Avalonia looks really nice!
Also, thanks for breaking down MVVM. I am going to be looking into this much more.

From some time I am working daily with WPF and when I have to switch to Eto it feels like step back. It would be nice if devs could give some examples UI in xeto. Dear @dale do you think this is possible?

@mlukasz87 - I am happy to code up sample. If you want to see something, please start a new thread and outline what you need help with.

Thanks,

– Dale

1 Like

(Ich kann Deutsch lesen…aber das Buch erschient mir nicht erhältlich.)

The MSDN article is indeed pretty dense, and hard to work through, though I’ll probably learn something from it.

One other tip if you use WPF: create an IoC container. Reason being, certain interactions, such as VM binding, notifications, and page navigation, etc, can’t be implemented without one otherwise the model will invariably end up having an awareness of the view or you’ll find yourself declaring business logic in the view’s code-behind (which should only contain a parameterless constructor and nothing else).

Views need static access to the VMs and that’s where the IoC container is used; we use a simple service locator class for this purpose, with static properties that return VMs via the the container (global state is bad BTW, but there is an exception to every rule, this being one of the few). In doing so, we’re able to bind our VMs in XAML, making the design-view context-aware and this will give you full linting functionality making your app easier to write and reducing the risk of messing up your bindings. Just note that Rhino apps have to be compiled into assemblies, which means App.xaml can’t be used for providing any resources to the views, so the only catch is you’ll need to declare an instance of the service locator class in your view resources before you can bind the VM (i.e. the view will have to have awareness of a class in the view model layer).

MVVM is just a design pattern not a set of rules, so you don’t need to do any of the above and your app will still work, it just wont separate the model from the view and make your unit tests more difficult. If its only you working on the app then it wont matter.

Has anyone tried Avalonia with Rhino yet?

I was looking up Eto stuff this weekend. It doesn’t look so bad it’s just quite hidden and like other’s mentioned, there isn’t a ton of resources on it. I’m thinking about either doing a side project with Eto to get the hang of it, or doing Avalonia. To be honest, I was even watching a few videos on C++ and the GUI’s they demo’d looked easier than what I’ve been dealing with in C#/.NET (MVVM just doesn’t click with me). I think .NET is really becoming convoluted and changing too frequently.

It always depend at end on what you want, but personally I would definitly go for Avalonia. MVVM is not required unless you are planing to write larger applications. However, one benefit of MVVM is, that you can write ViewModel and Model independent from the GUI framework. So you could write it for Avalonia, and replace it for Eto later on. In theory, you would just need to replace the view markup. This holds true unless you are not doing anything fancy.

I would definitly go for Avalonia, because of the degree of freedom. There is only two drawbacks I could think of. It is very difficult, if not almost impossible to use the (window) docking inside Rhino. You’ll likely need to live with a separate window. And another drawback is that your installer will be large. You need to ship the Avalonia libraries, whereas the Eto framework comes with Rhino. Apart from these concerns, I only see advantages.

One thing, If you decide for MVVM, I personaly find the proposed MVVM toolkit of Avalonia a bit strange. Its called ReactiveUI, but I would rather go for MVVMLight which is now Microsoft or I would write my own ViewModelBase and my own RelayCommand implementation.

1 Like

I would suggest CommunityToolkit.Mvvm; it superseded MVVMLight, plus it has async/synchronous relay commands so there’s no need to do your own implementations, and other really useful stuff like observable attributes which you can use to decorate fields instead of doing full blown properties with change notification.

2 Likes

I usually prefer an own (modified) implementation of MVVM Light /CommunityToolkit.Mvvm (Forgot the name). The reason is that I write my own VMBase with additional functionality. Now since its Microsoft, I might think about it differently, but before you rely on a single person who you don’t know. This is a great risk for a relative small benefit. I tend to reduce my dependencies as much as possible. Rightnow I’m in need to migrate my tools, and again, there is one dependency which doesn’t work as expected anymore. This is always a great risk and a big problem. So unless you can rely on a bigger corporation, I try to use as few dependencies as possible, even if this means more work in the first place!

2 Likes

We follow the same principle. Generally all the NuGets we depend on are from either Microsoft or really popular ones that are highly unlikely to suddenly be withdrawn (e.g. Material Design).

100% agree with minimizing dependencies; I wrote a quite complex plugin user interface in silverlight, and of course then had to recreate it in js/html/css (and I believe that rewrite is still in use today, over a decade later, as it has literally zero dependencies beyond the languages themselves)

I feel more comfortable with eto, seeing the investment mcneel is making by moving rhino 8 to it, and how long they stuck with mfc