Skip to content

Modernize HumanUI for Rhino 8 / .NET 7 + add test suite#9

Open
andylebihan wants to merge 35 commits into
andrewheumann:masterfrom
andylebihan:modernize-net7-rh8
Open

Modernize HumanUI for Rhino 8 / .NET 7 + add test suite#9
andylebihan wants to merge 35 commits into
andrewheumann:masterfrom
andylebihan:modernize-net7-rh8

Conversation

@andylebihan
Copy link
Copy Markdown

Modernize HumanUI for Rhino 8 / .NET 7 + add test suite

Why

The 0.7.x line targets the .NET Framework, Rhino 6/7, and MahApps.Metro 1.x. Three of those are now retired or breaking:

  • .NET Framework 4.x — no longer the Rhino target for v8.
  • MahApps.Metro 1.x — pulled from NuGet; the 2.x line renamed dozens of resource keys (SquareButton ? MahApps.Styles.Button.Square) and split off ControlzEx, which broke CreateButton, SetWindowProperties, the title-bar buttons, and the accent-color picker at runtime.
  • MetroChart + Markdown.Xaml NuGets — both abandoned. The published packages still install but pull in transitive references that don't restore on modern SDKs.

This PR is a pure modernization pass — no new components, no GUID changes, no parameter-shape changes. Existing .gh files load unchanged.

What changed

Build / project structure

  • SDK-style .csproj × 3 (HumanUI, HumanUIBaseApp, HumanUI.Tests). Drops app.config / packages.config. Retargets to net7.0-windows.
  • Vendored MetroChart.dll and Markdown.Xaml.dll under lib/ — both abandoned upstream, this avoids the NuGet restore breaking on a fresh clone.
  • HumanUI.dll ? HumanUI.gha copy via SDK post-build target. No more manual rename.
  • dist/ consolidated — one location for the published yak + manifest.

Source adaptations

  • MahApps 2.x renames — every SquareButton / BorderlessButton / CircleButton style key remapped to the 2.x equivalent. ThemeManager.ChangeAppTheme() (gone in 2.x) replaced with ThemeManager.Current.ChangeTheme(). Resource-dictionary pack URIs updated.
  • ControlzEx splitThemeManager references in SetWindowProperties_Component moved from MahApps.Metro to ControlzEx.Theming.

