55using System . Collections . Immutable ;
66using System . Text ;
77
8- using ReactiveUI . Binding . SourceGenerators . Generators . CommandBinding ;
98using ReactiveUI . Binding . SourceGenerators . Models ;
9+ using ReactiveUI . Binding . SourceGenerators . Plugins ;
1010
1111namespace ReactiveUI . Binding . SourceGenerators . CodeGeneration ;
1212
@@ -332,27 +332,22 @@ internal static void GenerateBindCommandMethod(
332332 ObservationCodeGenerator . EmitInlineObservation (
333333 sb , "viewModel" , inv . CommandPropertyPath , inv . CommandTypeFullName , vmClassInfo , "commandObs" ) ;
334334
335- // Try plugins in affinity order (highest first)
336- if ( CommandPropertyBindingPlugin . CanHandle ( inv ) )
337- {
338- CommandPropertyBindingPlugin . EmitBinding ( sb , inv , controlAccess ) ;
339- }
340- else if ( EventEnabledBindingPlugin . CanHandle ( inv ) )
341- {
342- // Emit custom binder check before event+Enabled binding
343- EmitCustomBinderFallback ( sb , inv , controlAccess , hasEvent : true ) ;
344- EventEnabledBindingPlugin . EmitBinding ( sb , inv , controlAccess ) ;
345- }
346- else if ( DefaultEventBindingPlugin . CanHandle ( inv ) )
335+ // Try plugins in affinity order (highest first) via registry
336+ var plugin = CommandBindingPluginRegistry . GetBestPlugin ( inv ) ;
337+ var generatedAffinity = plugin is not null ? plugin . Affinity : - 1 ;
338+ var hasEvent = inv . ResolvedEventName is not null ;
339+
340+ // Emit affinity check: let user-registered ICreatesCommandBinding plugins override
341+ // if they have higher affinity than the source-generated binding
342+ EmitCommandAffinityCheck ( sb , inv , controlAccess , generatedAffinity , hasEvent ) ;
343+
344+ if ( plugin is not null )
347345 {
348- // Emit custom binder check before basic event binding
349- EmitCustomBinderFallback ( sb , inv , controlAccess , hasEvent : true ) ;
350- DefaultEventBindingPlugin . EmitBinding ( sb , inv , controlAccess ) ;
346+ plugin . EmitBinding ( sb , inv , controlAccess ) ;
351347 }
352348 else
353349 {
354- // No plugin matched — emit runtime fallback for custom binders, then throw
355- EmitCustomBinderFallback ( sb , inv , controlAccess , hasEvent : false ) ;
350+ // No plugin matched — throw after the affinity check fallback
356351 sb . AppendLine ( """
357352 throw new global::System.InvalidOperationException(
358353 "No bindable event found on the control. Specify the 'toEvent' parameter.");
@@ -364,57 +359,73 @@ internal static void GenerateBindCommandMethod(
364359 }
365360
366361 /// <summary>
367- /// Emits the custom binder check that tries registered <c>ICreatesCommandBinding</c> binders
368- /// before falling through to the generated event subscription code.
362+ /// Emits the command binding affinity check that allows user-registered
363+ /// <c>ICreatesCommandBinding</c> implementations to override the generated binding
364+ /// when they have higher affinity. If no user plugin has higher affinity, falls through
365+ /// to the generated event subscription code.
369366 /// </summary>
370367 /// <param name="sb">The string builder.</param>
371368 /// <param name="inv">The BindCommand invocation info.</param>
372369 /// <param name="controlAccess">The control access chain (e.g., "view.MyButton").</param>
370+ /// <param name="generatedAffinity">The affinity of the source-generated plugin, or -1 if none.</param>
373371 /// <param name="hasEvent">Whether a resolved event was found at compile time.</param>
374- internal static void EmitCustomBinderFallback (
372+ internal static void EmitCommandAffinityCheck (
375373 StringBuilder sb ,
376374 BindCommandInvocationInfo inv ,
377375 string controlAccess ,
376+ int generatedAffinity ,
378377 bool hasEvent )
379378 {
380379 // Build the parameter observable expression for the custom binder
381- string paramObsExpr ;
380+ var paramObsExpr = BuildParameterObservableExpression ( inv ) ;
381+
382+ sb . AppendLine ( $$ """
383+
384+ if (global::ReactiveUI.Binding.Fallback.CommandBindingAffinityChecker
385+ .HasHigherAffinityPlugin<{{ inv . ControlTypeFullName }} >({{ generatedAffinity }} , {{ ( hasEvent ? "true" : "false" ) }} ))
386+ {
387+ var __customBinder = global::ReactiveUI.Binding.CommandBinding.CommandBinderService
388+ .GetBinder<{{ inv . ControlTypeFullName }} >({{ ( hasEvent ? "true" : "false" ) }} );
389+ if (__customBinder != null)
390+ {
391+ var __serial = new global::ReactiveUI.Binding.Observables.SerialDisposable();
392+ var __binderCmdSub = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Subscribe(commandObs, __cmd =>
393+ {
394+ __serial.Disposable = global::ReactiveUI.Binding.Observables.EmptyDisposable.Instance;
395+ global::System.IObservable<object> __paramObs = {{ paramObsExpr }} ;
396+ __serial.Disposable = __customBinder.BindCommandToObject<{{ inv . ControlTypeFullName }} >(
397+ __cmd, {{ controlAccess }} , __paramObs)
398+ ?? global::ReactiveUI.Binding.Observables.EmptyDisposable.Instance;
399+ });
400+ return new global::ReactiveUI.Binding.Observables.CompositeDisposable2(__binderCmdSub, __serial);
401+ }
402+ }
403+
404+ """ ) ;
405+ }
406+
407+ /// <summary>
408+ /// Builds the parameter observable expression string for custom binder fallback code.
409+ /// </summary>
410+ /// <param name="inv">The BindCommand invocation info.</param>
411+ /// <returns>The parameter observable expression to embed in generated code.</returns>
412+ internal static string BuildParameterObservableExpression ( BindCommandInvocationInfo inv )
413+ {
382414 if ( inv . HasObservableParameter )
383415 {
384416 // Cast the typed observable to IObservable<object> via Select
385- paramObsExpr = "new global::ReactiveUI.Binding.Observables.SelectObservable<"
417+ return "new global::ReactiveUI.Binding.Observables.SelectObservable<"
386418 + inv . ParameterTypeFullName + ", object>(withParameter, __p => __p)" ;
387419 }
388- else if ( inv is { HasExpressionParameter : true , ParameterPropertyPath : not null } )
420+
421+ if ( inv is { HasExpressionParameter : true , ParameterPropertyPath : not null } )
389422 {
390423 // Read the parameter property at call time
391424 var paramAccess = CodeGeneratorHelpers . BuildPropertyAccessChain ( "viewModel" , inv . ParameterPropertyPath . Value ) ;
392- paramObsExpr = "new global::ReactiveUI.Binding.Observables.ReturnObservable<object>(" + paramAccess + ")" ;
393- }
394- else
395- {
396- paramObsExpr = "global::ReactiveUI.Binding.Observables.EmptyObservable<object>.Instance" ;
425+ return "new global::ReactiveUI.Binding.Observables.ReturnObservable<object>(" + paramAccess + ")" ;
397426 }
398427
399- sb . AppendLine ( $$ """
400-
401- var __customBinder = global::ReactiveUI.Binding.CommandBinding.CommandBinderService
402- .GetBinder<{{ inv . ControlTypeFullName }} >({{ ( hasEvent ? "true" : "false" ) }} );
403- if (__customBinder != null)
404- {
405- var __serial = new global::ReactiveUI.Binding.Observables.SerialDisposable();
406- var __binderCmdSub = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Subscribe(commandObs, __cmd =>
407- {
408- __serial.Disposable = global::ReactiveUI.Binding.Observables.EmptyDisposable.Instance;
409- global::System.IObservable<object> __paramObs = {{ paramObsExpr }} ;
410- __serial.Disposable = __customBinder.BindCommandToObject<{{ inv . ControlTypeFullName }} >(
411- __cmd, {{ controlAccess }} , __paramObs)
412- ?? global::ReactiveUI.Binding.Observables.EmptyDisposable.Instance;
413- });
414- return new global::ReactiveUI.Binding.Observables.CompositeDisposable2(__binderCmdSub, __serial);
415- }
416-
417- """ ) ;
428+ return "global::ReactiveUI.Binding.Observables.EmptyObservable<object>.Instance" ;
418429 }
419430
420431 /// <summary>
0 commit comments