EmbeddingRhinoView in WPF Problem

Hi Everyone,

I am developing a RhinoCommon plugin where I need to embed a fully interactive native Rhino Viewport into a WPF window (using HwndHost).

I cannot use Rhino.UI.Controls.ViewportControl because I need full native navigation behavior, context menus, and standard object interaction, which the lightweight control lacks.

My Approach:

  1. Create a new view using RhinoDoc.ActiveDoc.Views.Add(...).

  2. Use P/Invoke SetParent to re-parent the native Rhino View handle into a WPF HwndHost.

  3. Strip the window styles (WS_CAPTION, WS_THICKFRAME, etc.) to make it look like a control.

The Problem:
The embedding works perfectly (rendering and interaction are fine). However, destroying the WPF window causes severe stability issues:

  1. Black Screen on New File: After closing the WPF window (and the embedded view), if I open a new file or create a new document, all Rhino viewports turn completely black. It seems the OpenGL/Display Pipeline context is corrupted or lost.

  2. Process Deadlock: Often, Rhino cannot be closed normally after this operation and must be terminated via Task Manager.

What I have tried (but failed):
I implemented a “Safe Cleanup” logic in the DestroyWindowCore of the HwndHost:

  1. Deactivate View: Switched ActiveView to a different safe viewport and forced Redraw().

  2. Reparenting: Before calling Close(), I used SetParent to move the view handle back to its original parent (or the main Rhino window) to detach it from WPF.

  3. Deferred Close: I used RhinoApp.Idle to delay the RhinoView.Close() call to avoid conflict with the WPF destruction cycle.

Despite these efforts, the display pipeline still breaks (Black Screen) upon loading a new document.

using System;
using System.Runtime.InteropServices;
using Rhino;
using Rhino.Commands;
using Rhino.Display;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Input;
using Rhino.Input.Custom;
using Rhino.UI;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Controls;
using System.Windows.Media;
using Label = System.Windows.Controls.Label;

namespace Test
{
    public class TestWpfEmbeddedView : Command
    {
        public override string EnglishName => "TestWpfEmbeddedView";

        protected override Result RunCommand(RhinoDoc doc, RunMode mode)
        {
            var wpfWindow = new MyWpfWindow();
       
            new System.Windows.Interop.WindowInteropHelper(wpfWindow).Owner = RhinoApp.MainWindowHandle();
            wpfWindow.Show();
            return Result.Success;
        }
    }

    public class MyWpfWindow : System.Windows.Window
    {
        private RhinoViewportHost _rhinoHost;

        private SimplePreviewConduit _conduit;

        private ViewportInteraction _mouseHandler;

        private Brep _sphere;

        private void InitLayout()
        {
            var grid = new Grid();
            grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(30) });
            grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

            var label = new Label { Content = "WPF Window", HorizontalAlignment = System.Windows.HorizontalAlignment.Center };
            Grid.SetRow(label, 0);
            grid.Children.Add(label);

            _rhinoHost = new RhinoViewportHost();
            Grid.SetRow(_rhinoHost, 1);
            grid.Children.Add(_rhinoHost);

