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

Change the Store to have a typed Payload explaining what the change has been in StateHasChangedEventArgs when mutating the State of the Store. #18

Merged
merged 3 commits into from
Jul 10, 2023
Merged
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
12 changes: 3 additions & 9 deletions Memento.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{4EBD97E1-0
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{95938898-9BDC-4379-BFFF-A70E72877728}"
ProjectSection(SolutionItems) = preProject
docs\Blazor\DependencyInjection.md = docs\Blazor\DependencyInjection.md
docs\BasicConcept.md = docs\BasicConcept.md
docs\Blazor.md = docs\Blazor.md
docs\Middleware.md = docs\Middleware.md
docs\README.md = docs\README.md
docs\Tutorial.cs.md = docs\Tutorial.cs.md
docs\ReduxDevTools.md = docs\ReduxDevTools.md
EndProjectSection
EndProject
Expand All @@ -47,12 +48,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Memento.Sample.BlazorWasm",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Memento.Sample.ConsoleApp", "samples\Memento.Sample.ConsoleApp\Memento.Sample.ConsoleApp.csproj", "{A089BC00-80C4-4074-8127-C414F2B79585}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Blazor", "Blazor", "{395CB6B4-8E41-46EA-B984-F8C38FD4D482}"
ProjectSection(SolutionItems) = preProject
docs\Blazor\DependencyInjection.md = docs\Blazor\DependencyInjection.md
docs\Blazor\README.md = docs\Blazor\README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -116,7 +111,6 @@ Global
{4481A230-D66C-486F-B06A-29DECB3940FA} = {AD16691B-0034-4F49-8F90-0932C029DA3C}
{8DA8C555-F65E-4419-8732-7CAD00507166} = {AD16691B-0034-4F49-8F90-0932C029DA3C}
{A089BC00-80C4-4074-8127-C414F2B79585} = {AD16691B-0034-4F49-8F90-0932C029DA3C}
{395CB6B4-8E41-46EA-B984-F8C38FD4D482} = {95938898-9BDC-4379-BFFF-A70E72877728}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {52122159-362B-41B0-BE89-5FC9BF19BE01}
Expand Down
91 changes: 89 additions & 2 deletions docs/BasicConcept.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,96 @@ public class AsyncCounterStore : Store<AsyncCounterState> {
});
}
}



```

### Includes the typed Payload explaining what the change has been in StateHasChangedEventArgs

```Store<TState, TPayload>``` allows you to have a typed Payload explaining what the change has been in StateHasChangedEventArgs when mutating the State of the Store.


## Usage

Define the payload type.

```cs

public enum StateChangedType {
BeginLoading,
EndLoading,
SetCount,
Increment
}

```

The Payload type is specified in Store Type params in the following way.

```Store<AsyncCounterState, StateChangedType>```

If you set Payload as the second argument of ```Mutate(..., StateChangedType.CountUp)```, you can have Payload in the StateHasChangedEventArgs.

```cs

store.Subscribe(e => {
Console.WriteLine(e.Command.Payload); // Specified Paylaod
});

```

### Sample CounterStore Overview

```cs

public record CounterStoreState {
public int Count { get; init; } = 0;
public ImmutableArray<int> History { get; init; } = ImmutableArray.Create<int>();
public bool IsLoading { get; init; } = false;
}

public enum StateChangedType {
BeginLoading,
EndLoading,
SetCount,
Increment
}

public class CounterStore: Store<CounterStoreState, StateChangedType> {
public CounterStore() : base(() => new()) {
}

public async Task CountUpAsync() {
Mutate(state => state with { IsLoading = true }, StateChangedType.BeginLoading);

await Task.Delay(500);

Mutate(HandleIncrement, StateChangedType.Increment);
Mutate(state => state with { IsLoading = false }, StateChangedType.EndLoading);
}

private static CounterStoreState HandleIncrement(CounterStoreState state) {
var count = state.Count + 1;
return state with {
Count = count,
History = state.History.Add(count),
};
}

public void SetCount(int num) {
Mutate(state => state with {
Count = num,
History = state.History.Add(num),
}, StateChangedType.SetCount);
}
}

