Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")]`
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute
/// The name of the CanExecute Observable of bool.
/// </value>
public string? CanExecute { get; init; }

/// <summary>
/// Gets the output scheduler.
/// </summary>
/// <value>
/// The output scheduler.
/// </value>
public string? OutputScheduler { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute
/// The name of the CanExecute Observable of bool.
/// </value>
public string? CanExecute { get; init; }

/// <summary>
/// Gets the output scheduler.
/// </summary>
/// <value>
/// The output scheduler.
/// </value>
public string? OutputScheduler { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute
/// The name of the CanExecute Observable of bool.
/// </value>
public string? CanExecute { get; init; }

/// <summary>
/// Gets the output scheduler.
/// </summary>
/// <value>
/// The output scheduler.
/// </value>
public string? OutputScheduler { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute
/// The name of the CanExecute Observable of bool.
/// </value>
public string? CanExecute { get; init; }

/// <summary>
/// Gets the output scheduler.
/// </summary>
/// <value>
/// The output scheduler.
/// </value>
public string? OutputScheduler { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
@@ -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;

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace ReactiveUI.SourceGenerators;

/// <summary>
/// ReativeCommandAttribute.
/// </summary>
/// <seealso cref="Attribute" />
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class ReactiveCommandAttribute : Attribute
{
/// <summary>
/// Gets the can execute method or property.
/// </summary>
/// <value>
/// The name of the CanExecute Observable of bool.
/// </value>
public string? CanExecute { get; init; }

/// <summary>
/// Gets the output scheduler.
/// </summary>
/// <value>
/// The output scheduler.
/// </value>
public string? OutputScheduler { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ internal sealed class ReactiveCommandAttribute : Attribute
/// The name of the CanExecute Observable of bool.
/// </value>
public string? CanExecute { get; init; }

/// <summary>
/// Gets the output scheduler.
/// </summary>
/// <value>
/// The output scheduler.
/// </value>
public string? OutputScheduler { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,32 @@ private async Task Test3(string baseString)
return TestHelper.TestPass(sourceCode);
}

/// <summary>
/// Froms the reactive command with output scheduler.
/// </summary>
/// <returns>A task to monitor the async.</returns>
[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);
}

/// <summary>
/// Froms the reactive command with nested classes.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/ReactiveUI.SourceGenerators.Execute/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ public static class Program
/// <summary>
/// Defines the entry point of the application.
/// </summary>
public static void Main() => _ = TestViewModel.Instance;
public static void Main() => Application.Run(new TestViewWinForms());
}
11 changes: 10 additions & 1 deletion src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +29,8 @@ public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDis
private readonly Subject<double?> _testSubject = new();
private readonly Subject<double> _testNonNullSubject = new();

private IScheduler _scheduler = RxApp.MainThreadScheduler;

[property: JsonInclude]
[DataMember]
[ObservableAsProperty]
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -362,6 +367,10 @@ protected virtual void Dispose(bool disposing)
[ReactiveCommand]
private async Task<Point> 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<System.Collections.IEnumerable> GetData(CancellationToken ct) =>
Task.FromResult<System.Collections.IEnumerable>(Array.Empty<System.Collections.IEnumerable>());
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public TestViewWinForms()
{
InitializeComponent();
ViewModel = TestViewModel.Instance;
ViewModel.Activator.Activate();
}
}
}
14 changes: 14 additions & 0 deletions src/ReactiveUI.SourceGenerators/AttributeDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ internal sealed class ReactiveObjectAttribute : Attribute;

public const string ReactiveCommandAttributeType = "ReactiveUI.SourceGenerators.ReactiveCommandAttribute";

/// <summary>
/// Gets the reactive command attribute.
/// </summary>
/// <value>
/// The reactive command attribute.
/// </value>
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.
Expand Down Expand Up @@ -110,6 +116,14 @@ internal sealed class ReactiveCommandAttribute : Attribute
/// The name of the CanExecute Observable of bool.
/// </value>
public string? CanExecute { get; init; }

/// <summary>
/// Gets the output scheduler.
/// </summary>
/// <value>
/// The output scheduler.
/// </value>
public string? OutputScheduler { get; init; }
}
#nullable restore
#pragma warning restore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal record CommandInfo(
bool IsObservable,
string? CanExecuteObservableName,
CanExecuteTypeInfo? CanExecuteTypeInfo,
string? OutputScheduler,
EquatableArray<string> ForwardedPropertyAttributes)
{
private const string UnitTypeName = "global::System.Reactive.Unit";
Expand Down
Loading