https://github.com/prophetlamb/immutableeditableobjectadapter
Source generator adapting immutable state `record`s into an `IEditableObject` replacing the `record` on edit, intended for `Binding` in a UNO WindowsCommunityToolkit `DataGrid`.
https://github.com/prophetlamb/immutableeditableobjectadapter
Last synced: 5 months ago
JSON representation
Source generator adapting immutable state `record`s into an `IEditableObject` replacing the `record` on edit, intended for `Binding` in a UNO WindowsCommunityToolkit `DataGrid`.
- Host: GitHub
- URL: https://github.com/prophetlamb/immutableeditableobjectadapter
- Owner: ProphetLamb
- License: mit
- Created: 2025-05-15T16:25:10.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-06-09T12:55:59.000Z (about 1 year ago)
- Last Synced: 2025-06-20T18:09:07.218Z (about 1 year ago)
- Language: C#
- Size: 273 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Contributing: .github/CONTRIBUTING.md
- License: LICENSE
- Code of conduct: .github/CODE_OF_CONDUCT.md
- Security: .github/SECURITY.md
Awesome Lists containing this project
README
[](https://www.nuget.org/packages/ImmutableEditableObjectAdapter) [](https://www.nuget.org/packages/ImmutableEditableObjectAdapter)
```bash
dotnet add package ImmutableEditableObjectAdapter
```
# ImmutableEditableObjectAdapter
Adapts immutable state `record`s into an `IEditableObject` replacing the `record` on edit, intended for `Binding` in a `DataGrid`.
```csharp
using System.ComponentModel;
Person p = new("Max", "Green", DateTimeOffset.Now.AddYears(-43), null);
EditablePerson editable = new(p);
editable.Edited += (s, e) => p = s.IsPropertyChanged(nameof(Person.Name)) ? e.NewValue : p;
editable.BeginEdit();
editable.Name = "Müller";
editable.EndEdit();
Console.WriteLine("Hello, World!");
internal sealed record Person(string Name, string FavouriteColor, DateTimeOffset BirthDay, DateTimeOffset? DeceasedAt);
internal sealed partial class EditablePerson : ImmutableEditableObjectAdapter;
```
Feel 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.
Generated `ImmutableEditableObjectAdapter` types mirrors the `public` Properties of the `record` passed as a generic type parameter. However, all properties have setter.
Each property, set to a different value than the property in the `Unedited` reference, is used to reconstruct `Unedited` into a new `record`:
```csharp
Person edited = Unedited with {
Name = NamePropertyChanged ? Name : Unedited.Name,
}
```
The constructed record is passed as `NewValue` to the `Edited` event, then set as the new `Unedited`.
## UNO Platform Integration
https://github.com/user-attachments/assets/26737d4e-9eeb-48d0-814c-687048ce235d
Binding commands to edits in UNO requires
- a `IValueConverter`
- a `ICommand` attached property
in addition to the above example. `ImmutableEditableObjectAdapter` generates these implementations.
**Declare the models**
```csharp
namespace ImmutableEditableObjectAdapter.Samples.Uno.Models;
public sealed record Person(
string Name,
[property: Display(Name = "Color")] string FavouriteColor,
DateTimeOffset BirthDay,
DateTimeOffset? DeceasedAt
);
public sealed partial class EditablePerson : System.ComponentModel.ImmutableEditableObjectAdapter;
```
**Declare the converter**
Annotate the `EditablePersonValueConverter` implementing `IValueConverter` with the `ImmutableEditableValueConverter` attribute for the type `EditablePerson`.
```csharp
namespace ImmutableEditableObjectAdapter.Samples.Uno.Converters;
[ImmutableEditableValueConverter(typeof(EditablePerson))]
public sealed partial class EditablePersonValueConverter : IValueConverter;
```
**Create the model**
- `Persons` provides data for the `DataGrid`.
- `LastEdited` informs the user about the latest changes.
- `PersonChanged` is invoked when a person changed.
```csharp
namespace ImmutableEditableObjectAdapter.Samples.Uno.Presentation;
public partial record MainModel
{
public IState LastEdited => State.Empty(this);
public IListState Persons => ListState.Value(this, IImmutableList () => [
new("Max", "Green", DateTimeOffset.Now.AddYears(-43), null),
new("Günter", "Orange", DateTimeOffset.Now.AddYears(-32), null),
]);
public async Task PersonChanged(EditedEventArgs edited)
{
if (edited.CancelledOrUnchanged)
{
return;
}
await LastEdited.UpdateAsync(_ => edited.NewValue);
}
}
```
**Create the UI**
```xml
```
```xml
```
Feel 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.
## Customization and API
`ImmutableEditableObjectAdapter` API allows customizing the creation of events.
- OnPropertyChanging
- OnPropertyChanged
- OnEdited
```csharp
///
/// Provides the old, and new value of the .
///
/// The type of the contract record.
public sealed class EditedEventArgs : EventArgs
{
public TContract OldValue { get; }
public TContract NewValue { get; }
public bool CancelledOrUnchanged { get; }
}
///
/// Represents the method that will handle the event of an instance.
///
/// The type of the contract record.
public delegate void EditedEventHandler(
ImmutableEditableObjectAdapter sender,
EditedEventArgs args
) where TContract : notnull;
///
/// Non-generic interface implemented by .
///
public interface IImmutableEditableObjectAdapter : IEditableObject, INotifyPropertyChanged, INotifyPropertyChanging
{
///
/// Occurs once, before replaces the immutable state record, or discards changes.
///
/// sender is
///
/// event args is
///
void RegisterOnce(EventHandler callback);
}
///
/// Derive a sealed partial class to generate a from a immutable state record type.
///
/// Update the immutable state when the event indicates the state is replaced.
///
/// The type of the contract record.
public abstract class ImmutableEditableObjectAdapter
: IImmutableEditableObjectAdapter
where TContract : notnull
{
///
public event PropertyChangedEventHandler? PropertyChanged;
///
public event PropertyChangingEventHandler? PropertyChanging;
///
/// Occurs before replaces the immutable state record.
///
public event EditedEventHandler? Edited;
///
public abstract void BeginEdit();
///
public abstract void CancelEdit();
///
public abstract void EndEdit();
///
/// Enumerate names of all changed properties during edit, and .
///
public abstract IEnumerable ChangedProperties();
///
/// Indicates whether the property with the name name has changed during edit, and .
///
public abstract bool IsPropertyChanged(string propertyName);
protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null);
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null);
protected virtual void OnEdited(TContract oldValue, TContract newValue);
protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null);
}
///
/// Generates an for your type, by annotating it with the converter type you wish to generate the members of.
///
/// The sealed partial class type of the to generate.
public sealed class ImmutableEditableValueConverterAttribute(Type valueConverterToGenerateType) : Attribute
{
public Type ValueConverterToGenerateType { get; } = valueConverterToGenerateType;
}
```