dotnet / dotnet/maui

[XSG] Proposal: Allow third-party types to participate in XAML source generation via `[MauiXamlValuePattern]`

Open
#33,445 6 comments 0 reactions 0 assignees View on GitHub
proposal/open xsg
Dominant language
C#
Stars
23.3k
Forks
2k
Avg merge
1d 15h
Merged PRs (30d)
290

Description

## Status

This proposal is in the **draft stage** for discussion purposes. Before implementing this feature, we should:

1. **Gather customer feedback** to understand demand for compile-time type conversion extensibility
2. **Validate the design** with real-world third-party library scenarios
3. **Assess complexity vs. benefit** — is the implementation cost justified by customer need?

We should not proceed with implementation until we have clear evidence of customer demand.

## Summary

This proposal describes an extensibility mechanism for XAML source generation that would allow third-party libraries to benefit from compile-time type parsing.

## Background

Today, the XAML source generator can parse strings like `"8,4"` into `new Thickness(8, 4)` at compile time for built-in MAUI types. However, third-party types with `TypeConverterAttribute` always fall back to runtime conversion—even when the parsing logic is simple and deterministic.

This proposal introduces an attribute that type authors can apply to declare how string values should be parsed at compile time.

## Proposed API

```csharp
[AttributeUsage(
AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field,
AllowMultiple = true)]
public sealed class MauiXamlValuePatternAttribute : Attribute
{
public string Pattern { get; }

public MauiXamlValuePatternAttribute(string pattern) => Pattern = pattern;
}
```

The attribute's placement determines the generated code:

| Target | Pattern | Generated Code |
|--------|---------|----------------|
| Constructor | `"{h}, {v}"` | `new Thickness(8, 4)` |
| Static method | `"https://{path}"` | `ImageSource.FromUri(new Uri("https://..."))` |
| Static property | `"Auto"` | `GridLength.Auto` |
| Static field | `"Linear"` | `Easing.Linear` |

Patterns without placeholders (like `"Auto"`) perform exact string matching and return the static member directly.

## Usage Examples

### Basic Types

```csharp
public readonly struct Thickness
{
[MauiXamlValuePattern("{uniformSize}")]
public Thickness(double uniformSize) { ... }

[MauiXamlValuePattern("{horizontalSize}, {verticalSize}")]
public Thickness(double horizontalSize, double verticalSize) { ... }

[MauiXamlValuePattern("{left}, {top}, {right}, {bottom}")]
public Thickness(double left, double top, double right, double bottom) { ... }
}
```

**XAML**:
```xml



```

### Third-Party Types

```csharp
public readonly struct Money
{
[MauiXamlValuePattern("{amount} {currency}")]
public Money(decimal amount, string currency) { ... }
}
```

**XAML**:
```xml

```

### Complex Types with Multiple Formats

```csharp
public readonly partial struct Color
{
[MauiXamlValuePattern("#{r:x2}{g:x2}{b:x2}")] // #RRGGBB
[MauiXamlValuePattern("rgb({r}, {g}, {b})")] // rgb(255, 128, 0)
public Color(int r, int g, int b) { ... }

[MauiXamlValuePattern("#{a:x2}{r:x2}{g:x2}{b:x2}")] // #AARRGGBB
[MauiXamlValuePattern("rgba({r}, {g}, {b}, {a})")] // rgba(255, 128, 0, 128)
public Color(int a, int r, int g, int b) { ... }
}
```

**XAML**:
```xml




```

### Static Values and Suffix Patterns

Patterns without placeholders perform exact string matching and return the static member. Patterns with placeholders and literal suffixes handle suffix-based syntax:

```csharp
public readonly struct GridLength
{
[MauiXamlValuePattern("Auto")]
public static GridLength Auto { get; }

[MauiXamlValuePattern("*")]
public static GridLength Star { get; }

[MauiXamlValuePattern("{value}")] // "100" → new GridLength(100)
public GridLength(double value) { ... }

[MauiXamlValuePattern("{value}*")] // "2*" → new GridLength(2, GridUnitType.Star)
public GridLength(double value, GridUnitType type = GridUnitType.Star) { ... }
}
```

