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
126 changes: 126 additions & 0 deletions docs/_pipeline/apps/extending-reactor-controls/App.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Reactor.Core.V1Protocol;
using Microsoft.UI.Reactor.Core.V1Protocol.Descriptor;
using static Microsoft.UI.Reactor.Factories;
using Microsoft.UI.Xaml;
using WinUI = Microsoft.UI.Xaml.Controls;

System.AppContext.SetSwitch("Reactor.UseV1Protocol", true);
ReactorApp.Run<ExtendingApp>(
"Extending Reactor", width: 540, height: 360, devtools: true,
configure: host => StarMeterInterop.Register(host.Reconciler));

// ════════════════════════════════════════════════════════════════════════
// Step 1 — Define the Element record
// ════════════════════════════════════════════════════════════════════════

// <snippet:star-meter-element>
// An Element subclass with one controlled prop (Value), three one-way
// props (MaxRating, Caption, IsClearEnabled), and one callback (OnValueChanged).
// Records give the reconciler value-equality for free — two StarMeterElement
// instances with identical fields compare equal and Update becomes a no-op.
public sealed record StarMeterElement : Element
{
public double Value { get; init; }
public int MaxRating { get; init; } = 5;
public string? Caption { get; init; }
public bool IsClearEnabled { get; init; } = true;
public System.Action<double>? OnValueChanged { get; init; }
}
// </snippet:star-meter-element>

// ════════════════════════════════════════════════════════════════════════
// Step 2 — Wire the descriptor
// ════════════════════════════════════════════════════════════════════════

// <snippet:star-meter-descriptor>
public static class StarMeterDescriptor
{
public static readonly ControlDescriptor<StarMeterElement, WinUI.RatingControl> Descriptor =
new ControlDescriptor<StarMeterElement, WinUI.RatingControl>
{
// Leaf control — no children. (See ChildrenStrategy survey for
// the other shapes: SingleContent, Panel, NamedSlots, ItemsHost…)
Children = new None<StarMeterElement, WinUI.RatingControl>(),
}
// OneWay props: written on Mount, diff-and-written on Update.
.OneWay(
get: static e => e.MaxRating,
set: static (c, v) => c.MaxRating = v)
.OneWay(
get: static e => e.IsClearEnabled,
set: static (c, v) => c.IsClearEnabled = v)
// OneWayConditional skips the write when the predicate is false —
// leaves Caption at the control's default for elements that didn't
// supply one, rather than forcing it to null and losing a style.
.OneWayConditional(
get: static e => e.Caption,
set: static (c, v) => c.Caption = v!,
shouldWrite: static e => e.Caption is not null)
// Controlled is the two-way binding shape: the framework writes the
// element's value at Mount (and on diff), suppresses the echo when
// the framework is the writer, and forwards user input back through
// OnValueChanged. Subscription is gated on the callback being non-
// null — if the caller didn't pass OnValueChanged, no trampoline
// is wired and the per-fire dispatch cost stays at zero.
.Controlled<double, object>(
get: static e => e.Value,
set: static (c, v) => c.Value = v,
subscribe: static (fe, h) => ((WinUI.RatingControl)fe).ValueChanged += (s, e) => h(s, e!),
unsubscribe: static (fe, h) => { /* trampoline anchored for control lifetime */ },
callback: static e => e.OnValueChanged,
readBack: static c => c.Value);
}
// </snippet:star-meter-descriptor>

// ════════════════════════════════════════════════════════════════════════
// Step 3 — Register
// ════════════════════════════════════════════════════════════════════════

// <snippet:star-meter-registration>
public static class StarMeterInterop
{
// One call per Reactor host. RegisterHandler accepts any IElementHandler,
// and DescriptorHandler<TElement,TControl> is the canonical interpreter
// for a ControlDescriptor. Duplicate registration for the same element
// type throws — register exactly once on each host you mount.
public static void Register(Reconciler reconciler) =>
reconciler.RegisterHandler<StarMeterElement, WinUI.RatingControl>(
new DescriptorHandler<StarMeterElement, WinUI.RatingControl>(
StarMeterDescriptor.Descriptor));
}
// </snippet:star-meter-registration>

// ════════════════════════════════════════════════════════════════════════
// Step 4 — Use the element
// ════════════════════════════════════════════════════════════════════════

// <snippet:star-meter-usage>
class ExtendingApp : Component
{
public override Element Render()
{
var (rating, setRating) = UseState(3.5);

return VStack(16,
TextBlock("StarMeter — custom element wrapping WinUI RatingControl")
.FontSize(14).SemiBold(),

new StarMeterElement
{
Value = rating,
MaxRating = 5,
Caption = "Rate this page",
OnValueChanged = setRating,
},

TextBlock($"current rating: {rating:0.0}"),

HStack(8,
Button("Reset", () => setRating(0)),
Button("5 stars", () => setRating(5)))
).Padding(20);
}
}
// </snippet:star-meter-usage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
app:
title: "Extending Reactor"
width: 540
height: 360
startup-delay: 1500

screenshots:
- id: star-meter
description: "Custom StarMeter element wrapping WinUI RatingControl, registered via a descriptor and used like any built-in."
component: ExtendingApp
region: client
format: png
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<Platforms>x64;ARM64</Platforms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWinUI>true</UseWinUI>
<WindowsPackageType>None</WindowsPackageType>
<NoWarn>$(NoWarn);REACTOR_V1_PREVIEW</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="$(WindowsAppSDKVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Reactor\Reactor.csproj" />
</ItemGroup>
</Project>
121 changes: 121 additions & 0 deletions docs/_pipeline/apps/v1-protocol/App.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Reactor.Core.V1Protocol;
using Microsoft.UI.Reactor.Core.V1Protocol.Descriptor;
using static Microsoft.UI.Reactor.Factories;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using WinUI = Microsoft.UI.Xaml.Controls;
using Windows.UI;

