Skip to content
Closed
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
26 changes: 25 additions & 1 deletion dev/Templates/Dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
25 changes: 22 additions & 3 deletions dev/Templates/Dotnet/Test-DotnetNewTemplates.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <name>.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

Expand All @@ -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
Expand Down Expand Up @@ -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 <name>.cs
# whose #: directives replace the .csproj, built with 'dotnet build <name>.cs'.
@{ ShortName = 'reactor-singlefile'; Kind = 'FileBasedApp' }
)

foreach ($template in $projectTemplates) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -161,7 +163,7 @@ ExpandedItems = output.ToArray();

<Target Name="_ExpandProjectTemplatePackItems"
BeforeTargets="_GetPackageFiles;GenerateNuspec">
<!-- Project templates: shared + dotnet-new-only. Each gets a copy of the shared .gitignore. -->
<!-- Project templates: shared + dotnet-new-only. Each gets a copy of the shared .gitignore unless it sets IncludeGitignore=false. -->
<ExpandProjectTemplatePackItems Templates="@(ProjectTemplate);@(DotnetNewOnlyProjectTemplate)"
GitignoreSource="$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)$(_GitignoreSource)'))">
<Output TaskParameter="ExpandedItems" ItemName="None" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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": ""
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/ide.host",
"icon": "icon.png",
"unsupportedHosts": [
{ "id": "vs" }
]
}
Original file line number Diff line number Diff line change
@@ -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 <name>.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 <name>.cs'" } ]
}
]
}
Original file line number Diff line number Diff line change
@@ -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<App>("$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);
}
}
28 changes: 27 additions & 1 deletion dev/Templates/templates.props
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
-->

<ItemDefinitionGroup>
<ProjectTemplate>
<RenameManifestFrom></RenameManifestFrom>
<IncludeGitignore>true</IncludeGitignore>
</ProjectTemplate>
<ItemTemplate>
<!-- No optional metadata today; listed for symmetry with the table above. -->
Expand All @@ -48,6 +53,7 @@
</VsixOnlyProjectTemplate>
<DotnetNewOnlyProjectTemplate>
<RenameManifestFrom></RenameManifestFrom>
<IncludeGitignore>true</IncludeGitignore>
</DotnetNewOnlyProjectTemplate>

<VsixOnlyItemTemplate>
Expand Down Expand Up @@ -151,6 +157,26 @@
<DotnetConfigDir>$(MSBuildThisFileDirectory)Dotnet\templates\reactor-tabview-app</DotnetConfigDir>
<RenameManifestFrom>Package-managed.appxmanifest</RenameManifestFrom>
</DotnetNewOnlyProjectTemplate>

<!--
The blank Reactor app as a single-file .NET 10 file-based app: the
whole template is ProjectTemplate.cs (renamed to <name>.cs), whose
#:package and #:property directives take the place of the .csproj. It
has no Package.appxmanifest (RenameManifestFrom unset): for 'dotnet run
<name>.cs', Microsoft.Windows.SDK.BuildTools.WinApp 0.7.0+ synthesizes
one from the directives and launches the app with package identity.
The file also references Microsoft.WindowsAppSDK: with
Microsoft.UI.Reactor 0.1.0-preview.16 alone, the Windows App SDK
defaults to self-contained and the build fails with
"WindowsAppSDKSelfContained requires a supported Windows architecture".
IncludeGitignore=false keeps the scaffolded output to that one file.
-->
<DotnetNewOnlyProjectTemplate Include="reactor.singlefile">
<BaseName>Reactor.Desktop.Cs.SingleFileApp</BaseName>
<SourceDir>$(MSBuildThisFileDirectory)Source\ProjectTemplates\Desktop\CSharp\ReactorSingleFileApp</SourceDir>
<DotnetConfigDir>$(MSBuildThisFileDirectory)Dotnet\templates\reactor-singlefile-app</DotnetConfigDir>
<IncludeGitignore>false</IncludeGitignore>
</DotnetNewOnlyProjectTemplate>
</ItemGroup>

<ItemGroup>
Expand Down
Loading