```

#### Sample Source

https://github.com/le-nn/memento/blob/main/samples/Memento.Sample.Blazor/Stores/AsyncCounterStore.cs

---

## Define FluxStore pattern

Expand Down
27 changes: 18 additions & 9 deletions samples/Memento.Sample.Blazor/Stores/AsyncCounterStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,53 @@ public record AsyncCounterState {
public ImmutableArray<int> Histories { get; init; } = ImmutableArray.Create<int>();
}

public class AsyncCounterStore : Store<AsyncCounterState> {
public AsyncCounterStore() : base(() => new()) { }
public enum StateChangedType {
CountUp,
Loading,
CountUpAsync,
SetCount,
CountUpWithAmount
}

public class AsyncCounterStore : Store<AsyncCounterState, StateChangedType> {
public AsyncCounterStore() : base(() => new()) {
}

public void CountUp() {
Mutate(state => state with {
Count = state.Count + 1,
IsLoading = false,
Histories = state.Histories.Add(state.Count + 1),
});
}, StateChangedType.CountUp);
}

public async Task CountUpAsync() {
Mutate(state => state with { IsLoading = true, });
Mutate(state => state with { IsLoading = true, }, StateChangedType.Loading);
await Task.Delay(800);
Mutate(state => state with {
Count = state.Count + 1,
IsLoading = false,
Histories = state.Histories.Add(state.Count + 1),
});
}, StateChangedType.CountUp);
}

public void CountUpManyTimes(int count) {
for (var i = 0; i < count; i++) {
Mutate(state => state with {
Count = state.Count + 1,
});
}, StateChangedType.CountUpAsync);
}
}

public void SetCount(int count) {
Mutate(state => state with {
Count = count,
});
}, StateChangedType.SetCount);
}

public void CountUpWithAmount(int amount) {
Mutate(state => state with {
Count = state.Count + amount,
});
}, StateChangedType.CountUpWithAmount);
}
}
}
2 changes: 1 addition & 1 deletion samples/Memento.Sample.Blazor/Stores/RedoUndoTodoStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public record RedoUndoTodoState {
public bool IsLoading { get; init; }
}

public class RedoUndoTodoStore : MementoStore<RedoUndoTodoState> {
public class RedoUndoTodoStore : MementoStore<RedoUndoTodoState, string> {
ITodoService TodoService { get; }

public RedoUndoTodoStore(ITodoService todoService)
Expand Down
2 changes: 1 addition & 1 deletion samples/Memento.Sample.ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
// Observe a store state
store.Subscribe(e => {
Console.WriteLine();
Console.WriteLine($"// {e.Command.GetType().Name}");
Console.WriteLine($"// {e.Command?.GetType().Name}");
Console.WriteLine(JsonSerializer.Serialize(
e.State,
new JsonSerializerOptions() {
Expand Down
29 changes: 15 additions & 14 deletions src/Memento.Core/AbstractStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Memento.Core;
/// <typeparam name="TState">The type of the state managed by the store.</typeparam>
/// <typeparam name="TCommand">The type of the commands used to mutate the state.</typeparam>
public abstract class AbstractStore<TState, TCommand>
: IStore, IObservable<StateChangedEventArgs<TState>>, IDisposable
: IStore, IObservable<StateChangedEventArgs<TState, TCommand>>, IDisposable
where TState : class
where TCommand : Command {
readonly List<IObserver<StateChangedEventArgs>> _observers = new();
Expand Down Expand Up @@ -92,8 +92,8 @@ protected virtual IEnumerable<IDisposable> OnHandleDisposable() {
/// </summary>
/// <param name="observer">The observer to subscribe to the store.</param>
/// <returns>An IDisposable instance that can be used to unsubscribe from the store.</returns>
public IDisposable Subscribe(Action<StateChangedEventArgs<TState>> observer) {
return Subscribe(new GeneralObserver<StateChangedEventArgs<TState>>(observer));
public IDisposable Subscribe(Action<StateChangedEventArgs<TState, TCommand>> observer) {
return Subscribe(new GeneralObserver<StateChangedEventArgs<TState, TCommand>>(observer));
}

/// <summary>
Expand All @@ -115,9 +115,9 @@ public TStore AsStore<TStore>() where TStore : IStore {
/// </summary>
/// <param name="observer">The observer to subscribe to the store.</param>
/// <returns>An IDisposable instance that can be used to unsubscribe from the store.</returns>
public IDisposable Subscribe(IObserver<StateChangedEventArgs<TState>> observer) {
public IDisposable Subscribe(IObserver<StateChangedEventArgs<TState, TCommand>> observer) {
var obs = new StoreObserver(e => {
if (e is StateChangedEventArgs<TState> o) {
if (e is StateChangedEventArgs<TState, TCommand> o) {
observer.OnNext(o);
}
});
Expand All @@ -139,10 +139,11 @@ public IDisposable Subscribe(IObserver<StateChangedEventArgs<TState>> observer)
/// via <see cref="FluxStore{TState, TCommand}.Dispatch"/> or <see cref="Store{TState}.Mutate"/>.
/// </summary>
public void StateHasChanged() {
InvokeObserver(new StateChangedEventArgs<TState>() {
InvokeObserver(new StateChangedEventArgs<TState, TCommand>() {
State = State,
LastState = State,
Command = new Command.StateHasChanged(State),
Command = null,
StateChangeType = StateChangeType.StateHasChanged,
Sender = this,
});
}
Expand Down Expand Up @@ -178,9 +179,9 @@ void IStore.SetStateForce(object state) {

var previous = State;
State = tState;
var command = new Command.ForceReplaced(State);
InvokeObserver(new StateChangedEventArgs<TState>() {
Command = command,
InvokeObserver(new StateChangedEventArgs<TState, TCommand>() {
Command = null,
StateChangeType = StateChangeType.ForceReplaced,
LastState = previous,
Sender = this,
State = State,
Expand Down Expand Up @@ -232,13 +233,13 @@ internal void ComputedAndApplyState(TState state, TCommand command) {
}
}

(TState?, StateChangedEventArgs<TState>?) ComputeNewState() {
(TState?, StateChangedEventArgs<TState, TCommand>?) ComputeNewState() {
var previous = state;
var postState = OnBeforeDispatch(previous, command);

if (MiddlewareHandler.Invoke(postState, command) is TState s) {
var newState = OnAfterDispatch(s, command);
var e = new StateChangedEventArgs<TState> {
var e = new StateChangedEventArgs<TState, TCommand> {
LastState = previous,
Command = command,
State = newState,
Expand Down Expand Up @@ -299,8 +300,8 @@ async Task IStore.InitializeAsync(StoreProvider provider) {
);
}

internal void InvokeObserver(StateChangedEventArgs<TState> e) {
foreach (var obs in _observers) {
internal void InvokeObserver(StateChangedEventArgs<TState, TCommand> e) {
foreach (var obs in _observers.ToArray()) {
obs.OnNext(e);
}
}
Expand Down
66 changes: 58 additions & 8 deletions src/Memento.Core/Command.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,73 @@
using System.Text.Json.Serialization;

namespace Memento.Core;

public enum StateHasChangedType {
StateHasChanged,
ForceReplaced,
Restored,
}

/// <summary>
/// Represents an abstract base class for commands that mutate the state of a store.
/// </summary>
public abstract record Command {
public record Command {
/// <summary>
/// Represents a command that forces a state replacement.
/// Represents a command that indicates a state change has occurred.
/// </summary>
public record ForceReplaced(object State) : Command;
public record StateHasChanged : Command {
public StateHasChangedType StateHasChangedType { get; internal set; }

/// <summary>
/// Represents a command that restores the previous state.
/// </summary>
public record Restored : Command;
public object State { get; }

public object? Message { get; }

[JsonIgnore]
public Type? StoreType { get; } = null;

public override string Type => $"{StoreType?.Name ?? "Store"}+{GetType().Name}";

public StateHasChanged(object state, object? message = null, Type? storeType = null) {
State = state;
Message = message;
StoreType = storeType;
}

public static StateHasChanged CreateForceReplaced(object State) => new(State) {
StateHasChangedType = StateHasChangedType.ForceReplaced
};

public static StateHasChanged CreateRestored(object State) => new(State) {
StateHasChangedType = StateHasChangedType.Restored
};
}

/// <summary>
/// Represents a command that indicates a state change has occurred.
/// </summary>
public record StateHasChanged(object State) : Command;
public record StateHasChanged<TState, TMessage> : StateHasChanged
where TState : notnull
where TMessage : notnull {

public new TState State => (TState)base.State;

public new TMessage? Message => (TMessage?)base.Message;

public override string Type => $"{StoreType?.Name ?? "Store"}+{GetType().Name}";

public StateHasChanged(TState mutateState, TMessage? message = default, Type? storeType = null)
: base(mutateState, message, storeType) {

}

public static StateHasChanged<TState, TMessage> CreateForceReplaced(TState State) => new(State) {
StateHasChangedType = StateHasChangedType.ForceReplaced
};

public static StateHasChanged<TState, TMessage> CreateRestored(TState State) => new(State) {
StateHasChangedType = StateHasChangedType.Restored
};
}

/// <summary>
/// Gets the type of the command, excluding the assembly name.
Expand Down
5 changes: 3 additions & 2 deletions src/Memento.Core/FluxMementoStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ await _historyManager.ExcuteCommitAsync(

var lastState = State;
State = state.State;
InvokeObserver(new StateChangedEventArgs<TState> {
InvokeObserver(new StateChangedEventArgs<TState,TCommand> {
LastState = lastState,
State = State,
Command = new Command.Restored(),
Command = null,
StateChangeType = StateChangeType.Restored,
Sender = this,
});
},
Expand Down
Loading