Skip to content

Commit d9133b6

Browse files
authored
fix: support .NET Core in winforms activation design mode check (#4358)
## What kind of change does this PR introduce? 1. Delay the design mode check in Winforms activation until after the control is sited. 2. Use `Control.IsAncestorSiteInDesignMode` (polyfilled on older targets) to support deeply nested controls. 3. Remove the `LicenseManager.UsageMode` check because it doesn't work on .NET Core and is no longer needed on .NET Framework after the other fixes. 4. While at it, reduce nesting of RX operators if the control is a `Form`. ## What is the current behavior? Design mode detection is broken on .NET Core. 1. The detection is done too early. Controls are not yet sited in their constructor, so `control.Site?.DesignMode` is never true. `LicenseManager` is not supported on .NET Core. `LicenseManager.UsageMode == LicenseUsageMode.Designtime` is always false. 2. Design mode detection does not support deeply nested controls. Only the current control and its parent are checked for design mode. ## What is the new behavior? Design mode detection works both on .NET Framework and .NET Core and supports deeply nested controls. ## What might this PR break? This shouldn't change anything at runtime, only design time. ## Checklist - [X] I have read the [Contribute guide](https://www.reactiveui.net/contribute/index.html) - [ ] Tests have been added or updated (for bug fixes / features) - [ ] Docs have been added or updated (for bug fixes / features) - [X] Changes target the `main` branch - [X] PR title follows [Conventional Commits](https://www.conventionalcommits.org/) ## Additional information Manually tested on `net481` and `net10.0-windows10.0.19041.0`. There are still more issues with the Winforms Designer. Unless you inherit your controls from a base class that ensures ReactiveUI is initialized in its constructor, `WhenActivated` fails to resolve `IActivationForViewFetcher`. I don't know how to fix this in a way that can be upstreamed.
1 parent bd27fbc commit d9133b6

2 files changed

Lines changed: 42 additions & 14 deletions

File tree

‎src/ReactiveUI.Winforms/ActivationForViewFetcher.cs‎

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,10 @@ public IObservable GetActivationForView(IActivatableView view)
2626
// Startup: Control.HandleCreated > Control.BindingContextChanged > Form.Load > Control.VisibleChanged > Form.Activated > Form.Shown
2727
// Shutdown: Form.Closing > Form.FormClosing > Form.Closed > Form.FormClosed > Form.Deactivate
2828
// https://docs.microsoft.com/en-us/dotnet/framework/winforms/order-of-events-in-windows-forms
29+
//
30+
// Note: A control is not yet set sited in its constructor. We must delay the design mode check until after one of the events fires.
2931
if (view is Control control)
3032
{
31-
if (GetCachedIsDesignMode(control))
32-
{
33-
return Observable<bool>.Empty;
34-
}
35-
3633
var handleDestroyed = Observable.FromEvent<EventHandler, bool>(
3734
eventHandler => (_, _) => eventHandler(false),
3835
h => control.HandleDestroyed += h,
@@ -48,8 +45,7 @@ public IObservable GetActivationForView(IActivatableView view)
4845
h => control.VisibleChanged += h,
4946
h => control.VisibleChanged -= h);
5047

51-
var controlActivation = Observable.Merge(handleDestroyed, handleCreated, visibleChanged)
52-
.DistinctUntilChanged();
48+
IObservable<bool> controlActivation;
5349

5450
if (view is Form form)
5551
{
@@ -61,11 +57,15 @@ public IObservable GetActivationForView(IActivatableView view)
6157
},
6258
h => form.FormClosed += h,
6359
h => form.FormClosed -= h);
64-
controlActivation = controlActivation.Merge(formClosed)
65-
.DistinctUntilChanged();
60+
61+
controlActivation = Observable.Merge(handleDestroyed, handleCreated, visibleChanged, formClosed);
62+
}
63+
else
64+
{
65+
controlActivation = Observable.Merge(handleDestroyed, handleCreated, visibleChanged);
6666
}
6767

68-
return controlActivation;
68+
return controlActivation.DistinctUntilChanged().Where(_ => !GetCachedIsDesignMode(control));
6969
}
7070

7171
if (view is null)
@@ -86,12 +86,9 @@ public IObservable GetActivationForView(IActivatableView view)
8686
return Observable<bool>.Empty;
8787
}
8888

89-
private static bool GetIsDesignMode(Control control) =>
90-
LicenseManager.UsageMode == LicenseUsageMode.Designtime || control.Site?.DesignMode == true || control.Parent?.Site?.DesignMode == true;
91-
9289
private bool GetCachedIsDesignMode(Control control)
9390
{
94-
_isDesignModeCache ??= GetIsDesignMode(control);
91+
_isDesignModeCache ??= control.GetIsAncestorSiteInDesignMode();
9592

9693
return _isDesignModeCache.Value;
9794
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
namespace ReactiveUI.Winforms;
7+
8+
internal static class ControlPolyfills
9+
{
10+
public static bool GetIsAncestorSiteInDesignMode(this Control control)
11+
{
12+
#if NET6_0_OR_GREATER
13+
return control.IsAncestorSiteInDesignMode;
14+
#else
15+
ArgumentExceptionHelper.ThrowIfNull(control);
16+
17+
if (control.Site is { DesignMode: true })
18+
{
19+
return true;
20+
}
21+
else if (control.Parent is null)
22+
{
23+
return false;
24+
}
25+
else
26+
{
27+
return control.Parent.GetIsAncestorSiteInDesignMode();
28+
}
29+
#endif
30+
}
31+
}

0 commit comments

Comments
 (0)