Skip to content

fix: resolve SettingsPage crash in NativeAOT build#119

Closed
Copilot wants to merge 2 commits into
feat/aotfrom
copilot/sub-pr-116
Closed

fix: resolve SettingsPage crash in NativeAOT build#119
Copilot wants to merge 2 commits into
feat/aotfrom
copilot/sub-pr-116

Conversation

Copilot AI commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

SettingsPage crashed with System.ArgumentException: Value does not fall within the expected range. because ComboBox.ItemsSource was set to IReadOnlyList<SelectionItem<T>> while the XAML DataTemplate used x:DataType="helpers:BindableSelectionItem" with compiled {x:Bind} — requiring actual BindableSelectionItem instances at runtime.

Changes

  • SettingsPage.xaml.cs
    • Wrap controller's SelectionItem<T> lists into List<BindableSelectionItem> (stored as private fields) before assigning to ItemsSource
    • Replace SelectSelectionItem() extension (expects IEnumerable<ISelectionItem>, incompatible with List<BindableSelectionItem>) with direct .FirstOrDefault(x => x.ShouldSelect) for pre-selection
    • Replace SelectedItem as SelectionItem<T> cast (now always null) with SelectedIndex-based lookup into the controller's typed lists
// Before — SelectionItem<Theme> items don't satisfy BindableSelectionItem compiled binding
CmbTheme.ItemsSource = _controller.Themes;

// After — items are BindableSelectionItem as the DataTemplate expects
_bindableThemes = _controller.Themes.Select(t => new BindableSelectionItem(t)).ToList();
CmbTheme.ItemsSource = _bindableThemes;

// Pre-selection
CmbTheme.SelectedItem = _bindableThemes.FirstOrDefault(x => x.ShouldSelect);

// Saving — index into the original typed list
_controller.Theme = _controller.Themes[CmbTheme.SelectedIndex];

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI commented Mar 20, 2026

Copy link
Copy Markdown
Contributor Author

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • crl.godaddy.com
    • Triggering command: /usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-297de09d6e55df2d/dbs/csharp/working/1D28AAEF2CB9C63BCE9DBD21A68FB8D9/missingpackages_workingdir --packages /tmp/codeql-scratch-297de09d6e55df2d/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-297de09d6e55df2d/dbs/csharp/working/nugetconfig/nuget.config --force EnableCommentTool=true;EnablePlanTool=true] (dns block)
  • https://api.github.com/repos/NickvisionApps/Desktop
    • Triggering command: /usr/bin/curl curl -s REDACTED (http block)
  • ocsp.godaddy.com
    • Triggering command: /usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-297de09d6e55df2d/dbs/csharp/working/1D28AAEF2CB9C63BCE9DBD21A68FB8D9/missingpackages_workingdir --packages /tmp/codeql-scratch-297de09d6e55df2d/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-297de09d6e55df2d/dbs/csharp/working/nugetconfig/nuget.config --force EnableCommentTool=true;EnablePlanTool=true] (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] Fix crash in WinUI settings page for NativeAOT app fix: resolve SettingsPage crash in NativeAOT build Mar 20, 2026
Copilot AI requested a review from nlogozzo March 20, 2026 19:33
@nlogozzo

Copy link
Copy Markdown
Member

@copilot Why doesn't the BindableSelectionItem's implicit cast work here?

Copilot AI commented Mar 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Why doesn't the BindableSelectionItem's implicit cast work here?

The implicit cast is defined for SelectionItem<object>, not for SelectionItem<T>:

public static implicit operator BindableSelectionItem(SelectionItem<object> item) => ...

C# generics are invariant by default — SelectionItem<Theme> and SelectionItem<string> are not assignable to SelectionItem<object>, so the implicit operator never matches. Covariance (out T) is only supported on interfaces and delegates, not concrete generic classes. That's why the explicit new BindableSelectionItem(t) wrapping is required.

@nlogozzo

Copy link
Copy Markdown
Member

@copilot How can I make an implicit cast to SelectionItem work, while supporting NativeAOT still?

Copilot AI commented Mar 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot How can I make an implicit cast to SelectionItem work, while supporting NativeAOT still?

The only NativeAOT-safe way is to introduce a non-generic base class in the Desktop library so that SelectionItem<T> is a subtype the implicit operator can match:

In Desktop — add a non-generic base:

// New: Nickvision.Desktop/Application/SelectionItem.cs (non-generic base)
public abstract class SelectionItem : ISelectionItem
{
    public abstract string Label { get; }
    public abstract bool ShouldSelect { get; set; }
    public abstract event PropertyChangedEventHandler? PropertyChanged;
}

// Existing SelectionItem<T> extends it
public class SelectionItem<T> : SelectionItem { ... }

In BindableSelectionItem — change the operator source type:

