diff --git a/LICENSE b/LICENSE index cab1c93..f15da85 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023-2025 ReactiveUI +Copyright (c) ReactiveUI 2023-2025 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6511b43..9997a4f 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ ReactiveUI Source Generators automatically generate ReactiveUI objects to stream - `[ObservableAsProperty(PropertyName = "ReadOnlyPropertyName")]` - `[ReactiveCommand]` - `[ReactiveCommand(CanExecute = nameof(IObservableBoolName))]` with CanExecute +- `[ReactiveCommand(OutputScheduler = "RxApp.MainThreadScheduler")]` using a ReactiveUI Scheduler +- `[ReactiveCommand(OutputScheduler = nameof(_isheduler))]` using a Scheduler defined in the class - `[ReactiveCommand][property: AttributeToAddToCommand]` with Attribute passthrough - `[IViewFor(nameof(ViewModelName))]` - `[RoutedControlHost("YourNameSpace.CustomControl")]` @@ -411,6 +413,30 @@ public partial class MyReactiveClass } ``` +### Usage ReactiveCommand with ReactiveUI OutputScheduler +```csharp +using ReactiveUI.SourceGenerators; + +public partial class MyReactiveClass +{ + [ReactiveCommand(OutputScheduler = "RxApp.MainThreadScheduler")] + private void Execute() { } +} +``` + +### Usage ReactiveCommand with custom OutputScheduler +```csharp +using ReactiveUI.SourceGenerators; + +public partial class MyReactiveClass +{ + private IScheduler _customScheduler = new TestScheduler(); + + [ReactiveCommand(OutputScheduler = nameof(_customScheduler))] + private void Execute() { } +} +``` + ## Usage IViewFor `[IViewFor(nameof(ViewModelName))]` ### IViewFor usage diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 92df71c..d2aeb2b 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute /// The name of the CanExecute Observable of bool. /// public string? CanExecute { get; init; } + + /// + /// Gets the output scheduler. + /// + /// + /// The output scheduler. + /// + public string? OutputScheduler { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 92df71c..d2aeb2b 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute /// The name of the CanExecute Observable of bool. /// public string? CanExecute { get; init; } + + /// + /// Gets the output scheduler. + /// + /// + /// The output scheduler. + /// + public string? OutputScheduler { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 92df71c..d2aeb2b 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute /// The name of the CanExecute Observable of bool. /// public string? CanExecute { get; init; } + + /// + /// Gets the output scheduler. + /// + /// + /// The output scheduler. + /// + public string? OutputScheduler { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNestedClasses#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNestedClasses#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 92df71c..d2aeb2b 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNestedClasses#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNestedClasses#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute /// The name of the CanExecute Observable of bool. /// public string? CanExecute { get; init; } + + /// + /// Gets the output scheduler. + /// + /// + /// The output scheduler. + /// + public string? OutputScheduler { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithOutputScheduler#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithOutputScheduler#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs new file mode 100644 index 0000000..d2aeb2b --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithOutputScheduler#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -0,0 +1,38 @@ +//HintName: ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.cs +// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; + +// +#pragma warning disable +#nullable enable +namespace ReactiveUI.SourceGenerators; + +/// +/// ReativeCommandAttribute. +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +internal sealed class ReactiveCommandAttribute : Attribute +{ + /// + /// Gets the can execute method or property. + /// + /// + /// The name of the CanExecute Observable of bool. + /// + public string? CanExecute { get; init; } + + /// + /// Gets the output scheduler. + /// + /// + /// The output scheduler. + /// + public string? OutputScheduler { get; init; } +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 92df71c..d2aeb2b 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute /// The name of the CanExecute Observable of bool. /// public string? CanExecute { get; init; } + + /// + /// Gets the output scheduler. + /// + /// + /// The output scheduler. + /// + public string? OutputScheduler { get; init; } } #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs index 4331301..9e77a8c 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveCMDGeneratorTests.cs @@ -134,6 +134,32 @@ private async Task Test3(string baseString) return TestHelper.TestPass(sourceCode); } + /// + /// Froms the reactive command with output scheduler. + /// + /// A task to monitor the async. + [Fact] + public Task FromReactiveCommandWithOutputScheduler() + { + // Arrange: Setup the source code that matches the generator input expectations. + const string sourceCode = """ + using System; + using ReactiveUI; + using ReactiveUI.SourceGenerators; + using System.Reactive.Linq; + using System.Threading.Tasks; + namespace TestNs; + public partial class TestVM : ReactiveObject + { + [ReactiveCommand(OutputScheduler = "RxApp.MainThreadScheduler")] + private int Test1() => 10; + } + """; + + // Act: Initialize the helper and run the generator. Assert: Verify the generated code. + return TestHelper.TestPass(sourceCode); + } + /// /// Froms the reactive command with nested classes. /// diff --git a/src/ReactiveUI.SourceGenerators.Execute/Program.cs b/src/ReactiveUI.SourceGenerators.Execute/Program.cs index ec0abc4..76c9db9 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/Program.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/Program.cs @@ -13,5 +13,5 @@ public static class Program /// /// Defines the entry point of the application. /// - public static void Main() => _ = TestViewModel.Instance; + public static void Main() => Application.Run(new TestViewWinForms()); } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 02aed1c..d893d97 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reactive; +using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -28,6 +29,8 @@ public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDis private readonly Subject _testSubject = new(); private readonly Subject _testNonNullSubject = new(); + private IScheduler _scheduler = RxApp.MainThreadScheduler; + [property: JsonInclude] [DataMember] [ObservableAsProperty] @@ -74,6 +77,8 @@ public TestViewModel() { Console.Out.WriteLine("Activated"); _test11PropertyHelper = this.WhenAnyValue(x => x.Test12Property).ToProperty(this, x => x.Test11Property, out _).DisposeWith(disposables); + GetDataCommand.Do(_ => Console.Out.WriteLine("GetDataCommand Executed")).Subscribe().DisposeWith(disposables); + GetDataCommand.Execute().Subscribe().DisposeWith(disposables); }); Console.Out.WriteLine("MyReadOnlyProperty before init"); @@ -362,6 +367,10 @@ protected virtual void Dispose(bool disposing) [ReactiveCommand] private async Task Test10Async(int size, CancellationToken ct) => await Task.FromResult(new Point(size, size)); - [ReactiveCommand(CanExecute = nameof(_observable))] + [ReactiveCommand(CanExecute = nameof(_observable), OutputScheduler = nameof(_scheduler))] private void TestPrivateCanExecute() => Console.Out.WriteLine("TestPrivateCanExecute"); + + [ReactiveCommand] + private Task GetData(CancellationToken ct) => + Task.FromResult(Array.Empty()); } diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs index 6abc7d7..d022952 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs @@ -21,6 +21,7 @@ public TestViewWinForms() { InitializeComponent(); ViewModel = TestViewModel.Instance; + ViewModel.Activator.Activate(); } } } diff --git a/src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs index bbce0d7..11d65c6 100644 --- a/src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs @@ -82,6 +82,12 @@ internal sealed class ReactiveObjectAttribute : Attribute; public const string ReactiveCommandAttributeType = "ReactiveUI.SourceGenerators.ReactiveCommandAttribute"; + /// + /// Gets the reactive command attribute. + /// + /// + /// The reactive command attribute. + /// public static string ReactiveCommandAttribute => $$""" // Copyright (c) {{DateTime.Now.Year}} .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. @@ -110,6 +116,14 @@ internal sealed class ReactiveCommandAttribute : Attribute /// The name of the CanExecute Observable of bool. /// public string? CanExecute { get; init; } + + /// + /// Gets the output scheduler. + /// + /// + /// The output scheduler. + /// + public string? OutputScheduler { get; init; } } #nullable restore #pragma warning restore diff --git a/src/ReactiveUI.SourceGenerators/Core/Extensions/ITypeSymbolExtensions.cs b/src/ReactiveUI.SourceGenerators/Core/Extensions/ITypeSymbolExtensions.cs index 7efdd1f..585320b 100644 --- a/src/ReactiveUI.SourceGenerators/Core/Extensions/ITypeSymbolExtensions.cs +++ b/src/ReactiveUI.SourceGenerators/Core/Extensions/ITypeSymbolExtensions.cs @@ -172,6 +172,23 @@ public static bool IsObservableReturnType(this ITypeSymbol? typeSymbol) return false; } + public static bool IsIShedulerType(this ITypeSymbol? typeSymbol) + { + var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; + do + { + var typeName = typeSymbol?.ToDisplayString(nameFormat); + if (typeName == "global::System.Reactive.Concurrency.IScheduler") + { + return true; + } + + typeSymbol = typeSymbol?.BaseType; + } + while (typeSymbol != null); + return false; + } + public static bool IsObservableBoolType(this ITypeSymbol? typeSymbol) { var nameFormat = SymbolDisplayFormat.FullyQualifiedFormat; diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandInfo.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandInfo.cs index 3f4a5e9..f8ffc46 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandInfo.cs +++ b/src/ReactiveUI.SourceGenerators/ReactiveCommand/Models/CommandInfo.cs @@ -18,6 +18,7 @@ internal record CommandInfo( bool IsObservable, string? CanExecuteObservableName, CanExecuteTypeInfo? CanExecuteTypeInfo, + string? OutputScheduler, EquatableArray ForwardedPropertyAttributes) { private const string UnitTypeName = "global::System.Reactive.Unit"; diff --git a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs index 592186e..21ec950 100644 --- a/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ReactiveCommand/ReactiveCommandGenerator.Execute.cs @@ -35,6 +35,9 @@ public partial class ReactiveCommandGenerator private const string CreateO = ".CreateFromObservable"; private const string CreateT = ".CreateFromTask"; private const string CanExecute = "CanExecute"; + private const string OutputScheduler = "OutputScheduler"; + private const string MainThreadScheduler = "RxApp.MainThreadScheduler"; + private const string TaskpoolScheduler = "RxApp.TaskpoolScheduler"; private static CommandInfo? GetMethodInfo(in GeneratorAttributeSyntaxContext context, CancellationToken token) { @@ -83,6 +86,10 @@ public partial class ReactiveCommandGenerator token.ThrowIfCancellationRequested(); + TryGetOutputScheduler(methodSymbol, attributeData, out var outputScheduler); + + token.ThrowIfCancellationRequested(); + var methodSyntax = (MethodDeclarationSyntax)context.TargetNode; methodSymbol.GatherForwardedAttributesFromMethod(context.SemanticModel, methodSyntax, token, out var attributes); var forwardedPropertyAttributes = attributes.Select(static a => a.ToString()).ToImmutableArray(); @@ -103,6 +110,7 @@ public partial class ReactiveCommandGenerator isObservable, canExecuteObservableName, canExecuteTypeInfo, + outputScheduler, forwardedPropertyAttributes); } @@ -186,43 +194,46 @@ private static string GetCommandSyntax(CommandInfo commandExtensionInfo) return $$""" - private ReactiveUI.ReactiveCommand<{{inputType}}, {{outputType}}>? {{fieldName}}; + private {{RxCmd}}<{{inputType}}, {{outputType}}>? {{fieldName}}; {{forwardedPropertyAttributesString}} - public ReactiveUI.ReactiveCommand<{{inputType}}, {{outputType}}> {{commandName}} { get => {{initializer}} } + public {{RxCmd}}<{{inputType}}, {{outputType}}> {{commandName}} { get => {{initializer}} } """; static string GenerateBasicCommand(CommandInfo commandExtensionInfo, string fieldName) { var commandType = commandExtensionInfo.IsObservable ? CreateO : commandExtensionInfo.IsTask ? CreateT : Create; + var outputScheduler = string.IsNullOrEmpty(commandExtensionInfo.OutputScheduler) ? string.Empty : $", outputScheduler: {commandExtensionInfo.OutputScheduler}"; if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - return $"{fieldName} ??= {RxCmd}{commandType}({commandExtensionInfo.MethodName});"; + return $"{fieldName} ??= {RxCmd}{commandType}({commandExtensionInfo.MethodName}{outputScheduler});"; } - return $"{fieldName} ??= {RxCmd}{commandType}({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"; + return $"{fieldName} ??= {RxCmd}{commandType}({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)}{outputScheduler});"; } static string GenerateInOutCommand(CommandInfo commandExtensionInfo, string fieldName, string outputType, string inputType) { var commandType = commandExtensionInfo.IsObservable ? CreateO : commandExtensionInfo.IsTask ? CreateT : Create; + var outputScheduler = string.IsNullOrEmpty(commandExtensionInfo.OutputScheduler) ? string.Empty : $", outputScheduler: {commandExtensionInfo.OutputScheduler}"; if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName});"; + return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}{outputScheduler});"; } - return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"; + return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}, {outputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)}{outputScheduler});"; } static string GenerateInCommand(CommandInfo commandExtensionInfo, string fieldName, string inputType) { var commandType = commandExtensionInfo.IsTask ? CreateT : Create; + var outputScheduler = string.IsNullOrEmpty(commandExtensionInfo.OutputScheduler) ? string.Empty : $", outputScheduler: {commandExtensionInfo.OutputScheduler}"; if (string.IsNullOrEmpty(commandExtensionInfo.CanExecuteObservableName)) { - return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName});"; + return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName}{outputScheduler});"; } - return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)});"; + return $"{fieldName} ??= {RxCmd}{commandType}<{inputType}>({commandExtensionInfo.MethodName}, {commandExtensionInfo.CanExecuteObservableName}{(commandExtensionInfo.CanExecuteTypeInfo == CanExecuteTypeInfo.MethodObservable ? "()" : string.Empty)}{outputScheduler});"; } } @@ -283,6 +294,93 @@ private static void TryGetCanExecuteExpressionType( return; } + private static void TryGetOutputScheduler( + IMethodSymbol methodSymbol, + AttributeData attributeData, + out string? outputScheduler) + { + if (!attributeData.TryGetNamedArgument(OutputScheduler, out string? scheduler)) + { + outputScheduler = null; + return; + } + + if (scheduler is null) + { + outputScheduler = null; + return; + } + + if (scheduler == MainThreadScheduler || scheduler == TaskpoolScheduler) + { + outputScheduler = scheduler; + return; + } + + var outputSchedulerSymbols = methodSymbol.ContainingType!.GetAllMembers(scheduler).ToImmutableArray(); + if (outputSchedulerSymbols.IsEmpty) + { + outputScheduler = null; + return; + } + + if (outputSchedulerSymbols.Length > 1) + { + outputScheduler = null; + return; + } + + if (TryGetOutputSchedulerFromSymbol(outputSchedulerSymbols[0], out outputScheduler)) + { + return; + } + + outputScheduler = null; + } + + private static bool TryGetOutputSchedulerFromSymbol( + ISymbol outputSchedulerSymbol, + [NotNullWhen(true)] out string? outputScheduler) + { + if (outputSchedulerSymbol is IFieldSymbol outputSchedulerFieldSymbol) + { + // The property type must always be a bool + if (!outputSchedulerFieldSymbol.Type.IsIShedulerType()) + { + goto Failure; + } + + outputScheduler = outputSchedulerFieldSymbol.Name; + return true; + } + else if (outputSchedulerSymbol is IPropertySymbol { GetMethod: not null } outputSchedulerPropertySymbol) + { + // The property type must always be a bool + if (!outputSchedulerPropertySymbol.Type.IsIShedulerType()) + { + goto Failure; + } + + outputScheduler = outputSchedulerPropertySymbol.Name; + return true; + } + else if (outputSchedulerSymbol is IMethodSymbol outputSchedulerMethodSymbol) + { + // The return type must always be a bool + if (!outputSchedulerMethodSymbol.ReturnType.IsIShedulerType()) + { + goto Failure; + } + + outputScheduler = outputSchedulerMethodSymbol.Name; + return true; + } + + Failure: + outputScheduler = null; + return false; + } + /// /// Gets the expression type for the can execute logic, if possible. ///