Skip to content

Commit 223b928

Browse files
authored
Fix Blazor activation render ordering and update dependencies (#4318)
## What changed for end users Blazor components (ReactiveComponentBase, ReactiveInjectableComponentBase, ReactiveLayoutComponentBase, ReactiveOwningComponentBase) had a lifecycle ordering bug where property changes made during `WhenActivated` were not reflected in the UI. This happened because: 1. `WhenActivated` fires during `OnInitialized` -- your Rx chains execute and set reactive properties 2. Blazor renders the component 3. ViewModel change subscriptions are wired in `OnAfterRender` -- too late to catch changes from step 1 The fix adds a `StateHasChanged()` call after wiring subscriptions in `OnAfterRender` so the component re-renders with the latest state from activation. This results in one additional render cycle on first load but ensures the UI is always consistent. Fixes #4317 ## Dependency updates | Package | From | To | |---------|------|----| | TUnit | 1.17.54 | 1.24.18 | | bunit | 2.6.2 | 2.7.2 | | Microsoft.SourceLink.GitHub | 10.0.103 | 10.0.201 | | Microsoft.AspNetCore.Components (net8) | 8.0.24 | 8.0.25 | | Microsoft.AspNetCore.Components (net9) | 9.0.13 | 9.0.14 | | Microsoft.AspNetCore.Components (net10) | 10.0.3 | 10.0.5 | | Microsoft.Testing.Extensions.CodeCoverage | 18.4.1 | 18.5.2 | | Verify.TUnit | 31.13.2 | 31.14.0 | | Microsoft.Xaml.Behaviors.Wpf | 1.1.135/1.1.141 | 1.1.142 | | Microsoft.Maui.Controls (net10 only) | 10.0.41 | 10.0.51 | | Xamarin.AndroidX.Preference | 1.2.1.16 | 1.2.1.17 | | Xamarin.AndroidX.Lifecycle.LiveData | 2.10.0.1 | 2.10.0.2 | | Xamarin.AndroidX.Fragment | 1.8.9.1 | 1.8.9.2 | | Xamarin.AndroidX.Collection.Jvm | 1.5.0.4 | 1.5.0.5 | | Xamarin.AndroidX.SavedState | 1.4.0.1 | 1.4.0.2 | TUnit implicit usings were enabled to match TUnit 1.24 requirements. One test assertion was updated for TUnit 1.24 compatibility (nullable Func evaluation change). ## Test plan - [x] All 12,514 tests pass across net8.0, net9.0, net10.0 - [x] Full solution build succeeds with 0 warnings, 0 errors - [x] Blazor test render counts updated to reflect the additional activation render
1 parent 1645c9a commit 223b928

11 files changed

Lines changed: 45 additions & 29 deletions

src/Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242

4343
<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
4444
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
45-
<TUnitImplicitUsings>false</TUnitImplicitUsings>
46-
<TUnitAssertionsImplicitUsings>false</TUnitAssertionsImplicitUsings>
45+
<TUnitImplicitUsings>true</TUnitImplicitUsings>
46+
<TUnitAssertionsImplicitUsings>true</TUnitAssertionsImplicitUsings>
4747
<NoWarn>$(NoWarn);CS4014</NoWarn>
4848
</PropertyGroup>
4949

src/Directory.Packages.props

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,35 @@
55
</PropertyGroup>
66
<PropertyGroup>
77
<SplatVersion>19.3.1</SplatVersion>
8-
<TUnitVersion>1.17.54</TUnitVersion>
8+
<TUnitVersion>1.24.18</TUnitVersion>
99
<XamarinAndroidXCoreVersion>1.17.0</XamarinAndroidXCoreVersion>
10-
<XamarinAndroidXLifecycleLiveDataVersion>2.10.0.1</XamarinAndroidXLifecycleLiveDataVersion>
11-
<MauiVersion Condition="$(TargetFramework.StartsWith('net10'))">10.0.41</MauiVersion>
10+
<XamarinAndroidXLifecycleLiveDataVersion>2.10.0.2</XamarinAndroidXLifecycleLiveDataVersion>
11+
<MauiVersion Condition="$(TargetFramework.StartsWith('net10'))">10.0.51</MauiVersion>
1212
<MauiVersion Condition="$(TargetFramework.StartsWith('net9'))">9.0.120</MauiVersion>
13-
<XamlBehaviorsWpfVersion Condition="$(TargetFramework.StartsWith('net4'))">1.1.141</XamlBehaviorsWpfVersion>
14-
<XamlBehaviorsWpfVersion Condition="$(TargetFramework.StartsWith('net8'))">1.1.135</XamlBehaviorsWpfVersion>
15-
<XamlBehaviorsWpfVersion Condition="$(TargetFramework.StartsWith('net9'))">1.1.141</XamlBehaviorsWpfVersion>
16-
<XamlBehaviorsWpfVersion Condition="$(TargetFramework.StartsWith('net10'))">1.1.141</XamlBehaviorsWpfVersion>
17-
<AspNetVersion Condition="$(TargetFramework.StartsWith('net10'))">10.0.3</AspNetVersion>
18-
<AspNetVersion Condition="$(TargetFramework.StartsWith('net9'))">9.0.13</AspNetVersion>
19-
<AspNetVersion Condition="$(TargetFramework.StartsWith('net8'))">8.0.24</AspNetVersion>
13+
<XamlBehaviorsWpfVersion Condition="$(TargetFramework.StartsWith('net4'))">1.1.142</XamlBehaviorsWpfVersion>
14+
<XamlBehaviorsWpfVersion Condition="$(TargetFramework.StartsWith('net8'))">1.1.142</XamlBehaviorsWpfVersion>
15+
<XamlBehaviorsWpfVersion Condition="$(TargetFramework.StartsWith('net9'))">1.1.142</XamlBehaviorsWpfVersion>
16+
<XamlBehaviorsWpfVersion Condition="$(TargetFramework.StartsWith('net10'))">1.1.142</XamlBehaviorsWpfVersion>
17+
<AspNetVersion Condition="$(TargetFramework.StartsWith('net10'))">10.0.5</AspNetVersion>
18+
<AspNetVersion Condition="$(TargetFramework.StartsWith('net9'))">9.0.14</AspNetVersion>
19+
<AspNetVersion Condition="$(TargetFramework.StartsWith('net8'))">8.0.25</AspNetVersion>
2020
<AspNetVersion Condition="$(TargetFramework.StartsWith('netstandard'))">3.1.32</AspNetVersion>
2121
</PropertyGroup>
2222
<ItemGroup>
2323
<PackageVersion Include="Akavache.Sqlite3" Version="11.5.1" />
2424
<PackageVersion Include="Akavache.SystemTextJson" Version="11.5.1" />
25-
<PackageVersion Include="bunit" Version="2.6.2" />
25+
<PackageVersion Include="bunit" Version="2.7.2" />
2626
<PackageVersion Include="DynamicData" Version="9.4.31" />
2727
<PackageVersion Include="JetBrains.DotMemoryUnit" Version="3.2.20220510" />
2828
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
2929
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
3030
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="10.0.3" />
3131
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
32-
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.4.1" />
32+
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.5.2" />
3333
<PackageVersion Include="Microsoft.Testing.Platform.MSBuild" Version="2.1.0" />
3434
<PackageVersion Include="Microsoft.Reactive.Testing" Version="6.1.0" />
3535
<PackageVersion Include="System.Reactive" Version="6.1.0" />
36-
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.103" />
36+
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.201" />
3737
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7705" />
3838
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="$(XamlBehaviorsWpfVersion)" />
3939
<PackageVersion Include="Mocks.Maui" Version="1.2.5" />
@@ -54,7 +54,7 @@
5454
<PackageVersion Include="System.Text.Json" Version="10.0.3" />
5555
<PackageVersion Include="TUnit" Version="$(TUnitVersion)" />
5656
<PackageVersion Include="TUnit.Core" Version="$(TUnitVersion)" />
57-
<PackageVersion Include="Verify.TUnit" Version="31.13.2" />
57+
<PackageVersion Include="Verify.TUnit" Version="31.14.0" />
5858
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.3.0-prerelease.251115.2" />
5959
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="$(AspNetVersion)" />
6060
<PackageVersion Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
@@ -72,13 +72,13 @@
7272
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.LiveData" Version="$(XamarinAndroidXLifecycleLiveDataVersion)" />
7373
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.LiveData.Core.Ktx" Version="$(XamarinAndroidXLifecycleLiveDataVersion)" />
7474
<!-- Fragment/Collection/SavedState to match their latest constraints -->
75-
<PackageVersion Include="Xamarin.AndroidX.Fragment" Version="1.8.9.1" />
76-
<PackageVersion Include="Xamarin.AndroidX.Fragment.Ktx" Version="1.8.9.1" />
77-
<PackageVersion Include="Xamarin.AndroidX.Collection.Jvm" Version="1.5.0.4" />
78-
<PackageVersion Include="Xamarin.AndroidX.Collection.Ktx" Version="1.5.0.4" />
79-
<PackageVersion Include="Xamarin.AndroidX.SavedState" Version="1.4.0.1" />
80-
<PackageVersion Include="Xamarin.AndroidX.SavedState.SavedState.Ktx" Version="1.4.0.1" />
81-
<PackageVersion Include="Xamarin.AndroidX.Preference" Version="1.2.1.16" />
75+
<PackageVersion Include="Xamarin.AndroidX.Fragment" Version="1.8.9.2" />
76+
<PackageVersion Include="Xamarin.AndroidX.Fragment.Ktx" Version="1.8.9.2" />
77+
<PackageVersion Include="Xamarin.AndroidX.Collection.Jvm" Version="1.5.0.5" />
78+
<PackageVersion Include="Xamarin.AndroidX.Collection.Ktx" Version="1.5.0.5" />
79+
<PackageVersion Include="Xamarin.AndroidX.SavedState" Version="1.4.0.2" />
80+
<PackageVersion Include="Xamarin.AndroidX.SavedState.SavedState.Ktx" Version="1.4.0.2" />
81+
<PackageVersion Include="Xamarin.AndroidX.Preference" Version="1.2.1.17" />
8282
</ItemGroup>
8383
<ItemGroup Condition="'$(UseMaui)' != 'true'">
8484
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />

src/ReactiveUI.Blazor/ReactiveComponentBase.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ protected override void OnAfterRender(bool firstRender)
105105
h => PropertyChanged -= h,
106106
nameof(ViewModel),
107107
() => InvokeAsync(StateHasChanged));
108+
109+
// Re-render to pick up any property changes that occurred during activation (OnInitialized)
110+
// before these subscriptions were wired.
111+
InvokeAsync(StateHasChanged);
108112
}
109113

