Repository navigation
Markup and Tooling
Code-first is the main way to build a UI; markup is an option for the parts that read better
as a tree — a whole window included. A .zfml file names its class with Class; at build
time a source generator turns it into C# — a field for every Name and an
InitializeComponent() of plain new, assignments, += and Add. No reflection, so
trimming and native AOT keep it all.
The root element is the class's base type — for a window, Form — and nothing has to come
before it but the optional XML declaration:
<?xml version="1.0" encoding="utf-8"?>
<Form Class="MyApp.MainForm" Title="Hello" Size="480,320" WindowStartupLocation="CenterScreen">
<StackPanel Orientation="Vertical" Spacing="10" Padding="24">
<Label Text="What is your name?"/>
<TextBox Name="NameBox" Watermark="Your name"/>
<PrimaryButton Text="Greet" Click="OnGreet"/>
</StackPanel>
</Form>MainForm.zfml next to MainForm.cs, which holds the behaviour:
using ZeppelinForms.Forms.Dialogs;
using ZeppelinForms.Input.Mouse;
namespace MyApp;
public partial class MainForm
{
private async void OnGreet(object? sender, MouseClickEventArgs e) =>
await MessageBox.ShowAsync(this, $"Hello, {NameBox.Text}!");
}and the form is run as any other:
new App(new WindowsPlatform()) { MainForm = new MainForm() }.Run();- The markup declares the class —
partial class MainForm : Form— and its fields. The code part needs neither the base class nor a constructor: a class whose code declares no constructor gets one that callsInitializeComponent. One that declares its own callsInitializeComponent()itself. - A form with no behaviour needs no code at all: the markup alone makes a public class.
- The form's own properties are attributes of the root,
<Form.Styles>its styles, and its child is itsContent. -
x:Class,x:Nameand thexmlnslines of XAML are understood too, and needed by none: the file is read as if it declared the framework's namespace andx:. A file that writesx:without declaring it builds, but the XML editor of Visual Studio underlines the undeclared prefix — writeClass/Name, or declarexmlns:x="https://zeppelinforms.dev/markup/x".
examples/ZF SharedLib/Showcase/FeedbackForm.zfml is a dialog written this way.
Any element can be the root — a card, a page, a panel reused in several forms:
<StackPanel Class="MyApp.SignUpCard" xmlns:my="using:MyApp.Controls"
Orientation="Vertical" Spacing="10">
<TextBox Name="NameBox" Watermark="Your name" Size="260,NaN"/>
<Label Text="{Bind NameBox.Text}"/>
<PrimaryButton Text="Sign up" Classes="wide" Click="OnSubmit"/>
<my:Gauge Level="0.5"/>
</StackPanel>public partial class SignUpCard
{
private void OnSubmit(object? sender, MouseClickEventArgs e) { /* NameBox.Text … */ }
}-
Name="NameBox"names the element and makes a field of it, asx:Namedoes. A name that can't be a field,Name="main-panel", only names the element — for#main-panelin a style sheet. -
Values are written as in style sheets:
Padding="14 6",Size="260,NaN",Background="#0d6efd", enum members, static members by name. -
Children go to the first property that takes them — one marked
[Content], thenChildren,Items,Nodes,Content,Child;<Border.Child>names one. -
{Bind Name.Property}binds a styled property to a named element,Mode=TwoWayboth ways;{x:Null}is null;<x:String>is a text item of a list. -
x:DataType="vm:OrderViewModel"says what the data context is, and{Bind Path}then reads from it —{Bind Customer.Name, Mode=TwoWay},Command="{Bind Save}"— checked and compiled at build time. See MVVM. -
A selected index or item is set after the items, wherever its attribute is written:
<ComboBox SelectedIndex="1">with the items inside it selects the second.[SetAfterContent]marks such a property of a control of one's own. -
xmlns:my="using:MyApp.Controls"brings the application's controls in as<my:Gauge>. -
Command="Save"takes a command of the class — a property or a field — andCommand="AppCommands.Save"a static one of another type, found in the class's namespace, in ausing:namespace of the markup or by its full name. -
An unknown type, property, event, command or value is a ZF2xxx error at its line of the markup, and so is a handler of the wrong signature.
-
A class whose code declares no constructor gets one that calls
InitializeComponent.
The package's build picks .zfml files up by itself.
A command property — Command of a button or a menu item — names a command instead of
taking a value written in the markup:
<Button Text="Save" Command="Save"/>
<Button Text="Quit" Command="AppCommands.Quit"/>public partial class EditorBar
{
public Command Save { get; }
public EditorBar()
{
Save = new Command(DoSave, () => _document.IsDirty);
InitializeComponent();
}
}InitializeComponent reads the command when it builds the button: make it before the call
— in the constructor or an initializer. A name the markup can't find, a member that isn't a
command, or another type's member that isn't static is ZF2011.
<Type.Styles> holds styles for the element and everything inside it — the element's own
Styles, as if the code had added them — written as XAML writes them:
<StackPanel x:Class="MyApp.SignUpCard" …>
<StackPanel.Styles>
<StyleInclude Source="Styles/cards.zss"/>
<Style Selector="#Result">
<Setter Property="TextColor" Value="@Accent"/>
</Style>
<Style Selector="Button.quiet">
<Setter Property="Background" Value="transparent"/>
<Transition Property="Background" Duration="120ms" Easing="ease-out"/>
<Style Selector="^:hover">
<Setter Property="Background" Value="alpha(@Accent, 0.1)"/>
</Style>
</Style>
</StackPanel.Styles>
<Label x:Name="Result"/>
<Button Text="Clear" Classes="quiet"/>
</StackPanel>| Element | Does |
|---|---|
<Style Selector="…"> |
A rule: the selectors of the style sheets, Button.primary > Label:hover
|
<Setter Property="…" Value="…"/> |
A property and its value; the value may also be the element's text |
<Transition Property="…" Duration="…" Easing="…"/> |
Animates the property's changes; the easing is optional |
<Style> inside a <Style>
|
A nested rule: ^ stands for the enclosing selector, ^:hover; without ^ it is a descendant |
<StyleInclude Source="…"/> |
A .zss file of the assets folder, as StyleSheet.LoadAsset finds it |
- Values are written as in style sheets, so they follow the theme:
@Accent,alpha(@Accent, 0.1),lighten(…),Colors.RoyalBlue,14 6. - The order is the cascade's: of two rules of equal specificity the later wins, an included sheet included.
- The build checks the styles: a style it can't read — no
Selector, a setter without a property or a value — is ZF2012, an error; a control or a property nobody has is ZF2013, a warning, as the style sheets themselves say: the selector never matches. The values are checked by the Visual Studio extension when the file is opened or saved — in the Error List at their lines — and when the element is built, in the debug output. - The package hands the project's
.zssfiles to the build: aStyleIncludeof a file that isn't among them is ZF2014 — a warning, since it may reach the assets another way. While a debugger is attached an included sheet is watched: a save restyles the running form. - Built again by Hot Reload, the element drops its old styles before it takes the new.
A form in code needn't mix its controls with what they do. MainForm.design.cs holds the
design — the controls' fields and an InitializeComponent() that makes, sets up, wires and
lays them out — and MainForm.cs the behaviour, as WinForms' Form1.Designer.cs and
Form1.cs. No designer owns the file: it is code, written and edited by hand.
// MainForm.design.cs
partial class MainForm
{
private TextBox _name;
private PrimaryButton _greet;
private void InitializeComponent()
{
Title = "Hello";
Size = new Size(480, 320);
_name = new TextBox { Watermark = "Your name" };
_greet = new PrimaryButton { Text = "Greet" };
_greet.Click += OnGreet;
Content = new StackPanel
{
Orientation = Orientation.Vertical,
Spacing = 10,
Padding = new Thickness(24),
Children = { new Label { Text = "What is your name?" }, _name, _greet },
};
}
}// MainForm.cs
public partial class MainForm : Form
{
private async void OnGreet(object? sender, MouseClickEventArgs e) =>
await MessageBox.ShowAsync(this, $"Hello, {_name.Text}!");
}What the framework adds to a *.design.cs, as it does to markup:
-
A constructor that calls
InitializeComponent(), when the class declares none. A constructor of the class's own calls it itself — ZF0007 warns of one that doesn't, since its form would open empty with its fields null. -
Hot Reload:
InitializeComponent()runs again after an edit, so a change of the design shows in the open form. That is why the controls are made inside it rather than in the fields' initializers, and their events wired there too: run again, it makes fresh controls with one handler each, where controls made once would get every handler twice. A class that overridesOnHotReloaditself keeps its own. A control whose design adds to itsChildrenclears them first. -
No CS8618 for the fields
InitializeComponent()assigns: the compiler doesn't follow the call from the constructor, the ZeppelinForms analyzers do. A field the method forgets keeps the warning. -
Nesting:
MainForm.design.csandMainForm.zfmlshow underMainForm.csin Solution Explorer and Rider (the package's build targets setDependentUpon).
The showcase's OrderForm.design.cs and OrderForm.cs are a whole example; dotnet new zf-form makes the pair.
The ZeppelinForms package brings analyzers that catch the framework's own pitfalls at build
time — ZF0xxx for code (an assignment in a constructor that locks the theme out, a styled
property declared wrong, a constructor that doesn't build its design part…) and ZF2xxx for
markup.
Three editors, one previewer: the extensions start the same host process,
ZeppelinForms.Designer, which loads the project's build and draws its previews with Skia.
Every form with a parameterless constructor and every method marked [Preview] (namespace
ZeppelinForms.Design) is in its list; what Main does before showing a form — loading
style sheets, choosing a theme — goes into a [PreviewSetup] method. The preview is live —
hover, click, type, scroll — and reloads after every build of the project.
| Editor | Where | The extension |
|---|---|---|
| Visual Studio 2022 17.10+, 2026 | View › Other Windows › ZeppelinForms Preview (Ctrl+Alt+Shift+P) |
ZeppelinForms Tools, .vsix
|
| VS Code | ZeppelinForms: Open Preview, or the button of a .zfml/.zss editor |
ZeppelinForms, .vsix
|
| Rider 2025.2.1+ | View › Tool Windows › ZeppelinForms Preview |
ZeppelinForms, a plugin .zip
|
All three are attached to every release and need the .NET 10 runtime on the machine. The toolbar is the same everywhere: the size the preview is laid out at (its own, a device's, the window's), the zoom — of the picture only, drawn by the host with more or fewer pixels, so it stays sharp —, the theme, the culture, right-to-left layout and the text scale.
Visual Studio takes the active document's project from its project system; VS Code and Rider
read it from the files — the nearest .csproj above the active file and the newest
bin/**/<AssemblyName>.dll of it (in VS Code zeppelinforms.preview.assembly names another).
The Visual Studio extension also lists the problems of .zss sheets in the Error List, and
those of the values of the styles in .zfml markup.
ZeppelinForms.LanguageServer is a language server — the Language Server Protocol, on
stdin and stdout — of .zfml markup and .zss style sheets, run by the machine's .NET 10. Every extension
ships it: Visual Studio's asks it from its own completion and quick info sources, so .zfml
stays in the XML editor; VS Code's and Rider's are its LSP clients, problems included. In
Rider .zfml opens in the XML editor too, and .zss is highlighted by a TextMate bundle of
the grammar the other two have.
-
Markup: the elements after
<— the framework's controls, theprefix:types of ausing:namespace, property elements<Grid.Styles>; the properties and events of an element in its tag, the ones not written yet; enum members,true/falseand static members in a value; the class's methods for an event and its commands forCommand;{Bind …}with the path's properties fromx:DataType— or from a named element — step by step;prefix:Typeforx:DataType; the styled properties of a<Setter>of the type its<Style Selector>selects. -
Style sheets: the controls,
:pseudo-classesand nesting of selectors; the styled properties of the control a block styles; enum members,@ThemeColorsand$variablesin a value. - Hover: an element's type, an attribute's property or event, a step of a binding's path — the declaration and its summary.
- Problems, for the editors whose build doesn't show them as you type (Visual Studio's does) — VS Code and Rider: the markup generator's ZF2xxx and the compiler's errors in the code it writes, at the markup's line; a sheet's shape and the controls and properties it names.
The server reads the project as the build does, without building it: its C# files, the
packages and projects obj/project.assets.json lists — the restore an IDE runs on opening
a project writes it — and the framework's generators over its markup. A view model just
written is offered at once.
The extensions speak to the previewer's host in a protocol of their own —
tools/ZeppelinForms.Design/Protocol/DesignerProtocol.cs, written again in JavaScript for
VS Code and in Kotlin for Rider, whose tests read bytes .NET wrote:
[int32 length][byte kind][payload] little-endian; length counts kind + payload
Strings are those of .NET's BinaryWriter: a 7-bit encoded byte count and UTF-8. The
extension listens and starts dotnet ZeppelinForms.Designer.dll --pipe <name> (Visual
Studio, and VS Code on Windows) or --socket <path> (a Unix domain socket: Rider everywhere,
VS Code on Linux and macOS); the host connects back.
Visual Studio's Hot Reload and dotnet watch work with code-first forms. After every applied
edit the open forms restyle, measure and redraw everything, so an edited Draw,
MeasureOverride or theme rule shows at once; pages made by factories — the entries of a
SplitView, the pages of a PageControl — are made again when their assembly changed. A
form built in its constructor builds itself again in OnHotReload:
public class MainForm : Form
{
public MainForm()
{
Title = "Orders";
Content = CreateContent();
}
private UIElement CreateContent() => new StackPanel { /* … */ };
protected override void OnHotReload(HotReloadEventArgs e) => Content = CreateContent();
}UIElement.OnHotReload does the same for a control that builds its children itself, and
HotReload.Applied is raised once the forms have caught up. Style sheets loaded with
watch: true follow their saves by themselves; markup and a
design part are built again after an edit with no code of one's own.
F12 or Ctrl+Shift+I over a running form: the element under the cursor with its margin and padding, its properties in a property grid (editable), and the accessibility audit. It opens as an overlay or docked to the side.
ZeppelinForms — fully free, for everyone. This wiki is generated from
docs/wiki:
edit it there, not here.
Start
Building the UI
Looks
Behaviour
Tooling