Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: (mvux) Add new UpdateItemasync Extension Method #2642

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions doc/Learn/Mvux/ListStates.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,60 @@ public async ValueTask TrimAll(CancellationToken ct = default)
}
```

Another overload is `UpdateAsync`, which allows you to apply an update on items that match a criteria. The `match` predicate is checked for each item. It the item matches the criteria, the updater is invoked which returns a new instance of the item with the update applied:
Another overload is `UpdateAllAsync`, which allows you to apply an update on items that match a criteria. The `match` predicate is checked for each item. If the item matches the criteria, the updater is invoked which returns a new instance of the item with the update applied:

```csharp
public async ValueTask TrimLongNames(CancellationToken ct = default)
{
await MyStrings.UpdateAsync(
await MyStrings.UpdateAllAsync(
match: item => item?.Length > 10,
updater: item => item.Trim(),
ct: ct);
}
```

Two more overloads are available for items that implement `IKeyEquatable<T>` :

`UpdateItemAsync` is another overload that allows you to apply an update on items that match the `key` of the specified item. The `key` is validated for each item. For every item that matches, the updater is invoked, returning a new instance of the item with the update applied:

> [!TIP]
> You don't have to implement `IKeyEquatable<T>` by yourself, see [generation](xref:Uno.Extensions.Equality.concept#generation) for more information.

```csharp
public partial record MyItem([property: Key] int Key, string Value);

IListState<MyItem> MyItems = ListState<MyItem>.Value(this, () => new[]
{
new MyItem(1, "One"),
new MyItem(2, "Two"),
new MyItem(3, "Three")
}.ToImmutableList());

public async ValueTask MakeUpperCase(CancellationToken ct = default)
{
var itemToUpdate = new MyItem(2, "Two");

await MyItems.UpdateItemAsync(
oldItem: itemToUpdate,
updater: item => item with { Value = item.Value.ToUpper()},
ct: ct);
}
```

Instead of using the `updater`, you can also pass in a new item to replace the old one:

```csharp
public async ValueTask MakeUpperCase(CancellationToken ct = default)
{
var itemToUpdate = new MyItem(2, "Two");

await MyItems.UpdateItemAsync(
oldItem: itemToUpdate,
newItem: new MyItem(2, "TWO"),
ct: ct);
}
```

#### Remove

The `RemoveAllAsync` method uses a predicate to determine which items are to be removed:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using Uno.Extensions.Reactive.Testing;
using KeyAttribute = Uno.Extensions.Equality.KeyAttribute;

namespace Uno.Extensions.Reactive.Tests.Operators;

Expand Down Expand Up @@ -733,6 +734,38 @@ public async Task WhenUpdateAllAsync_Then_ItemAdded()
}
#endregion

#region UpdateItemAsync
[TestMethod]
public async Task WhenUpdateItemAsync_WithValue_Then_ItemAdded()
{
LeItem itemToUpdate = new(2, 42);
var sut = ListState.Value(this, () => ImmutableList.Create<LeItem>(new(1,41), itemToUpdate, new(3, 43)));
var result = sut.Record();

await sut.UpdateItemAsync(itemToUpdate, new LeItem(2, 84), CT);

result.Should().Be(m => m
.Message(Items.Some(new LeItem(1, 41), new LeItem(2, 42), new LeItem(3, 43)))
.Message(Items.Some(new LeItem(1, 41), new LeItem(2, 84), new LeItem(3, 43)))
);
}

[TestMethod]
public async Task WhenUpdateItemAsync_WithUpdater_Then_ItemAdded()
{
LeItem itemToUpdate = new(3, 43);
var sut = ListState.Value(this, () => ImmutableList.Create<LeItem>(new(1, 41), new (2, 42), itemToUpdate));
var result = sut.Record();

await sut.UpdateItemAsync(itemToUpdate, i => new(3, i.Version * 3), CT);

result.Should().Be(m => m
.Message(Items.Some(new LeItem(1, 41), new LeItem(2, 42), new LeItem(3, 43)))
.Message(Items.Some(new LeItem(1, 41), new LeItem(2, 42), new LeItem(3, 129)))
);
}
#endregion

#region UpdateAsync
[TestMethod]
public async Task WhenUpdateAsync_Then_ItemAdded()
Expand All @@ -753,4 +786,6 @@ private record class MyClass;
private record struct MyStruct;

internal partial record class MyItem(int Id, int Version);

public partial record LeItem([property: Key] int Id, int Version);
}
108 changes: 108 additions & 0 deletions src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,114 @@ public static ValueTask AddAsync<T>(this IListState<T> state, T item, Cancellati
public static ValueTask RemoveAllAsync<T>(this IListState<T> state, Predicate<T> match, CancellationToken ct = default)
=> state.UpdateDataAsync(itemsOpt => itemsOpt.Map(items => items.RemoveAll(match)), ct);


/// <summary>
/// Updates all items from a list state that match the key of <paramref name="oldItem"/>.
/// </summary>
/// <typeparam name="T">The type of the items in the list.</typeparam>
/// <param name="state">The list state onto which the item should be added.</param>
/// <param name="oldItem">The old value of the item.</param>
/// <param name="newItem">The new value for the item.</param>
/// <param name="ct">A token to abort the async add operation.</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the ct param comment mentions it is for aborting the add operation. Probably just a copy-paste thing but this isn't necessarily an add operation. We could be more general and just mention it as an async operation

/// <returns></returns>
public static ValueTask UpdateItemAsync<T>(this IListState<T> state, T oldItem, T newItem, CancellationToken ct = default)
where T : notnull, IKeyEquatable<T>
=> state.UpdateDataAsync(
itemsOpt => itemsOpt.Map(items =>
{
var updated = items;
foreach (var item in items)
{
if (item.KeyEquals(oldItem))
{
updated = items.Replace(item, newItem);
}
}
return updated;
}),
ct);


/// <summary>
/// Updates all items from a list state that match the key of <paramref name="oldItem"/>.
/// </summary>
/// <typeparam name="T">The type of the items in the list.</typeparam>
/// <param name="state">The list state onto which the item should be added.</param>
/// <param name="oldItem">The old value of the item.</param>
/// <param name="newItem">The new value for the item.</param>
/// <param name="ct">A token to abort the async add operation.</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the ct param comment mentions it is for aborting the add operation. Probably just a copy-paste thing but this isn't necessarily an add operation. We could be more general and just mention it as an async operation

/// <returns></returns>
public static ValueTask UpdateItemAsync<T>(this IListState<T?> state, T oldItem, T? newItem, CancellationToken ct = default)
where T : struct, IKeyEquatable<T>
=> state.UpdateDataAsync(
itemsOpt => itemsOpt.Map(items =>
{
var updated = items;
foreach (var item in items)
{
if (item?.KeyEquals(oldItem) == true)
{
updated = items.Replace(item, newItem);
}
}
return updated;
}),
ct);


/// <summary>
/// Updates all items from a list state that match the key of <paramref name="oldItem"/>.
/// </summary>
/// <typeparam name="T">The type of the items in the list.</typeparam>
/// <param name="state">The list state onto which the item should be added.</param>
/// <param name="oldItem">The old value of the item.</param>
/// <param name="updater">How to update items.</param>
/// <param name="ct">A token to abort the async add operation.</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the ct param comment mentions it is for aborting the add operation. Probably just a copy-paste thing but this isn't necessarily an add operation. We could be more general and just mention it as an async operation

/// <returns></returns>
public static ValueTask UpdateItemAsync<T>(this IListState<T> state, T oldItem, Func<T, T> updater, CancellationToken ct = default)
where T : notnull, IKeyEquatable<T>
=> state.UpdateDataAsync(
itemsOpt => itemsOpt.Map(items =>
{
var updated = items;
foreach (var item in items)
{
if (item.KeyEquals(oldItem))
{
updated = items.Replace(item, updater(item));
}
}
return updated;
}),
ct);

/// <summary>
/// Updates all items from a list state that match the key of <paramref name="oldItem"/>.
/// </summary>
/// <typeparam name="T">The type of the items in the list.</typeparam>
/// <param name="state">The list state onto which the item should be added.</param>
/// <param name="oldItem">The old value of the item.</param>
/// <param name="updater">How to update items.</param>
/// <param name="ct">A token to abort the async add operation.</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the ct param comment mentions it is for aborting the add operation. Probably just a copy-paste thing but this isn't necessarily an add operation. We could be more general and just mention it as an async operation

/// <returns></returns>
public static ValueTask UpdateItemAsync<T>(this IListState<T?> state, T oldItem, Func<T?, T?> updater, CancellationToken ct = default)
where T : struct, IKeyEquatable<T>
=> state.UpdateDataAsync(
itemsOpt => itemsOpt.Map(items =>
{
var updated = items;
foreach (var item in items)
{
if (item?.KeyEquals(oldItem) == true)
{
updated = items.Replace(item, updater(item));
}
}
return updated;
}),
ct);


/// <summary>
/// Updates all matching items from a list state.
/// </summary>
Expand Down
Loading