From 1fa0fb42f353ac5e7f12cd990c3013903060a2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=BD=E7=AC=94?= <46271592+Skymly@users.noreply.github.com> Date: Tue, 26 May 2026 21:54:30 +0800 Subject: [PATCH] feat: support Task execute for DelegateCommand (0.5.0) Add async-lambda emission for Task, expand Roslyn5000 smoke tests, suppress NU5128 for bundled Core, and update docs. Fixes #56. --- .nuke/build.schema.json | 4 + AGENTS.zh-CN.md | 16 +++ CHANGELOG.md | 11 ++ .../Prism.SourceGenerators.Package.csproj | 4 +- .../Roslyn5000SmokeTests.cs | 110 ++++++++++++++++-- Prism.SourceGenerators.Tests/MatrixTests.cs | 25 ++++ .../DelegateCommandGenerator.cs | 48 ++++++-- .../DelegateCommandSyntax.cs | 17 ++- .../Models/CommandGenerationInfo.cs | 3 +- README.ja.md | 2 +- README.md | 2 +- README.zh-CN.md | 4 +- wiki/Architecture.md | 2 +- wiki/Commands.md | 2 +- 14 files changed, 219 insertions(+), 31 deletions(-) create mode 100644 AGENTS.zh-CN.md diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 9cd9748..fcd798d 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -104,6 +104,10 @@ "allOf": [ { "properties": { + "BclNuGetApiKey": { + "type": "string", + "description": "NuGet API key for MvvmAIO.Prism.Bcl.Commands (optional; falls back to --nuget-api-key)" + }, "Configuration": { "type": "string", "description": "Build configuration (Debug/Release)" diff --git a/AGENTS.zh-CN.md b/AGENTS.zh-CN.md new file mode 100644 index 0000000..2e06e01 --- /dev/null +++ b/AGENTS.zh-CN.md @@ -0,0 +1,16 @@ +# AGENTS.zh-CN.md + +面向中文贡献者与自动化代理的**摘要**。[**AGENTS.md**](AGENTS.md)(英文)为唯一 canonical 约束全文;若冲突以英文为准。 + +## 要点 + +1. **构建与 CI**:`dotnet run --project build/_build.csproj -- --target Ci --configuration Release`(或 `dotnet build` / `dotnet test`)。 +2. **解决方案**:使用根目录 **`.slnx`**,勿默认新建 `.sln`。 +3. **临时文件**:实验与一次性项目放在 **`.Temp/`**(已 gitignore,勿提交)。 +4. **GitHub 流程**:先 Issue → 分支 PR → squash merge 到 `master`;PR 正文链接 Issue(`Fixes #NN`)。 +5. **生成器变更**:同步测试(含 Verify 快照)、`CHANGELOG.md`、三语 README / wiki / [Docs 仓](https://github.com/MvvmAIO/Prism.SourceGenerators.Docs) 用户可见页。 +6. **NuGet**:主包 **MvvmAIO.Prism.SourceGenerators**;Prism 8 异步命令另需 **MvvmAIO.Prism.Bcl.Commands**(独立发布密钥 `NUGET_API_KEY_BCL`)。 +7. **Roslyn 变体**:`Roslyn4001` … `Roslyn5000` 多目标;Roslyn 5.0 冒烟见 `Prism.SourceGenerators.Tests.Roslyn5000`。 +8. **文档站点**:https://mvvmaio.github.io/Prism.SourceGenerators.Docs/ + +完整目录结构、诊断清单与发布步骤见 [**AGENTS.md**](AGENTS.md)。 diff --git a/CHANGELOG.md b/CHANGELOG.md index eea1537..50a72ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project are documented in this file. ## Unreleased +## [0.5.0] - 2026-05-26 + +### Added + +- **`[DelegateCommand]` / `[AsyncDelegateCommand]`** now support execute methods returning **`Task`** (result is awaited; not surfaced on the command). Closes [#56](https://github.com/MvvmAIO/Prism.SourceGenerators/issues/56). +- Additional **Roslyn 5.0** smoke tests for delegate commands. + +### Changed + +- Pack: suppress **NU5128** for bundled `MvvmAIO.Prism.Core` under `lib/netstandard2.0` (not a separate nuspec dependency). + ## [0.4.3] - 2026-05-25 ### Changed diff --git a/Prism.SourceGenerators.Package/Prism.SourceGenerators.Package.csproj b/Prism.SourceGenerators.Package/Prism.SourceGenerators.Package.csproj index a5cc286..8e90e13 100644 --- a/Prism.SourceGenerators.Package/Prism.SourceGenerators.Package.csproj +++ b/Prism.SourceGenerators.Package/Prism.SourceGenerators.Package.csproj @@ -10,7 +10,7 @@ true MvvmAIO.Prism.SourceGenerators - 0.4.3 + 0.5.0 MvvmAIO;Skymly;wys0610 Roslyn source generators for the Prism MVVM library. Generates ObservableProperty, DelegateCommand, AsyncDelegateCommand, and BindableBase implementations. This package contains Prism.SourceGenerators analyzers and MvvmAIO.Prism.Core (attribute definitions). For Prism.Core 8.1.97 async commands, install MvvmAIO.Prism.Bcl.Commands manually. prism;mvvm;source-generator;roslyn;wpf;observable-property;delegate-command @@ -21,6 +21,8 @@ true MIT README.md + + $(NoWarn);NU5128 diff --git a/Prism.SourceGenerators.Tests.Roslyn5000/Roslyn5000SmokeTests.cs b/Prism.SourceGenerators.Tests.Roslyn5000/Roslyn5000SmokeTests.cs index ba207da..1821fed 100644 --- a/Prism.SourceGenerators.Tests.Roslyn5000/Roslyn5000SmokeTests.cs +++ b/Prism.SourceGenerators.Tests.Roslyn5000/Roslyn5000SmokeTests.cs @@ -37,15 +37,100 @@ public partial class Vm : Prism.Mvvm.BindableBase Assert.Contains("public string Title", propertySource.Source); Assert.Contains("SetProperty", propertySource.Source); } + + [Fact] + public void DelegateCommand_Task_execute_on_Roslyn5000_emits_AsyncDelegateCommand() + { + const string userSource = """ + namespace Demo; + + public partial class Vm : Prism.Mvvm.BindableBase + { + [DelegateCommand] + private async System.Threading.Tasks.Task SaveAsync() + { + await System.Threading.Tasks.Task.CompletedTask; + } + } + """; + + GeneratorRunOutput output = Roslyn5000TestHarness.Run(userSource, includeCommands: true); + + Assert.Empty(output.Diagnostics.Where(static d => + d.Severity >= DiagnosticSeverity.Error && + d.Id.StartsWith("PSG", System.StringComparison.Ordinal))); + + GeneratedSource commandSource = Assert.Single( + output.GeneratedSources.Where(s => s.HintName.EndsWith(".SaveCommand.g.cs"))); + + Assert.Contains("AsyncDelegateCommand", commandSource.Source); + Assert.Contains("SaveAsync", commandSource.Source); + } + + [Fact] + public void DelegateCommand_TaskOfT_execute_on_Roslyn5000_emits_await_wrapper() + { + const string userSource = """ + namespace Demo; + + public partial class Vm : Prism.Mvvm.BindableBase + { + [DelegateCommand] + private async System.Threading.Tasks.Task CountAsync() + { + await System.Threading.Tasks.Task.CompletedTask; + return 0; + } + } + """; + + GeneratorRunOutput output = Roslyn5000TestHarness.Run(userSource, includeCommands: true); + + Assert.Empty(output.Diagnostics.Where(static d => + d.Severity >= DiagnosticSeverity.Error && + d.Id.StartsWith("PSG", System.StringComparison.Ordinal))); + + GeneratedSource commandSource = Assert.Single( + output.GeneratedSources.Where(s => s.HintName.EndsWith(".CountCommand.g.cs"))); + + Assert.Contains("async () => await CountAsync()", commandSource.Source); + } } internal static class Roslyn5000TestHarness { - internal static GeneratorRunOutput Run(string userSource) + internal static GeneratorRunOutput Run(string userSource, bool includeCommands = false) { + string commandStubs = includeCommands + ? """ + + namespace Prism.Commands + { + public class DelegateCommand + { + public DelegateCommand(System.Action execute) { } + public DelegateCommand(System.Func execute) { } + } + + public class AsyncDelegateCommand + { + public AsyncDelegateCommand(System.Func execute) { } + public AsyncDelegateCommand(System.Func execute, System.Func canExecute) { } + } + + public class AsyncDelegateCommand + { + public AsyncDelegateCommand(System.Func execute) { } + public AsyncDelegateCommand(System.Func execute, System.Func canExecute) { } + } + } + """ + : string.Empty; + string harness = """ #nullable enable using System; + using System.Threading.Tasks; using Prism.SourceGenerators; namespace Prism.Mvvm @@ -58,8 +143,7 @@ public abstract class BindableBase : System.ComponentModel.INotifyPropertyChange protected void RaisePropertyChanged(string? propertyName = null) { } } } - - """ + userSource; + """ + commandStubs + userSource; SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText( harness, @@ -71,12 +155,20 @@ protected void RaisePropertyChanged(string? propertyName = null) { } Roslyn5000MetadataReferences.Get(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - IIncrementalGenerator[] generators = - [ - new ObservablePropertyGenerator(), - new PropertyChangingGenerator(), - new BindableBaseGenerator(), - ]; + IIncrementalGenerator[] generators = includeCommands + ? + [ + new ObservablePropertyGenerator(), + new PropertyChangingGenerator(), + new BindableBaseGenerator(), + new DelegateCommandGenerator(), + ] + : + [ + new ObservablePropertyGenerator(), + new PropertyChangingGenerator(), + new BindableBaseGenerator(), + ]; GeneratorDriver driver = CSharpGeneratorDriver.Create( generators.Select(static g => g.AsSourceGenerator()), diff --git a/Prism.SourceGenerators.Tests/MatrixTests.cs b/Prism.SourceGenerators.Tests/MatrixTests.cs index 673f22d..8bd7b87 100644 --- a/Prism.SourceGenerators.Tests/MatrixTests.cs +++ b/Prism.SourceGenerators.Tests/MatrixTests.cs @@ -85,6 +85,31 @@ private async System.Threading.Tasks.ValueTask SaveAsync() Assert.Contains("() => SaveAsync().AsTask()", commandSource.Source); } + [Fact] + public void DelegateCommand_TaskOfT_execute_emits_await_wrapper() + { + const string source = """ + namespace Demo; + + public partial class Vm : Prism.Mvvm.BindableBase + { + [DelegateCommand] + private async System.Threading.Tasks.Task CountAsync() + { + await System.Threading.Tasks.Task.CompletedTask; + return 1; + } + } + """; + + GeneratorRunOutput output = GeneratorTestHarness.Run(source, languageVersion: LanguageVersion.Preview); + Assert.False(output.Diagnostics.Any(static d => d.Id is "PSG1001" or "PSG1002")); + + GeneratedSource commandSource = Assert.Single( + output.GeneratedSources.Where(s => s.HintName.EndsWith(".CountCommand.g.cs"))); + Assert.Contains("async () => await CountAsync()", commandSource.Source); + } + [Fact] public void AsyncDelegateCommand_ValueTaskOfT_execute_emits_parameterized_AsTask_wrapper() { diff --git a/Prism.SourceGenerators/DelegateCommandGenerator.cs b/Prism.SourceGenerators/DelegateCommandGenerator.cs index 083b9c9..4d319b0 100644 --- a/Prism.SourceGenerators/DelegateCommandGenerator.cs +++ b/Prism.SourceGenerators/DelegateCommandGenerator.cs @@ -15,7 +15,7 @@ namespace Prism.SourceGenerators; /// from methods annotated with [DelegateCommand] or [AsyncDelegateCommand]. /// /// For synchronous methods (void), generates DelegateCommand or DelegateCommand<T>. -/// For asynchronous methods (Task, ValueTask, or ValueTask<TResult>), generates AsyncDelegateCommand or AsyncDelegateCommand<T>. +/// For asynchronous methods (Task, Task<TResult>, ValueTask, or ValueTask<TResult>), generates AsyncDelegateCommand or AsyncDelegateCommand<T>. /// For Prism versions prior to 9.0, use NuGet MvvmAIO.Prism.SourceGenerators and install MvvmAIO.Prism.Bcl.Commands manually for Prism.Core 8.1.97 (see diagnostic PSG3002). /// /// @@ -168,6 +168,7 @@ private static Result ExtractDelegateCommandInfo( bool useFieldKeyword = SupportsFieldKeyword(context); bool wrapAsyncExecuteWithAsTask = isAsync && IsValueTaskReturnFamily(methodSymbol.ReturnType, compilation); + bool wrapAsyncExecuteWithAwaitLambda = isAsync && IsTaskOfTReturnFamily(methodSymbol.ReturnType, compilation); if (canExecute is not null && !HasMember(containingType, canExecute)) { @@ -216,7 +217,8 @@ private static Result ExtractDelegateCommandInfo( EnableParallelExecution: false, ObservesProperties: observesProperties, UseFieldKeyword: useFieldKeyword, - WrapAsyncExecuteWithAsTask: wrapAsyncExecuteWithAsTask), + WrapAsyncExecuteWithAsTask: wrapAsyncExecuteWithAsTask, + WrapAsyncExecuteWithAwaitLambda: wrapAsyncExecuteWithAwaitLambda), diagnostics.ToImmutable()); } @@ -239,6 +241,7 @@ private static ImmutableArray> ExtractAsyncDelegat HierarchyInfo hierarchy = HierarchyInfo.From(containingType); bool useFieldKeyword = SupportsFieldKeyword(context); bool wrapAsyncExecuteWithAsTask = IsValueTaskReturnFamily(methodSymbol.ReturnType, compilation); + bool wrapAsyncExecuteWithAwaitLambda = IsTaskOfTReturnFamily(methodSymbol.ReturnType, compilation); ImmutableArray>.Builder builder = ImmutableArray.CreateBuilder>(); @@ -354,7 +357,8 @@ private static ImmutableArray> ExtractAsyncDelegat enableParallelExecution, observesProperties, useFieldKeyword, - WrapAsyncExecuteWithAsTask: wrapAsyncExecuteWithAsTask), + WrapAsyncExecuteWithAsTask: wrapAsyncExecuteWithAsTask, + WrapAsyncExecuteWithAwaitLambda: wrapAsyncExecuteWithAwaitLambda), diagnostics.ToImmutable())); } @@ -451,8 +455,7 @@ private static bool IsValidDelegateCommandMethodSignature(IMethodSymbol methodSy if (!IsBindingSupportedAsyncReturnType(methodSymbol.ReturnType, compilation)) return false; - if (IsValueTaskReturnFamily(methodSymbol.ReturnType, compilation) - && methodSymbol.Parameters.Any(static p => IsCancellationToken(p.Type))) + if (UsesUnsupportedAsyncReturnWithCancellationToken(methodSymbol, compilation)) { return false; } @@ -471,8 +474,7 @@ private static bool IsValidAsyncDelegateCommandMethodSignature(IMethodSymbol met if (!IsBindingSupportedAsyncReturnType(methodSymbol.ReturnType, compilation)) return false; - if (IsValueTaskReturnFamily(methodSymbol.ReturnType, compilation) - && methodSymbol.Parameters.Any(static p => IsCancellationToken(p.Type))) + if (UsesUnsupportedAsyncReturnWithCancellationToken(methodSymbol, compilation)) { return false; } @@ -522,9 +524,7 @@ private static bool IsAsyncAwaitableReturnType(ITypeSymbol returnType, Compilati } /// - /// Async command binding supports non-generic Task, non-generic ValueTask, and ValueTask<TResult> - /// (Prism exposes matching Func<ValueTask> / Func<ValueTask<TResult>> overloads on AsyncDelegateCommand). - /// Task<TResult> is not supported for command execute methods (unchanged). + /// Async command binding supports non-generic Task, Task<TResult>, non-generic ValueTask, and ValueTask<TResult>. /// private static bool IsBindingSupportedAsyncReturnType(ITypeSymbol returnType, Compilation compilation) { @@ -539,6 +539,11 @@ private static bool IsBindingSupportedAsyncReturnType(ITypeSymbol returnType, Co return true; } + if (IsTaskOfTReturnFamily(returnType, compilation)) + { + return true; + } + INamedTypeSymbol? valueTask = compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask"); if (valueTask is not null && !named.IsGenericType && SymbolEqualityComparer.Default.Equals(returnType, valueTask)) { @@ -551,6 +556,29 @@ private static bool IsBindingSupportedAsyncReturnType(ITypeSymbol returnType, Co && SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, valueTaskOfT); } + private static bool IsTaskOfTReturnFamily(ITypeSymbol returnType, Compilation compilation) + { + if (returnType is not INamedTypeSymbol named || !named.IsGenericType) + { + return false; + } + + INamedTypeSymbol? taskOfT = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1"); + return taskOfT is not null + && SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, taskOfT); + } + + private static bool UsesUnsupportedAsyncReturnWithCancellationToken(IMethodSymbol methodSymbol, Compilation compilation) + { + if (!methodSymbol.Parameters.Any(static p => IsCancellationToken(p.Type))) + { + return false; + } + + return IsValueTaskReturnFamily(methodSymbol.ReturnType, compilation) + || IsTaskOfTReturnFamily(methodSymbol.ReturnType, compilation); + } + private static bool IsValueTaskReturnFamily(ITypeSymbol returnType, Compilation compilation) { if (returnType is not INamedTypeSymbol named) diff --git a/Prism.SourceGenerators/DelegateCommandSyntax.cs b/Prism.SourceGenerators/DelegateCommandSyntax.cs index b730aae..cda5390 100644 --- a/Prism.SourceGenerators/DelegateCommandSyntax.cs +++ b/Prism.SourceGenerators/DelegateCommandSyntax.cs @@ -144,13 +144,20 @@ info.CancellationTokenSourceFactory is not null || private static string GetAsyncCommandExecuteArgument(CommandGenerationInfo info) { - if (!info.WrapAsyncExecuteWithAsTask) + if (info.WrapAsyncExecuteWithAwaitLambda) { - return info.MethodName; + return info.ParameterType is null + ? $"async () => await {info.MethodName}()" + : $"async (__p) => await {info.MethodName}(__p)"; } - return info.ParameterType is null - ? $"() => {info.MethodName}().AsTask()" - : $"(__p) => {info.MethodName}(__p).AsTask()"; + if (info.WrapAsyncExecuteWithAsTask) + { + return info.ParameterType is null + ? $"() => {info.MethodName}().AsTask()" + : $"(__p) => {info.MethodName}(__p).AsTask()"; + } + + return info.MethodName; } } diff --git a/Prism.SourceGenerators/Models/CommandGenerationInfo.cs b/Prism.SourceGenerators/Models/CommandGenerationInfo.cs index ac1b398..731d847 100644 --- a/Prism.SourceGenerators/Models/CommandGenerationInfo.cs +++ b/Prism.SourceGenerators/Models/CommandGenerationInfo.cs @@ -21,4 +21,5 @@ internal sealed record CommandGenerationInfo( bool EnableParallelExecution, EquatableArray ObservesProperties, bool UseFieldKeyword, - bool WrapAsyncExecuteWithAsTask) : IEquatable; + bool WrapAsyncExecuteWithAsTask, + bool WrapAsyncExecuteWithAwaitLambda) : IEquatable; diff --git a/README.ja.md b/README.ja.md index 7d3679f..cac1cc4 100644 --- a/README.ja.md +++ b/README.ja.md @@ -185,7 +185,7 @@ public string Password { get { ... } set { ... } } メソッドから `DelegateCommand` または `AsyncDelegateCommand` プロパティを生成します。 - **同期メソッド**(`void`)は `DelegateCommand` / `DelegateCommand` を生成 -- **非同期メソッド**の戻り値が非ジェネリックの **`Task`**、**`ValueTask`**、または **`ValueTask`** のとき、`AsyncDelegateCommand` / `AsyncDelegateCommand` を生成します。`ValueTask` / `ValueTask` は生成コードで `.AsTask()` により Prism の `Func` / `Func` コンストラクタに接続します。**`Task`** を execute の戻り値にすることはできません(従来どおり)。**`CancellationToken`** を取る execute メソッドでは `ValueTask` / `ValueTask` はサポートされず(**PSG1001**)。 +- **非同期メソッド**の戻り値が **`Task`**、**`Task`**、**`ValueTask`**、または **`ValueTask`** のとき、`AsyncDelegateCommand` / `AsyncDelegateCommand` を生成します。`ValueTask` / `ValueTask` は生成コードで `.AsTask()` により Prism の `Func` / `Func` コンストラクタに接続します。**`Task`** は `async` lambda で execute を待機します。**`CancellationToken`** を取る execute メソッドでは `ValueTask`、`ValueTask`、**`Task`** はサポートされず(**PSG1001**)。 - Prism < 9.0 の場合、NuGet **`MvvmAIO.Prism.SourceGenerators`** を使用してください。**`MvvmAIO.Prism.Core`**(属性定義)を追加します。Prism.Core 8.1.97 の非同期コマンドを使う場合は **`MvvmAIO.Prism.Bcl.Commands`** を手動で追加してください。非同期コマンド使用時にこれらのアセンブリがない場合は **PSG3002** が報告されます。 - **C# 14+**:Command プロパティは `field` キーワードを使用(個別のバッキングフィールド不要) - **C# 13 以前**:Command プロパティは従来のバッキングフィールドを使用 diff --git a/README.md b/README.md index 702431f..f5fbeb0 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ For **partial property** targets, attributes inheriting from **`ValidationAttrib Generates `DelegateCommand` or `AsyncDelegateCommand` properties from methods. - **Synchronous methods** (`void`) generate `DelegateCommand` / `DelegateCommand` -- **Async methods** returning non-generic **`Task`**, **`ValueTask`**, or **`ValueTask`** generate `AsyncDelegateCommand` / `AsyncDelegateCommand`. `ValueTask` / `ValueTask` are wired via `.AsTask()` in the generated delegate so Prism’s `Func` / `Func` constructors are used. **`Task`** is not supported for execute methods (unchanged). Execute methods that take a **`CancellationToken`** cannot return `ValueTask` / `ValueTask` with the current emission shape (**PSG1001**). +- **Async methods** returning **`Task`**, **`Task`**, **`ValueTask`**, or **`ValueTask`** generate `AsyncDelegateCommand` / `AsyncDelegateCommand`. `ValueTask` / `ValueTask` are wired via `.AsTask()`; **`Task`** uses an `async` lambda that awaits the execute method so Prism’s `Func` / `Func` constructors are used. Execute methods that take a **`CancellationToken`** cannot return `ValueTask`, `ValueTask`, or **`Task`** with the current emission shape (**PSG1001**). - For Prism < 9.0, use NuGet **`MvvmAIO.Prism.SourceGenerators`**, which adds **`MvvmAIO.Prism.Core`** for source-generator attributes. For Prism.Core 8.1.97 async commands, install **`MvvmAIO.Prism.Bcl.Commands`** manually so `AsyncDelegateCommand` exists. If those assemblies are missing while async commands are used, **PSG3002** is reported. - **C# 14+**: Command properties use the `field` keyword (no separate backing field) - **C# 13 and earlier**: Command properties use a traditional backing field diff --git a/README.zh-CN.md b/README.zh-CN.md index 1096b05..587a775 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,6 +4,8 @@ 为 [Prism](https://github.com/PrismLibrary/Prism) MVVM 库提供的 Roslyn 源生成器。 +参与贡献见 [CONTRIBUTING.md](CONTRIBUTING.md)。自动化代理与 Cursor 的标准约束见 [AGENTS.md](AGENTS.md)([中文摘要](AGENTS.zh-CN.md))。 + ## CI 状态 [![.NET](https://github.com/MvvmAIO/Prism.SourceGenerators/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/MvvmAIO/Prism.SourceGenerators/actions/workflows/dotnet.yml) @@ -186,7 +188,7 @@ public string Password { get { ... } set { ... } } 从方法生成 `DelegateCommand` 或 `AsyncDelegateCommand` 属性。 - **同步方法**(`void`)生成 `DelegateCommand` / `DelegateCommand` -- **异步方法**若返回非泛型 **`Task`**、**`ValueTask`** 或 **`ValueTask`**,则生成 `AsyncDelegateCommand` / `AsyncDelegateCommand`。`ValueTask` / `ValueTask` 在生成代码中通过 `.AsTask()` 接到 Prism 的 `Func` / `Func` 构造函数。**`Task`** 仍不能作为 execute 返回类型(与此前一致)。若 execute 方法带有 **`CancellationToken`** 参数,则不能返回 `ValueTask` / `ValueTask`(**PSG1001**)。 +- **异步方法**若返回 **`Task`**、**`Task`**、**`ValueTask`** 或 **`ValueTask`**,则生成 `AsyncDelegateCommand` / `AsyncDelegateCommand`。`ValueTask` / `ValueTask` 在生成代码中通过 `.AsTask()` 接到 Prism 的 `Func` / `Func` 构造函数;**`Task`** 通过 `async` lambda 等待 execute 方法。若 execute 方法带有 **`CancellationToken`** 参数,则不能返回 `ValueTask`、`ValueTask` 或 **`Task`**(**PSG1001**)。 - 对于 Prism < 9.0,请使用 NuGet **`MvvmAIO.Prism.SourceGenerators`**:它会添加 **`MvvmAIO.Prism.Core`**,用于提供生成器特性定义。若使用 Prism.Core 8.1.97 的异步命令,请手动安装 **`MvvmAIO.Prism.Bcl.Commands`**,以便存在 `AsyncDelegateCommand`。若使用异步命令却缺少上述程序集,将报告 **PSG3002**。 - **C# 14+**:Command 属性使用 `field` 关键字(无需单独后备字段) - **C# 13 及更早版本**:Command 属性使用传统后备字段 diff --git a/wiki/Architecture.md b/wiki/Architecture.md index e86452c..f8e7b99 100644 --- a/wiki/Architecture.md +++ b/wiki/Architecture.md @@ -96,7 +96,7 @@ Avalonia 示例应用已迁至独立仓库 **[Prism.SourceGenerators.Samples](ht - **`PolyfillVersion`** — 全仓库 **Polyfill** 包版本(生成器与 **Core** 等一致)。 - **`PrismSourceGeneratorsTestsRoslynVersion`** — **单元测试 / 集成测试** 工程中 **`Microsoft.CodeAnalysis.*`** 的宿主版本,**必须与** 测试项目 **`ProjectReference`** 所指向的 **`Prism.SourceGenerators.Roslyn*`** 变体一致(当前默认 **4.12.0**,对应 **`Roslyn4120`**)。 -**`Prism.SourceGenerators.Tests.Roslyn5000`** 对 **Roslyn5000** 做冒烟测试(工程内覆盖 `PrismSourceGeneratorsTestsRoslynVersion` 为 **5.0.0**);完整快照与回归仍在 **`Prism.SourceGenerators.Tests`**(**4.12.0** / **Roslyn4120**)。 +**`Prism.SourceGenerators.Tests.Roslyn5000`** 对 **Roslyn5000** 做冒烟测试(工程内覆盖 `PrismSourceGeneratorsTestsRoslynVersion` 为 **5.0.0**),含 **`[ObservableProperty]`** 与 **`[DelegateCommand]`**(含 **`Task`** execute)路径;完整快照与回归仍在 **`Prism.SourceGenerators.Tests`**(**4.12.0** / **Roslyn4120**)。 ### 6.3 集成测试与 `Microsoft.Bcl.AsyncInterfaces` diff --git a/wiki/Commands.md b/wiki/Commands.md index 003c4e0..91e2834 100644 --- a/wiki/Commands.md +++ b/wiki/Commands.md @@ -12,7 +12,7 @@ | `async Task` / `Task`(无泛型结果) | `AsyncDelegateCommand` / `AsyncDelegateCommand` | | 返回 **`ValueTask`** / **`ValueTask`** | 同上;生成代码通过 **`.AsTask()`** 接到 Prism 的 **`Func`** / **`Func`** 构造函数 | -**不支持:** execute 返回 **`Task`**(带泛型结果的任务)作为该路径的异步 execute。 +**`Task`:** 生成代码通过 `async` lambda 等待 execute 方法(结果不暴露给命令);与 **`CancellationToken`** 参数组合时仍报 **PSG1001**(与 `ValueTask` 族相同)。 **限制(PSG1001):** execute 方法若带 **`CancellationToken`** 参数,则**不能**与 **`ValueTask` / `ValueTask`** 的返回类型组合(当前代码生成形状如此)。