diff --git a/Docs/disposable-callback-not-disposed.md b/Docs/disposable-callback-not-disposed.md index e19b8f50..7252caa2 100644 --- a/Docs/disposable-callback-not-disposed.md +++ b/Docs/disposable-callback-not-disposed.md @@ -1,7 +1,7 @@ # DisposableCallback was not disposed Components that descend from `FluxorComponent` or `FluxorLayout` automatically subscribe to the -`StateChanged` event on every `IState` property in the component automatically. When the component +`StateChanged` event on every `IState` and `IStateSelection` property in the component automatically. When the component is disposed, this subscription is removed, to avoid memory leaks. If ever you see an error message like the following diff --git a/Docs/releases.md b/Docs/releases.md index 3ce1e1a1..b2c107b5 100644 --- a/Docs/releases.md +++ b/Docs/releases.md @@ -3,6 +3,7 @@ ## New in 5.0 * Removed need to reference `_content/Fluxor.Blazor.Web/scripts/index.js` ([#235](https://github.com/mrpmorris/Fluxor/issues/235)) * Separated `IDispatcher` out of `IStore`. ([#209](https://github.com/mrpmorris/Fluxor/issues/209)) + * Added `IState` alternative `IStateSelector` for selecting and subscribing to subsets of state. ([#221](https://github.com/mrpmorris/Fluxor/issues/221)) ## New in 4.2.1 * Support .NET 6 diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index 9ceaece2..7fb7ef44 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -19,10 +19,11 @@ true false true + 9 - true + false NU5118 \ No newline at end of file diff --git a/Source/Fluxor.Blazor.Web.ReduxDevTools/Fluxor.Blazor.Web.ReduxDevTools.csproj b/Source/Fluxor.Blazor.Web.ReduxDevTools/Fluxor.Blazor.Web.ReduxDevTools.csproj index cc4ae6c9..df1edc1d 100644 --- a/Source/Fluxor.Blazor.Web.ReduxDevTools/Fluxor.Blazor.Web.ReduxDevTools.csproj +++ b/Source/Fluxor.Blazor.Web.ReduxDevTools/Fluxor.Blazor.Web.ReduxDevTools.csproj @@ -12,10 +12,6 @@ - - true - - diff --git a/Source/Fluxor.Blazor.Web/Components/FluxorComponent.cs b/Source/Fluxor.Blazor.Web/Components/FluxorComponent.cs index ef4c1682..8ce4ed52 100644 --- a/Source/Fluxor.Blazor.Web/Components/FluxorComponent.cs +++ b/Source/Fluxor.Blazor.Web/Components/FluxorComponent.cs @@ -5,7 +5,7 @@ namespace Fluxor.Blazor.Web.Components { /// - /// A component that auto-subscribes to state changes on all properties + /// A component that auto-subscribes to state changes on all properties /// and ensures is called /// public abstract class FluxorComponent : ComponentBase, IDisposable diff --git a/Source/Fluxor.Blazor.Web/Components/FluxorLayout.cs b/Source/Fluxor.Blazor.Web/Components/FluxorLayout.cs index e370da32..be095071 100644 --- a/Source/Fluxor.Blazor.Web/Components/FluxorLayout.cs +++ b/Source/Fluxor.Blazor.Web/Components/FluxorLayout.cs @@ -5,7 +5,7 @@ namespace Fluxor.Blazor.Web.Components { /// - /// A layout that auto-subscribes to state changes on all properties + /// A layout that auto-subscribes to state changes on all properties /// and ensures is called /// public abstract class FluxorLayout : LayoutComponentBase, IDisposable diff --git a/Source/Fluxor.Blazor.Web/Middlewares/Routing/Effects.cs b/Source/Fluxor.Blazor.Web/Middlewares/Routing/Effects.cs index 136c69c6..ee948522 100644 --- a/Source/Fluxor.Blazor.Web/Middlewares/Routing/Effects.cs +++ b/Source/Fluxor.Blazor.Web/Middlewares/Routing/Effects.cs @@ -14,7 +14,7 @@ public Effects(NavigationManager navigationManager) } [EffectMethod] - public Task HandleGoActionAsync(GoAction action, IDispatcher dispatcher) + public Task HandleGoActionAsync(GoAction action, IDispatcher _) { Uri fullUri = NavigationManager.ToAbsoluteUri(action.NewUri); if (fullUri.ToString() != NavigationManager.Uri || action.ForceLoad) diff --git a/Source/Fluxor.Blazor.Web/StoreInitializer.cs b/Source/Fluxor.Blazor.Web/StoreInitializer.cs index 3c43b20b..353ea6c8 100644 --- a/Source/Fluxor.Blazor.Web/StoreInitializer.cs +++ b/Source/Fluxor.Blazor.Web/StoreInitializer.cs @@ -99,23 +99,23 @@ protected virtual void Dispose(bool disposing) Store.UnhandledException -= OnUnhandledException; } - private void OnUnhandledException(object sender, Exceptions.UnhandledExceptionEventArgs args) + private void OnUnhandledException(object sender, Exceptions.UnhandledExceptionEventArgs e) { InvokeAsync(async () => { Exception exceptionThrownInHandler = null; try { - await UnhandledException.InvokeAsync(args).ConfigureAwait(false); + await UnhandledException.InvokeAsync(e).ConfigureAwait(false); } - catch (Exception e) + catch (Exception exception) { - exceptionThrownInHandler = e; + exceptionThrownInHandler = exception; } - if (exceptionThrownInHandler != null || !args.WasHandled) + if (exceptionThrownInHandler != null || !e.WasHandled) { - ExceptionToThrow = exceptionThrownInHandler ?? args.Exception; + ExceptionToThrow = exceptionThrownInHandler ?? e.Exception; StateHasChanged(); } }); diff --git a/Source/Fluxor/DependencyInjection/InfoFactories/EffectMethodInfoFactory.cs b/Source/Fluxor/DependencyInjection/InfoFactories/EffectMethodInfoFactory.cs index f46b16a9..25eeeaa1 100644 --- a/Source/Fluxor/DependencyInjection/InfoFactories/EffectMethodInfoFactory.cs +++ b/Source/Fluxor/DependencyInjection/InfoFactories/EffectMethodInfoFactory.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using System; using System.Collections.Generic; using System.Linq; using System.Reflection; diff --git a/Source/Fluxor/DependencyInjection/ServiceCollectionExtensions.cs b/Source/Fluxor/DependencyInjection/ServiceCollectionExtensions.cs index 1a6ec99b..5ac05916 100644 --- a/Source/Fluxor/DependencyInjection/ServiceCollectionExtensions.cs +++ b/Source/Fluxor/DependencyInjection/ServiceCollectionExtensions.cs @@ -43,6 +43,7 @@ public static IServiceCollection AddFluxor( typesToScan: options.TypesToScan, scanIncludeList: scanIncludeList); services.AddScoped(typeof(IState<>), typeof(State<>)); + services.AddScoped(typeof(IStateSelection<,>), typeof(StateSelection<,>)); return services; } diff --git a/Source/Fluxor/Feature.cs b/Source/Fluxor/Feature.cs index f6c2b8f0..3da2ac28 100644 --- a/Source/Fluxor/Feature.cs +++ b/Source/Fluxor/Feature.cs @@ -34,10 +34,10 @@ public abstract class Feature : IFeature /// /// A list of reducers registered with this feature /// - protected readonly List> Reducers = new List>(); + protected readonly List> Reducers = new(); private bool HasInitialState; - private SpinLock SpinLock = new SpinLock(); + private SpinLock SpinLock = new(); private readonly ThrottledInvoker TriggerStateChangedCallbacksThrottler; /// @@ -48,34 +48,17 @@ public Feature() TriggerStateChangedCallbacksThrottler = new ThrottledInvoker(TriggerStateChangedCallbacks); } - private EventHandler untypedStateChanged; - event EventHandler IFeature.StateChanged + private EventHandler _stateChanged; + public event EventHandler StateChanged { add { - SpinLock.ExecuteLocked(() => untypedStateChanged += value ); + SpinLock.ExecuteLocked(() => _stateChanged += value ); } remove { - SpinLock.ExecuteLocked(() => untypedStateChanged -= value); - } - } - - private EventHandler stateChanged; - /// - /// Event that is executed whenever the state changes - /// - public event EventHandler StateChanged - { - add - { - SpinLock.ExecuteLocked(() => stateChanged += value); - } - - remove - { - SpinLock.ExecuteLocked(() => stateChanged -= value); + SpinLock.ExecuteLocked(() => _stateChanged -= value); } } @@ -134,8 +117,7 @@ public virtual void ReceiveDispatchNotificationFromStore(object action) private void TriggerStateChangedCallbacks() { - stateChanged?.Invoke(this, State); - untypedStateChanged?.Invoke(this, EventArgs.Empty); + _stateChanged?.Invoke(this, EventArgs.Empty); } } } diff --git a/Source/Fluxor/IFeature.cs b/Source/Fluxor/IFeature.cs index 9b52633a..768e89ea 100644 --- a/Source/Fluxor/IFeature.cs +++ b/Source/Fluxor/IFeature.cs @@ -74,10 +74,5 @@ public interface IFeature : IFeature /// The reducer instance /// void AddReducer(IReducer reducer); - - /// - /// Event that is executed whenever the state changes - /// - new event EventHandler StateChanged; } } diff --git a/Source/Fluxor/IState.cs b/Source/Fluxor/IState.cs index 59d02ef7..771912fe 100644 --- a/Source/Fluxor/IState.cs +++ b/Source/Fluxor/IState.cs @@ -1,33 +1,15 @@ -using System; - -namespace Fluxor +namespace Fluxor { - /// - /// An interface that is injected into Blazor Components / pages for accessing - /// the state of an - /// - public interface IState - { - /// - /// Event that is executed whenever the state changes - /// - event EventHandler StateChanged; - } - /// /// An interface that is injected into Blazor Components / pages for accessing /// the state of an /// /// The type of the state - public interface IState : IState + public interface IState : IStateChangedNotifier { /// - /// Returns the current state of the feature + /// Returns the value selected from the feature state /// TState Value { get; } - /// - /// Event that is executed whenever the state changes - /// - new event EventHandler StateChanged; } } diff --git a/Source/Fluxor/IStateSelection.cs b/Source/Fluxor/IStateSelection.cs new file mode 100644 index 00000000..7b16fa1a --- /dev/null +++ b/Source/Fluxor/IStateSelection.cs @@ -0,0 +1,26 @@ +using System; + +namespace Fluxor +{ + /// + /// An interface that is injected into Blazor Components / pages for accessing + /// the a subset of state of an + /// + /// The type of the state + /// The type of the value selected from + public interface IStateSelection : IState + { + /// + /// Identifies the part of the feature state to select + /// + /// Function to select a value from the feature state + /// + /// Optional function used to check if two values are equal. + /// Used to determine if an update to state needs + /// to trigger a event + /// + void Select( + Func selector, + Func valueEquals = null); + } +} diff --git a/Source/Fluxor/IStateValueChangedNotifier.cs b/Source/Fluxor/IStateValueChangedNotifier.cs new file mode 100644 index 00000000..b2fd05cb --- /dev/null +++ b/Source/Fluxor/IStateValueChangedNotifier.cs @@ -0,0 +1,12 @@ +using System; + +namespace Fluxor +{ + public interface IStateChangedNotifier + { + /// + /// Event that is executed whenever the observed value of the state changes + /// + event EventHandler StateChanged; + } +} \ No newline at end of file diff --git a/Source/Fluxor/State.cs b/Source/Fluxor/State.cs index 9264046e..a99e21c3 100644 --- a/Source/Fluxor/State.cs +++ b/Source/Fluxor/State.cs @@ -1,44 +1,20 @@ -using System; - -namespace Fluxor +namespace Fluxor { /// /// A class that is injected into Blazor components/pages that provides access /// to an state. /// /// - public class State : IState + public class State : StateSelection, IStateSelection { - private readonly IFeature Feature; - - /// - /// Creates an instance of the state holder - /// - /// The feature that contains the state - public State(IFeature feature) - { - Feature = feature; - } - - /// - public TState Value => Feature.State; - - /// - /// Event that is executed whenever the state changes - /// - public event EventHandler StateChanged + public State(IFeature feature) : base(feature) { - add { Feature.StateChanged += value; } - remove { Feature.StateChanged -= value; } + Select( + x => x, // Select the state itself + valueEquals: DefaultObjectReferenceEquals); // Compare by object reference } - /// - /// Event that is executed whenever the state changes - /// - event EventHandler IState.StateChanged - { - add { (Feature as IFeature).StateChanged += value; } - remove { (Feature as IFeature).StateChanged -= value; } - } + private static bool DefaultObjectReferenceEquals(TState x, TState y) => + object.ReferenceEquals(x, y); } } diff --git a/Source/Fluxor/StateSelection.cs b/Source/Fluxor/StateSelection.cs new file mode 100644 index 00000000..1be64bf1 --- /dev/null +++ b/Source/Fluxor/StateSelection.cs @@ -0,0 +1,106 @@ +using Fluxor.Extensions; +using System; +using System.Threading; + +namespace Fluxor +{ + /// + /// A class that is injected into Blazor components/pages that provides access + /// to an state. + /// + /// + public class StateSelection : IStateSelection + { + private readonly IFeature Feature; + private bool HasSetSelector; + private TValue PreviousValue; + private Func Selector; + private Func ValueEquals; + private SpinLock SpinLock = new(); + private bool IsSubscribedToFeature => _stateChanged is not null; + + /// + /// Creates an instance of the state holder + /// + /// The feature that contains the state + public StateSelection(IFeature feature) + { + if (feature is null) + throw new ArgumentNullException(nameof(feature)); + + Feature = feature; + Selector = + _ => throw new InvalidOperationException($"Must call {nameof(Select)} before accessing {nameof(Value)}"); + ValueEquals = DefaultValueEquals; + } + + /// + public TValue Value => Selector(Feature.State); + + /// + public void Select( + Func selector, + Func valueEquals = null) + { + if (selector == null) + throw new ArgumentNullException(nameof(selector)); + + SpinLock.ExecuteLocked(() => + { + if (HasSetSelector) + throw new InvalidOperationException("Selector has alread been set"); + + Selector = selector; + HasSetSelector = true; + if (valueEquals is not null) + ValueEquals = valueEquals; + PreviousValue = Value; + }); + } + + private EventHandler _stateChanged; + /// + /// Event that is executed whenever the state changes + /// + public event EventHandler StateChanged + { + add + { + SpinLock.ExecuteLocked(() => + { + bool wasSubscribedToFeature = IsSubscribedToFeature; + _stateChanged += value; + if (!wasSubscribedToFeature) + Feature.StateChanged += FeatureStateChanged; + }); + } + remove + { + SpinLock.ExecuteLocked(() => + { + _stateChanged -= value; + if (!IsSubscribedToFeature) + Feature.StateChanged -= FeatureStateChanged; + }); + } + } + + private void FeatureStateChanged(object sender, EventArgs e) + { + if (!HasSetSelector) + return; + + TValue newValue = Selector(Feature.State); + if (ValueEquals(newValue, PreviousValue)) + return; + PreviousValue = newValue; + + _stateChanged?.Invoke(this, EventArgs.Empty); + } + + private static bool DefaultValueEquals(TValue x, TValue y) => + object.ReferenceEquals(x, y) + || (x as IEquatable)?.Equals(y) == true + || object.Equals(x, y); + } +} diff --git a/Source/Fluxor/StateSubscriber.cs b/Source/Fluxor/StateSubscriber.cs index 9192a4e2..eb473700 100644 --- a/Source/Fluxor/StateSubscriber.cs +++ b/Source/Fluxor/StateSubscriber.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using GetStateDelegate = System.Func; +using GetStateDelegate = System.Func; namespace Fluxor { @@ -27,7 +27,7 @@ static StateSubscriber() /// The object to scan for properties. /// The action to execute when one of the states are modified /// - public static IDisposable Subscribe(object subject, Action callback) + public static IDisposable Subscribe(object subject, Action callback) { if (subject == null) throw new ArgumentNullException(nameof(subject)); @@ -35,10 +35,10 @@ public static IDisposable Subscribe(object subject, Action callback) throw new ArgumentNullException(nameof(callback)); IEnumerable getStateDelegates = GetStateDelegatesForType(subject.GetType()); - var subscriptions = new List<(IState State, EventHandler Handler)>(); + var subscriptions = new List<(IStateChangedNotifier State, EventHandler Handler)>(); foreach (GetStateDelegate getState in getStateDelegates) { - var state = (IState)getState(subject); + var state = (IStateChangedNotifier)getState(subject); var handler = new EventHandler((s, a) => callback(state)); subscriptions.Add((state, handler)); @@ -75,8 +75,8 @@ private static IEnumerable GetStateDelegatesForType(Type type) Type stateType = currentProperty.PropertyType.GetGenericArguments()[0]; Type iStateType = typeof(IState<>).MakeGenericType(stateType); Type getterMethod = typeof(Func<,>).MakeGenericType(type, iStateType); - Delegate stronglyTypedDelegate = Delegate.CreateDelegate(getterMethod, currentProperty.GetGetMethod(true)); - var getValueDelegate = new GetStateDelegate(x => (IState)stronglyTypedDelegate.DynamicInvoke(x)); + var stronglyTypedDelegate = Delegate.CreateDelegate(getterMethod, currentProperty.GetGetMethod(true)); + var getValueDelegate = new GetStateDelegate(x => (IStateChangedNotifier)stronglyTypedDelegate.DynamicInvoke(x)); delegates.Add(getValueDelegate); } diff --git a/Tests/Fluxor.Blazor.Web.UnitTests/SupportFiles/MockState.cs b/Tests/Fluxor.Blazor.Web.UnitTests/SupportFiles/MockState.cs index 161d24ac..7b30ac74 100644 --- a/Tests/Fluxor.Blazor.Web.UnitTests/SupportFiles/MockState.cs +++ b/Tests/Fluxor.Blazor.Web.UnitTests/SupportFiles/MockState.cs @@ -2,16 +2,14 @@ namespace Fluxor.Blazor.Web.UnitTests.SupportFiles { - public class MockState : IState, IState + public class MockState : IStateChangedNotifier, IState { T IState.Value => throw new NotImplementedException(); public int SubscribeCount { get; private set; } public int UnsubscribeCount { get; private set; } - public int GenericSubscribeCount { get; private set; } - public int GenericUnsubscribeCount { get; private set; } - event EventHandler IState.StateChanged + event EventHandler IStateChangedNotifier.StateChanged { add { @@ -24,17 +22,5 @@ event EventHandler IState.StateChanged } } - event EventHandler IState.StateChanged - { - add - { - GenericSubscribeCount++; - } - - remove - { - GenericUnsubscribeCount++; - } - } } } diff --git a/Tests/Fluxor.UnitTests/Fluxor.UnitTests.csproj b/Tests/Fluxor.UnitTests/Fluxor.UnitTests.csproj index d7e64018..79834eba 100644 --- a/Tests/Fluxor.UnitTests/Fluxor.UnitTests.csproj +++ b/Tests/Fluxor.UnitTests/Fluxor.UnitTests.csproj @@ -2,7 +2,7 @@ netcoreapp3.1 - + 9 false diff --git a/Tests/Fluxor.UnitTests/StateSelectionTests/SelectTests.cs b/Tests/Fluxor.UnitTests/StateSelectionTests/SelectTests.cs new file mode 100644 index 00000000..bb05aaf2 --- /dev/null +++ b/Tests/Fluxor.UnitTests/StateSelectionTests/SelectTests.cs @@ -0,0 +1,15 @@ +using System; +using Xunit; + +namespace Fluxor.UnitTests.StateSelectionTests +{ + public class SelectTests : TestsBase + { + [Fact] + public void WhenSelectIsCalledTwice_ThenThrowsInvalidOperationException() + { + Subject.Select(x => x[0]); + Assert.Throws(() => Subject.Select(x => x[1])); + } + } +} diff --git a/Tests/Fluxor.UnitTests/StateSelectionTests/StateChangedTests.cs b/Tests/Fluxor.UnitTests/StateSelectionTests/StateChangedTests.cs new file mode 100644 index 00000000..a28de096 --- /dev/null +++ b/Tests/Fluxor.UnitTests/StateSelectionTests/StateChangedTests.cs @@ -0,0 +1,104 @@ +using System; +using Xunit; + +namespace Fluxor.UnitTests.StateSelectionTests +{ + public class StateChangedTests : TestsBase + { + [Fact] + public void WhenFeatureStateChanges_AndSelectedValueHasChanged_ThenTriggersStateChanged() + { + int subject1InvokeCount = 0; + Subject.StateChanged += (_, _) => subject1InvokeCount++; + Subject.Select(x => x[0]); + + int subject2InvokeCount = 0; + var subject2 = new StateSelection(MockFeature.Object); + subject2.StateChanged += (_, _) => subject2InvokeCount++; + subject2.Select(x => x[1]); + + FeatureState = "ABC"; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + Assert.Equal(1, subject1InvokeCount); + Assert.Equal(1, subject2InvokeCount); + + FeatureState = "BBC"; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + Assert.Equal(2, subject1InvokeCount); + Assert.Equal(1, subject2InvokeCount); + + FeatureState = "BYZ"; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + Assert.Equal(2, subject1InvokeCount); + Assert.Equal(2, subject2InvokeCount); + + FeatureState = "BYE"; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + Assert.Equal(2, subject1InvokeCount); + Assert.Equal(2, subject2InvokeCount); + } + + [Fact] + public void WhenFeatureStateChanges_AndSelectedValueIsTheSame_ThenDoesNotTriggerStateChanged() + { + int invokeCount = 0; + Subject.StateChanged += (_, _) => invokeCount++; + Subject.Select(x => x[0]); + + FeatureState = "ABC"; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + + FeatureState = "AYZ"; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + + Assert.Equal(1, invokeCount); + } + + [Fact] + public void WhenValueComparerSaysValueHasNotChanged_ThenDoesNotTriggerStateChanged() + { + int invokeCount = 0; + Subject.StateChanged += (_, _) => invokeCount++; + Subject.Select(x => x[0], valueEquals: (x, y) => x == '*' || x == y); + + FeatureState = "ABC"; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + Assert.Equal(1, invokeCount); + + FeatureState = "XYZ"; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + Assert.Equal(2, invokeCount); + + FeatureState = "*23"; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + Assert.Equal(2, invokeCount); + } + + [Fact] + public void WhenUserSetsSelector_ThenCurrentValueIsRetrievedFromState() + { + FeatureState = "ABC"; + int invokeCount = 0; + Subject.StateChanged += (_, _) => invokeCount++; + Subject.Select( + selector: x => x is null ? null : x[0], + valueEquals: (x, y) => x == '*' || x == y); + + FeatureState = null; + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + Assert.Equal(1, invokeCount); + } + + [Fact] + public void WhenFeatureStateChangesBeforeSelectorHasBeenSet_ThenDoesNotTriggerStateChanged() + { + int invokeCount = 0; + Subject.StateChanged += (_, _) => invokeCount++; + FeatureState = "ABC"; + + MockFeature.Raise(x => x.StateChanged += null, EventArgs.Empty); + + Assert.Equal(0, invokeCount); + } + } +} diff --git a/Tests/Fluxor.UnitTests/StateSelectionTests/TestsBase.cs b/Tests/Fluxor.UnitTests/StateSelectionTests/TestsBase.cs new file mode 100644 index 00000000..a419529f --- /dev/null +++ b/Tests/Fluxor.UnitTests/StateSelectionTests/TestsBase.cs @@ -0,0 +1,17 @@ +using Moq; + +namespace Fluxor.UnitTests.StateSelectionTests +{ + public class TestsBase + { + protected readonly IStateSelection Subject; + protected readonly Mock> MockFeature = new(); + protected string FeatureState = "---"; + + public TestsBase() + { + MockFeature.SetupGet(x => x.State).Returns(() => FeatureState); + Subject = new StateSelection(MockFeature.Object); + } + } +} diff --git a/Tests/Fluxor.UnitTests/StateSelectionTests/ValueTests.cs b/Tests/Fluxor.UnitTests/StateSelectionTests/ValueTests.cs new file mode 100644 index 00000000..b518d126 --- /dev/null +++ b/Tests/Fluxor.UnitTests/StateSelectionTests/ValueTests.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Fluxor.UnitTests.StateSelectionTests +{ + public class ValueTests : TestsBase + { + [Fact] + public void WhenReading_AndSelectHasNotBeenCalled_ThrowsInvalidOperationException() + { + var exception = Assert.Throws(() => Subject.Value); + Assert.Equal("Must call Select before accessing Value", exception.Message); + } + + [Fact] + public void WhenReading_AndSelectHasBeenCalled_ThenReturnsTransformedValue() + { + FeatureState = "ABC"; + Subject.Select(x => x[2]); + Assert.Equal('C', Subject.Value); + } + } +} diff --git a/Tests/Fluxor.UnitTests/StoreTests/UnhandledExceptionTests/UnhandledExceptionTests.cs b/Tests/Fluxor.UnitTests/StoreTests/UnhandledExceptionTests/UnhandledExceptionTests.cs index 40f42e42..cbd013ae 100644 --- a/Tests/Fluxor.UnitTests/StoreTests/UnhandledExceptionTests/UnhandledExceptionTests.cs +++ b/Tests/Fluxor.UnitTests/StoreTests/UnhandledExceptionTests/UnhandledExceptionTests.cs @@ -41,9 +41,9 @@ private async Task> SendAction(object action) var result = new List(); var resetEvent = new ManualResetEvent(false); - Subject.UnhandledException += (sender, args) => + Subject.UnhandledException += (sender, e) => { - result.Add(args.Exception); + result.Add(e.Exception); resetEvent.Set(); }; diff --git a/Tutorials/01-BasicConcepts/01A-StateActionsReducersTutorial/README.md b/Tutorials/01-BasicConcepts/01A-StateActionsReducersTutorial/README.md index 0949ba48..0929f10d 100644 --- a/Tutorials/01-BasicConcepts/01A-StateActionsReducersTutorial/README.md +++ b/Tutorials/01-BasicConcepts/01A-StateActionsReducersTutorial/README.md @@ -130,7 +130,7 @@ public class App - Add a method to show the contents of the state whenever it changes ```c# -private void CounterState_StateChanged(object sender, CounterState e) +private void CounterState_StateChanged(object sender, EventArgs e) { Console.WriteLine(""); Console.WriteLine("==========================> CounterState"); @@ -140,10 +140,6 @@ private void CounterState_StateChanged(object sender, CounterState e) } ``` -*Note: The current value of the state is also available in the parameter `e`, but this example shows how -to inject `IState` and retrieve the value from there.* - - #### Dispatching an Action to indicate our intention to change state - In `Store\CounterUseCase` create a new class `IncrementCounterAction`. This class can remain empty. @@ -172,7 +168,7 @@ public class App CounterState.StateChanged += CounterState_StateChanged; } - private void CounterState_StateChanged(object sender, CounterState e) + private void CounterState_StateChanged(object sender, EventArgs e) { Console.WriteLine(""); Console.WriteLine("==========================> CounterState"); diff --git a/Tutorials/01-BasicConcepts/01A-StateActionsReducersTutorial/StateActionsReducersTutorial/App.cs b/Tutorials/01-BasicConcepts/01A-StateActionsReducersTutorial/StateActionsReducersTutorial/App.cs index 9794d8ae..85ddc48f 100644 --- a/Tutorials/01-BasicConcepts/01A-StateActionsReducersTutorial/StateActionsReducersTutorial/App.cs +++ b/Tutorials/01-BasicConcepts/01A-StateActionsReducersTutorial/StateActionsReducersTutorial/App.cs @@ -4,55 +4,55 @@ namespace BasicConcepts.StateActionsReducersTutorial { - public class App + public class App + { + private readonly IStore Store; + public readonly IDispatcher Dispatcher; + public readonly IState CounterState; + + public App(IStore store, IDispatcher dispatcher, IState counterState) { - private readonly IStore Store; - public readonly IDispatcher Dispatcher; - public readonly IState CounterState; + Store = store; + Dispatcher = dispatcher; + CounterState = counterState; + CounterState.StateChanged += CounterState_StateChanged; + } - public App(IStore store, IDispatcher dispatcher, IState counterState) - { - Store = store; - Dispatcher = dispatcher; - CounterState = counterState; - CounterState.StateChanged += CounterState_StateChanged; - } + private void CounterState_StateChanged(object sender, EventArgs e) + { + Console.WriteLine(""); + Console.WriteLine("==========================> CounterState"); + Console.WriteLine("ClickCount is " + CounterState.Value.ClickCount); + Console.WriteLine("<========================== CounterState"); + Console.WriteLine(""); + } - private void CounterState_StateChanged(object sender, CounterState e) + public void Run() + { + Console.Clear(); + Console.WriteLine("Initializing store"); + Store.InitializeAsync().Wait(); + string input; + do { - Console.WriteLine(""); - Console.WriteLine("==========================> CounterState"); - Console.WriteLine("ClickCount is " + CounterState.Value.ClickCount); - Console.WriteLine("<========================== CounterState"); - Console.WriteLine(""); - } + Console.WriteLine("1: Increment counter"); + Console.WriteLine("x: Exit"); + Console.Write("> "); + input = Console.ReadLine(); - public void Run() - { - Console.Clear(); - Console.WriteLine("Initializing store"); - Store.InitializeAsync().Wait(); - string input = ""; - do + switch (input.ToLowerInvariant()) { - Console.WriteLine("1: Increment counter"); - Console.WriteLine("x: Exit"); - Console.Write("> "); - input = Console.ReadLine(); - - switch(input.ToLowerInvariant()) - { - case "1": - var action = new IncrementCounterAction(); - Dispatcher.Dispatch(action); - break; + case "1": + var action = new IncrementCounterAction(); + Dispatcher.Dispatch(action); + break; - case "x": - Console.WriteLine("Program terminated"); - return; - } + case "x": + Console.WriteLine("Program terminated"); + return; + } - } while (true); - } + } while (true); + } } } diff --git a/Tutorials/01-BasicConcepts/01B-EffectsTutorial/EffectsTutorial/App.cs b/Tutorials/01-BasicConcepts/01B-EffectsTutorial/EffectsTutorial/App.cs index d19dc1e0..0947c769 100644 --- a/Tutorials/01-BasicConcepts/01B-EffectsTutorial/EffectsTutorial/App.cs +++ b/Tutorials/01-BasicConcepts/01B-EffectsTutorial/EffectsTutorial/App.cs @@ -28,7 +28,7 @@ public App( WeatherState.StateChanged += WeatherState_StateChanged; } - private void CounterState_StateChanged(object sender, CounterState e) + private void CounterState_StateChanged(object sender, EventArgs e) { Console.WriteLine(""); Console.WriteLine("==========================> CounterState"); @@ -37,7 +37,7 @@ private void CounterState_StateChanged(object sender, CounterState e) Console.WriteLine(""); } - private void WeatherState_StateChanged(object sender, WeatherState e) + private void WeatherState_StateChanged(object sender, EventArgs e) { Console.WriteLine(""); Console.WriteLine("=========================> WeatherState"); @@ -61,7 +61,7 @@ public void Run() Console.Clear(); Console.WriteLine("Initializing store"); Store.InitializeAsync().Wait(); - string input = ""; + string input; do { Console.WriteLine("1: Increment counter"); diff --git a/Tutorials/01-BasicConcepts/01B-EffectsTutorial/README.md b/Tutorials/01-BasicConcepts/01B-EffectsTutorial/README.md index 25f297d3..27d3c293 100644 --- a/Tutorials/01-BasicConcepts/01B-EffectsTutorial/README.md +++ b/Tutorials/01-BasicConcepts/01B-EffectsTutorial/README.md @@ -129,7 +129,7 @@ public class App - Add the following code to output the current `WeatherState` to the console. ```c# -private void WeatherState_StateChanged(object sender, WeatherState e) +private void WeatherState_StateChanged(object sender, EventArgs e) { Console.WriteLine(""); Console.WriteLine("=========================> WeatherState"); @@ -149,11 +149,6 @@ private void WeatherState_StateChanged(object sender, WeatherState e) } ``` -*Note: As already mentioned, the current state is in the `e` parameter. As previously, this code -uses the injected state to demonstrate how to get hold of state at any point and not only when it is -updated*. - - #### Using an `Action` and a `Reducer` to alter state - In the `Store\WeatherUseCase` folder, create an empty class `FetchDataAction` (this can remain empty). diff --git a/Tutorials/02-Blazor/02A-StateActionsReducersTutorial/README.md b/Tutorials/02-Blazor/02A-StateActionsReducersTutorial/README.md index b7e53894..835a8fd3 100644 --- a/Tutorials/02-Blazor/02A-StateActionsReducersTutorial/README.md +++ b/Tutorials/02-Blazor/02A-StateActionsReducersTutorial/README.md @@ -128,7 +128,7 @@ Also, add the following line to the top of the razor file ``` *Note: This is required to ensure the component re-renders whenever its state changes. If you are unable -to descend from this component, you can instead subcribe to the `IState.StateChanged` event and execute +to descend from this component, you can instead subcribe to the `StateChanged` event and execute `InvokeAsync(StateHasChanged)`. If you do use the event, remember to implement `IDisposable` and unsubscribe from the event too, otherwise your app will leak memory.*