Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

BindInteraction does not follow ViewModel when the view inherits it from another assembly (e.g. ReactiveUserControl<T>)

已关闭
#170 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

已经有一个关联 PR 被合并了。

  • #180 来自 @ChrisPulman —— 已合并

评估

难度
4/5
预计耗时
3-5 天
新手友好度
32/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
csharp

调研方向

Start from BindingEmitterHelpers.ResolveViewModelObservation and TypeDetectionExtractor.ExtractProperties / CollectMembersThroughOwnBases, which stop walking bases at the assembly boundary so an inherited ViewModel is missed. Compare with FindViewModelProperty, which already walks bases. Reproduce with the IViewFor plus referenced-assembly base case in the issue; done when BindInteraction (and likely Bind/OneWayBind/BindCommand) follows view.ViewModel across that boundary as in ReactiveUI 24.

由索引模型根据 Issue 内容生成。

描述

bug
Prerequisites
  • I have searched this repository's open and closed issues for duplicates.
  • I am using the latest stable release.
What happens

On a view that derives from Avalonia's ReactiveUserControl<T>, BindInteraction registers the handler on the view model the view has at the moment of the call, and never moves it:

  • If the view has no view model yet (normal inside WhenActivated, before the host sets the DataContext), no handler is ever registered. Every Handle call later throws UnhandledInteractionException.
  • If the view model is replaced later (a view whose DataContext follows a selection, for example), the handler stays on the first view model. The new one has no handler.

The same call on a view that declares its own ViewModel property follows view.ViewModel correctly. The difference is only where the ViewModel property is declared: if the view inherits it from a base class in another assembly (as every ReactiveUserControl<T> subclass does), the binding stops following the view.

How we ran into it. Our app is a PDF viewer built on Avalonia, with one tab per open document. The main window hosts a single document view, a ReactiveUserControl<PdfFileViewModel>, whose DataContext is bound to the selected tab, so switching tabs swaps the view model under a view that stays loaded. In WhenActivated that view binds 17 interactions with BindInteraction: zoom in/out, page up/down, scroll to page, fit to width, the save dialog and so on. A search panel works the same way for "focus the search box" on Ctrl+F.

The view activates once, when the window loads. At that point usually no document is open yet, so ViewModel is null. On ReactiveUI 24 this worked: each handler moved to whichever document was selected. After upgrading to ReactiveUI 25 none of the documents had handlers, so the zoom buttons, page navigation, bookmarks and Ctrl+F focus all failed with UnhandledInteractionException. When the app restored a document at startup, only that one document worked, and its interactions acted on whichever document was on screen. Nothing warned at build time, and our tests stayed green because each test selects its document before the window finishes loading.

What you expected instead

The handler follows view.ViewModel, as it does in ReactiveUI 24 and as it already does here for a view that declares ViewModel itself:

  • bound while ViewModel is null → handled once a view model is assigned
  • ViewModel replaced → the new view model's interaction is handled, the old one's is not
Minimal reproduction
using Avalonia;
using Avalonia.Headless;
using ReactiveUI.Avalonia;
using ReactiveUI.Binding;

AppBuilder.Configure<Application>()
    .UseHeadless(new AvaloniaHeadlessPlatformOptions())
    .UseReactiveUI(static _ => { })
    .SetupWithoutStarting();

var view = new View();
using var binding = view.BindInteraction(view.ViewModel, vm => vm.Ask, Answer);   // ViewModel is still null here

var first = new ViewModel();
view.ViewModel = first;
Console.WriteLine(await IsHandled(first));    // False - expected True

var second = new ViewModel();
view.ViewModel = second;
Console.WriteLine(await IsHandled(second));   // False - expected True

static Task Answer(IInteractionContext<string, bool> context)
{
    context.SetOutput(true);
    return Task.CompletedTask;
}

static async Task<bool> IsHandled(ViewModel vm)
{
    try {
        await vm.Ask.Handle("question");
        return true;
    } catch (UnhandledInteractionException<string, bool>) {
        return false;
    }
}

public sealed class ViewModel
{
    public Interaction<string, bool> Ask { get; } = new();
}

public sealed class View : ReactiveUserControl<ViewModel>
{
}

Packages: Avalonia 12.1.3, Avalonia.Headless 12.1.3, ReactiveUI.Avalonia 12.1.6.

Without Avalonia: the same failure reproduces with a plain IViewFor<T> + INotifyPropertyChanged view, as long as ViewModel is inherited from a base class in a referenced assembly. Three views, identical except where ViewModel is declared:

View ViewModel declared Result
on the view itself view follows the view model ✔
inherited from a base in the same assembly same assembly follows the view model ✔
inherited from an identical base in a referenced class library other assembly does not follow ✘
Steps to reproduce
  1. Create a console app with the packages above and the code above.
  2. Run it.
  3. Both lines print False; expected True twice.
Exception and stack trace
When the interaction is raised from app code:


ReactiveUI.Binding.UnhandledInteractionException`2[System.String,System.Boolean]: Failed to find a registration for an Interaction.
   at ReactiveUI.Binding.Interaction`2.Handle(TInput input)
Package and version

ReactiveUI.Binding 8.8.0 and 9.1.0 (via ReactiveUI 25.1.1 / 26.0.1 and ReactiveUI.Avalonia 12.1.5 / 12.1.6)

UI framework

Avalonia

Operating system

Windows

Target framework

net10.0

Device and OS version

Windows 11 Pro 10.0.26200, x64

IDE

Visual Studio 2026 18.0

Did this work in an earlier version?

ReactiveUI 24.2.0

Workaround

Observe the interaction from the view instead of calling BindInteraction, e.g. this.WhenAnyValue(x => x.ViewModel) → select the interaction → register the handler on each new value, disposing the previous registration.

Additional information

Where it seems to come from, in case it saves time (same in v8.8.0 and v9.1.0):

  • BindingEmitterHelpers.ResolveViewModelObservation roots the binding at the view only when FindViewModelPropertyType finds a typed ViewModel in the view's detected properties. Otherwise it binds to the viewModel argument.
  • Those properties come from TypeDetectionExtractor.ExtractProperties → CollectMembersThroughOwnBases, which stops walking base classes at the assembly boundary, so an inherited ViewModel (e.g. ReactiveUserControl<T>.ViewModel) is never seen.
  • FindViewModelProperty in the same file does walk the bases, and is what the #158 field-owner path uses.

Since ResolveViewModelObservation is shared by Bind, OneWayBind and BindCommand, those view-first bindings are presumably affected on such views too. Only BindInteraction was tested.

主要语言
C#
星标
9
派生
4
平均合并
7 小时 7 分钟
30 天内合并 PR
75

环境准备

这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

reactiveui/ReactiveUI.Binding.SourceGenerators 的其他 Issue

查看 reactiveui/ReactiveUI.Binding.SourceGenerators 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。