Bug fixes uncovered by the modernization

  • Window lifecycle (3 fixes in LaunchWindow_Component) — re-launching after a window had been closed produced a null reference; closing via X-click destroyed the window object instead of hiding it (subsequent solves couldn't re-show); the close-handler chain leaked event handlers across solves.
  • User X-click should hide, not destroy — matches the long-documented behavior; the WPF default of destroying the window was a regression from .NET Framework.
  • ValueListener event storm + cross-talk — the eventedElements field was static, so two listeners on the same canvas trampled each other's wired controls mid-solve. Made instance-level. Also added a 50 ms trailing-edge debounce on ExpireSolution: WPF events arrive far faster than GH can solve under slider drags, and the synchronous re-entry caused nested-solve and recompute-storm symptoms (Tests E/F/G/T in the included regression checklist).

Test suite (new)

HumanUI.Tests/ — xUnit project covering three layers:

Test class What it does
HUI_UtilTests String-serializer round-trips for the helpers used by checklist / list-box state.
ComponentSmokeTests Walks every GH_Component in the assembly via [Theory]: parameterless ctor, non-empty Name/NickName/GUID, lives in "Human UI" category, GUID uniqueness across the assembly.
ContractSnapshotTests + SnapshotComparison Serializes the GH-facing contract (per-component param shapes, GUIDs, exposure) to JSON; compares against a frozen master baseline. Catches accidental parameter-shape drift across releases.
UiIntegrationTests Reserved for a Rhino-hosted integration runner. Currently skipped — needs the McNeel-signed Eto.dll + WPF platform handler with matching key, which isn't available standalone in xUnit.

A tools/SnapshotTool/ helper exists for regenerating the contract baseline against a master build.

What didn't change

  • Every component's ComponentGuid.gh files load identically.
  • Every component's parameter shape — wires don't need to be re-drawn.
  • Component categories / nicknames — toolbar layout unchanged.
  • External API surface (HUI_Util, UIElement_Goo, MainWindow) — third-party plugins that depend on HumanUI types still work.

Testing

dist/human-ui-0.8.10-rh8_0-any.yak is built from this branch and tested against released Rhino 8.0 (8.0.23304.9001). To smoke-test:

"C:\Program Files\Rhino 8\System\Yak.exe" install --source dist/ human-ui 0.8.10

Then open examples/Beginner Examples.gh and the various per-component .gh files — every component should resolve and behave identically to the 0.7.x release.

dotnet test runs the full xUnit suite (HUI_UtilTests + ComponentSmokeTests + ContractSnapshotTests) green on a clean checkout.

Follow-ups (not in this PR)

I have a separate branch (eto-migration-mac) that takes the next step — replacing the WPF toolkit with Eto.Forms so HumanUI runs on Mac as well as Windows. That work is much larger (~150 files touched, every component reimplemented, MahApps dropped entirely) and would land as a separate PR once you've had a chance to react to this one. Happy to discuss the strategy.

About the commit count

This branch reads as "35 ahead of master" because the modernization work was developed on a snapshot from before PR #1#4 landed, and the upstream commits were cherry-picked into the modernization rather than merged. The final commit on this branch is a no-op merge of master that records the integration — git diff HEAD~1 HEAD returns an empty diff because every line of upstream's PR #1#4 is already present at the same content under different SHAs.

marcsyp and others added 30 commits August 16, 2019 15:33
- Allows you to change the text and/or the icon for the component
- Icon included
- Updated debug target
- Encapsulates a common UI "separator" element, constructed with a rectangle and defaulting to center alignment
- Features a Horizontal/Vertical toggle and the ability to override color, height, and width
- Includes icon
- Encapsulates a common UI "separator" element, constructed with a rectangle and defaulting to center alignment
- Features a Horizontal/Vertical toggle and the ability to override color, height, and width
- Includes icon
- Changed from a Stroke model to a Fill model -- cleaner, better looking lines
- Changed the default Alignment options for elements put into Simple Grids.  This is because when building grids for interfaces, the previous options were almost always wrong for most use cases.  Changed to HorizontalAlign.Stretch and VerticalAlign.Top. 
- Sliders + PullDowns now fill the cells automatically.
- Labels now more or less align with most element types by default
- Deprecated the previous component to avoid major styling changes to existing interfaces.  No upgrader provided as upgrading should be done manually in most cases.
- This allows vertical separators to appear correctly in cells by default.  Does not affect labels.
- more elegant starting point
Added SetButton component

Approved-by: Andrew Heumann
Psd cleanup

Approved-by: Andrew Heumann <andheum@gmail.com>
Simple grid updates

Approved-by: Andrew Heumann <andheum@gmail.com>
Added auto column sizing in the DataTable component
Update Human UI from the original net45 / Rhino 5 NuGet build to a
working Rhino 8 build, then fix the breakage that surfaced once the
modernized .gha started loading.

Build
-----
- Convert HumanUI.csproj and HumanUIBaseApp.csproj from the legacy
  packages.config layout to SDK-style projects targeting
  net7.0-windows with <UseWPF>true</UseWPF>. The .gha is produced
  by copying the .dll output after build so MSBuild's standard
  assembly-name tooling still works (CoreCLR refuses to start when
  a .gha appears in the Trusted Platform Assemblies list).
- Replace HintPath-based references with PackageReferences:
  Grasshopper 8.31.26126.13431 (ExcludeAssets=runtime so the .gha
  does not redistribute Rhino), MahApps.Metro 2.4.10,
  HelixToolkit.Wpf 2.27.0, Extended.Wpf.Toolkit 4.6.1.
- Vendor De.TorstenMandelkow.MetroChart and Markdown.Xaml into
  lib/. Both packages were never ported to .NET Core / .NET 5+;
  the .NET Framework binaries load fine inside a net7-windows
  WPF host.
- Regenerate HumanUI.sln with simple Debug/Release|AnyCPU
  configurations and the SDK-style project type GUID.
- Drop the legacy packages.config / app.config / Settings designer
  files and the orphan duplicate slider files in Upgraders/ that
  the old csproj never compiled but SDK-style globbing would
  pick up.
- Pin the SDK to 7.0.x via global.json and rewrite build.cmd to
  drive msbuild + yak end-to-end into dist/.

Source updates for MahApps.Metro 2.x breaking changes
-----------------------------------------------------
- ToggleSwitch: IsChecked -> IsOn, OnLabel/OffLabel ->
  OnContent/OffContent, IsCheckedChanged event -> Toggled.
- Theming: replace MahApps.ThemeManager.ChangeAppTheme /
  ChangeAppStyle / DetectAppStyle with
  ControlzEx.Theming.ThemeManager.Current.ChangeTheme /
  DetectTheme. The new API rolls AppTheme + Accent into one
  "{BaseColorScheme}.{ColorScheme}" name; preserve the other half
  when only one is changed.
- ControlsHelper.SetHeaderFontSize moved to
  HeaderedControlHelper.SetHeaderFontSize.
- ColorPicker disambiguated via using alias to keep the existing
  Xceed-based behavior; MahApps 2.x added its own ColorPicker
  type with the same short name.
- MainWindow.xaml: drop EnableDWMDropShadow (removed in 2.x) and
  swap the Styles/Accents/* + Colors.xaml merged dictionaries for
  the per-theme Styles/Themes/Light.Blue.xaml. This was the root
  cause of the 0.8.9 "Set property 'Source' threw an exception"
  crash that prevented LaunchWindow from showing the dialog.
- TabContainer_Component: load Styles/Themes/Light.Blue.xaml
  instead of Colors.xaml; AccentColorBrush -> MahApps.Brushes.Accent;
  MetroTabItem style key -> MahApps.Styles.TabItem.
- CreateButton_Component: MetroButton -> MahApps.Styles.Button,
  SquareButtonStyle -> MahApps.Styles.Button.Square,
  MetroCircleButtonStyle -> MahApps.Styles.Button.Circle.
- Three hidden GH_Exposure.hidden DEPRECATED components removed
  because they relied exclusively on MahApps 1.x ThemeManager and
  ControlsHelper APIs:
  SetWindowProperties_Component_DEPRECATED,
  SetWindowProperties_Component_ALSO_DEPRECATED,
  TabContainer_Component_ALSO_DEPRECATED.
- Two longstanding "Human" -> "Human UI" category typos fixed in
  CreateGrid_Component_DEPRECATED and
  CreateSlider_Component_ALSO_DEPRECATED.

Distribution
------------
- dist/ refreshed with the 0.8.10 .gha and its runtime deps.
- Replace the 0.8.8 yak with human-ui-0.8.10-rh8_31-any.yak.
- Bump AssemblyVersion / CURRENT_VERSION to 0.8.10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A standalone test project with four layers of coverage, each
chosen to keep working through the planned WPF -> Eto migration.

Pure helpers (HUI_UtilTests, 8 tests)
  Round-trip tests for the small UI-agnostic serialization helpers
  in HUI_Util (boolsFromString / stringFromBools,
  stringFromStrings / stringsFromString). Documents the existing
  trailing-comma quirk the GH file format relies on.

Component smoke tests (ComponentSmokeTests, 418 theory cases)
  Reflection-driven sweep over every concrete GH_Component
  subclass in the assembly. For each: asserts that the
  parameterless constructor runs, the component lives in the
  "Human UI" category, Name / NickName are non-empty and the
  ComponentGuid is set; a separate check enforces ComponentGuid
  uniqueness across the assembly. Surfaces any constructor-time
  breakage from a backend swap.

Contract snapshot regression (ContractSnapshotTests, 1 test, plus
  HumanUI.Tests/contract-baseline.json)
  Captures the full GH-facing contract -- type FullName,
  ComponentGuid, Name, NickName, Category, SubCategory, Exposure,
  and per-parameter Type / Name / NickName / Description / Access
  / Optional for every input and output -- into a checked-in
  baseline JSON and fails on any drift. Intentional changes are
  applied with HUMANUI_REGENERATE_SNAPSHOT=1.

Master-vs-branch contract comparison (SnapshotComparison +
  HumanUI.Tests/master-snapshot.json + tools/SnapshotTool/)
  Semantic diff between the live snapshot and a frozen
  master-snapshot.json that came from running the standalone
  net48 SnapshotTool against master's HumanUI.dll. AcceptedRemovals
  and AcceptedFieldChanges allowlist the intentional drift of this
  branch (three DEPRECATED components dropped, two "Human" ->
  "Human UI" typo fixes); anything else fails.
  tools/SnapshotTool/ is the reproducer: a net48 console app
  that loads any HumanUI.dll, walks its components and emits the
  JSON expected by SnapshotComparison.

UI integration tests (UiIntegrationTests, 9 tests)
  Live STA-thread WPF tests via Xunit.StaFact. Catches the class
  of bug that shipped in 0.8.9: XAML pack-URI failures during
  MetroWindow construction and string-keyed Style / Brush lookups
  against renamed MahApps resource dictionaries. Theory data
  tables enumerate every MahApps URI and every keyed lookup the
  codebase performs, so adding a new lookup means adding a row.

Wire-up
- ShouldRemoveRhinoReferences=False in HumanUI.Tests.csproj so
  Grasshopper.dll / GH_IO.dll land in the test bin (the .gha
  distribution still excludes them).
- [assembly: InternalsVisibleTo("HumanUI.Tests")] in HumanUI
  AssemblyInfo to expose the internal helpers under test.
- HumanUI.Tests entry added to HumanUI.sln.
- .gitignore updated for bin/obj/diag artifacts.

Run with: dotnet test HumanUI.Tests/HumanUI.Tests.csproj

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
andylebihan and others added 5 commits May 17, 2026 01:59
Three small targeted changes addressing the most common
LaunchWindow forum complaints, plus a regression test for one of
them.

1. "Appears off-screen on multi-monitor"
   MainWindow.xaml defaults to WindowStartupLocation=Manual, which
   means Left=0/Top=0 on the primary monitor regardless of where
   Rhino lives. Set WindowStartupLocation="CenterScreen" so the
   window opens on whichever screen the active cursor is on -- in
   practice the same screen as Rhino.

2. "Appears behind Rhino"
   WPF Window.Show() does not bring a window forward when another
   HWND already has focus, and the canvas the user just clicked is
   exactly that HWND. Call mw.Activate() right after Show() in
   LaunchWindow_Component.SolveInstance.

3. "Nothing happens"
   The 0.8.9 crash chain was: SetupWin throws inside
   BeforeSolveInstance -> mw is null or partly-built -> next
   SolveInstance NREs on mw.Title and the toggle silently fails.
   Add an explicit null guard at the top of SolveInstance that
   raises a runtime error message instead.

Also, when SetupWin is triggered by something other than first
launch (menu change, mw_Closed, doc-changed reset), it was
recreating the window at the default location -- losing any
position the user had set via SetWindowProperties. Carry
Left/Top/Width/Height across the recreate.

Test:
- UiIntegrationTests.MainWindow_DefaultsToCenterScreen guards
  against the WindowStartupLocation default regressing back to
  Manual.

The other two fixes are runtime/threading behaviors that can't
be exercised from xUnit without a Grasshopper document context.
They're documented in code comments where they happen.

Refreshed dist/ artifacts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The window lifecycle bug behind several forum complaints (Tests
B/C/D in the discourse-pattern checklist):

- Test B: "Close and reopen" -> "window never comes back" /
  "duplicate windows" / "zombie window".
- Test C: "Rapid toggle spam" -> "crash" / "multiple instances".
- Test D: "Minimize Rhino" -> "window disappears, loses focus
  permanently".

All three stem from the same chain. The user clicks X on the
window:
  1. mw.Closed fires
  2. mw_Closed unwires itself, then synchronously rebuilds via
     SetupWin() -> mw = new MainWindow()
  3. The new mw is never Show()n
  4. Next solve: BeforeSolveInstance sees !mw.IsLoaded and
     rebuilds *again*
  5. SolveInstance: mw.Show() if input is true -> a *third* mw
     finally appears, with no element state from the previous one

Wired-up listeners, scroll position, intermediate text, etc are
all gone. Rapid toggling magnifies the churn -- enough fast clicks
and the BeforeSolveInstance / Closed / SetupWin recursion gets
ahead of itself and we leak windows.

Fix: intercept mw.Closing (before mw.Closed) and Cancel + Hide
when the close was initiated by a user X-click. mw stays alive,
all element state is preserved, and the next solve simply re-Show()s
the existing window. A new _allowProgrammaticClose flag lets
SetupWin (menu changes / doc-changed resets) and RemovedFromDocument
genuinely close the window when they need to.

RemovedFromDocument is also fixed to null-check mw and to reset
the flag in a finally so an exception during Close doesn't strand
the flag for the next instance.

Regression coverage: ContractSnapshotTests still passes (no
GH-facing surface change). Behavioral fixes are runtime/threading
in nature and aren't directly unit-testable without a Grasshopper
document context, but the smoke and integration suites confirm
the component still constructs and instantiates cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes in ValueListener_Component that together address the
"UI <-> GH sync breaks under pressure" cluster from the discourse-
pattern checklist (Tests C/E/F/G/S/T):

1. Debounce ExpireSolution via GH_Document.ScheduleSolution
   ExpireThis was calling ExpireSolution(true) synchronously from
   every WPF event. A slider drag fires 60 ValueChanged events / sec
   which all became 60 nested re-entrant ExpireSolution(true) calls
   on the WPF thread -- the source of "GH locks up while I drag"
   and timer-driven recompute storms. Switch to ExpireSolution(false)
   + ScheduleSolution(50). ScheduleSolution coalesces repeated calls
   inside the delay window, so a 60fps event burst becomes ~one
   solve when the user pauses.

2. Drop static on eventedElements
   The field was static, so the most-recently-constructed
   ValueListener's list was used by every instance. Two listeners on
   the same canvas trampled each other's wired-element bookkeeping
   mid-solve, dropping events from whichever instance got cleared.
   Now instance-level.

3. Fix the double-add to eventedElements
   AddEvents() also added u to eventedElements after the SolveInstance
   caller already did. Every solve doubled the list size before the
   next solve's Clear(). No event-handler leak (the -= path is
   idempotent) but obvious bookkeeping waste.

Regression coverage:
- UiIntegrationTests.ValueListener_EventedElementsIsInstanceLevel
  reflects on the field and asserts !field.IsStatic so the
  cross-talk bug stays fixed.
- The ScheduleSolution debounce is a runtime/threading behavior
  that can't be unit-tested without a Grasshopper document context.
  ComponentSmokeTests and the contract baseline still pass so the
  GH-facing surface is unchanged.

Refreshed dist/ artifacts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Grasshopper/RhinoCommon references on this branch already point at
8.0.23304.9001 (filter-branch rewrite), but the committed binaries and
yak were still the 8.31-WIP build. Rebuilding so the distributed yak
actually matches what the source says it does. New yak filename is
human-ui-0.8.10-rh8_0-any.yak (Yak auto-derives the rh8_0 distribution
tag from RhinoCommon's version).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ia cherry-pick during modernize-net7-rh8 development, so the merge is a no-op on content.

# Conflicts:
#	HumanUI/HumanUI/packages.config
#	dist/human-ui-0.8.8-any-win.yak
@andrewheumann
Copy link
Copy Markdown
Owner

Thank you so much for doing this @andylebihan — I really appreciate it. Threw in some comments from Codex, nothing critical it seems like.

@andylebihan
Copy link
Copy Markdown
Author

I don't see the comments from Codex.

I know that's quite a wall of text to read (and dry!). An important part is that there is another branch which is much more ambitios which converts the whole back end to Eto and makes it cross platform.

Let's figure out how we should handle getting this out to the public. BTW - there's already a post on Discourse with the links to the two Yak files for people to try.

Comment thread dist/HumanUI.dll
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file should not be included in /dist

//through rather than intercepting it as a user X-click.
try
{
_allowProgrammaticClose = true;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pre-existing issue, not introduced by your PR, but since we're making an attempt to fix this behavior:

_allowProgrammaticClose only bypasses Closing; the subsequent Closed event still calls SetupWin(). That means SetupWin() closes the old window, mw_Closed recursively creates a replacement, then the outer SetupWin() creates another replacement. More seriously, RemovedFromDocument() also sets _allowProgrammaticClose and closes mw, so removing the component can immediately create a new MainWindow after teardown. The Closed handler should likely detach and return without rebuilding when the close is programmatic, or SetupWin should suppress the handler before closing.

Comment thread build.cmd

del /Q "%REPO%dist\*.yak" 2>nul
copy /Y "%REPO%HumanUI\HumanUI\bin\Release\*.gha" "%REPO%dist\" >nul
copy /Y "%REPO%HumanUI\HumanUI\bin\Release\*.dll" "%REPO%dist\" >nul
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be sure to package only supplementary DLLs and not HumanUI.dll — which is identical to the .gha

@andrewheumann
Copy link
Copy Markdown
Owner

Sorry, forgot to submit the review 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants