Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4220c00
Reminder for future me.
grokys Jan 8, 2026
dc684e7
Move compiled bindings to Avalonia base.
grokys Jan 9, 2026
0a2147e
Update suppressions.
grokys Jan 9, 2026
732121c
Tweaked `BindingExpressionVisitor`.
grokys Jan 9, 2026
00c82c4
Fix ncrunch config.
grokys Jan 9, 2026
a76ed3a
Add comprehensive unit tests for BindingExpressionVisitor.
grokys Jan 9, 2026
15ccbd2
Fix backwards IsAssignableFrom check in BindingExpressionVisitor.
grokys Jan 9, 2026
0ee8452
Allow downcasts and all reference type casts in binding expressions.
grokys Jan 9, 2026
9878a53
Add clarifying test for cast transparency in binding expressions.
grokys Jan 9, 2026
f833182
Fix: Casts should create ReflectionTypeCastNode, not be transparent.
grokys Jan 9, 2026
feba103
Use compiled cast functions instead of reflection-based type checks.
grokys Jan 9, 2026
19b9d73
Reuse TypeCastPathElement<T> cast function directly.
grokys Jan 9, 2026
05fe513
Revert to lambda compilation approach for cast functions.
grokys Jan 9, 2026
5450e38
Code cleanup: Fix XML docs and remove unused usings.
grokys Jan 9, 2026
156bd8f
Refactor BindingExpressionVisitor to use CompiledBindingPathBuilder.
grokys Jan 9, 2026
6c1e4df
Remove CompiledBindingPathFromExpressionBuilder in favor of BindingEx…
grokys Jan 9, 2026
33f2404
Move test-only methods from production code to test extensions.
grokys Jan 9, 2026
dc7c853
Add public CompiledBinding.Create factory methods from LINQ expressions.
grokys Jan 9, 2026
73e35cf
Merge CompiledBinding.Create overloads and add all binding property p…
grokys Jan 15, 2026
81a1f22
Merge branch 'master' into feature/compiled-bindings-from-expressions
grokys Jan 15, 2026
724da5a
(Re-)update suppressions.
grokys Jan 15, 2026
1ca3d26
Merge branch 'master' into feature/compiled-bindings-from-expressions
grokys Jan 22, 2026
f2c02e7
Merge branch 'master' into feature/compiled-bindings-from-expressions
grokys Feb 5, 2026
b724245
Add missing using.
grokys Feb 5, 2026
6bf05d1
Fix ncrunch build.
grokys Feb 5, 2026
e0dda30
Remove file I committed by accident.
grokys Feb 5, 2026
de3eba1
PR feedback.
grokys Feb 5, 2026
f23245a
Store static members outside generic class.
grokys Feb 5, 2026
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
3 changes: 2 additions & 1 deletion Avalonia.v3.ncrunchsolution
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
<Value>TargetFrameworks = net10.0</Value>
</CustomBuildProperties>
<EnableRDI>False</EnableRDI>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
<ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir>
<RdiConfigured>True</RdiConfigured>
<SolutionConfigured>True</SolutionConfigured>
</Settings>
</SolutionConfiguration>
</SolutionConfiguration>
231 changes: 231 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands

### Prerequisites
- .NET SDK 10.0.101 (specified in `global.json`)
- Required workloads: `dotnet workload install android ios maccatalyst wasm-tools`
- Optional (Tizen): Install via PowerShell script from Samsung/Tizen.NET

### Building
```bash
# Build and run sample app (quickest way to test changes)
cd samples/ControlCatalog.Desktop
dotnet restore
dotnet run

# Build entire solution with Nuke
nuke --target Compile --configuration Release

# Or run Nuke directly without global tool
dotnet run --project nukebuild/_build.csproj -- --configuration Debug
```

### Testing
```bash
# Run all tests with Nuke
nuke --target RunTests --configuration Release

# Run specific test project
dotnet test tests/Avalonia.Base.UnitTests

# Run integration tests (requires setup - see tests/Avalonia.IntegrationTests.Appium/readme.md)
# Windows: Install WinAppDriver, run it, then run tests
# macOS: Install Appium, bundle IntegrationTestApp, then run tests
```