// Before — only works for SelectionItem<object> due to generic invariance
public static implicit operator BindableSelectionItem(SelectionItem<object> item) => ...

// After — works for any SelectionItem<T> since it IS-A SelectionItem
public static implicit operator BindableSelectionItem(SelectionItem item) => new BindableSelectionItem(item);

Why this is NativeAOT-safe: it's a simple inheritance cast with no reflection or runtime generic instantiation. The cast SelectionItem<Theme>SelectionItem is checked statically at compile time.

Two alternatives that don't work:

  • implicit operator BindableSelectionItem(ISelectionItem item) — C# forbids implicit operators where either type is an interface
  • A generic implicit operator on a non-generic class — C# doesn't allow T in conversion operators unless T is a type parameter of the enclosing class

@nlogozzo

Copy link
Copy Markdown
Member

@copilot I'm making this:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Nickvision.Desktop.Application;

public abstract class SelectionItem : INotifyPropertyChanged
{
    public string Label { get; init; }

    public event PropertyChangedEventHandler? PropertyChanged;

    public SelectionItem(string label, bool shouldSelect)
    {
        Label = label;
        ShouldSelect = shouldSelect;
    }

    public bool ShouldSelect
    {
        get => field;

        set
        {
            field = value;
            OnPropertyChanged();
        }
    }

    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

public class SelectionItem<T> : SelectionItem
{
    public T Value { get; }

    public SelectionItem(T value, string label, bool shouldSelect) : base(label, shouldSelect)
    {
        Value = value;
    }
}

Can the WinUI project then make a bindable selectionitem that inherits from the selectionitem in a way that would make this conversion possible without storing an instance of selectionitem?

Copilot AI commented Mar 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot I'm making this:

using System.ComponentModel;

...

Yes — BindableSelectionItem can inherit from the non-generic SelectionItem base you're defining, using a copy constructor. This eliminates the private _selectionItem field entirely:

using WinRT;

namespace Nickvision.Desktop.WinUI.Helpers;

[GeneratedBindableCustomProperty]
public sealed partial class BindableSelectionItem : SelectionItem
{
    // Copy the base properties at construction time — no wrapper field needed
    public BindableSelectionItem(SelectionItem source) : base(source.Label, source.ShouldSelect) { }

    // Now works for any SelectionItem<T> since SelectionItem<T> IS-A SelectionItem
    public static implicit operator BindableSelectionItem(SelectionItem item) => new BindableSelectionItem(item);
}

How the implicit cast flows at the call site:

// SelectionItem<Theme> → SelectionItem (upcasts automatically) → BindableSelectionItem (implicit operator)
BindableSelectionItem b = _controller.Themes[0]; // works

One trade-off to be aware of: this is a value copy, not a live wrapper — if you mutate ShouldSelect on the original SelectionItem<T> after construction, the BindableSelectionItem won't see the change (and vice versa). For the SettingsPage pattern (read ShouldSelect once for pre-selection, then use SelectedIndex to write back) this is fine.

Why it's NativeAOT-safe: the implicit operator resolves entirely at compile time via standard inheritance. No reflection, no generic instantiation at runtime.

@nlogozzo

Copy link
Copy Markdown
Member

@copilot How to make live wrapper?

Copilot AI commented Mar 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot How to make live wrapper?

A live wrapper requires storing a reference to the source — "no stored instance" and "live" are mutually exclusive. The approach is to store the source, delegate ShouldSelect through it, and propagate its PropertyChanged events:

[GeneratedBindableCustomProperty]
public sealed partial class BindableSelectionItem : INotifyPropertyChanged
{
    private readonly SelectionItem _source;

    public event PropertyChangedEventHandler? PropertyChanged;

    public BindableSelectionItem(SelectionItem source)
    {
        _source = source;
        _source.PropertyChanged += (s, e) => PropertyChanged?.Invoke(this, e);
    }

    public string Label => _source.Label;

    public bool ShouldSelect
    {
        get => _source.ShouldSelect;
        set => _source.ShouldSelect = value;
    }

    public static implicit operator BindableSelectionItem(SelectionItem item) => new(item);
}

With this design:

  • Setting ShouldSelect on BindableSelectionItem writes through to the original SelectionItem<T>
  • When the original fires PropertyChanged, the BindableSelectionItem re-fires it, so WinUI bindings update
  • The implicit operator still works because SelectionItem<T> IS-A SelectionItem

The [GeneratedBindableCustomProperty] attribute handles the WinRT binding glue for NativeAOT without reflection.

@nlogozzo nlogozzo marked this pull request as ready for review March 20, 2026 20:18
@nlogozzo nlogozzo closed this Mar 20, 2026
@nlogozzo nlogozzo deleted the copilot/sub-pr-116 branch March 20, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants