Skip to content

Commit e5dbcfe

Browse files
authored
fix: Winforms design mode detection regressed (#4369)
1 parent b50c47a commit e5dbcfe

5 files changed

Lines changed: 85 additions & 8 deletions

File tree

src/ReactiveUI.Winforms/ActivationForViewFetcher.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,16 @@ public IObservable<bool> GetActivationForView(IActivatableView view)
3030
{
3131
switch (view)
3232
{
33-
// Startup: Control.HandleCreated > Control.BindingContextChanged > Form.Load > Control.VisibleChanged > Form.Activated > Form.Shown
34-
// Shutdown: Form.Closing > Form.FormClosing > Form.Closed > Form.FormClosed > Form.Deactivate
35-
// https://docs.microsoft.com/en-us/dotnet/framework/winforms/order-of-events-in-windows-forms
36-
case Control control when GetCachedIsDesignMode(control):
37-
break;
3833
case Control control:
39-
return GetActivationForControl(control);
34+
// We are very likely being called from control's constructor, which means control.Site is not yet set.
35+
// We must delay the design mode check until after one of the activation events fires.
36+
return new WhereObservable<bool>(GetActivationForControl(control), _ => !GetCachedIsDesignMode(control));
4037

4138
case null:
4239
{
4340
this.Log().Warn(
44-
CultureInfo.InvariantCulture,
45-
"Expected a view of type System.Windows.Forms.Control it was null");
41+
CultureInfo.InvariantCulture,
42+
"Expected a view of type System.Windows.Forms.Control it was null");
4643
break;
4744
}
4845

@@ -65,6 +62,9 @@ public IObservable<bool> GetActivationForView(IActivatableView view)
6562
/// <returns>An observable that signals when the control is activated and deactivated.</returns>
6663
private static MergedDistinctObservable<bool> GetActivationForControl(Control control)
6764
{
65+
// Startup: Control.HandleCreated > Control.BindingContextChanged > Form.Load > Control.VisibleChanged > Form.Activated > Form.Shown
66+
// Shutdown: Form.Closing > Form.FormClosing > Form.Closed > Form.FormClosed > Form.Deactivate
67+
// https://docs.microsoft.com/en-us/dotnet/framework/winforms/order-of-events-in-windows-forms
6868
var handleDestroyed = new FromEventObservable<bool>(onNext =>
6969
{
7070
void Handler(object? sender, EventArgs e) => onNext(false);

src/ReactiveUI.Winforms/ReactiveUI.Winforms.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<Compile Include="..\Shared\Platform\EmptyObservable.cs" Link="Shared\Platform\EmptyObservable.cs"/>
2828
<Compile Include="..\Shared\Platform\ReturnObservable.cs" Link="Shared\Platform\ReturnObservable.cs"/>
2929
<Compile Include="..\Shared\Platform\StartWithObservable.cs" Link="Shared\Platform\StartWithObservable.cs"/>
30+
<Compile Include="..\Shared\Platform\WhereObservable.cs" Link="Shared\Platform\WhereObservable.cs"/>
3031
<Compile Include="..\Shared\Platform\CombineLatestSink.cs" Link="Shared\Platform\CombineLatestSink.cs"/>
3132
<Compile Include="..\ReactiveUI\Polyfills\**\*.cs" Link="Polyfills\%(RecursiveDir)%(Filename)%(Extension)"/>
3233
</ItemGroup>

src/tests/ReactiveUI.WinForms.Tests/winforms/ActivationTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,35 @@ public async Task SmokeTestUserControl()
149149
parent.Close();
150150
await Assert.That(userControlDeActivateCount).IsEqualTo(ExpectedSecondCount);
151151
}
152+
153+
/// <summary>
154+
/// Tests that view activation is skipped in design mode.
155+
/// </summary>
156+
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
157+
[Test]
158+
public async Task ActivationIsSkippedInDesignMode()
159+
{
160+
using var control = new DesignModeTestControl
161+
{
162+
Site = new DesignModeSite(),
163+
};
164+
165+
_ = control.Handle;
166+
167+
await Assert.That(control.Activated).IsFalse();
168+
}
169+
170+
/// <summary>
171+
/// Tests that view activation is not skipped outside of design mode.
172+
/// </summary>
173+
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
174+
[Test]
175+
public async Task ActivationIsNotSkippedNotInDesignMode()
176+
{
177+
using var control = new DesignModeTestControl();
178+
179+
_ = control.Handle;
180+
181+
await Assert.That(control.Activated).IsTrue();
182+
}
152183
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) 2009-2026 .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+
using System.ComponentModel;
7+
8+
namespace ReactiveUI.WinForms.Tests.Winforms.Mocks
9+
{
10+
internal sealed class DesignModeSite : ISite
11+
{
12+
public IComponent Component { get; } = new Component();
13+
14+
public IContainer? Container => null;
15+
16+
public bool DesignMode => true;
17+
18+
public string? Name { get; set; }
19+
20+
public object? GetService(Type serviceType) => null;
21+
}
22+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2009-2026 .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+
using System.Windows.Forms;
7+
8+
namespace ReactiveUI.WinForms.Tests.Winforms.Mocks
9+
{
10+
internal sealed class DesignModeTestControl : Control, IActivatableView
11+
{
12+
public DesignModeTestControl()
13+
{
14+
this.WhenActivated(() =>
15+
{
16+
Activated = true;
17+
return [];
18+
});
19+
}
20+
21+
public bool Activated { get; private set; }
22+
}
23+
}

0 commit comments

Comments
 (0)