### Packaging
```bash
# Create NuGet packages (includes compile + tests)
nuke --target Package --configuration Release
```

### Opening in Visual Studio
- **Avalonia.sln**: Full solution (requires all workloads)
- **Avalonia.Desktop.slnf**: Desktop-only filter (no extra workloads needed)
- Requires Visual Studio 2022 or newer
- First build may require manually building `Avalonia.Build.Tasks` project

## Architecture Overview

### Core Layers

**Avalonia.Base** - Foundation layer with no platform dependencies:
- Property system: `AvaloniaObject` → `AvaloniaProperty` with priority-based resolution via `ValueStore`
- Class hierarchy: `AvaloniaObject` → `Animatable` → `StyledElement` → `Visual` → `Layoutable` → `Interactive`
- Data binding infrastructure (CompiledBinding, ReflectionBinding)
- Styling system (selectors, styles, themes)
- Rendering primitives and composition

**Avalonia.Controls** - UI layer:
- Extends base: `Interactive` → `InputElement` → `Control`
- All UI controls (Button, TextBox, Window, etc.)
- Layout panels (Grid, StackPanel, Canvas)
- Template system (ControlTemplate, DataTemplate)

**Markup Layer**:
- **Avalonia.Markup**: Core markup abstractions
- **Avalonia.Markup.Xaml**: Runtime XAML loading, markup extensions
- **Avalonia.Build.Tasks**: Build-time XAML compilation using XamlX + Mono.Cecil

**Platform Implementations** (all reference Base, no reverse dependencies):
- **Windows**: Avalonia.Win32 (+ Win32.Automation, Win32.Interoperability)
- **macOS**: Avalonia.Native (requires Xcode for native libs)
- **Linux**: Avalonia.X11, Avalonia.FreeDesktop
- **Mobile**: Avalonia.Android, Avalonia.iOS
- **Browser**: Avalonia.Browser (WebAssembly)

**Avalonia.Skia**: Primary rendering backend implementing `IPlatformRenderInterface`

**Avalonia.Desktop**: Meta-package aggregating Win32 + X11 + Native + Skia

### Key Subsystems

**Data Binding**:
- Three types: `CompiledBinding` (strongly-typed, preferred), `ReflectionBinding` (runtime), `TemplateBinding`
- Priority system: Animation → LocalValue → TemplatedParent → StyleTrigger → Style → Inherited → Unset
- `ValueStore` manages effective values via stacked `ValueFrame`s
- Located in `src/Avalonia.Base/Data`

**Styling**:
- CSS-like selectors (type, class, pseudoclass, property, combinators)
- `Style` = selector + setters collection
- `ControlTheme` for control-scoped theming
- Applied via `ValueFrame` in property store
- Located in `src/Avalonia.Base/Styling`

**Layout**:
- Two-pass: Measure then Arrange
- `Layoutable` base class, panels override `MeasureOverride`/`ArrangeOverride`
- Invalidation propagates up visual tree

**Rendering**:
- Modern: Composition-based with separate render thread (`Compositor`, `ServerCompositor`)
- Legacy: `ImmediateRenderer` for simpler scenarios
- Platform abstraction via `IPlatformRenderInterface`
- Located in `src/Avalonia.Base/Rendering`

**XAML Compilation**:
- Build-time: `CompileAvaloniaXamlTask` → XamlX parses XAML → IL emitted via Mono.Cecil
- Runtime: `AvaloniaXamlLoader` for dynamic XAML
- Located in `src/Avalonia.Build.Tasks` and `src/Markup`

### Platform Abstraction Pattern

