dotnet add package ImmutableEditableObjectAdapter
Adapts immutable state record
s into an IEditableObject
replacing the record
on edit, intended for Binding
in a DataGrid
.
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<Person>;
Feel free to review the generated code 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
:
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
.
immutableeditableobjectadapter-samples-uno.mp4
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
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<Person>;
Declare the converter
Annotate the EditablePersonValueConverter
implementing IValueConverter
with the ImmutableEditableValueConverter
attribute for the type EditablePerson
.
namespace ImmutableEditableObjectAdapter.Samples.Uno.Converters;
[ImmutableEditableValueConverter(typeof(EditablePerson))]
public sealed partial class EditablePersonValueConverter : IValueConverter;
Create the model
Persons
provides data for theDataGrid
.LastEdited
informs the user about the latest changes.PersonChanged
is invoked when a person changed.
namespace ImmutableEditableObjectAdapter.Samples.Uno.Presentation;
public partial record MainModel
{
public IState<Person> LastEdited => State<Person>.Empty(this);
public IListState<Person> Persons => ListState.Value(this, IImmutableList<Person> () => [
new("Max", "Green", DateTimeOffset.Now.AddYears(-43), null),
new("Günter", "Orange", DateTimeOffset.Now.AddYears(-32), null),
]);
public async Task PersonChanged(EditedEventArgs<Person> edited)
{
if (edited.CancelledOrUnchanged)
{
return;
}
await LastEdited.UpdateAsync(_ => edited.NewValue);
}
}
Create the UI
<Page.Resources>
<converters:EditablePersonValueConverter x:Key="EditablePersonValueConverter" />
</Page.Resources>
<TextBox IsReadOnly="True" Header="Changed Name" Text="{Binding LastEdited.Name}" />
<TextBox IsReadOnly="True" Header="Changed Favourite Colour" Text="{Binding LastEdited.FavouriteColor}" />
<ui:FeedView Source="{Binding Persons, Converter={StaticResource EditablePersonValueConverter}}">
<ui:FeedView.ValueTemplate>
<DataTemplate>
<wuc:DataGrid
ItemsSource="{Binding Data, Mode=TwoWay}"
utu:EditableExtensions.Command="{utu:AncestorBinding Path=DataContext.PersonChanged, AncestorType=ui:FeedView}">
</wuc:DataGrid>
</DataTemplate>
</ui:FeedView.ValueTemplate>
</ui:FeedView>
Feel free to review the generated code for this example.
ImmutableEditableObjectAdapter
API allows customizing the creation of events.
- OnPropertyChanging
- OnPropertyChanged
- OnEdited
/// <summary>
/// Provides the old, and new value of the <see cref="EditedEventHandler{TContract}"/>.
/// </summary>
/// <typeparam name="TContract">The type of the contract <c>record</c>.</typeparam>
public sealed class EditedEventArgs<TContract> : EventArgs
{
public TContract OldValue { get; }
public TContract NewValue { get; }
public bool CancelledOrUnchanged { get; }
}
/// <summary>
/// Represents the method that will handle the <see cref="ImmutableEditableObjectAdapter{TContract}.Edited"/> event of an <see cref="ImmutableEditableObjectAdapter{TContract}"/> instance.
/// </summary>
/// <typeparam name="TContract">The type of the contract <c>record</c>.</typeparam>
public delegate void EditedEventHandler<TContract>(
ImmutableEditableObjectAdapter<TContract> sender,
EditedEventArgs<TContract> args
) where TContract : notnull;
/// <summary>
/// Non-generic interface implemented by <see cref="ImmutableEditableObjectAdapter{TContract}"/>.
/// </summary>
public interface IImmutableEditableObjectAdapter : IEditableObject, INotifyPropertyChanged, INotifyPropertyChanging
{
/// <summary>
/// Occurs once, before <see cref="IEditableObject.EndEdit"/> replaces the immutable state <c>record</c>, or <see cref="IEditableObject.CancelEdit"/> discards changes.
/// <br/>
/// sender is <cref see="ImmutableEditableObjectAdapter{TContract}"/>
/// <br/>
/// event args is <cref see="EditedEventArgs{TContract}"/>
/// </summary>
void RegisterOnce(EventHandler callback);
}
/// <summary>
/// Derive a <c>sealed partial class</c> to generate a <see cref="IEditableObject"/> from a immutable state <c>record</c> type.
/// <br/>
/// Update the immutable state when the <see cref="Edited"/> event indicates the state is replaced.
/// </summary>
/// <typeparam name="TContract">The type of the contract <c>record</c>.</typeparam>
public abstract class ImmutableEditableObjectAdapter<TContract>
: IImmutableEditableObjectAdapter
where TContract : notnull
{
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc />
public event PropertyChangingEventHandler? PropertyChanging;
/// <summary>
/// Occurs before <see cref="EndEdit"/> replaces the immutable state <c>record</c>.
/// </summary>
public event EditedEventHandler<TContract>? Edited;
/// <inheritdoc />
public abstract void BeginEdit();
/// <inheritdoc />
public abstract void CancelEdit();
/// <inheritdoc />
public abstract void EndEdit();
/// <summary>
/// Enumerate names of all changed properties during edit, and <see cref="Edited"/>.
/// </summary>
public abstract IEnumerable<string> ChangedProperties();
/// <summary>
/// Indicates whether the property with the name name has changed during edit, and <see cref="Edited"/>.
/// </summary>
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<T>(ref T field, T value, [CallerMemberName] string? propertyName = null);
}
/// <summary>
/// Generates an <see cref="Microsoft.UI.Xaml.Data.IValueConverter"/> for your <see cref="ImmutableEditableObjectAdapter{TContract}"/> type, by annotating it with the converter type you wish to generate the members of.
/// </summary>
/// <param name="valueConverterToGenerateType">The <c>sealed partial class</c> type of the <see cref="Microsoft.UI.Xaml.Data.IValueConverter"/> to generate.</param>
public sealed class ImmutableEditableValueConverterAttribute(Type valueConverterToGenerateType) : Attribute
{
public Type ValueConverterToGenerateType { get; } = valueConverterToGenerateType;
}