-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Add CompiledBinding.Create factory methods from LINQ expressions #20443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 22 commits
4220c00
dc684e7
0a2147e
732121c
00c82c4
a76ed3a
15ccbd2
0ee8452
9878a53
f833182
feba103
19b9d73
05fe513
5450e38
156bd8f
6c1e4df
33f2404
dc7c853
73e35cf
81a1f22
724da5a
1ca3d26
f2c02e7
b724245
6bf05d1
e0dda30
de3eba1
f23245a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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` |
| 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; | ||
|
|
||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is quite counterintuitive that compiled bindings require dynamic/unreferenced code.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.