diff --git a/dev/Templates/Dotnet/README.md b/dev/Templates/Dotnet/README.md index cfede35b79..0a1c4bba5a 100644 --- a/dev/Templates/Dotnet/README.md +++ b/dev/Templates/Dotnet/README.md @@ -56,6 +56,7 @@ Get the list via `dotnet new list reactor` | `reactor-mvu` | Reactor app using the Model-View-Update pattern with `UseReducer` | | `reactor-navview` | Reactor app with a `NavigationView` shell and multiple pages | | `reactor-tabview` | Reactor app with a `TabView` shell whose tabs live in the title bar | +| `reactor-singlefile` | Blank Reactor app as a single `.cs` [file-based app](https://learn.microsoft.com/dotnet/core/sdk/file-based-apps), no `.csproj` | Each also has a `winui-reactor*` alias (e.g. `winui-reactor`, `winui-reactor-mvu`) so they show up under `dotnet new list winui` alongside the other WinUI templates. @@ -66,7 +67,8 @@ preview NuGet package. They require .NET 10 or later. Like the WinUI templates, they reference `Microsoft.Windows.SDK.BuildTools.WinApp` so `dotnet run` launches the app with full MSIX package identity. Pin a specific Reactor package version at scaffold time with `--reactor-version`, or the Windows App SDK version with -`--wasdk-version`. +`--wasdk-version`. `reactor-singlefile` is the one exception to the project +layout: see [Quickstart a single-file Reactor app](#quickstart-a-single-file-reactor-app). ### Item Templates @@ -114,6 +116,25 @@ learn the component model, hooks, and layout system. Swap `reactor` for `reactor-mvu`, `reactor-navview`, or `reactor-tabview` to start from a richer shell. +### Quickstart a single-file Reactor app + +```powershell +dotnet new reactor-singlefile -n MyReactorApp +cd MyReactorApp +dotnet run MyReactorApp.cs +``` + +`reactor-singlefile` scaffolds a single `MyReactorApp.cs`: a .NET 10 +[file-based app](https://learn.microsoft.com/dotnet/core/sdk/file-based-apps) +whose `#:package` and `#:property` directives take the place of a `.csproj`. +It still runs packaged: `Microsoft.Windows.SDK.BuildTools.WinApp` (0.7.0 or +later) makes `dotnet run MyReactorApp.cs` synthesize an appxmanifest from those +directives, register the app, and launch it with package identity (Developer +Mode required). `winapp run MyReactorApp.cs` does the same. To run it +unpackaged instead, add `#:property WindowsPackageType=None` to the file. When +the app outgrows one file, `dotnet project convert MyReactorApp.cs` turns it +into a regular project. + ### Add items to an existing project ```powershell @@ -155,6 +176,9 @@ generated `.csproj` to tweak how `dotnet run` launches the app: These properties are provided by the [`Microsoft.Windows.SDK.BuildTools.WinApp`](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools.WinApp) NuGet package, which is referenced automatically by the project templates. +A single-file app (`reactor-singlefile`) has no `.csproj`: set them with +`#:property` directives in the `.cs` file instead (for example +`#:property WinAppRunNoLaunch=true`), or pass them to `dotnet run` with `-p:`. ## Prerequisites diff --git a/dev/Templates/Dotnet/Test-DotnetNewTemplates.ps1 b/dev/Templates/Dotnet/Test-DotnetNewTemplates.ps1 index ce692812dd..40036b6a8b 100644 --- a/dev/Templates/Dotnet/Test-DotnetNewTemplates.ps1 +++ b/dev/Templates/Dotnet/Test-DotnetNewTemplates.ps1 @@ -350,7 +350,7 @@ function Write-ResolvedPackageVersions { function Test-WinUiProjectTemplate { param( [string]$TemplateShortName, - [ValidateSet('App', 'Library', 'Test')] + [ValidateSet('App', 'FileBasedApp', 'Library', 'Test')] [string]$Kind, [string]$Platform, [string]$WorkingRoot @@ -361,7 +361,9 @@ function Test-WinUiProjectTemplate { New-ProjectFromTemplate -TemplateShortName $TemplateShortName -ProjectName $projectName -OutputPath $projectPath -WorkingDirectory $WorkingRoot -WindowsAppSdkVersion $script:windowsAppSdkVersion Add-Result -Template $TemplateShortName -Platform $Platform -Step 'create' -Status 'Succeeded' -Path $projectPath - $projectFile = Join-Path -Path $projectPath -ChildPath "$projectName.csproj" + # A file-based app template scaffolds a single .cs whose #: directives replace the .csproj. + $projectFileExtension = if ($Kind -eq 'FileBasedApp') { '.cs' } else { '.csproj' } + $projectFile = Join-Path -Path $projectPath -ChildPath "$projectName$projectFileExtension" Write-ResolvedPackageVersions -ProjectFile $projectFile -ProjectPath $projectPath @@ -382,6 +384,20 @@ function Test-WinUiProjectTemplate { Write-Warning "Unable to locate executable for template '$TemplateShortName' at '$projectPath'." } } + 'FileBasedApp' { + # The scaffold must stay a single file: no .csproj, .gitignore, or assets. + $scaffolded = @(Get-ChildItem -Path $projectPath -Recurse -Force -File) + if ($scaffolded.Count -ne 1 -or $scaffolded[0].Name -ne "$projectName.cs") { + $scaffoldedNames = ($scaffolded | ForEach-Object { $_.Name }) -join ', ' + throw "Template '$TemplateShortName' should scaffold only '$projectName.cs', but produced: $scaffoldedNames" + } + + # Builds the default (packaged) configuration. The output goes to the SDK's + # file-based app cache (%TEMP%\dotnet\runfile), not $projectPath, so there is + # no executable to record for launching. + Invoke-DotnetCommand -Arguments @('build', $projectFile, '-p:Configuration=Debug', "-p:Platform=$Platform") -WorkingDirectory $projectPath -Description 'build file-based app template' + Add-Result -Template $TemplateShortName -Platform $Platform -Step 'build' -Status 'Succeeded' -Path $projectFile + } 'Library' { Invoke-DotnetCommand -Arguments @('build', $projectFile, '-c', 'Debug') -WorkingDirectory $projectPath -Description 'build library template' Add-Result -Template $TemplateShortName -Platform 'AnyCPU' -Step 'build' -Status 'Succeeded' -Path $projectFile @@ -670,7 +686,10 @@ try { @{ ShortName = 'reactor'; Kind = 'App' }, @{ ShortName = 'reactor-mvu'; Kind = 'App' }, @{ ShortName = 'reactor-navview'; Kind = 'App' }, - @{ ShortName = 'reactor-tabview'; Kind = 'App' } + @{ ShortName = 'reactor-tabview'; Kind = 'App' }, + # Reactor single-file app: a .NET file-based app, i.e. a single .cs + # whose #: directives replace the .csproj, built with 'dotnet build .cs'. + @{ ShortName = 'reactor-singlefile'; Kind = 'FileBasedApp' } ) foreach ($template in $projectTemplates) { diff --git a/dev/Templates/Dotnet/WinAppSdk.CSharp.DotnetNewTemplates.csproj b/dev/Templates/Dotnet/WinAppSdk.CSharp.DotnetNewTemplates.csproj index be907de907..ab86fdc422 100644 --- a/dev/Templates/Dotnet/WinAppSdk.CSharp.DotnetNewTemplates.csproj +++ b/dev/Templates/Dotnet/WinAppSdk.CSharp.DotnetNewTemplates.csproj @@ -146,8 +146,10 @@ foreach (var t in Templates) // 3. dotnet-new template configuration. EmitTree(Path.Combine(configDir, ".template.config"), contentRoot + "/.template.config"); - // 4. Shared .gitignore (one copy per template). - if (!string.IsNullOrEmpty(GitignoreSource) && File.Exists(GitignoreSource)) + // 4. Shared .gitignore (one copy per template), unless the template opts out with + // IncludeGitignore=false (e.g. a file-based app whose output is a single .cs file). + bool includeGitignore = !string.Equals(t.GetMetadata("IncludeGitignore"), "false", StringComparison.OrdinalIgnoreCase); + if (includeGitignore && !string.IsNullOrEmpty(GitignoreSource) && File.Exists(GitignoreSource)) { Emit(GitignoreSource, contentRoot + "/gitignore.txt"); } @@ -161,7 +163,7 @@ ExpandedItems = output.ToArray(); - + diff --git a/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/dotnetcli.host.json b/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..d143d64a6e --- /dev/null +++ b/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/dotnetcli.host.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "dotnetVersion": { + "longName": "dotnet-version", + "shortName": "tfm", + "description": "Base TFM for the app (file-based apps and Microsoft.UI.Reactor require net10.0 or later)." + }, + "reactorVersion": { + "longName": "reactor-version", + "shortName": "rv", + "description": "Pin to a specific Microsoft.UI.Reactor NuGet version." + }, + "windowsAppSdkVersion": { + "longName": "wasdk-version", + "shortName": "wasdkv", + "description": "Pin to a specific Windows App SDK NuGet version." + }, + "skipRestore": { + "longName": "no-restore", + "shortName": "" + } + } +} diff --git a/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/icon.png b/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/icon.png new file mode 100644 index 0000000000..1e450cb9b3 Binary files /dev/null and b/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/icon.png differ diff --git a/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/ide.host.json b/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/ide.host.json new file mode 100644 index 0000000000..87030cf8eb --- /dev/null +++ b/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/ide.host.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/ide.host", + "icon": "icon.png", + "unsupportedHosts": [ + { "id": "vs" } + ] +} diff --git a/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/template.json b/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/template.json new file mode 100644 index 0000000000..7ca0a1a6ee --- /dev/null +++ b/dev/Templates/Dotnet/templates/reactor-singlefile-app/.template.config/template.json @@ -0,0 +1,90 @@ +{ + "$schema": "https://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ "Windows", "WinUI", "Desktop", "Reactor", "SingleFile", "Experimental" ], + "tags": { + "language": "C#", + "type": "project" + }, + "identity": "Microsoft.WindowsAppSDK.Reactor.CSharp.SingleFileApp", + "groupIdentity": "Microsoft.WindowsAppSDK.Reactor.CSharp.SingleFileApp", + "name": "Reactor Single-File App (Experimental)", + "shortName": [ "reactor-singlefile", "winui-reactor-singlefile" ], + "sourceName": "ProjectTemplate", + "defaultName": "ReactorApp", + "description": "Experimental. Creates a blank Microsoft.UI.Reactor (WinUI 3) desktop app as a single-file .NET 10 file-based app: one .cs file whose #:package and #:property directives replace the .csproj. Pure C#, no XAML. Run it with 'dotnet run .cs'. Microsoft.UI.Reactor is prerelease and its API may change.", + "preferNameDirectory": true, + "constraints": { + "windows-only": { + "type": "os", + "args": "Windows" + }, + "sdk-version": { + "type": "sdk-version", + "args": [ "[10.0,)" ] + } + }, + "symbols": { + "projectName": { + "type": "derived", + "valueSource": "name", + "valueTransform": "identity", + "replaces": "$projectname$" + }, + "dotnetVersion": { + "type": "parameter", + "datatype": "choice", + "description": "Base target framework moniker (TFM). File-based apps and Microsoft.UI.Reactor require .NET 10 or later.", + "choices": [ + { "choice": "net10.0", "description": "Target .NET 10" } + ], + "defaultValue": "net10.0", + "replaces": "$DotNetVersion$" + }, + "reactorVersion": { + "type": "parameter", + "datatype": "text", + "defaultValue": "0.1.0-preview.16", + "replaces": "$ReactorVersion$", + "description": "Microsoft.UI.Reactor NuGet version to reference." + }, + "windowsAppSdkVersion": { + "type": "parameter", + "datatype": "text", + "defaultValue": "*", + "replaces": "$WindowsAppSdkVersion$", + "description": "Microsoft.WindowsAppSDK NuGet version. Requires 2.2.0 or later; use * for the latest stable." + }, + "windowsSdkBuildToolsWinAppVersion": { + "type": "parameter", + "datatype": "text", + "defaultValue": "*", + "replaces": "$WindowsSdkBuildToolsWinAppVersion$", + "description": "Microsoft.Windows.SDK.BuildTools.WinApp NuGet version. Requires 0.7.0 or later (file-based app support); use * for the latest stable." + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "If specified, skips the automatic restore of the app on create." + } + }, + "primaryOutputs": [ + { "path": "ProjectTemplate.cs" } + ], + "forms": { + "identity": { + "identifier": "identity" + } + }, + "postActions": [ + { + "id": "restore", + "condition": "(!skipRestore)", + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true, + "description": "Restore NuGet packages required by this app.", + "manualInstructions": [ { "text": "Run 'dotnet restore .cs'" } ] + } + ] +} diff --git a/dev/Templates/Source/ProjectTemplates/Desktop/CSharp/ReactorSingleFileApp/ProjectTemplate.cs b/dev/Templates/Source/ProjectTemplates/Desktop/CSharp/ReactorSingleFileApp/ProjectTemplate.cs new file mode 100644 index 0000000000..e3d6c48cb1 --- /dev/null +++ b/dev/Templates/Source/ProjectTemplates/Desktop/CSharp/ReactorSingleFileApp/ProjectTemplate.cs @@ -0,0 +1,37 @@ +#:package Microsoft.UI.Reactor@$ReactorVersion$ +#:package Microsoft.WindowsAppSDK@$WindowsAppSdkVersion$ +#:package Microsoft.Windows.SDK.BuildTools.WinApp@$WindowsSdkBuildToolsWinAppVersion$ +#:property OutputType=WinExe +#:property TargetFramework=$DotNetVersion$-windows10.0.26100.0 +#:property UseWinUI=true + +// A .NET file-based app: the #: directives above take the place of a .csproj. +// `dotnet run $projectname$.cs` launches it with package identity (needs Developer Mode). +// To run it unpackaged instead, add `#:property WindowsPackageType=None` above. + +using Microsoft.UI.Reactor; +using Microsoft.UI.Reactor.Core; +using Microsoft.UI.Reactor.Layout; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using static Microsoft.UI.Reactor.Factories; + +// To learn more about Reactor, the Reactor project structure, and more about +// our project templates, see: https://github.com/microsoft/microsoft-ui-reactor +ReactorApp.Run("$projectname$"); + +class App : Component +{ + public override Element Render() + { + var titleBar = TitleBar("$projectname$") + .Flex(shrink: 0); + + // This is the main content area of your application. + // Add your UI elements here. + var content = Border(Empty()).Flex(grow: 1, basis: 0); + + return FlexColumn(titleBar, content) + .Backdrop(BackdropKind.Mica); + } +} diff --git a/dev/Templates/templates.props b/dev/Templates/templates.props index 48d8a10a94..7e9396fde8 100644 --- a/dev/Templates/templates.props +++ b/dev/Templates/templates.props @@ -16,7 +16,7 @@ ItemTemplate yes yes single-file 'Add New Item' templates (Page, UserControl, ...) VsixOnlyProjectTemplate no yes e.g. .wapproj-based packaging not usable from the CLI - DotnetNewOnlyProjectTemplate yes no (framework slot; no instances today) + DotnetNewOnlyProjectTemplate yes no e.g. the experimental Reactor (Microsoft.UI.Reactor) templates VsixOnlyItemTemplate no yes (framework slot; no instances today) DotnetNewOnlyItemTemplate yes no (framework slot; no instances today) @@ -33,11 +33,16 @@ RenameManifestFrom Optional. Source-side manifest filename (typically Package-managed.appxmanifest) to repack as the reserved Package.appxmanifest. Leave unset for templates that have no manifest. + IncludeGitignore Optional, dotnet-new project templates only. Defaults to true: the pack + adds the shared templates\gitignore.txt, which template.json renames to + .gitignore. Set to false for templates whose output must stay a single + file, such as a .NET file-based app. --> + true @@ -48,6 +53,7 @@ + true @@ -151,6 +157,26 @@ $(MSBuildThisFileDirectory)Dotnet\templates\reactor-tabview-app Package-managed.appxmanifest + + + + Reactor.Desktop.Cs.SingleFileApp + $(MSBuildThisFileDirectory)Source\ProjectTemplates\Desktop\CSharp\ReactorSingleFileApp + $(MSBuildThisFileDirectory)Dotnet\templates\reactor-singlefile-app + false +