diff --git a/samples/Memento.Sample.Blazor/Stores/AsyncCounterStore.cs b/samples/Memento.Sample.Blazor/Stores/AsyncCounterStore.cs index 7be5225..c367e9a 100644 --- a/samples/Memento.Sample.Blazor/Stores/AsyncCounterStore.cs +++ b/samples/Memento.Sample.Blazor/Stores/AsyncCounterStore.cs @@ -10,44 +10,53 @@ public record AsyncCounterState { public ImmutableArray Histories { get; init; } = ImmutableArray.Create(); } -public class AsyncCounterStore : Store { - public AsyncCounterStore() : base(() => new()) { } +public enum StateChangedType { + CountUp, + Loading, + CountUpAsync, + SetCount, + CountUpWithAmount +} + +public class AsyncCounterStore : Store { + 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); } } \ No newline at end of file diff --git a/samples/Memento.Sample.Blazor/Stores/RedoUndoTodoStore.cs b/samples/Memento.Sample.Blazor/Stores/RedoUndoTodoStore.cs index 1eb9ff7..a5a5eef 100644 --- a/samples/Memento.Sample.Blazor/Stores/RedoUndoTodoStore.cs +++ b/samples/Memento.Sample.Blazor/Stores/RedoUndoTodoStore.cs @@ -9,7 +9,7 @@ public record RedoUndoTodoState { public bool IsLoading { get; init; } } -public class RedoUndoTodoStore : MementoStore { +public class RedoUndoTodoStore : MementoStore { ITodoService TodoService { get; } public RedoUndoTodoStore(ITodoService todoService) diff --git a/samples/Memento.Sample.ConsoleApp/Program.cs b/samples/Memento.Sample.ConsoleApp/Program.cs index 20e714e..610ac66 100644 --- a/samples/Memento.Sample.ConsoleApp/Program.cs +++ b/samples/Memento.Sample.ConsoleApp/Program.cs @@ -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() { diff --git a/src/Memento.Core/AbstractStore.cs b/src/Memento.Core/AbstractStore.cs index 2a6f998..941c86d 100644 --- a/src/Memento.Core/AbstractStore.cs +++ b/src/Memento.Core/AbstractStore.cs @@ -10,7 +10,7 @@ namespace Memento.Core; /// The type of the state managed by the store. /// The type of the commands used to mutate the state. public abstract class AbstractStore - : IStore, IObservable>, IDisposable + : IStore, IObservable>, IDisposable where TState : class where TCommand : Command { readonly List> _observers = new(); @@ -92,8 +92,8 @@ protected virtual IEnumerable OnHandleDisposable() { /// /// The observer to subscribe to the store. /// An IDisposable instance that can be used to unsubscribe from the store. - public IDisposable Subscribe(Action> observer) { - return Subscribe(new GeneralObserver>(observer)); + public IDisposable Subscribe(Action> observer) { + return Subscribe(new GeneralObserver>(observer)); } /// @@ -115,9 +115,9 @@ public TStore AsStore() where TStore : IStore { /// /// The observer to subscribe to the store. /// An IDisposable instance that can be used to unsubscribe from the store. - public IDisposable Subscribe(IObserver> observer) { + public IDisposable Subscribe(IObserver> observer) { var obs = new StoreObserver(e => { - if (e is StateChangedEventArgs o) { + if (e is StateChangedEventArgs o) { observer.OnNext(o); } }); @@ -139,10 +139,11 @@ public IDisposable Subscribe(IObserver> observer) /// via or . /// public void StateHasChanged() { - InvokeObserver(new StateChangedEventArgs() { + InvokeObserver(new StateChangedEventArgs() { State = State, LastState = State, - Command = new Command.StateHasChanged(State), + Command = null, + StateChangeType = StateChangeType.StateHasChanged, Sender = this, }); } @@ -178,9 +179,9 @@ void IStore.SetStateForce(object state) { var previous = State; State = tState; - var command = new Command.ForceReplaced(State); - InvokeObserver(new StateChangedEventArgs() { - Command = command, + InvokeObserver(new StateChangedEventArgs() { + Command = null, + StateChangeType = StateChangeType.ForceReplaced, LastState = previous, Sender = this, State = State, @@ -232,13 +233,13 @@ internal void ComputedAndApplyState(TState state, TCommand command) { } } - (TState?, StateChangedEventArgs?) ComputeNewState() { + (TState?, StateChangedEventArgs?) 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 { + var e = new StateChangedEventArgs { LastState = previous, Command = command, State = newState, @@ -299,8 +300,8 @@ async Task IStore.InitializeAsync(StoreProvider provider) { ); } - internal void InvokeObserver(StateChangedEventArgs e) { - foreach (var obs in _observers) { + internal void InvokeObserver(StateChangedEventArgs e) { + foreach (var obs in _observers.ToArray()) { obs.OnNext(e); } } diff --git a/src/Memento.Core/Command.cs b/src/Memento.Core/Command.cs index 927fa64..dbd1865 100644 --- a/src/Memento.Core/Command.cs +++ b/src/Memento.Core/Command.cs @@ -1,23 +1,73 @@ +using System.Text.Json.Serialization; + namespace Memento.Core; +public enum StateHasChangedType { + StateHasChanged, + ForceReplaced, + Restored, +} + /// /// Represents an abstract base class for commands that mutate the state of a store. /// -public abstract record Command { +public record Command { /// - /// Represents a command that forces a state replacement. + /// Represents a command that indicates a state change has occurred. /// - public record ForceReplaced(object State) : Command; + public record StateHasChanged : Command { + public StateHasChangedType StateHasChangedType { get; internal set; } - /// - /// Represents a command that restores the previous state. - /// - 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 + }; + } /// /// Represents a command that indicates a state change has occurred. /// - public record StateHasChanged(object State) : Command; + public record StateHasChanged : 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 CreateForceReplaced(TState State) => new(State) { + StateHasChangedType = StateHasChangedType.ForceReplaced + }; + + public static StateHasChanged CreateRestored(TState State) => new(State) { + StateHasChangedType = StateHasChangedType.Restored + }; + } /// /// Gets the type of the command, excluding the assembly name. diff --git a/src/Memento.Core/FluxMementoStore.cs b/src/Memento.Core/FluxMementoStore.cs index 9ccbce9..02042ba 100644 --- a/src/Memento.Core/FluxMementoStore.cs +++ b/src/Memento.Core/FluxMementoStore.cs @@ -91,10 +91,11 @@ await _historyManager.ExcuteCommitAsync( var lastState = State; State = state.State; - InvokeObserver(new StateChangedEventArgs { + InvokeObserver(new StateChangedEventArgs { LastState = lastState, State = State, - Command = new Command.Restored(), + Command = null, + StateChangeType = StateChangeType.Restored, Sender = this, }); }, diff --git a/src/Memento.Core/MementoStore.cs b/src/Memento.Core/MementoStore.cs index 02b05ca..9dfdda0 100644 --- a/src/Memento.Core/MementoStore.cs +++ b/src/Memento.Core/MementoStore.cs @@ -4,9 +4,10 @@ namespace Memento.Core; public record MementoStoreContext(TState State); -public abstract class MementoStore - : Store - where TState : class { +public abstract class MementoStore + : Store + where TState : class + where TMessage : notnull { readonly HistoryManager _historyManager; public bool CanReDo => _historyManager.CanReDo; @@ -84,10 +85,11 @@ await _historyManager.ExcuteCommitAsync( var lastState = State; State = state.State; - InvokeObserver(new StateChangedEventArgs { + InvokeObserver(new StateChangedEventArgs> { LastState = lastState, State = State, - Command = new Command.Restored(), + Command = null, + StateChangeType = StateChangeType.Restored, Sender = this, }); }, @@ -128,4 +130,10 @@ public async ValueTask ReExecuteAsync() { await _historyManager.ReExecuteAsync(); } +} + +public class MementoStore : MementoStore + where TState : class { + public MementoStore(StateInitializer initializer, HistoryManager historyManager) : base(initializer, historyManager) { + } } \ No newline at end of file diff --git a/src/Memento.Core/StateChangedEventArgs.cs b/src/Memento.Core/StateChangedEventArgs.cs index 2342b88..fd87f5c 100644 --- a/src/Memento.Core/StateChangedEventArgs.cs +++ b/src/Memento.Core/StateChangedEventArgs.cs @@ -1,9 +1,18 @@ namespace Memento.Core; +public enum StateChangeType { + Command, + StateHasChanged, + ForceReplaced, + Restored, +} + public record StateChangedEventArgs { protected object? sender; - public Command Command { get; init; } = default!; + public StateChangeType StateChangeType { get; init; } + + public Command? Command { get; init; } = default!; public object? LastState { get; init; } @@ -21,15 +30,21 @@ public IStore? Sender { } } -public record StateChangedEventArgs : StateChangedEventArgs - where TState : class { +public record StateChangedEventArgs : StateChangedEventArgs + where TState : class + where TCommand : Command { + + public new TCommand? Command { + get => (TCommand)base.Command!; + init => base.Command = value; + } - public new required TState LastState { + public new required TState? LastState { get => (TState)base.LastState!; init => base.LastState = value; } - public new required TState State { + public new required TState? State { get => (TState)base.State!; init => base.State = value; } diff --git a/src/Memento.Core/Store.cs b/src/Memento.Core/Store.cs index d765ba4..5e37e85 100644 --- a/src/Memento.Core/Store.cs +++ b/src/Memento.Core/Store.cs @@ -5,12 +5,16 @@ /// You can observe the state by subscribing to the StateChanged event. /// /// The type of state managed by the store. -public class Store : AbstractStore - where TState : class { +/// The type of message that describes what state change has occurred. +public class Store : AbstractStore> + where TState : class + where TMessage : notnull { + /// /// Initializes a new instance of the Store class. /// /// The state initializer for creating the initial state. + /// The type of message that describes what state change has occurred. public Store(StateInitializer initializer) : base(initializer, Reducer) { } @@ -18,26 +22,40 @@ public Store(StateInitializer initializer) : base(initializer, Reducer) /// Reduces the state using the provided StateHasChanged command. /// /// The current state. - /// The StateHasChanged command to apply. + /// The StateHasChanged command to apply. /// The new state after applying the command. - static TState Reducer(TState state, Command.StateHasChanged command) { - return (TState)command.State; + static TState Reducer(TState state, Command.StateHasChanged message) { + return (TState)message.State; } /// /// Mutates the state using a reducer function. /// /// The reducer function to apply. - public void Mutate(Func reducer) { + /// The message that describes what state change has occurred. + public void Mutate(Func reducer, TMessage? message = default) { var state = State; - ComputedAndApplyState(state, new Command.StateHasChanged(reducer(state))); + var type = GetType(); + ComputedAndApplyState(state, new Command.StateHasChanged(reducer(state), message, type)); } /// /// Mutates the state using a new state. /// /// The new state to apply. - public void Mutate(TState state) { - ComputedAndApplyState(State, new Command.StateHasChanged(state)); + /// The message that describes what state change has occurred. + public void Mutate(TState state, TMessage? command = default) { + ComputedAndApplyState(State, new Command.StateHasChanged(state, command, GetType())); + } +} + +/// +/// Represents a store for managing state of type TState. +/// You can observe the state by subscribing to the StateChanged event. +/// +/// The type of state managed by the store. +public class Store : Store> + where TState : class { + public Store(StateInitializer initializer) : base(initializer) { } } diff --git a/src/Memento.ReduxDevTool/ReduxDevToolMiddlewareHandler.cs b/src/Memento.ReduxDevTool/ReduxDevToolMiddlewareHandler.cs index e4eebf3..f62d801 100644 --- a/src/Memento.ReduxDevTool/ReduxDevToolMiddlewareHandler.cs +++ b/src/Memento.ReduxDevTool/ReduxDevToolMiddlewareHandler.cs @@ -97,7 +97,7 @@ protected override async Task OnInitializedAsync() { } public async Task SendAsync(StateChangedEventArgs e, RootState rootState, string stackTrace) { - if (e.Command is ForceReplaced) { + if (e.StateChangeType is StateChangeType.ForceReplaced) { return; } diff --git a/test/Memento.Test/Core/FluxStoreTest.cs b/test/Memento.Test/Core/FluxStoreTest.cs index b3112b1..15b306c 100644 --- a/test/Memento.Test/Core/FluxStoreTest.cs +++ b/test/Memento.Test/Core/FluxStoreTest.cs @@ -57,7 +57,7 @@ public async Task Ensure_CanChangeStateAsync(int count) { public async Task Command_CouldBeSubscribeCorrectly() { var store = new FluxAsyncCounterStore(); - var commands = new List(); + var commands = new List(); var lastState = store.State; using var subscription = store.Subscribe(e => { Assert.Equal(e.Sender, store); @@ -86,7 +86,7 @@ public async Task Command_CouldBeSubscribeCorrectly() { public async Task Force_ReplaceState() { var store = new FluxAsyncCounterStore(); - var commands = new List(); + var commands = new List(); var lastState = store.State; using var subscription = store.Subscribe(e => { @@ -94,7 +94,7 @@ public async Task Force_ReplaceState() { Assert.NotEqual(e.State, lastState); Assert.Equal(e.LastState, lastState); lastState = e.State; - commands.Add(e.Command); + commands.Add(e); }); await store.CountUpAsync(); @@ -108,14 +108,14 @@ public async Task Force_ReplaceState() { await store.CountUpAsync(); Assert.True(commands is [ - FluxAsyncCounterCommands.BeginLoading, - FluxAsyncCounterCommands.Increment, - FluxAsyncCounterCommands.EndLoading, - FluxAsyncCounterCommands.ModifyCount(1234), - Command.ForceReplaced { State: FluxAsyncCounterState { Count: 5678 } }, - FluxAsyncCounterCommands.BeginLoading, - FluxAsyncCounterCommands.Increment, - FluxAsyncCounterCommands.EndLoading + { Command: FluxAsyncCounterCommands.BeginLoading }, + { Command: FluxAsyncCounterCommands.Increment }, + { Command: FluxAsyncCounterCommands.EndLoading }, + { Command: FluxAsyncCounterCommands.ModifyCount(1234) }, + { State: FluxAsyncCounterState { Count: 5678 }, StateChangeType: StateChangeType.ForceReplaced }, + { Command: FluxAsyncCounterCommands.BeginLoading }, + { Command: FluxAsyncCounterCommands.Increment }, + { Command: FluxAsyncCounterCommands.EndLoading }, ]); } @@ -123,7 +123,7 @@ public async Task Force_ReplaceState() { public void Performance() { var store = new FluxAsyncCounterStore(); - var commands = new List(); + var commands = new List(); var lastState = store.State; using var subscription = store.Subscribe(e => { @@ -145,11 +145,11 @@ public void Performance() { [Fact] public void Ensure_StateHasChangedInvoked() { var store = new FluxAsyncCounterStore(); - var commands = new List(); + var commands = new List(); var lastState = store.State; using var subscription = store.Subscribe(e => { - commands.Add(e.Command); + commands.Add(e); }); store.StateHasChanged(); @@ -160,12 +160,12 @@ public void Ensure_StateHasChangedInvoked() { store.StateHasChanged(); Assert.True(commands is [ - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, ]); } } diff --git a/test/Memento.Test/Core/StoreTest.cs b/test/Memento.Test/Core/StoreTest.cs index 7ed3ea0..8e07e2f 100644 --- a/test/Memento.Test/Core/StoreTest.cs +++ b/test/Memento.Test/Core/StoreTest.cs @@ -1,5 +1,6 @@ using Memento.Core; using Memento.Test.Core.Mock; +using System.Data; using System.Diagnostics; namespace Memento.Test.Core; @@ -53,7 +54,7 @@ public async Task Ensure_CanChangeStateAsync(int count) { public async Task Command_CouldBeSubscribeCorrectly() { var store = new AsyncCounterStore(); - var commands = new List(); + var commands = new List(); var lastState = store.State; using var subscription = store.Subscribe(e => { Assert.Equal(e.Sender, store); @@ -82,7 +83,7 @@ public async Task Command_CouldBeSubscribeCorrectly() { public async Task Force_ReplaceState() { var store = new AsyncCounterStore(); - var commands = new List(); + var commands = new List(); var lastState = store.State; using var subscription = store.Subscribe(e => { @@ -90,7 +91,7 @@ public async Task Force_ReplaceState() { Assert.NotEqual(e.State, lastState); Assert.Equal(e.LastState, lastState); lastState = e.State; - commands.Add(e.Command); + commands.Add(e); }); await store.CountUpAsync(); @@ -104,14 +105,14 @@ public async Task Force_ReplaceState() { await store.CountUpAsync(); Assert.True(commands is [ - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, - Command.ForceReplaced { State: AsyncCounterState { Count: 5678 } }, - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, + { Command: Command.StateHasChanged }, + { Command: Command.StateHasChanged }, + { Command: Command.StateHasChanged }, + { Command: Command.StateHasChanged }, + { Command: null, State : AsyncCounterState { Count: 5678 } ,StateChangeType: StateChangeType.ForceReplaced}, + { Command: Command.StateHasChanged }, + { Command: Command.StateHasChanged }, + { Command: Command.StateHasChanged }, ]); } @@ -119,7 +120,7 @@ public async Task Force_ReplaceState() { public void Performance() { var store = new AsyncCounterStore(); - var commands = new List(); + var commands = new List(); var lastState = store.State; using var subscription = store.Subscribe(e => { @@ -142,11 +143,11 @@ public void Performance() { public void Ensure_StateHasChangedInvoked() { var store = new AsyncCounterStore(); - var commands = new List(); + var commands = new List(); var lastState = store.State; using var subscription = store.Subscribe(e => { - commands.Add(e.Command); + commands.Add(e); }); store.StateHasChanged(); @@ -157,12 +158,12 @@ public void Ensure_StateHasChangedInvoked() { store.StateHasChanged(); Assert.True(commands is [ - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, - Command.StateHasChanged, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, + { StateChangeType: StateChangeType.StateHasChanged }, ]); } }