110114
base.OnAfterRender(firstRender);

src/ReactiveUI.Blazor/ReactiveInjectableComponentBase.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ protected override void OnAfterRender(bool firstRender)
108108
h => PropertyChanged -= h,
109109
nameof(ViewModel),
110110
() => InvokeAsync(StateHasChanged));
111+
112+
// Re-render to pick up any property changes that occurred during activation (OnInitialized)
113+
// before these subscriptions were wired.
114+
InvokeAsync(StateHasChanged);
111115
}
112116

113117
base.OnAfterRender(firstRender);

src/ReactiveUI.Blazor/ReactiveLayoutComponentBase.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ protected override void OnAfterRender(bool firstRender)
104104
h => PropertyChanged -= h,
105105
nameof(ViewModel),
106106
() => InvokeAsync(StateHasChanged));
107+
108+
// Re-render to pick up any property changes that occurred during activation (OnInitialized)
109+
// before these subscriptions were wired.
110+
InvokeAsync(StateHasChanged);
107111
}
108112

109113
base.OnAfterRender(firstRender);

src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ protected override void OnAfterRender(bool firstRender)
103103
h => PropertyChanged -= h,
104104
nameof(ViewModel),
105105
() => InvokeAsync(StateHasChanged));
106+
107+
// Re-render to pick up any property changes that occurred during activation (OnInitialized)
108+
// before these subscriptions were wired.
109+
InvokeAsync(StateHasChanged);
106110
}
107111