            this.Content = grid;
        }

        private void OnWindowLoaded(object sender, RoutedEventArgs e)
        {
            if (_rhinoHost.RhinoView != null)
            {
                var viewId = _rhinoHost.RhinoView.ActiveViewportID;
                _conduit.TargetViewportId = viewId;
                _rhinoHost.RhinoView.ActiveViewport.ZoomBoundingBox(_conduit.ContentBoundingBox);
                _rhinoHost.RhinoView.Redraw();

                _mouseHandler = new ViewportInteraction(viewId, _sphere, _conduit);
                _mouseHandler.Enabled = true;
            }
        }

        private void OnWindowClosed(object sender, EventArgs e)
        {
            _conduit.Enabled = false;
            if (_mouseHandler != null) _mouseHandler.Enabled = false;
        }

        public MyWpfWindow()
        {
            Title = "WPF Embed Fixed";
            Width = 800;
            Height = 600;
            WindowStartupLocation = WindowStartupLocation.CenterOwner;

            _sphere = new Sphere(Point3d.Origin, 15).ToBrep();
            _conduit = new SimplePreviewConduit(_sphere, System.Drawing.Color.Red);
            _conduit.Enabled = true;

            InitLayout();

            this.Loaded += OnWindowLoaded;
            this.Closed += OnWindowClosed;
        }
    }

    public class RhinoViewportHost : HwndHost
    {
        private const int GWL_STYLE = -16;

        private const int WS_CHILD = 0x40000000;

        private const int WS_VISIBLE = 0x10000000;

        private const int WS_CAPTION = 0xC00000;

        private const int WS_THICKFRAME = 0x40000;

        private const int WS_POPUP = unchecked((int)0x80000000);

        private const int SW_HIDE = 0;

        private const uint SWP_FRAMECHANGED = 0x0020;

        private const uint SWP_NOMOVE = 0x0002;

        private const uint SWP_NOSIZE = 0x0001;

        private const uint SWP_NOZORDER = 0x0004;

        private const uint SWP_SHOWWINDOW = 0x0040;

        private IntPtr _originalParent = IntPtr.Zero;

        private bool _isClosing = false;

        public RhinoView RhinoView { get; private set; }

        protected override HandleRef BuildWindowCore(HandleRef hwndParent)
        {
            string viewName = "Embedded_" + Guid.NewGuid().ToString();
            var bounds = new System.Drawing.Rectangle(0, 0, 100, 100);
            RhinoView = RhinoDoc.ActiveDoc.Views.Add(viewName, DefinedViewportProjection.Perspective, bounds, true);

            if (RhinoView == null) throw new Exception("Create View Failed");
            IntPtr viewHandle = RhinoView.Handle;

            _originalParent = GetParent(viewHandle);
            if (_originalParent != IntPtr.Zero) ShowWindow(_originalParent, SW_HIDE);

            int style = GetWindowLong(viewHandle, GWL_STYLE);
            style = (style & ~WS_POPUP & ~WS_CAPTION & ~WS_THICKFRAME) | WS_CHILD | WS_VISIBLE;
            SetWindowLong(viewHandle, GWL_STYLE, style);

            SetParent(viewHandle, hwndParent.Handle);
            SetWindowPos(viewHandle, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW);

            RhinoView.ActiveViewport.DisplayMode = DisplayModeDescription.FindByName("Shaded");
            return new HandleRef(this, viewHandle);
        }

        protected override void DestroyWindowCore(HandleRef hwnd)
        {
            SafeDestroy();
        }

        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

        [DllImport("user32.dll")]
        private static extern IntPtr GetParent(IntPtr hWnd);

        [DllImport("user32.dll")]
        private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        [DllImport("user32.dll")]
        private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

        [DllImport("user32.dll")]
        private static extern int GetWindowLong(IntPtr hWnd, int nIndex);

        [DllImport("user32.dll")]
        private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

        private void SafeDestroy()
        {
            if (RhinoView == null || _isClosing) return;
            _isClosing = true;

            try
            {
                var doc = RhinoDoc.ActiveDoc;
                var viewToClose = RhinoView;

                if (doc.Views.ActiveView.ActiveViewportID == viewToClose.ActiveViewportID)
                {
                    var safeView = System.Linq.Enumerable.FirstOrDefault(doc.Views, v => v.ActiveViewportID != viewToClose.ActiveViewportID);
                    if (safeView != null)
                    {
                        doc.Views.ActiveView = safeView;
                    }

                    doc.Views.Redraw();
                    RhinoApp.Wait();
                }

                if (_originalParent != IntPtr.Zero)
                {
                    SetParent(viewToClose.Handle, _originalParent);
                }
                else
                {
                    SetParent(viewToClose.Handle, RhinoApp.MainWindowHandle());
                }

                RhinoApp.Idle += OnIdleCloseView;
            }
            catch (Exception ex)
            {
                RhinoApp.WriteLine($"SafeDestroy Error: {ex.Message}");
            }
        }

        private void OnIdleCloseView(object sender, EventArgs e)
        {
            RhinoApp.Idle -= OnIdleCloseView;

            try
            {
                if (RhinoView != null)
                {
                    RhinoView.Close();
                }

                RhinoDoc.ActiveDoc?.Views.Redraw();
            }
            catch
            {
            }
            finally
            {
                RhinoView = null;
                _originalParent = IntPtr.Zero;
            }
        }
    }

    public class ViewportInteraction : Rhino.UI.MouseCallback
    {
        private readonly Guid _targetViewportId;

        private readonly Brep _targetGeometry;

        private readonly SimplePreviewConduit _conduit;

        protected override void OnMouseDown(Rhino.UI.MouseCallbackEventArgs e)
        {
            if (e.View.ActiveViewportID != _targetViewportId) return;
            if (e.Button != System.Windows.Forms.MouseButtons.Left) return;

            var line = e.View.ActiveViewport.ClientToWorld(e.ViewportPoint);
            var intersection = Rhino.Geometry.Intersect.Intersection.RayShoot(
                new Ray3d(line.From, line.Direction), new[] { _targetGeometry }, 1
            );

            if (intersection != null && intersection.Length > 0)
            {
                _conduit.Highlight = !_conduit.Highlight;
                e.View.Redraw();
            }
        }

        public ViewportInteraction(Guid viewId, Brep geo, SimplePreviewConduit conduit)
        {
            _targetViewportId = viewId;
            _targetGeometry = geo;
            _conduit = conduit;
        }
    }

    public class SimplePreviewConduit : Rhino.Display.DisplayConduit
    {
        private readonly Brep _geometry;

        private readonly DisplayMaterial _normalMat;

        private readonly DisplayMaterial _highlightMat;

        public bool Highlight { get; set; } = false;

        public Guid TargetViewportId { get; set; } = Guid.Empty;

        public BoundingBox ContentBoundingBox => _geometry.GetBoundingBox(true);

        protected override void ObjectCulling(CullObjectEventArgs e)
        {
            if (TargetViewportId != Guid.Empty && e.Viewport.Id != TargetViewportId) { base.ObjectCulling(e); return; }
            if (e.RhinoObject != null) e.CullObject = true;
        }

        protected override void PostDrawObjects(DrawEventArgs e)
        {
            if (TargetViewportId != Guid.Empty && e.Viewport.Id != TargetViewportId) return;
            var mat = Highlight ? _highlightMat : _normalMat;
            e.Display.DrawBrepShaded(_geometry, mat);
            e.Display.DrawBrepWires(_geometry, System.Drawing.Color.Black, 1);
        }

        protected override void CalculateBoundingBox(CalculateBoundingBoxEventArgs e)
        {
            if (TargetViewportId != Guid.Empty && e.Viewport.Id != TargetViewportId) return;
            e.IncludeBoundingBox(ContentBoundingBox);
        }

        public SimplePreviewConduit(Brep geometry, System.Drawing.Color color)
        {
            _geometry = geometry;
            _normalMat = new DisplayMaterial(color);
            _highlightMat = new DisplayMaterial(System.Drawing.Color.Yellow);
        }
    }
}

My Question:
Is there a supported or “correct” way to cleanly destroy a RhinoView that has been re-parented via SetParent without corrupting the global Display Pipeline? Or is there an alternative way to host a fully native RhinoView inside a WPF container?

Any insights would be greatly appreciated.