Interface-based (no #ifdefs in core code):
- `IWindowingPlatform`, `IPlatformRenderInterface`, `IPlatformThreadingInterface`, etc.
- Services registered via `AvaloniaLocator` at platform initialization
- Example: `Win32Platform.Initialize()` registers all Win32-specific implementations

### Assembly Dependency Flow
```
Application
Avalonia.Desktop (package)
├── Platform-specific (Win32/X11/Native)
├── Avalonia.Skia
└── Avalonia.Markup.Xaml
Avalonia.Markup
Avalonia.Controls
Avalonia.Base (foundation)
```

## Test Organization

**Unit Tests** (tests/):
- `Avalonia.Base.UnitTests`: Property system, bindings, styling
- `Avalonia.Controls.UnitTests`: Control behavior
- `Avalonia.Markup.Xaml.UnitTests`: XAML loading and compilation
- Named: `Method_Name_Should_Do_Something()`

**Render Tests**:
- `Avalonia.RenderTests`: Platform-agnostic render test definitions
- `Avalonia.Skia.RenderTests`: Skia-based execution
- Named: `Rectangle_2px_Stroke_Filled()`

**Integration Tests**:
- `Avalonia.IntegrationTests.Appium`: Cross-platform UI automation tests
- Requires WinAppDriver (Windows) or Appium (macOS)
- See tests/Avalonia.IntegrationTests.Appium/readme.md for setup

**Build Tests**:
- `BuildTests/`: Verify XAML compilation across platforms
- Not in main solution (requires local NuGet packages first)

**Headless Tests**:
- `Avalonia.Headless.XUnit.*`: XUnit integration for UI tests without platform
- `Avalonia.Headless.NUnit.*`: NUnit integration

## Contribution Guidelines

**Bug Fixes**:
- Ideally include test demonstrating the issue
- Commit pattern: failing test commit → fix commit
- Unit tests (tests/) for non-platform issues
- Integration tests for platform-specific issues
- Render tests for visual issues

**Features**:
- Always include tests where possible
- New controls should have `AutomationPeer` for accessibility
- Follow existing patterns in codebase

**Style**:
- [.NET Core coding style](https://github.com/dotnet/runtime/blob/master/docs/coding-guidelines/coding-style.md)
- ~120 char line length (soft limit)
- 100 char line limit for XML docs (hard limit)
- Public methods need XML docs
- **NO #REGIONS**
- Test naming: `Method_Name_Should_Do_Expected_Thing()`

**PR Guidelines**:
- Fill in PR template sections (discretionary, delete irrelevant)
- Link issues with `Fixes #1234`
- No unrelated formatting/whitespace changes
- Breaking changes during major release cycles require `TODOXX:` comments (XX = next major version)

**Building Avalonia.Build.Tasks**:
- If you get MSB4062 errors, manually build `Avalonia.Build.Tasks` project once
- Or build entire solution once with Nuke

## Platform-Specific Notes

**macOS**:
- Can't build full solution (only .NET Standard/.NET Core subset)
- Requires Xcode for native libraries
- Run `./build.sh CompileNative` to build native libs
- Use Rider or VS Code (Visual Studio for Mac not supported)

**Linux**:
- Same as macOS - subset only
- Use Rider or VS Code

**Browser/WASM**:
- Requires NodeJS (latest LTS from nodejs.org)

**Windows**:
- Can build everything including .NET Framework samples
- Requires .NET Framework 4.7+ SDK

## Important File Locations

- **Property System**: `src/Avalonia.Base/AvaloniaObject.cs`, `src/Avalonia.Base/AvaloniaProperty.cs`
- **Value Store**: `src/Avalonia.Base/PropertyStore/`
- **Binding**: `src/Avalonia.Base/Data/`
- **Styling**: `src/Avalonia.Base/Styling/`
- **Controls**: `src/Avalonia.Controls/`
- **XAML Build**: `src/Avalonia.Build.Tasks/`
- **Rendering**: `src/Avalonia.Base/Rendering/`, `src/Avalonia.Skia/`
- **Platform**: `src/Windows/`, `src/Avalonia.X11/`, `src/Avalonia.Native/`

## This Repository

This is a public repository. Use `gh` command to read issues and PRs.

Main branch for PRs: `master`
92 changes: 92 additions & 0 deletions src/Avalonia.Base/Data/CompiledBinding.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq.Expressions;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
using Avalonia.Utilities;

namespace Avalonia.Data;

Expand All @@ -26,6 +29,95 @@ public CompiledBinding() { }
/// <param name="path">The binding path.</param>
public CompiledBinding(CompiledBindingPath path) => Path = path;

/// <summary>
/// Creates a <see cref="CompiledBinding"/> from a lambda expression.
/// </summary>
/// <typeparam name="TIn">The input type of the binding expression.</typeparam>
/// <typeparam name="TOut">The output type of the binding expression.</typeparam>
/// <param name="expression">
/// The lambda expression representing the binding path
/// (e.g., <c>vm => vm.PropertyName</c>).
/// </param>
/// <param name="source"
/// >The source object for the binding. If null, uses the target's DataContext.
/// </param>
/// <param name="converter">
/// Optional value converter to transform values between source and target.
/// </param>
/// <param name="mode">
/// The binding mode. Default is <see cref="BindingMode.Default"/> which resolves to the
/// property's default binding mode.
/// </param>
/// <param name="priority">The binding priority.</param>
/// <param name="converterCulture">The culture in which to evaluate the converter.</param>
/// <param name="converterParameter">A parameter to pass to the converter.</param>
/// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value.
/// </param>
/// <param name="stringFormat">The string format for the binding result.</param>
/// <param name="targetNullValue">The value to use when the binding result is null.</param>
/// <param name="updateSourceTrigger">
/// The timing of binding source updates for TwoWay/OneWayToSource bindings.
/// </param>
/// <param name="delay">
/// The amount of time, in milliseconds, to wait before updating the binding source.
/// </param>
/// <returns>
/// A configured <see cref="CompiledBinding"/> instance ready to be applied to a property.
/// </returns>
/// <exception cref="ExpressionParseException">
/// Thrown when the expression contains unsupported operations or invalid syntax for binding
/// expressions.
/// </exception>
/// <remarks>
/// This builds a <see cref="CompiledBinding"/> with a path described by a lambda expression.
/// The resulting binding avoids reflection for property access, providing better performance
/// than reflection-based bindings.
///
/// Supported expressions include:
/// <list type="bullet">
/// <item>Property access: <c>x => x.Property</c></item>
/// <item>Nested properties: <c>x => x.Property.Nested</c></item>
/// <item>Indexers: <c>x => x.Items[0]</c></item>
/// <item>Type casts: <c>x => ((DerivedType)x).Property</c></item>
/// <item>Logical NOT: <c>x => !x.BoolProperty</c></item>
/// <item>Stream bindings: <c>x => x.TaskProperty</c> (Task/Observable)</item>
/// <item>AvaloniaProperty access: <c>x => x[MyProperty]</c></item>
/// </list>
/// </remarks>
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
Comment on lines +88 to +89
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is quite counterintuitive that compiled bindings require dynamic/unreferenced code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, though do note that it's only required to build the compiled binding. We can probably do better with interceptors and source generators, but this API would be useful anyway if we want to e.g. migrate TreeDataGrid from using its current experimental bindings (which are constructed using LINQ expressions).

public static CompiledBinding Create<TIn, TOut>(
Expression<Func<TIn, TOut>> expression,
object? source = null,
IValueConverter? converter = null,
BindingMode mode = BindingMode.Default,
BindingPriority priority = BindingPriority.LocalValue,
CultureInfo? converterCulture = null,
object? converterParameter = null,
object? fallbackValue = null,
string? stringFormat = null,
object? targetNullValue = null,
UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.Default,
int delay = 0)
{
var path = BindingExpressionVisitor<TIn>.BuildPath(expression);
return new CompiledBinding(path)
{
Source = source ?? AvaloniaProperty.UnsetValue,
Converter = converter,
ConverterCulture = converterCulture,
ConverterParameter = converterParameter,
FallbackValue = fallbackValue ?? AvaloniaProperty.UnsetValue,
Mode = mode,
Priority = priority,
StringFormat = stringFormat,
TargetNullValue = targetNullValue ?? AvaloniaProperty.UnsetValue,
UpdateSourceTrigger = updateSourceTrigger,
Delay = delay
};
}

/// <summary>
/// Gets or sets the amount of time, in milliseconds, to wait before updating the binding
/// source after the value on the target changes.
Expand Down
Loading