108112
base.OnAfterRender(firstRender);

src/tests/ReactiveUI.Blazor.Tests/ReactiveComponentBaseTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task ViewModel_Change_Triggers_StateHasChanged()
2626
var cut = Render<TestComponent>(parameters => parameters.Add(p => p.ViewModel, viewModel));
2727

2828
// Initial render should have occurred once.
29-
await Assert.That(cut.Instance.RenderCount).IsEqualTo(1);
29+
await Assert.That(cut.Instance.RenderCount).IsEqualTo(2);
3030

3131
// Change a property on the ViewModel to trigger a notification.
3232
viewModel.SomeProperty = "Changed";
@@ -48,7 +48,7 @@ public async Task ViewModel_Instance_Change_Triggers_StateHasChanged()
4848
var viewModel1 = new TestViewModel();
4949
var cut = Render<TestComponent>(parameters => parameters.Add(p => p.ViewModel, viewModel1));
5050

51-
await Assert.That(cut.Instance.RenderCount).IsEqualTo(1);
51+
await Assert.That(cut.Instance.RenderCount).IsEqualTo(2);
5252

5353
var viewModel2 = new TestViewModel();
5454
cut.Render(parameters => parameters.Add(p => p.ViewModel, viewModel2));

src/tests/ReactiveUI.Blazor.Tests/ReactiveInjectableComponentBaseTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task ViewModel_Injected_Works()
2929

3030
// Verify injection was successful.
3131
await Assert.That(cut.Instance.ViewModel).IsEqualTo(viewModel);
32-
await Assert.That(cut.Instance.RenderCount).IsEqualTo(1);
32+
await Assert.That(cut.Instance.RenderCount).IsEqualTo(2);
3333

3434
// Trigger a change to verify the component is listening.
3535
viewModel.SomeProperty = "Changed";

src/tests/ReactiveUI.Blazor.Tests/ReactiveLayoutComponentBaseTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public async Task ViewModel_Change_Triggers_StateHasChanged()
2121
var viewModel = new TestViewModel();
2222
var cut = Render<TestLayoutComponent>(parameters => parameters.Add(p => p.ViewModel, viewModel));
2323

24-
await Assert.That(cut.Instance.RenderCount).IsEqualTo(1);
24+
await Assert.That(cut.Instance.RenderCount).IsEqualTo(2);
2525

2626
viewModel.SomeProperty = "Changed";
2727

src/tests/ReactiveUI.Blazor.Tests/ReactiveOwningComponentBaseTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public async Task ViewModel_Change_Triggers_StateHasChanged()
2727

2828
var cut = Render<TestOwningComponent>(parameters => parameters.Add(p => p.ViewModel, viewModel));
2929

30-
await Assert.That(cut.Instance.RenderCount).IsEqualTo(1);
30+
await Assert.That(cut.Instance.RenderCount).IsEqualTo(2);
3131

3232
viewModel.SomeProperty = "Changed";
3333

0 commit comments

Comments
 (0)