System.AppContext.SetSwitch("Reactor.UseV1Protocol", true);
ReactorApp.Run<V1ProtocolApp>(
"V1 Protocol Demo", width: 520, height: 360, devtools: true,
configure: host => LedIndicatorRegistration.Register(host.Reconciler));

// <snippet:element-record>
// An Element record describes what you want on screen — no WinUI types, no
// mutable state. The reconciler diffs Element values across renders;
// records get value-equality for free, so unchanged subtrees skip Update.
public sealed record LedIndicatorElement : Element
{
public required Color Color { get; init; }
public bool IsOn { get; init; } = true;
public double Size { get; init; } = 16;
}
// </snippet:element-record>

// <snippet:descriptor>
// A descriptor declares property bindings against the WinUI control the
// element targets. The framework's DescriptorHandler interprets the entries
// during Mount and Update — there is no per-element interpreter overhead
// beyond a dictionary lookup and the entry-loop iteration.
public static class LedIndicatorDescriptor
{
public static readonly ControlDescriptor<LedIndicatorElement, WinUI.Border> Descriptor =
new ControlDescriptor<LedIndicatorElement, WinUI.Border>
{
// The descriptor's children strategy says "no children" — this is
// a leaf control. See ChildrenStrategy survey in the prose.
Children = new None<LedIndicatorElement, WinUI.Border>(),
}
// OneWay: write on Mount, diff-and-write on Update. Equality skips
// the write when the element value didn't change.
.OneWay(
get: static e => e.Size,
set: static (c, v) => { c.Width = v; c.Height = v; c.CornerRadius = new Microsoft.UI.Xaml.CornerRadius(v / 2); })
// The IsOn → Background mapping coerces both inputs onto one WinUI
// property. A single OneWay entry observes (Color, IsOn) jointly
// by reading both off the element in the get/set lambdas.
.OneWay(
get: static e => (e.Color, e.IsOn),
set: static (c, v) =>
c.Background = new SolidColorBrush(
v.IsOn ? v.Color : Color.FromArgb(0x40, v.Color.R, v.Color.G, v.Color.B)));
}
// </snippet:descriptor>

// <snippet:registration>
// Registration is one call per Reactor host. RegisterDescriptor wraps
// RegisterHandler<...>(new DescriptorHandler<...>(descriptor)) — both shapes
// land on the same dispatch table. Duplicate registrations for the same
// element type throw.
public sealed class LedIndicatorRegistration
{
public static void Register(Reconciler reconciler)
{
reconciler.RegisterHandler<LedIndicatorElement, WinUI.Border>(
new DescriptorHandler<LedIndicatorElement, WinUI.Border>(
LedIndicatorDescriptor.Descriptor));
}
}
// </snippet:registration>

// <snippet:usage>
// Once registered, the element is used the same way as any built-in. The
// reconciler dispatches LedIndicatorElement to the registered handler;
// every other element type continues to flow through the built-in path.
class V1ProtocolApp : Component
{
public override Element Render()
{
var (level, setLevel) = UseState(2);

return VStack(12,
TextBlock("LED indicator (custom V1 element)").FontSize(14).SemiBold(),
HStack(8,
Led(Colors.Red, isOn: level >= 1),
Led(Colors.Orange, isOn: level >= 2),
Led(Colors.Yellow, isOn: level >= 3),
Led(Colors.Green, isOn: level >= 4)),
HStack(8,
Button("-", () => setLevel(System.Math.Max(0, level - 1))),
Button("+", () => setLevel(System.Math.Min(4, level + 1))),
TextBlock($"level = {level}").VAlign(VerticalAlignment.Center))
).Padding(20);

Element Led(Color c, bool isOn) =>
new LedIndicatorElement { Color = c, IsOn = isOn, Size = 28 };
}
}
// </snippet:usage>

// Bootstrap: register the descriptor against the host's reconciler once,
// before the first render. ReactorApp.Run exposes the host via a startup
// callback; we use the simpler ConfigureHost extension that matches the
// pattern most app authors will follow.
//
// (In a real app this lives next to other interop registrations, e.g.
// DockingNativeInterop.Register. The doc-app shape keeps it inline.)
public static class Bootstrap
{
static Bootstrap() { /* registration happens via app callback below */ }
}

public static class Colors
{
public static readonly Color Red = Color.FromArgb(0xFF, 0xE8, 0x1A, 0x1A);
public static readonly Color Orange = Color.FromArgb(0xFF, 0xF5, 0x9E, 0x0B);
public static readonly Color Yellow = Color.FromArgb(0xFF, 0xFA, 0xCC, 0x15);
public static readonly Color Green = Color.FromArgb(0xFF, 0x22, 0xC5, 0x5E);
}
13 changes: 13 additions & 0 deletions docs/_pipeline/apps/v1-protocol/doc-manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
app:
title: "V1 Protocol Demo"
width: 520
height: 360
startup-delay: 1500

screenshots:
- id: led-indicator
description: "Custom LED indicator element registered via a descriptor and rendered alongside built-in controls."
component: V1ProtocolApp
region: client
crop: none
format: png
18 changes: 18 additions & 0 deletions docs/_pipeline/apps/v1-protocol/v1-protocol.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<Platforms>x64;ARM64</Platforms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWinUI>true</UseWinUI>
<WindowsPackageType>None</WindowsPackageType>
<NoWarn>$(NoWarn);REACTOR_V1_PREVIEW</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="$(WindowsAppSDKVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Reactor\Reactor.csproj" />
</ItemGroup>
</Project>
Loading
Loading