{"id":28898155,"url":"https://github.com/prophetlamb/immutableeditableobjectadapter","last_synced_at":"2026-03-11T17:03:42.507Z","repository":{"id":293508736,"uuid":"984254909","full_name":"ProphetLamb/ImmutableEditableObjectAdapter","owner":"ProphetLamb","description":"Source generator adapting immutable state `record`s into an `IEditableObject` replacing the `record` on edit, intended for `Binding` in a UNO WindowsCommunityToolkit `DataGrid`.","archived":false,"fork":false,"pushed_at":"2025-06-09T12:55:59.000Z","size":280,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-20T18:09:07.218Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ProphetLamb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-15T16:25:10.000Z","updated_at":"2025-05-22T15:03:47.000Z","dependencies_parsed_at":"2025-05-15T18:22:52.374Z","dependency_job_id":"967850bb-d3ca-4ed8-b9e6-3c83b7b7a414","html_url":"https://github.com/ProphetLamb/ImmutableEditableObjectAdapter","commit_stats":null,"previous_names":["prophetlamb/immutableeditableobjectadapter"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/ProphetLamb/ImmutableEditableObjectAdapter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ProphetLamb%2FImmutableEditableObjectAdapter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ProphetLamb%2FImmutableEditableObjectAdapter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ProphetLamb%2FImmutableEditableObjectAdapter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ProphetLamb%2FImmutableEditableObjectAdapter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ProphetLamb","download_url":"https://codeload.github.com/ProphetLamb/ImmutableEditableObjectAdapter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ProphetLamb%2FImmutableEditableObjectAdapter/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261080621,"owners_count":23106602,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-21T07:08:57.388Z","updated_at":"2026-03-11T17:03:42.501Z","avatar_url":"https://github.com/ProphetLamb.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![NuGet Version](https://img.shields.io/nuget/v/ImmutableEditableObjectAdapter)](https://www.nuget.org/packages/ImmutableEditableObjectAdapter) [![NuGet Downloads](https://img.shields.io/nuget/dt/ImmutableEditableObjectAdapter)](https://www.nuget.org/packages/ImmutableEditableObjectAdapter)\n\n```bash\ndotnet add package ImmutableEditableObjectAdapter\n```\n\n# ImmutableEditableObjectAdapter\n\nAdapts immutable state `record`s into an `IEditableObject` replacing the `record` on edit, intended for `Binding` in a `DataGrid`. \n\n```csharp\nusing System.ComponentModel;\n\nPerson p = new(\"Max\", \"Green\", DateTimeOffset.Now.AddYears(-43), null);\nEditablePerson editable = new(p);\neditable.Edited += (s, e) =\u003e p = s.IsPropertyChanged(nameof(Person.Name)) ? e.NewValue : p;\neditable.BeginEdit();\neditable.Name = \"Müller\";\neditable.EndEdit();\nConsole.WriteLine(\"Hello, World!\");\n\ninternal sealed record Person(string Name, string FavouriteColor, DateTimeOffset BirthDay, DateTimeOffset? DeceasedAt);\n\ninternal sealed partial class EditablePerson : ImmutableEditableObjectAdapter\u003cPerson\u003e;\n```\n\nFeel free to review the [generated code](https://github.com/ProphetLamb/ImmutableEditableObjectAdapter/blob/main/sample/ImmutableEditableObjectAdapter.Samples/GeneratedFiles/ImmutableEditableObjectAdapter/ImmutableEditableObjectAdapter.ImmutableEditableObjectAdapterGenerator/EditablePerson.g.cs#L7) for this example.\n\nGenerated `ImmutableEditableObjectAdapter` types mirrors the `public` Properties of the `record` passed as a generic type parameter. However, all properties have setter.\nEach property, set to a different value than the property in the `Unedited` reference, is used to reconstruct `Unedited` into a new `record`:\n\n```csharp\nPerson edited = Unedited with {\n  Name = NamePropertyChanged ? Name : Unedited.Name,\n}\n```\n\nThe constructed record is passed as `NewValue` to the `Edited` event, then set as the new `Unedited`.\n\n## UNO Platform Integration\n\nhttps://github.com/user-attachments/assets/26737d4e-9eeb-48d0-814c-687048ce235d\n\nBinding commands to edits in UNO requires\n- a `IValueConverter`\n- a `ICommand` attached property\n\nin addition to the above example. `ImmutableEditableObjectAdapter` generates these implementations.\n\n**Declare the models**\n\n```csharp\nnamespace ImmutableEditableObjectAdapter.Samples.Uno.Models;\n\npublic sealed record Person(\n  string Name,\n  [property: Display(Name = \"Color\")] string FavouriteColor,\n  DateTimeOffset BirthDay,\n  DateTimeOffset? DeceasedAt\n);\n\npublic sealed partial class EditablePerson : System.ComponentModel.ImmutableEditableObjectAdapter\u003cPerson\u003e;\n```\n\n**Declare the converter**\n\nAnnotate the `EditablePersonValueConverter` implementing `IValueConverter` with the `ImmutableEditableValueConverter` attribute for the type `EditablePerson`.\n\n```csharp\nnamespace ImmutableEditableObjectAdapter.Samples.Uno.Converters;\n\n[ImmutableEditableValueConverter(typeof(EditablePerson))]\npublic sealed partial class EditablePersonValueConverter : IValueConverter;\n```\n\n**Create the model**\n\n- `Persons` provides data for the `DataGrid`.\n- `LastEdited` informs the user about the latest changes.\n- `PersonChanged` is invoked when a person changed.\n\n```csharp\nnamespace ImmutableEditableObjectAdapter.Samples.Uno.Presentation;\n\npublic partial record MainModel\n{\n    public IState\u003cPerson\u003e LastEdited =\u003e State\u003cPerson\u003e.Empty(this); \n    \n    public IListState\u003cPerson\u003e Persons =\u003e ListState.Value(this, IImmutableList\u003cPerson\u003e () =\u003e [\n        new(\"Max\", \"Green\", DateTimeOffset.Now.AddYears(-43), null),\n        new(\"Günter\", \"Orange\", DateTimeOffset.Now.AddYears(-32), null),\n    ]);\n    \n    public async Task PersonChanged(EditedEventArgs\u003cPerson\u003e edited)\n    {\n        if (edited.CancelledOrUnchanged)\n        {\n            return;\n        }\n    \n        await LastEdited.UpdateAsync(_ =\u003e edited.NewValue);\n    }\n}\n```\n\n**Create the UI**\n\n```xml\n\u003cPage.Resources\u003e\n  \u003cconverters:EditablePersonValueConverter x:Key=\"EditablePersonValueConverter\" /\u003e\n\u003c/Page.Resources\u003e\n```\n\n```xml\n\u003cTextBox IsReadOnly=\"True\" Header=\"Changed Name\" Text=\"{Binding LastEdited.Name}\" /\u003e\n\u003cTextBox IsReadOnly=\"True\" Header=\"Changed Favourite Colour\" Text=\"{Binding LastEdited.FavouriteColor}\" /\u003e\n\n\u003cui:FeedView Source=\"{Binding Persons, Converter={StaticResource EditablePersonValueConverter}}\"\u003e\n  \u003cui:FeedView.ValueTemplate\u003e\n    \u003cDataTemplate\u003e\n      \u003cwuc:DataGrid\n        ItemsSource=\"{Binding Data, Mode=TwoWay}\"\n        utu:EditableExtensions.Command=\"{utu:AncestorBinding Path=DataContext.PersonChanged, AncestorType=ui:FeedView}\"\u003e\n      \u003c/wuc:DataGrid\u003e\n    \u003c/DataTemplate\u003e\n  \u003c/ui:FeedView.ValueTemplate\u003e\n\u003c/ui:FeedView\u003e\n```\n\nFeel free to review the [generated code](https://github.com/ProphetLamb/ImmutableEditableObjectAdapter/blob/main/sample/ImmutableEditableObjectAdapter.Samples.Uno/ImmutableEditableObjectAdapter.Samples.Uno/GeneratedFiles/ImmutableEditableObjectAdapter/ImmutableEditableObjectAdapter.ImmutableEditableObjectAdapterGenerator/EditablePersonValueConverter.g.cs#L5) for this example.\n\n## Customization and API\n\n`ImmutableEditableObjectAdapter` API allows customizing the creation of events.\n\n- OnPropertyChanging\n- OnPropertyChanged\n- OnEdited\n\n```csharp\n/// \u003csummary\u003e\n/// Provides the old, and new value of the \u003csee cref=\"EditedEventHandler{TContract}\"/\u003e.\n/// \u003c/summary\u003e\n/// \u003ctypeparam name=\"TContract\"\u003eThe type of the contract \u003cc\u003erecord\u003c/c\u003e.\u003c/typeparam\u003e\npublic sealed class EditedEventArgs\u003cTContract\u003e : EventArgs\n{\n    public TContract OldValue { get; }\n    public TContract NewValue { get; }\n    public bool CancelledOrUnchanged { get; }\n}\n\n/// \u003csummary\u003e\n/// Represents the method that will handle the \u003csee cref=\"ImmutableEditableObjectAdapter{TContract}.Edited\"/\u003e event of an \u003csee cref=\"ImmutableEditableObjectAdapter{TContract}\"/\u003e instance.\n/// \u003c/summary\u003e\n/// \u003ctypeparam name=\"TContract\"\u003eThe type of the contract \u003cc\u003erecord\u003c/c\u003e.\u003c/typeparam\u003e\npublic delegate void EditedEventHandler\u003cTContract\u003e(\n    ImmutableEditableObjectAdapter\u003cTContract\u003e sender,\n    EditedEventArgs\u003cTContract\u003e args\n) where TContract : notnull;\n\n/// \u003csummary\u003e\n/// Non-generic interface implemented by \u003csee cref=\"ImmutableEditableObjectAdapter{TContract}\"/\u003e.\n/// \u003c/summary\u003e\npublic interface IImmutableEditableObjectAdapter : IEditableObject, INotifyPropertyChanged, INotifyPropertyChanging\n{\n   /// \u003csummary\u003e\n   /// Occurs once, before \u003csee cref=\"IEditableObject.EndEdit\"/\u003e replaces the immutable state \u003cc\u003erecord\u003c/c\u003e, or \u003csee cref=\"IEditableObject.CancelEdit\"/\u003e discards changes.\n   /// \u003cbr/\u003e\n   /// sender is \u003ccref see=\"ImmutableEditableObjectAdapter{TContract}\"/\u003e\n   /// \u003cbr/\u003e\n   /// event args is \u003ccref see=\"EditedEventArgs{TContract}\"/\u003e\n   /// \u003c/summary\u003e\n   void RegisterOnce(EventHandler callback);\n}\n\n/// \u003csummary\u003e\n/// Derive a \u003cc\u003esealed partial class\u003c/c\u003e to generate a \u003csee cref=\"IEditableObject\"/\u003e from a immutable state \u003cc\u003erecord\u003c/c\u003e type.\n/// \u003cbr/\u003e\n/// Update the immutable state when the \u003csee cref=\"Edited\"/\u003e event indicates the state is replaced.\n/// \u003c/summary\u003e\n/// \u003ctypeparam name=\"TContract\"\u003eThe type of the contract \u003cc\u003erecord\u003c/c\u003e.\u003c/typeparam\u003e\npublic abstract class ImmutableEditableObjectAdapter\u003cTContract\u003e\n    : IImmutableEditableObjectAdapter\n    where TContract : notnull\n{\n    /// \u003cinheritdoc /\u003e\n    public event PropertyChangedEventHandler? PropertyChanged;\n\n    /// \u003cinheritdoc /\u003e\n    public event PropertyChangingEventHandler? PropertyChanging;\n\n    /// \u003csummary\u003e\n    /// Occurs before \u003csee cref=\"EndEdit\"/\u003e replaces the immutable state \u003cc\u003erecord\u003c/c\u003e.\n    /// \u003c/summary\u003e\n    public event EditedEventHandler\u003cTContract\u003e? Edited;\n\n    /// \u003cinheritdoc /\u003e\n    public abstract void BeginEdit();\n\n    /// \u003cinheritdoc /\u003e\n    public abstract void CancelEdit();\n\n    /// \u003cinheritdoc /\u003e\n    public abstract void EndEdit();\n\n    /// \u003csummary\u003e\n    /// Enumerate names of all changed properties during edit, and \u003csee cref=\"Edited\"/\u003e.\n    /// \u003c/summary\u003e\n    public abstract IEnumerable\u003cstring\u003e ChangedProperties();\n\n    /// \u003csummary\u003e\n    /// Indicates whether the property with the name name has changed during edit, and \u003csee cref=\"Edited\"/\u003e.\n    /// \u003c/summary\u003e\n    public abstract bool IsPropertyChanged(string propertyName);\n\n    protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null);\n    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null);\n    protected virtual void OnEdited(TContract oldValue, TContract newValue);\n    protected bool SetField\u003cT\u003e(ref T field, T value, [CallerMemberName] string? propertyName = null);\n}\n\n/// \u003csummary\u003e\n/// Generates an \u003csee cref=\"Microsoft.UI.Xaml.Data.IValueConverter\"/\u003e for your \u003csee cref=\"ImmutableEditableObjectAdapter{TContract}\"/\u003e type, by annotating it with the converter type you wish to generate the members of.\n/// \u003c/summary\u003e\n/// \u003cparam name=\"valueConverterToGenerateType\"\u003eThe \u003cc\u003esealed partial class\u003c/c\u003e type of the \u003csee cref=\"Microsoft.UI.Xaml.Data.IValueConverter\"/\u003e to generate.\u003c/param\u003e\npublic sealed class ImmutableEditableValueConverterAttribute(Type valueConverterToGenerateType) : Attribute\n{\n   public Type ValueConverterToGenerateType { get; } = valueConverterToGenerateType;\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprophetlamb%2Fimmutableeditableobjectadapter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprophetlamb%2Fimmutableeditableobjectadapter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprophetlamb%2Fimmutableeditableobjectadapter/lists"}