fix: resolve SettingsPage crash in NativeAOT build#119
Conversation
…x ItemsSource Co-authored-by: nlogozzo <17648453+nlogozzo@users.noreply.github.com> Agent-Logs-Url: https://github.com/NickvisionApps/Application/sessions/67bceac1-dcc2-4a6e-a869-d2c85b497ea5
|
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:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot Why doesn't the BindableSelectionItem's implicit cast work here? |
The implicit cast is defined for public static implicit operator BindableSelectionItem(SelectionItem<object> item) => ...C# generics are invariant by default — |
|
@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 In // 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 // 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 Two alternatives that don't work:
|
|
@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? |
... Yes — 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]; // worksOne trade-off to be aware of: this is a value copy, not a live wrapper — if you mutate Why it's NativeAOT-safe: the implicit operator resolves entirely at compile time via standard inheritance. No reflection, no generic instantiation at runtime. |
|
@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 [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:
The |
SettingsPagecrashed withSystem.ArgumentException: Value does not fall within the expected range.becauseComboBox.ItemsSourcewas set toIReadOnlyList<SelectionItem<T>>while the XAML DataTemplate usedx:DataType="helpers:BindableSelectionItem"with compiled{x:Bind}— requiring actualBindableSelectionIteminstances at runtime.Changes
SettingsPage.xaml.csSelectionItem<T>lists intoList<BindableSelectionItem>(stored as private fields) before assigning toItemsSourceSelectSelectionItem()extension (expectsIEnumerable<ISelectionItem>, incompatible withList<BindableSelectionItem>) with direct.FirstOrDefault(x => x.ShouldSelect)for pre-selectionSelectedItem as SelectionItem<T>cast (now alwaysnull) withSelectedIndex-based lookup into the controller's typed lists💡 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.