**XAML**:
```xml

```

Another example with easing functions:

```csharp
public class Easing
{
[MauiXamlValuePattern("Linear")]
public static Easing Linear { get; }

[MauiXamlValuePattern("SinIn")]
public static Easing SinIn { get; }

[MauiXamlValuePattern("BounceOut")]
public static Easing BounceOut { get; }

// ... other easing functions
}
```

**XAML**:
```xml

```

### Static Factory Methods

When applied to static methods, the attribute enables implicit factory method selection based on patterns:

```csharp
public abstract class ImageSource
{
[MauiXamlValuePattern("http://{path}")]
[MauiXamlValuePattern("https://{path}")]
public static ImageSource FromUri(Uri uri) { }

[MauiXamlValuePattern("{path}")] // Fallback for non-URL strings
public static ImageSource FromFile(string path) { }
}
```

**XAML**:
```xml


```

**Note**: This is distinct from XAML's explicit `x:FactoryMethod` syntax, which allows XAML authors to call any static method with typed `x:Arguments`. `[MauiXamlValuePattern]` on static methods enables *implicit* factory method selection—the XAML author writes `Source="icon.png"` and the source generator determines which factory method to call based on pattern matching.

## Pattern Syntax

This proposal uses a simple placeholder syntax rather than full regex. A constrained syntax is more appropriate because:

- **Predictability**: Simple patterns are easier to reason about and less error-prone
- **Sufficient for most cases**: The vast majority of type conversions are comma/space-separated values mapping to constructor parameters
- **Better error messages**: We can provide clear diagnostics when patterns don't match
- **Avoids regex edge cases**: No need to worry about escaping, greedy matching, etc.

### Placeholder Syntax

- `{paramName}` — Matches based on the parameter's declared type
- `{paramName:format}` — Matches with a specific format specifier

### Supported Parameter Types

| Type | Example Input | Notes |
|------|---------------|-------|
| `int`, `long`, `short`, `byte` | `"123"` | Integer parsing |
| `float`, `double`, `decimal` | `"8.5"` | Floating-point parsing |
| `string` | `"USD"` | Captures until next delimiter or end |
| `bool` | `"true"`, `"false"` | Boolean parsing |
| Enums | `"Center"` | Enum member name parsing |

### Format Specifiers

| Specifier | Meaning | Example |
|-----------|---------|---------|
| `:x` | Hex, variable length | `{value:x}` matches `"FF"` or `"AABBCC"` |
| `:x2` | Hex, exactly 2 chars | `{r:x2}` matches `"FF"` |
| `:x8` | Hex, exactly 8 chars | `{argb:x8}` matches `"FF00AAFF"` |
| `:d` | Strict decimal (no thousands separator) | `{n:d}` matches `"123"` |
| (none) | Default for type | `{value}` matches numeric literals |

**Note**: All numeric parsing uses `CultureInfo.InvariantCulture`. The thousands separator `,` is **not** supported since comma is commonly used as a delimiter in patterns like `"{x}, {y}"`.

### Pattern Matching Rules

- Literal characters (`,`, ` `, `#`, etc.) match exactly
- Whitespace around placeholders is flexible (trims values)
- Multiple patterns on a single constructor are supported
- Patterns are evaluated across all constructors; first match wins

## Culture Handling

All parsing uses `CultureInfo.InvariantCulture` to match existing XAML runtime inflation behavior. This is the consistent pattern across all MAUI type converters—`ThicknessTypeConverter`, `PointTypeConverter`, `GridLengthTypeConverter`, etc. all use invariant culture. This ensures XAML files are portable across locales and produce identical results regardless of system culture settings.

## Fallback Behavior

If no pattern matches the input string, the source generator falls back to the runtime `TypeConverter` (if one exists). This ensures backward compatibility and handles edge cases that simple patterns can't express.

## Scope and Limitations

This proposal intentionally targets the common case: types with straightforward string-to-constructor mappings. It is **not** intended to replace all `TypeConverter` implementations.

### What This Proposal Covers

- Simple value types with comma/space-separated constructor parameters
- Types with well-known static values (patterns without placeholders)
- Suffix-based patterns like `"2*"` for `GridLength`
- Hex color formats and other format-specifier patterns
- Static factory methods selected by pattern matching

### What Remains with TypeConverter

Some conversions require logic that cannot be expressed as simple patterns:

| Converter | Why It Needs TypeConverter |
|-----------|---------------------------|
| `PathGeometryConverter` | Mini-language parser (M, L, C, Z drawing commands) |
| `BindablePropertyConverter` | Requires semantic resolution of property names against type hierarchy |
| `TypeTypeConverter` | Requires XAML namespace resolution and assembly scanning |
| `FontSizeConverter` | Context-dependent: returns `Device.GetNamedSize()` based on parent element type |
| `RDSourceConverter` | Requires file path resolution relative to XAML file location |
| `BrushTypeConverter` | Complex gradient syntax with nested structures |

Third-party libraries with similarly complex parsing requirements should continue using `TypeConverter`. The fallback mechanism ensures these continue to work at runtime.

### Design Philosophy

The goal is to provide a **low-friction opt-in** for the ~70% of type conversions that are simple and deterministic, while acknowledging that the remaining ~30% require richer infrastructure that `TypeConverter` provides. We explicitly avoid trying to express complex parsing logic through attributes—that path leads to an attribute-based DSL that would be harder to use than just writing a `TypeConverter`.

## Compile-Time Validation

- **Syntactically invalid patterns** produce a compiler warning and fall back to runtime `TypeConverter`.
- **Ambiguous patterns** where multiple patterns could match the same input produce a compiler warning and fall back to runtime `TypeConverter`.

In both cases, the generated code falls back to runtime inflation. It is up to app and library authors to address warnings and ensure patterns are correct.

## Testing Requirements

This feature must:
- Pass all existing XAML parsing tests to ensure behavioral parity with runtime inflation
- Include additional tests covering:
- Pattern matching edge cases
- Format specifier behavior
- Warning diagnostics
- Fallback to runtime `TypeConverter`

## Future Considerations

The initial implementation should be designed with extensibility in mind, allowing future revisions to address more advanced scenarios without breaking changes.

### Collection Types

Collection converters like `RowDefinitionCollectionConverter` follow a composition pattern:

```xml

```

This involves:
1. Splitting by separator (`,`)
2. Parsing each element using the `GridLength` patterns
3. Wrapping results in a `RowDefinitionCollection`

A future `[MauiXamlCollectionPattern]` attribute could express this:

```csharp
[MauiXamlCollectionPattern(Separator = ",", ElementType = typeof(GridLength))]
public class RowDefinitionCollection : List { }
```

The element type (`GridLength`) would use `[MauiXamlValuePattern]` for parsing individual items.

## Alternatives Considered

- **Full regex patterns**: More powerful but harder to use correctly and validate. The added flexibility doesn't justify the complexity for the common case.
- **Partial methods for third-party source generators**: Would require coordination between generators and adds significant complexity.
- **Static `TryParse` convention**: Less explicit, harder to discover, doesn't express the mapping to constructor parameters.
- **[`IParsable`](https://learn.microsoft.com/en-us/dotnet/api/system.iparsable-1) / [`ISpanParsable`](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1)**: Simpler and leaner than `TypeConverter`, but still involves runtime parsing overhead. The goal of this proposal is to eliminate runtime parsing entirely for deterministic patterns.

Contributor guide

Open the contributing guide

Research direction

No implementation files or entry points are identified; begin by reviewing the existing XAML source generator and the existing XAML parsing tests named in the requirements. First validate customer demand and the proposed API and pattern semantics before coding. The issue is done only when the design is accepted and, if approved, behavioral-parity, edge-case, diagnostics, and TypeConverter-fallback tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
build-system, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.