| name | uno-csharpmarkup2 |
| description | Build or edit Uno Platform 6 UI in declarative C# with C# Markup 2 (CSharpForMarkup), instead of XAML. Use for Uno 6 apps on .NET 10/9 with MVVM or MVUX, Uno Extensions Navigation/Toolkit, and supported C# Markup 2 integrations such as LiveCharts2, ScottPlot, and Mapsui. |
| metadata | {"author":"https://github.com/VincentH-Net","version":"1.4.2","framework":"uno-platform","category":"ui-markup","sources":["https://github.com/VincentH-Net/Modern.CSharp.Templates (mcs-uno-markup2, mcs-uno-view)","https://github.com/VincentH-Net/CSharpForMarkup"]} |
C# Markup 2 (CSharpForMarkup) for Uno Platform 6
Use when you want to build a Uno Platform 6 UI in pure C# using the concise, declarative, strongly-typed C# Markup 2 (CSharpForMarkup) library — no XAML, no CSS, no HTML, no JS / TS. Provides a Flutter-like fluent-builder developer experience with allocation-free, reflection-free implementation and compile-time-checked bindings.
This skill is about CSharpForMarkup / C# Markup 2 by VincentH-Net, distributed as the CSharpMarkup.WinUI.* NuGet package family. It is not the same technology as the separately-named "C# Markup" shipped by the Uno Platform team under Uno.Extensions.Markup. The APIs, project structure, and conventions differ — only use the sources listed in the References section of this skill.
1. When to use this skill
Select this skill for any of:
- Starting a new Uno 6 app and choosing C# over XAML for the UI layer
- Adding a C# Markup 2 Presentation layer to an existing Uno 6 app (partial conversion is supported — existing XAML pages can coexist with new C# Markup 2 pages)
- Authoring new or existing UI in an existing C# Markup 2 Presentation project
- Integrating charts (LiveCharts2 2.0), plots (ScottPlot 5.1), or maps (Mapsui 5.0) into a C# Markup 2 UI
2. STOP — prerequisite gate before reading further
Before you read the rest of this skill, run this prerequisite check in the current working folder and act on the result.
Step 0: Verify prerequisites
- A Uno Platform 6 app project must exist in the working folder. Find the Uno app
.csproj in the working folder. A valid Uno app project should use the Uno SDK and contain App.xaml.cs in or under the project folder.
- The Uno app project references a C# Markup 2 Presentation project generated by
mcs-uno-markup2. Inspect the Uno platform app .csproj for a ProjectReference to a Presentation project, then verify that referenced project is template-generated by checking for CSharpMarkup.WinUI package references and the generated New-View.ps1 helper in the Presentation project folder.
- PowerShell 7+ (required for
New-View.ps1 and the post-action scripts — pass --allow-scripts yes)
If any of these are not true, STOP and point the user to the "Agentic Engineering with C# Markup 2 for Uno Platform" article on X for appropriate setup steps.
Step 1: Ensure that the C# Markup 2 Presentation project is wired up to the Uno Platform app
- Read
App.xaml.cs, verify whether all instructions in the Readme.md file within the C# Markup 2 Presentation project folder have been followed, and if not, STOP and follow those instructions before proceeding.
Step 2: Are you adding a new page?
- Yes → Your next action is
pwsh ./New-View.ps1 <Name> from the Presentation project folder (see §5). Do not hand-create <Name>Page.cs / <Name>Page.logic.cs / <Name>Model.cs. Do not call dotnet new mcs-uno-view directly — the script computes the namespace, output path, and template args correctly.
- No (editing pages that already exist) → Continue to §3 for authoring patterns.
If Modern.CSharp.Templates is not installed, install it (dotnet new install Modern.CSharp.Templates) instead of working around it by hand-scaffolding.
The rest of this skill is reference material for authoring inside an already-generated project. If you find yourself about to create a .csproj, a BasePage, or a <Name>Page.cs file from scratch, stop and re-read this section.
3. Core C# Markup 2 patterns to know
The template sets up the Presentation layer; these are the patterns you apply inside it when authoring UI.
3.1 Declarative fluent builder
Each markup file defines UI via a chain of extension-method property setters on layouts and views. Property values are strongly typed (enums, not strings). Automatic type conversion lets you write .Margin(12) instead of new Thickness(12).
3.2 Bind without strings (CallerArgumentExpression)
.Bind(...) accepts a C# expression and the compiler captures the path text via CallerArgumentExpression. No nameof(), no string literals, full rename / refactor support:
.Bind (vm.SelectedTweet)
.Bind ((vm.SelectedTweet).Title)
.Bind ((vm.SelectedTweet).Author.Name)
Performance is equivalent to string paths; there is no runtime reflection.
ONLY if the C# expression form truly doesn't fit (e.g. binding to a property name you only know as a string), use the explicit string-escape overloads:
.BindWithString("Title")
.BindCommandWithString("DeleteCommand")
In practice you almost never need the *WithString overloads. For DataTemplate / ControlTemplate / other scopes where the source type isn't naturally in scope, prefer the typed null-proxy pattern in §3.7 — it keeps the expression-based API and full rename support.
3.3 Children composition, conditional children, Spread
Layouts take a children list. Two patterns make composition concise:
- Conditional children:
null values in a children list are ignored. Use a ternary cond ? view : null to include/exclude a child.
Spread(...): inserts a variable-length child list at a specific position in a parent's children. Similar to Flutter's spread operator.
3.4 Attached property syntax
Attached properties are prefixed with the defining type plus underscore:
Grid_Row(0)
Grid_Column(2)
Grid_RowSpan(2)
Multiple attached-property setters can be chained on the same element.
3.5 Partial class markup/logic split
Split each page across two partial class files:
| File | Content | Allowed usings |
|---|
<Name>Page.cs | Markup only — Build() method returning the UI tree | CSharpMarkup.* namespaces only — no Microsoft.UI.Xaml.* / Windows.UI.Xaml.* usings |
<Name>Page.logic.cs | Event handlers, code-behind logic, ViewModel wiring | Actual Uno / WinUI UI types — no CSharpMarkup.* usings |
The strict usings separation prevents IntelliSense pollution and keeps markup readable. Repo review rule: any Microsoft.UI.Xaml using in a <Page>.cs file is wrong; move that code to <Page>.logic.cs.
3.6 Assign() and Invoke() — bridging markup and logic
.Assign(out var control) — capture a reference to a created control from within the markup chain, for later use in the .logic.cs file.
.Invoke(control => { ... }) — run imperative setup on a created control inline in the markup chain.
Both are the supported mechanism for hooking into controls that need imperative setup (event handlers, focus, etc.) without breaking the markup file structure.
3.7 Typed null proxies for bind-without-strings
.Bind(...) extracts the binding path from the caller's C# expression (the substring after the last .). This means the source object referenced in the expression does not need to exist at runtime — only its type matters, so the compiler can resolve the property name. A typed null proxy is the standard way to get that type in scope whenever the natural source isn't already reachable.
Common cases:
DataTemplate — items have a DataContext of type T, but no T variable is in scope in the markup. A T? item = null proxy lets you write .Bind(item?.Prop).
ControlTemplate / Style.Setters — templated-parent or styled-element properties need a typed reference to resolve the path.
- Any binding where the source type isn't reachable via
this.vm / a local — e.g. binding to a sibling element's property when no .Assign(out var x) reference exists yet.
Rule: declare the null proxy as a field in <Page>.logic.cs, never inline in the .cs markup file.
.ItemTemplate(DataTemplate(() =>
{
TodoItem? item = null;
return Grid(
CheckBox().Bind(item?.IsDone, mode: BindingMode.TwoWay),
TextBlock().Bind(item?.Text)
);
}))
public sealed partial class TodoPage : BasePage<TodoViewModel>, IBuildUI
{
static readonly TodoItem? item = null;
public TodoPage() => BuildUI();
}
.ItemTemplate(DataTemplate(() =>
Grid(
CheckBox().Bind(item?.IsDone, mode: BindingMode.TwoWay),
TextBlock().Bind(item?.Text)
)
))
Why the logic-partial: proxies are state declarations, not markup. Keeping them in .logic.cs preserves the declarative, expression-bodied style of the markup file, avoids block-bodied lambdas, and matches the partial-class split in §3.5 — the logic partial already owns non-markup fields.
Conventions for the proxy:
static readonly — never allocated, never mutated; only its compile-time type is used.
= null — always. The ?. in the binding expression safely short-circuits at capture time.
- Lowercase name — reads naturally in the markup (
item?.Text, not Item?.Text).
- One proxy per source type — if a page has both a
TodoItem list and a TagItem list, declare both fields in .logic.cs.
4. Package ecosystem
The template wires up a subset of these depending on --presentation and --renderer; add the rest as features are needed:
| Package | Purpose |
|---|
CSharpMarkup.WinUI | Core — WinUI 3 / Uno WinUI C# Markup 2 API |
CSharpMarkup.WinUI.Uno.Toolkit | Markup for Uno.Toolkit.UI controls (Card, Chip, NavigationBar, …) |
CSharpMarkup.WinUI.Uno.Extensions.Reactive | Markup + bindings for MVUX feeds/states |
CSharpMarkup.WinUI.Uno.Extensions.Navigation | Markup for uen:Region.* / uen:Navigation.* attached properties |
CSharpMarkup.WinUI.Uno.Extensions.Navigation.Toolkit | Markup for NavigationBar / TabBar / Drawer navigation |
CSharpMarkup.WinUI.LiveChartsCore.SkiaSharpView | LiveCharts2 charts in C# Markup |
CSharpMarkup.WinUI.ScottPlot | ScottPlot plots in C# Markup |
CSharpMarkup.WinUI.Mapsui | Mapsui maps in C# Markup |
5. After setup — adding pages
The generated Presentation project includes a New-View.ps1 helper script at its root. This is the supported path for adding new pages — do not invoke dotnet new mcs-uno-view directly.
Running the script
From the Presentation project folder:
pwsh ./New-View.ps1 Home # Use the Presentation project's default presentation pattern (mvvm / mvux / none)
pwsh ./New-View.ps1 Home mvvm # MVVM page
pwsh ./New-View.ps1 Home mvux # MVUX page
pwsh ./New-View.ps1 Home none # No model; logic lives in HomePage.logic.cs
pwsh ./New-View.ps1 Features.Home.Details # Creates Features/Home/DetailsPage in sub-namespace Features.Home
On Windows PowerShell, .\New-View.ps1 Home is also valid.
Parameters
| Parameter | Purpose | Default |
|---|
Name (positional, required) | View identifier. "Page" suffix is added automatically. Dots split the name into sub-folders and sub-namespaces — Features.Home.Details produces Features/Home/DetailsPage.cs under namespace <PresentationRoot>.Features.Home. | — |
Presentation (positional, optional) | Model pattern for the page. | mvvm (none / mvvm / mvux) |
What the script produces
<Name>Page.cs — markup file
<Name>Page.logic.cs — code-behind partial class
<Name>Model.cs — viewmodel (only when Presentation is mvvm or mvux)
Under the hood the script calls dotnet new mcs-uno-view with the correct --name, --namespace (prefixed with the Presentation project's root namespace), --presentation, and --output path. That wiring is non-trivial — always go through the script.
Windows native (WinAppSDK) rebuild note
After adding a new page, the Windows native / WinAppSDK target may need a rebuild of the main Uno project first to regenerate XamlTypeInfo for the new view. Other targets (Desktop / Skia / Wasm) pick the new page up via C# Hot Reload without a rebuild.
6. Validation
After editing or adding C# Markup 2 UI:
-
build and run the Uno app using the repository's normal Uno app build and run instructions and inspect the tool result and the app log file for build or runtime errors.
-
If you added a page, verify the generated files are under the expected Presentation project namespace and folder, and that the Uno app can resolve the new view.
7. Conventions to enforce
- Never hand-scaffold the Presentation project,
BasePage / BaseViewModel, Directory.Packages.props entries, or page files. Use dotnet new mcs-uno-markup2 and pwsh ./New-View.ps1 — see §2 and §5.
- Never
using Microsoft.UI.Xaml (or any UI object-model namespace) inside a <Page>.cs markup file.
- Never put logic that directly uses the UI object-model in a
<Page>.cs markup file — move it to <Page>.logic.cs.
- Typed null-proxy fields (
static readonly T? item = null;) used to drive .Bind(item?.Prop) in DataTemplate / ControlTemplate / any scope without a natural source live in <Page>.logic.cs, never inline in the .cs markup. See §3.7.
- Use
.Assign(out ...) and .Invoke(...) instead of creating named fields for controls in the markup.
- Compose with
null children for conditional UI and Spread(...) for dynamic-length lists — avoid building the tree imperatively in the code-behind.
References