Skip to content

Commit 9c42bd6

Browse files
authored
DYN-9903: Avoid using closed Window as EditWindow owner (#17014)
1 parent 978c081 commit 9c42bd6

4 files changed

Lines changed: 130 additions & 4 deletions

File tree

src/DynamoCoreWpf/UI/Prompts/EditWindow.xaml.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,23 @@ public EditWindow(DynamoViewModel dynamoViewModel,
3333
InitializeComponent();
3434
this.dynamoViewModel = dynamoViewModel;
3535

36-
Owner = dynamoViewModel.Owner;
37-
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
36+
var ownerWindow = dynamoViewModel.Owner;
37+
if (ownerWindow != null && ownerWindow.IsVisible)
38+
{
39+
Owner = ownerWindow;
40+
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
41+
}
42+
else
43+
{
44+
var mainWindow = Application.Current?.MainWindow;
45+
if (mainWindow != null && mainWindow.IsVisible)
46+
{
47+
Owner = mainWindow;
48+
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
49+
}
50+
}
3851

39-
// do not accept value if user closes
40-
this.Closing += (sender, args) => this.DialogResult = false;
52+
this.Closing += EditWindow_Closing;
4153
if (false != updateSourceOnTextChange)
4254
{
4355
this.editText.TextChanged += delegate
@@ -200,9 +212,19 @@ private void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e)
200212
DragMove();
201213
}
202214

215+
// Do not accept value if user closes without confirming.
216+
// DialogResult is only settable when opened via ShowDialog(); guard with try/catch
217+
// since WPF provides no public API to distinguish dialog vs non-dialog mode.
218+
private void EditWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
219+
{
220+
try { this.DialogResult = false; }
221+
catch (InvalidOperationException) { }
222+
}
223+
203224
private void EditWindow_Closed(object sender, EventArgs e)
204225
{
205226
this.editText.PreviewKeyDown -= EditText_PreviewKeyDown;
227+
this.Closing -= EditWindow_Closing;
206228
this.Closed -= EditWindow_Closed;
207229
}
208230

src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2582,6 +2582,9 @@ private void HandlePackageManagerWindowClosed(object sender, EventArgs e)
25822582
{
25832583
packageManagerWindow.Closed -= HandlePackageManagerWindowClosed;
25842584
packageManagerWindow = null;
2585+
// Reset owner back to the main Dynamo window so that dialogs opened after
2586+
// the Package Manager closes do not attempt to use the now-closed window as owner.
2587+
dynamoViewModel.Owner = this;
25852588

25862589
var cmd = Analytics.TrackCommandEvent("PackageManager");
25872590
cmd.Dispose();

src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public PreferencesView(DynamoView dynamoView)
7676

7777
Owner = dynamoView;
7878
dynViewModel.Owner = this;
79+
this.Closed += PreferencesView_Closed;
7980
if (DataContext is PreferencesViewModel viewModelTemp)
8081
{
8182
this.viewModel = viewModelTemp;
@@ -175,6 +176,15 @@ private void CloseButton_Click(object sender, RoutedEventArgs e)
175176
Close();
176177
}
177178

179+
private void PreferencesView_Closed(object sender, EventArgs e)
180+
{
181+
this.Closed -= PreferencesView_Closed;
182+
// Reset owner back to the main Dynamo window so that dialogs opened after
183+
// Preferences closes do not attempt to use the now-closed window as owner.
184+
// This runs regardless of how the window was closed (button, Alt+F4, programmatic).
185+
dynViewModel.Owner = this.Owner;
186+
}
187+
178188
/// <summary>
179189
/// handler for preferences dialog dragging action. When the TitleBar is clicked this method will be executed.
180190
/// </summary>

test/DynamoCoreWpf3Tests/NodeViewTests.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
using System.IO;
55
using System.Linq;
66
using System.Threading;
7+
using System.Windows;
78
using System.Windows.Input;
89
using System.Windows.Media;
910
using Dynamo.Controls;
1011
using Dynamo.Graph;
1112
using Dynamo.Graph.Workspaces;
1213
using Dynamo.Models;
1314
using Dynamo.Selection;
15+
using Dynamo.UI.Prompts;
1416
using Dynamo.ViewModels;
1517
using DynamoCoreWpfTests.Utility;
1618
using NUnit.Framework;
@@ -381,6 +383,95 @@ private void NodeNameTest_PropChangedHandler(object sender, System.ComponentMode
381383
var temp = (sender as NodeViewModel).Name;
382384
}
383385

386+
/// <summary>
387+
/// Regression test: opening the Package Manager or Preferences window temporarily sets
388+
/// dynamoViewModel.Owner to that child window. If the child window is closed without
389+
/// restoring the owner, the EditWindow constructor throws InvalidOperationException
390+
/// ("Cannot set owner to a Window that has been closed") when a node rename is triggered.
391+
/// </summary>
392+
[Test]
393+
[Category("RegressionTests")]
394+
public void WhenDynamoViewModelOwnerIsClosedWindowThenEditWindowDoesNotThrow()
395+
{
396+
// Arrange: open a graph with at least one node.
397+
Open(@"UI\CoreUINodes.dyn");
398+
DispatcherUtil.DoEvents();
399+
400+
Window originalOwner = null;
401+
try
402+
{
403+
// Simulate the stale-owner state left by PackageManagerView / PreferencesView:
404+
// a secondary window sets itself as dynamoViewModel.Owner, then closes
405+
// without restoring the owner back to the main DynamoView.
406+
View.Dispatcher.Invoke(() =>
407+
{
408+
originalOwner = ViewModel.Owner;
409+
var childWindow = new Window { Owner = View };
410+
childWindow.Show();
411+
childWindow.Close();
412+
// Owner now points to a closed window — the bug condition.
413+
ViewModel.Owner = childWindow;
414+
});
415+
416+
// Act & Assert: constructing EditWindow (what node rename does) must not throw.
417+
// Before the fix, this raised:
418+
// System.InvalidOperationException: Cannot set owner to a Window that has been closed.
419+
Assert.DoesNotThrow(() =>
420+
{
421+
View.Dispatcher.Invoke(() =>
422+
{
423+
var editWindow = new EditWindow(ViewModel, false, true);
424+
editWindow.Close();
425+
});
426+
});
427+
}
428+
finally
429+
{
430+
// Restore owner to avoid leaking stale state into subsequent tests.
431+
View.Dispatcher.Invoke(() => ViewModel.Owner = originalOwner ?? View);
432+
}
433+
}
434+
435+
/// <summary>
436+
/// Verifies that dynamoViewModel.Owner is restored to the main DynamoView after a
437+
/// child window that took over ownership is closed via its Closed event — the same
438+
/// pattern used by HandlePackageManagerWindowClosed and PreferencesView_Closed.
439+
/// </summary>
440+
[Test]
441+
[Category("RegressionTests")]
442+
public void WhenChildWindowClosesOwnerIsRestoredToDynamoView()
443+
{
444+
View.Dispatcher.Invoke(() =>
445+
{
446+
Assert.AreEqual(View, ViewModel.Owner,
447+
"Owner should be the main DynamoView after startup.");
448+
449+
// Simulate PackageManagerView / PreferencesView taking ownership.
450+
var childWindow = new Window { Owner = View };
451+
childWindow.Show();
452+
ViewModel.Owner = childWindow;
453+
454+
Assert.AreEqual(childWindow, ViewModel.Owner,
455+
"Owner should be the child window while it is open.");
456+
457+
// Wire up the Closed handler the same way HandlePackageManagerWindowClosed
458+
// and PreferencesView_Closed do — restore the owner to the parent window.
459+
childWindow.Closed += (s, e) => ViewModel.Owner = ((Window)s).Owner ?? View;
460+
461+
// Act: close the child window, which fires the Closed handler.
462+
childWindow.Close();
463+
});
464+
465+
DispatcherUtil.DoEvents();
466+
467+
// Assert: the Closed handler restored the owner — not manual assignment.
468+
View.Dispatcher.Invoke(() =>
469+
{
470+
Assert.AreEqual(View, ViewModel.Owner,
471+
"Owner must be restored to the main DynamoView after the child window closes.");
472+
});
473+
}
474+
384475
/// <summary>
385476
/// Check if elements are correctly displayed/collapsed on zoom level change
386477
/// Current zoom level is 0.4 (hard-coded in multiple Converters

0 commit comments

Comments
 (0)