| name | comet-go |
| description | Write and edit Comet Go single-file apps (.cs files using the Comet MVU framework for .NET MAUI). Use when writing, editing, or debugging Comet Go apps, or when the user mentions "maui go", "comet go", or is working with .cs files that import Comet. |
Comet Go โ Single-File App Development
Comet Go is a single-file app experience for .NET MAUI using the Comet MVU framework.
The user writes ONE .cs file, runs maui go, and it live-reloads on their device.
How It Works
- User writes a single
.cs file with a MainPage : View class
maui go starts GoDevServer โ Roslyn compiles to DLL (OutputKind.DynamicallyLinkedLibrary)
- DLL is sent over WebSocket to the companion app
- Companion app loads via
Assembly.Load โ MetadataUpdater.ApplyUpdate for hot reload
- Scaffold new apps with:
maui go create <AppName>
Required Imports
Every Comet Go file MUST start with:
#:package Comet
using Comet;
using Microsoft.Maui;
using Microsoft.Maui.Graphics;
using static Comet.CometControls;
using static Comet.CometControls; enables VStack(...), Text(...), Button(...) etc. as top-level functions
- Add
using System; if you need Math, Convert, DateTime, or TimeSpan directly
Action and Func<T> resolve without using System; via implicit usings
App Structure
namespace MyApp;
public class MainPage : View
{
readonly Reactive<int> count = new(0);
[Body]
View body() =>
VStack(spacing: 16f,
Text(() => $"Count: {count.Value}"),
Button("Tap", () => count.Value++)
);
}
CRITICAL: VStack/HStack Spacing Parameter
VStack(0, ...) is ambiguous between VStack(float?, params View[]) and VStack(LayoutAlignment, params View[]).
- โ
VStack(spacing: 0f, ...) โ named parameter (clearest)
- โ
VStack(16f, ...) โ float literal with f suffix
- โ
VStack(0, ...) โ CS0121 ambiguous call
- โ
VStack(16, ...) โ may be ambiguous
Always use f suffix or spacing: named parameter.
Available Controls (via using static Comet.CometControls)
Layout
VStack(float? spacing, params View[] children)
HStack(float? spacing, params View[] children)
ZStack(params View[] children)
Grid(object[] columns, object[] rows, params View[] children)
ScrollView(View content)
Border(View content)
Spacer()
Display & Input
Text("static text")
Text(() => $"dynamic {reactive.Value}")
Text(() => reactive.Value)
Button("label", () => DoSomething())
Button("label", DoSomething)
TextField(signal, "placeholder")
SecureField(signal, "Password")
Toggle(reactiveBool)
Slider(value: 0.5, minimum: 0, maximum: 1)
Image("https://example.com/image.png")
Picker(0, "Option A", "Option B", "Option C")
Grid Layout
Grid(
columns: new object[] { "*", "*", "*", "*" },
rows: new object[] { 70, 70, 70 },
Button("1", () => {}).Cell(row: 0, column: 0),
Button("2", () => {}).Cell(row: 0, column: 1),
Button("wide", () => {}).Cell(row: 1, column: 0, colSpan: 2)
)
.ColumnSpacing(10).RowSpacing(10)
Column/row definitions: "*" (star), "2*" (weighted), "Auto", or integer pixels.
Fluent Styling API
Chain these after any control:
.FontSize(24)
.FontWeight(FontWeight.Bold)
.Color(Colors.White)
.HorizontalTextAlignment(TextAlignment.End)
.Background(new SolidPaint(myColor))
.Background(Colors.Blue)
.CornerRadius(12)
.RoundedBorder(radius: 12, color: Colors.Gray, width: 1)
.Frame(width: 100, height: 50)
.Padding(new Thickness(16))
.Margin(new Thickness(8))
.FillHorizontal()
.FillVertical()
.IgnoreSafeArea()
.Alignment(Alignment.Center)
.Opacity(0.8)
.IsVisible(true)
.AutomationId("my-button")
โ ๏ธ .CornerRadius() works on Button and Border but NOT on layout views (VStack, HStack, TextField, etc.). Using it on unsupported views is silently ignored or crashes. Use .RoundedBorder() instead for rounded corners on containers and other views.
Reactive State: Reactive vs Signal
Reactive โ Display binding (read in UI, write in code)
readonly Reactive<int> count = new(0);
readonly Reactive<string> display = new("0");
Text(() => $"Count: {count.Value}")
Button("Add", () => count.Value++)
Signal โ Two-way binding (for text input)
using Comet.Reactive;
readonly Signal<string> username = new Signal<string>("");
TextField(username, "Enter name")
Text(() => $"Hello, {username.Value}!")
๐ Use Reactive<T> for display-only state. Use Signal<T> (from Comet.Reactive) for TextField two-way binding.
FontWeight Values
Available: Bold, Semibold, Medium, Regular, Light, Heavy, Thin, UltraLight, UltraBold, Black
โ FontWeight.SemiBold (capital B) does NOT exist โ use FontWeight.Semibold (lowercase b).
Color Reference
Colors.* from Microsoft.Maui.Graphics:
Colors.White, Colors.Black, Colors.Red, Colors.Green, Colors.Blue,
Colors.Orange, Colors.Yellow, Colors.Purple, Colors.Pink,
Colors.Grey, Colors.Gray, Colors.DarkGray, Colors.LightGray,
Colors.Transparent, Colors.DodgerBlue, Colors.Crimson, Colors.Teal,
Colors.Coral, Colors.Gold, Colors.Indigo, Colors.Lime, Colors.Navy
Custom hex colors: Color.FromArgb("#FF9F0A")
Hot Reload Constraints
The Go dev server uses Edit-and-Continue (EnC). Know what works and what doesn't:
โ
Works โ delta produced, UI updates
- Method body changes (text, colors, layout, logic)
- Field initializer value changes
- New methods in existing types
- Lambda expression changes
โ ๏ธ Unreliable โ sometimes works, sometimes crashes
- Adding new fields or properties to existing classes
โ Restart required (Ctrl+C and re-run maui go)
- Adding new classes or types
- Changing constructor signatures
- Changing method signatures (parameters, return type)
- Changing base classes or interfaces
Design Principle
Design your initial file with all methods you'll need. During hot reload, modify method bodies freely. If you need structural changes (new types, new fields), restart.
void Append(string digit) { }
void SetOp(string op) { }
void Calculate() { }
Anti-Patterns That Cause Crashes
-
Unicode operators in switch expressions โ "รท", "ร", "โ" cause encoding issues in the hot reload pipeline. Use ASCII: "/", "x", "-".
-
.CornerRadius() on non-Button views โ silently ignored or crashes. Use .RoundedBorder() for containers.
-
Button("Go", MyMethod) โ CS1503 error. Method groups don't work. Must be Button("Go", () => MyMethod()).
-
Reactive<string> for TextField โ won't bind two-way. TextField requires Signal<string>.
-
FontWeight.SemiBold (capital B) โ doesn't exist. Use FontWeight.Semibold.
-
String interpolation lambdas to string params โ SomeHelper(() => $"val {x}") fails if the method takes string. Inline the interpolation in body() since the whole body is reactive.
-
using new VStack { ... } collection initializer syntax โ use VStack(spacing: 8f, child1, child2).
Common Mistakes to Avoid
- Missing
using static Comet.CometControls; โ VStack, Text, Button won't resolve
- Using
VStack(0, ...) without f suffix โ ambiguous overload
- Using
new Text(...) instead of Text(...) โ use the clean static API
- Forgetting
() => wrapper for reactive text โ Text(() => $"{x.Value}") not Text($"{x.Value}")
- Using MAUI Controls APIs (Shell, NavigationPage) โ Comet has its own navigation
- Using
Reactive<string> for TextField โ use Signal<string> from Comet.Reactive
Example: Calculator App (E2E Verified)
Built from template, hot-reloaded, and interactively tested with button taps and arithmetic verification.
#:package Comet
using Comet;
using Microsoft.Maui;
using Microsoft.Maui.Graphics;
using static Comet.CometControls;
namespace Calculator;
public class MainPage : View
{
readonly Reactive<string> display = new("0");
readonly Reactive<string> subDisplay = new("");
double accumulator = 0;
string pendingOp = "";
bool resetOnNext = false;
void Append(string digit)
{
if (resetOnNext) { display.Value = "0"; resetOnNext = false; }
if (display.Value == "0" && digit != ".") display.Value = digit;
else if (digit == "." && display.Value.Contains(".")) return;
else display.Value += digit;
}
void SetOp(string op)
{
if (pendingOp != "") DoCalculate();
accumulator = double.Parse(display.Value);
pendingOp = op;
subDisplay.Value = $"{accumulator} {op}";
resetOnNext = true;
}
void DoCalculate()
{
if (pendingOp == "") return;
double current = double.Parse(display.Value);
double result = pendingOp switch
{
"+" => accumulator + current,
"-" => accumulator - current,
"x" => accumulator * current,
"/" => current != 0 ? accumulator / current : 0,
_ => current
};
display.Value = result.ToString();
subDisplay.Value = "";
accumulator = result;
pendingOp = "";
resetOnNext = true;
}
[Body]
View body()
{
var darkBg = Color.FromArgb("#1C1C1E");
var numBg = Color.FromArgb("#3A3A3C");
var opBg = Color.FromArgb("#FF9F0A");
var fnBg = Color.FromArgb("#636366");
var eqBg = Color.FromArgb("#30D158");
return VStack(spacing: 0f,
new Spacer(),
Text(() => subDisplay.Value)
.FontSize(18).Color(Colors.Grey)
.HorizontalTextAlignment(TextAlignment.End)
.Margin(new Thickness(20, 0)),
Text(() => display.Value)
.FontSize(56).FontWeight(FontWeight.Bold)
.Color(Colors.White)
.HorizontalTextAlignment(TextAlignment.End)
.Margin(new Thickness(20, 0, 20, 16)),
Grid(
columns: new object[] { "*", "*", "*", "*" },
rows: new object[] { 70, 70, 70, 70, 70 },
Button("AC", () => { display.Value = "0"; subDisplay.Value = ""; accumulator = 0; pendingOp = ""; resetOnNext = false; })
.Color(Colors.White).Background(new SolidPaint(fnBg)).CornerRadius(36).Cell(row: 0, column: 0),
Button("+/-", () => { if (display.Value != "0") display.Value = display.Value.StartsWith("-") ? display.Value[1..] : "-" + display.Value; })
.Color(Colors.White).Background(new SolidPaint(fnBg)).CornerRadius(36).Cell(row: 0, column: 1),
Button("%", () => { display.Value = (double.Parse(display.Value) / 100).ToString(); })
.Color(Colors.White).Background(new SolidPaint(fnBg)).CornerRadius(36).Cell(row: 0, column: 2),
Button("/", () => SetOp("/")).Color(Colors.White).Background(new SolidPaint(opBg)).CornerRadius(36).Cell(row: 0, column: 3),
Button("7", () => Append("7")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 1, column: 0),
Button("8", () => Append("8")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 1, column: 1),
Button("9", () => Append("9")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 1, column: 2),
Button("x", () => SetOp("x")).Color(Colors.White).Background(new SolidPaint(opBg)).CornerRadius(36).Cell(row: 1, column: 3),
Button("4", () => Append("4")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 2, column: 0),
Button("5", () => Append("5")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 2, column: 1),
Button("6", () => Append("6")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 2, column: 2),
Button("-", () => SetOp("-")).Color(Colors.White).Background(new SolidPaint(opBg)).CornerRadius(36).Cell(row: 2, column: 3),
Button("1", () => Append("1")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 3, column: 0),
Button("2", () => Append("2")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 3, column: 1),
Button("3", () => Append("3")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 3, column: 2),
Button("+", () => SetOp("+")).Color(Colors.White).Background(new SolidPaint(opBg)).CornerRadius(36).Cell(row: 3, column: 3),
Button("0", () => Append("0")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 4, column: 0, colSpan: 2),
Button(".", () => Append(".")).Color(Colors.White).Background(new SolidPaint(numBg)).CornerRadius(36).Cell(row: 4, column: 2),
Button("=", () => DoCalculate()).Color(Colors.White).Background(new SolidPaint(eqBg)).CornerRadius(36).Cell(row: 4, column: 3)
).ColumnSpacing(10).RowSpacing(10).Padding(new Thickness(14, 0, 14, 14))
).Background(new SolidPaint(darkBg)).IgnoreSafeArea();
}
}