NoesisToolkit.Mvvm
0.3.7
dotnet add package NoesisToolkit.Mvvm --version 0.3.7
NuGet\Install-Package NoesisToolkit.Mvvm -Version 0.3.7
<PackageReference Include="NoesisToolkit.Mvvm" Version="0.3.7" />
<PackageVersion Include="NoesisToolkit.Mvvm" Version="0.3.7" />
<PackageReference Include="NoesisToolkit.Mvvm" />
paket add NoesisToolkit.Mvvm --version 0.3.7
#r "nuget: NoesisToolkit.Mvvm, 0.3.7"
#:package NoesisToolkit.Mvvm@0.3.7
#addin nuget:?package=NoesisToolkit.Mvvm&version=0.3.7
#tool nuget:?package=NoesisToolkit.Mvvm&version=0.3.7
NoesisToolkit
Build-time tooling for NoesisGUI on .NET. XAML becomes C# the compiler can see, binding paths fail the build instead of the frame, and the MVVM boilerplate writes itself.
Not affiliated with or endorsed by Noesis Technologies.
| Package | What it does |
|---|---|
NoesisToolkit.XamlCompiler |
compiles XAML to C# — no runtime parse, plus InitializeComponent and typed x:Name accessors |
NoesisToolkit.Analyzers |
validates binding paths, clr-namespace declarations, x:Static references, and the event and command lifetimes that pin an element |
NoesisToolkit.Mvvm |
[DependencyProperty], [DelegateCommand], and the commands behind them |
NoesisToolkit.Testing |
proves your controls are collectable, against a live renderless view |
Each is independent. Take one, take all four.
NoesisToolkit.XamlCompiler
<PackageReference Include="NoesisToolkit.XamlCompiler" PrivateAssets="all" />
Generated code builds the same object graph the native Noesis parser would, so the tree is constructed without XML on the hot path and every type reference is a real symbol the compiler and the trimmer can see.
An x:Class document needs the matching partial:
public partial class Shell : UserControl
{
public Shell() => InitializeComponent();
}
Wire the flush into startup — after the dictionaries are assembled, before
GUI.SetApplicationResources, which seals Setter values:
var resources = XamlGenerated.MyApp.XamlResources.Resolve("MyApp;Themes/Generic.xaml");
XamlGenerated.MyApp.XamlResources.Flush(resources);
GUI.SetApplicationResources(resources);
| Property | Default | Meaning |
|---|---|---|
NoesisXamlPackPrefix |
the assembly name | the pack name a Source= URI uses: prefix;Relative/Path.xaml |
NoesisXamlExtensions |
.xaml |
;-separated extensions to compile |
NoesisXamlNamespaces |
— | extra xmlns → CLR namespace mappings, uri=Ns1,Ns2, ;-separated; extends the built-ins rather than replacing them |
EnableDefaultNoesisXamlItems |
true |
set false to supply the AdditionalFiles items yourself |
clr-namespace: URIs need no mapping — they name their namespace directly.
Every compiled assembly gets a XamlCompileSurvey.g.cs whose header comment lists each document as
OK, FAIL <reason> or DEAD <markup the compiler reached but nothing consumes>. A document the
compiler cannot handle has two escape hatches: give it an event handler in markup, which moves it to
the native loader wholesale, or exclude it from AdditionalFiles.
A trimmed or NativeAOT build needs no hand-kept roots for what markup reaches by name: a binding
left native, markup left to the parser and an EventName are rooted on the method that builds their
document, and NTK1004 marks what the compiler cannot type. Keep Noesis.GUI itself whole
(<TrimmerRootAssembly Include="Noesis.GUI" />).
The generated surface, resource timing, trimming and the event-handler fallback are in docs/xaml-compiler.md.
NoesisToolkit.Analyzers
<PackageReference Include="NoesisToolkit.Analyzers" PrivateAssets="all" />
Noesis resolves binding paths, namespaces and x:Static references reflectively at load and only
logs when it misses, so a rename that skips the XAML renders nothing and leaves no trace. These
rules move that failure to the build.
A binding is only checkable where the type its DataContext resolves to is known. A DataTemplate
carrying a DataType states it; a keyed template, a bare Style or a template with no host does
not, and those bindings are skipped rather than guessed. ntk:DataType states it where markup
cannot, and ntk:CompileBindings makes stating it mandatory. Both live in the toolkit xmlns, which
Noesis drops as unknown, so they reach the build and never reach the graph:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:MyApp.ViewModels"
xmlns:ntk="https://github.com/vrimar/NoesisToolkit"
ntk:CompileBindings="True">
<DataTemplate x:Key="Row" ntk:DataType="ui:ItemViewModel">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</ResourceDictionary>
ntk:DataType goes on any element and covers everything inside it. Declare ntk:CompileBindings
per document rather than globally: turning it on across a codebase that has never used it fails
every document at once.
NoesisXamlExtensions and EnableDefaultNoesisXamlItems apply here too.
NoesisAnalyzeXamlBindings (default true) gates NTK2001–NTK2004 only; NTK2101 and NTK2102
analyse C# rather than XAML and are not gated by a property — adjust them through .editorconfig
severity like any other analyzer rule.
NTK2101 and NTK2102 are the two leak rules. Noesis holds handler delegates, and the managed
objects a native property picks up, in static tables cleaned only when the element's native object
dies — which the pinned managed proxy prevents. So an instance handler left on an element or a
template child, and a command whose delegates captured the control that binds it, both keep that
control alive for the process lifetime. A -= that the same flow re-attaches does not settle the
first: it stops a second OnApplyTemplate stacking the handler, but never runs on unload. The way
out of both is a static handler that takes its owner from sender — or, for a template child,
from sender's TemplatedParent. Details in
docs/diagnostics.md.
With the XAML compiler also referenced, a {Binding} in an x:Class document that resolves end to
end is emitted as a chain of typed reads instead of a reflective path string, so a rename that
misses the XAML fails the build. Everything it cannot resolve keeps its native Noesis.Binding, so
opting a document in is safe regardless — see
docs/compiled-bindings.md.
NoesisToolkit.Mvvm
<PackageReference Include="NoesisToolkit.Mvvm" />
public partial class Shell : UserControl
{
[DependencyProperty]
public partial string? Title { get; set; }
[DelegateCommand]
void Save() { } // SaveCommand
[DelegateCommand]
async ValueTask Load() { } // LoadCommand, refuses re-entry while in flight
}
[DependencyProperty] takes the metadata Noesis needs:
[DependencyProperty(0f, nameof(OnWidthChanged), FrameworkPropertyMetadataOptions.AffectsMeasure)]
public partial float Slot { get; set; }
static void OnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { }
A static partial property registers as an attached property and gains generated GetSlot /
SetSlot accessors instead.
A callback may take its object by handle instead, (ElementHandle element, DependencyPropertyChangedEventArgs e). Noesis holds a native element's managed proxy weakly and mints
a new one on the first touch after every collection; a callback that reads through GetSlot(element)
or DependencyRead, writes through DependencyWrite and subscribes through Events.On never has one
minted, which matters for an attached property bound in a template that a list rebinds per row.
Events.On and ElementGeometry are the same reads Noesis' managed layer offers, without the
object it mints per call — an element's events deliver the element itself rather than a fresh args
object, and a translated point or a desired size comes back unboxed. Both matter where something
runs per child, such as a panel arranging its children or testing them against the viewport.
A subscription through Events.On or DependencyWatcher.Watch lasts until the element is
destroyed, as one on a Noesis event does. The handler is handed the element for that reason: one
that captured the element, or the control around it, would keep both alive.
AttachedObjects.AssociatedObjectOf reads what a behavior or trigger is attached to. A teardown
detaches it after Noesis has let the element go, where AssociatedObject logs "Extend already
removed"; this returns null there instead, so OnDetaching can tell a teardown from a removal.
NoesisToolkit.Testing
<PackageReference Include="NoesisToolkit.Testing" />
NTK2101 and NTK2102 read code, so they cannot see what markup wired or what a native property
picked up at runtime. This proves it instead: every constructible control in the assemblies you name
is added to a live renderless view, removed, collected, and reported if the heap still holds it.
[Test]
public async Task Controls_are_collectable()
{
if (ControlCollectability.Unavailable() is { } reason)
throw new SkipTestException(reason);
var report = ControlCollectability.Probe([typeof(Shell).Assembly]);
await Assert.That(report.Leaked).IsEmpty();
await Assert.That(report.Verified).IsGreaterThan(0); // a sweep that skips everything is green
}
Run it with your theme installed. Without a template a control has no template children to subscribe to and no bindings to carry, so the sweep passes on controls that leak in the running app.
Probe also takes exclude for a control the host retains by design, betweenRounds for a host
that ticks its own frame work, and settleRounds (default 16) because a native destroy can lag a
collection by several rounds. Unavailable() returns null when the environment can host the probe
and otherwise says why, so a headless machine skips instead of reporting a false pass.
Requirements
NoesisToolkit.Mvvmemits partial properties and thefieldkeyword, so it needs a C# 14 compiler (.NET 10 SDK or later). The runtime library targetsnet10.0.- It depends on
Noesis.GUI>= 4.0.0, whose package declareswin10-*runtime identifiers the modern SDK no longer probes — consumers may want<NoWarn>$(NoWarn);NETSDK1206</NoWarn>. - The two analyzer packages need Roslyn 4.8 or later (Visual Studio 2022 17.8 / .NET 8 SDK).
NoesisToolkit.Testingtargetsnet10.0and drives real Noesis, solibNoesis.sohas to load — X11 and GL present on Linux. AskControlCollectability.Unavailable()and skip on a reason.
Diagnostics
Every NTK* id the toolkit raises, and how to read the generated code, are in
docs/diagnostics.md.
Building
dotnet build NoesisToolkit.slnx
dotnet run --project tests/NoesisToolkit.Tests
dotnet run --project tests/NoesisToolkit.Equivalence.Tests
dotnet run --project tests/NoesisToolkit.Testing.Tests
csharpier format .
NoesisToolkit.Tests compiles each generator's output against a stub Noesis surface, so emitted C#
that does not bind fails the suite. NoesisToolkit.Equivalence.Tests is the gate that proves
faithfulness: every fixture is realized twice — once by the real Noesis parser, once by the compiled
path — and the two object graphs are diffed. NoesisToolkit.Testing.Tests runs the collectability
harness over a control pinned on purpose and one that is not, so a harness that reports nothing
fails. The last two link libNoesis.so, so they need X11 and GL present.
Releasing
The version lives in the tag, nowhere in the tree. Push v<semver> and the release workflow builds,
runs every suite, packs and pushes to NuGet:
git tag v0.1.0 && git push origin v0.1.0
All four packages share one version. A tag carrying a -suffix (v0.3.0-rc.1) publishes as a
prerelease. Below 1.0.0 a minor bump may break API; that is what 0.x is for.
Never reuse a version — NuGet keeps the first upload of one forever. If a push half-fails, re-running
the workflow is safe (--skip-duplicate); if a bad package got through, unlist it and ship the next
patch. To rehearse without publishing, run the workflow manually: it takes a version and a publish
flag that defaults to off.
Licence
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Noesis.GUI (>= 4.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.3.7 | 44 | 10/2/2026 |
| 0.3.6 | 48 | 10/2/2026 |
| 0.3.5 | 94 | 9/28/2026 |
| 0.3.4 | 94 | 9/27/2026 |
| 0.3.3 | 84 | 9/24/2026 |
| 0.3.2 | 90 | 9/23/2026 |
| 0.3.1 | 88 | 9/23/2026 |
| 0.3.0 | 91 | 9/23/2026 |
| 0.2.7 | 90 | 9/22/2026 |
| 0.2.6 | 87 | 9/22/2026 |
| 0.2.5 | 91 | 9/22/2026 |
| 0.2.4 | 102 | 9/18/2026 |
| 0.2.3 | 90 | 9/18/2026 |
| 0.2.2 | 98 | 9/17/2026 |
| 0.2.1 | 90 | 9/17/2026 |
| 0.2.0 | 85 | 9/16/2026 |
| 0.1.2 | 86 | 9/16/2026 |
| 0.1.1 | 82 | 9/16/2026 |
| 0.1.0 | 93 | 9/16/2026 |
See CHANGELOG.md.