Skip to content

Commit 4a4efcf

Browse files
fix(wpf): resolve XAML namespace under REACTIVE_SHIM in AutoDataTemplateBindingHook (#4404)
## Summary Fixes #4398 — `AutoDataTemplateBindingHook` throws a `XamlObjectReaderException` in the `ReactiveUI.Wpf.Reactive` (shim) build because its inline `DataTemplate` XAML references the wrong namespace. ## Root cause `src/ReactiveUI.Wpf.Shared/Common/AutoDataTemplateBindingHook.cs` is **shared source** compiled into two assemblies: | Build | Project | `REACTIVE_SHIM` | C# namespace | |---|---|---|---| | lean | `ReactiveUI.Wpf` | undefined | `ReactiveUI` | | shim | `ReactiveUI.Wpf.Reactive` | defined | `ReactiveUI.Reactive` | The file already conditionally sets the **C# namespace** at the top: ```csharp #if REACTIVE_SHIM namespace ReactiveUI.Reactive; #else namespace ReactiveUI; #endif ``` …but the inline `DataTemplate` XAML string hardcodes `clr-namespace:ReactiveUI`: ```csharp const string Template = " ..."; ``` In the shim build, `ViewModelViewHost` actually lives in `ReactiveUI.Reactive`, so `XamlReader.Parse` can't resolve `{clr-namespace:ReactiveUI;assembly=ReactiveUI.Wpf.Reactive}ViewModelViewHost` and throws — exactly the exception in the issue. ## The fix Make the XAML `clr-namespace` conditional on `REACTIVE_SHIM`, mirroring the existing namespace block: ```csharp #if REACTIVE_SHIM const string XamlClrNamespace = "clr-namespace:ReactiveUI.Reactive"; #else const string XamlClrNamespace = "clr-namespace:ReactiveUI"; #endif const string Template = " ..."; ``` The `const string` concatenation stays a compile-time constant, so the template remains a `const`. The lean build is byte-for-byte unchanged (`XamlClrNamespace` resolves to the same `clr-namespace:ReactiveUI`); the shim build now resolves `ViewModelViewHost` correctly. ## Test Added `DefaultItemTemplate_LoadsUnderReactiveShim` to `src/tests/ReactiveUI.Wpf.Tests/Wpf/AutoDataTemplateBindingHookTest.cs`, guarded by `#if REACTIVE_SHIM`. The test file is cross-included into the `ReactiveUI.Wpf.Tests.Reactive` project via the csproj wildcard ``, so the conditional test compiles **only** in the shim test project — i.e. only where the bug manifests. It materializes the lazy `DefaultItemTemplate.Value` (which threw `XamlObjectReaderException` before the fix) and asserts the parse completes without throwing: ```csharp #if REACTIVE_SHIM [Test] public async Task DefaultItemTemplate_LoadsUnderReactiveShim() { DataTemplate? template = null; await Assert.That(() => template = AutoDataTemplateBindingHook.DefaultItemTemplate.Value).ThrowsNothing(); await Assert.That(template).IsNotNull(); } #endif ``` This fails before the fix (the lazy `.Value` throws) and passes after. The existing `DefaultItemTemplate_IsNotNull` test continues to cover the lean build. ## Scope - `src/ReactiveUI.Wpf.Shared/Common/AutoDataTemplateBindingHook.cs` — conditional `clr-namespace` - `src/tests/ReactiveUI.Wpf.Tests/Wpf/AutoDataTemplateBindingHookTest.cs` — one guarded regression test No changes to `ExecuteHook`, binding logic, or any other platform. ## Out of scope (flagging for maintainers) `src/ReactiveUI.Maui/Common/AutoDataTemplateBindingHook.cs` has the same hardcoded `clr-namespace:ReactiveUI` pattern, but for the Maui host (different namespace handling, no `assembly=` suffix in its template). I deliberately left it untouched to keep this PR scoped to the reported WPF issue and because I haven't verified the Maui namespace resolution path. Worth a separate look — happy to follow up if you'd like. ## AI usage An AI assistant was used to help locate the relevant code and draft this change. I have reviewed and understand every line of the diff, and I am the sole author of this contribution. ## CLA This is a .NET Foundation project; if a CLA is required, please point me to the sign-in flow and I'll complete it before merge. Fixes #4398
1 parent e0eea47 commit 4a4efcf

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

‎src/ReactiveUI.Wpf.Shared/Common/AutoDataTemplateBindingHook.cs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,20 @@ public class AutoDataTemplateBindingHook : IPropertyBindingHook
2222
/// Gets the default item template.
2323
public static Lazy<DataTemplate> DefaultItemTemplate { get; } = new(static () =>
2424
{
25+
// The clr-namespace in the inline XAML template must match the namespace
26+
// this type is actually compiled into. Under REACTIVE_SHIM the shared
27+
// source is recompiled into the ReactiveUI.Reactive namespace (see the
28+
// conditional namespace above), so the XAML must reference that namespace
29+
// too — otherwise XamlReader.Parse throws a XamlObjectReaderException
30+
// because '{clr-namespace:ReactiveUI;assembly=ReactiveUI.Wpf.Reactive}'
31+
// cannot resolve ViewModelViewHost. See issue #4398.
32+
#if REACTIVE_SHIM
33+
const string XamlClrNamespace = "clr-namespace:ReactiveUI.Reactive";
34+
#else
35+
const string XamlClrNamespace = "clr-namespace:ReactiveUI";
36+
#endif
2537
const string Template = " +
26-
"xmlns:xaml='clr-namespace:ReactiveUI;assembly=__ASSEMBLYNAME__'> " +
38+
"xmlns:xaml='" + XamlClrNamespace + ";assembly=__ASSEMBLYNAME__'> " +
2739
"\"{Binding Mode=OneWay}\" VerticalContentAlignment=\"Stretch\" HorizontalContentAlignment=\"Stretch\" IsTabStop=\"False\" />" +
2840
"";
2941

‎src/tests/ReactiveUI.Wpf.Tests/Wpf/AutoDataTemplateBindingHookTest.cs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,29 @@ public class AutoDataTemplateBindingHookTest
2121
public async Task DefaultItemTemplate_IsNotNull() =>
2222
await Assert.That(AutoDataTemplateBindingHook.DefaultItemTemplate.Value).IsNotNull();
2323

24+
#if REACTIVE_SHIM
25+
///
26+
/// Under REACTIVE_SHIM the shared source is recompiled into the
27+
/// ReactiveUI.Reactive namespace, so the inline XAML template's
28+
/// clr-namespace must also be ReactiveUI.Reactive. If it is left hardcoded
29+
/// to ReactiveUI, XamlReader.Parse throws a XamlObjectReaderException
30+
/// because ViewModelViewHost cannot be resolved in the ReactiveUI.Wpf.Reactive
31+
/// assembly. This regression test forces the lazy template to materialize and
32+
/// asserts it loads without throwing. See issue #4398.
33+
///
34+
/// A representing the asynchronous operation.
35+
[Test]
36+
public async Task DefaultItemTemplate_LoadsUnderReactiveShim()
37+
{
38+
// Materializing the lazy value must not throw. Before the fix this threw
39+
// System.Xaml.XamlObjectReaderException: Cannot create unknown type
40+
// '{clr-namespace:ReactiveUI;assembly=ReactiveUI.Wpf.Reactive}ViewModelViewHost'.
41+
DataTemplate? template = null;
42+
await Assert.That(() => template = AutoDataTemplateBindingHook.DefaultItemTemplate.Value).ThrowsNothing();
43+
await Assert.That(template).IsNotNull();
44+
}
45+
#endif
46+
2447
/// A null view-property accessor throws.
2548
/// A representing the asynchronous operation.
2649
[Test]

0 commit comments

Comments
 (0)