From 464e0952392b91b26c9945f0373da610d53deed0 Mon Sep 17 00:00:00 2001 From: Dion Williams Date: Tue, 9 May 2017 22:06:25 +0100 Subject: [PATCH] Initial commit of some Bridge.NET Flux bindings Features a basic Dispatcher, Store, ReduceStore and a React-based Container. --- .gitattributes | 63 ++++++ .gitignore | 261 +++++++++++++++++++++++++ Bridge.Flux.sln | 22 +++ Bridge.Flux/Bridge.Flux.csproj | 74 +++++++ Bridge.Flux/Dispatcher.cs | 151 ++++++++++++++ Bridge.Flux/IDispatchToken.cs | 5 + Bridge.Flux/IDispatcher.cs | 56 ++++++ Bridge.Flux/IDispatcherAction.cs | 5 + Bridge.Flux/Properties/AssemblyInfo.cs | 45 +++++ Bridge.Flux/Utils/Container.cs | 85 ++++++++ Bridge.Flux/Utils/ReduceStore.cs | 64 ++++++ Bridge.Flux/Utils/Store.cs | 40 ++++ Bridge.Flux/packages.config | 8 + LICENCE.txt | 19 ++ 14 files changed, 898 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Bridge.Flux.sln create mode 100644 Bridge.Flux/Bridge.Flux.csproj create mode 100644 Bridge.Flux/Dispatcher.cs create mode 100644 Bridge.Flux/IDispatchToken.cs create mode 100644 Bridge.Flux/IDispatcher.cs create mode 100644 Bridge.Flux/IDispatcherAction.cs create mode 100644 Bridge.Flux/Properties/AssemblyInfo.cs create mode 100644 Bridge.Flux/Utils/Container.cs create mode 100644 Bridge.Flux/Utils/ReduceStore.cs create mode 100644 Bridge.Flux/Utils/Store.cs create mode 100644 Bridge.Flux/packages.config create mode 100644 LICENCE.txt diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c4efe2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,261 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/Bridge.Flux.sln b/Bridge.Flux.sln new file mode 100644 index 0000000..d12cb29 --- /dev/null +++ b/Bridge.Flux.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26403.7 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bridge.Flux", "Bridge.Flux\Bridge.Flux.csproj", "{43FC4667-2824-4880-9BE7-AC941A2E2252}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {43FC4667-2824-4880-9BE7-AC941A2E2252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43FC4667-2824-4880-9BE7-AC941A2E2252}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43FC4667-2824-4880-9BE7-AC941A2E2252}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43FC4667-2824-4880-9BE7-AC941A2E2252}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Bridge.Flux/Bridge.Flux.csproj b/Bridge.Flux/Bridge.Flux.csproj new file mode 100644 index 0000000..59a1c62 --- /dev/null +++ b/Bridge.Flux/Bridge.Flux.csproj @@ -0,0 +1,74 @@ + + + + + Debug + AnyCPU + {43FC4667-2824-4880-9BE7-AC941A2E2252} + Library + Properties + Bridge.Flux + Bridge.Flux + v4.5.2 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + ..\packages\Bridge.Core.15.7.0\lib\net40\Bridge.dll + + + ..\packages\Bridge.Collections.1.3.4\lib\net40\Bridge.Collections.dll + + + ..\packages\Bridge.Html5.15.7.0\lib\net40\Bridge.Html5.dll + + + ..\packages\Bridge.React.1.12.6\lib\net40\Bridge.React.dll + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/Bridge.Flux/Dispatcher.cs b/Bridge.Flux/Dispatcher.cs new file mode 100644 index 0000000..026a5e3 --- /dev/null +++ b/Bridge.Flux/Dispatcher.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Bridge.Flux +{ + // TODO: Documentation + public sealed class Dispatcher : IDispatcher + { + private readonly Dictionary> _callbacks = new Dictionary>(); + private readonly HashSet _executingCallbacks = new HashSet(); + private readonly HashSet _finishedCallbacks = new HashSet(); + private bool _isDispatching = false; + private IDispatcherAction _currentAction; + + /// + /// Dispatches an action that will be sent to all callbacks registered with this dispatcher. + /// + /// The action to dispatch; may not be null. + /// + /// This method cannot be called while a dispatch is in progress. + /// + public void Dispatch(IDispatcherAction action) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + if (_isDispatching) + throw new InvalidOperationException("Cannot dispatch while already dispatching."); + + _isDispatching = true; + _currentAction = action; + _executingCallbacks.Clear(); + _finishedCallbacks.Clear(); + + foreach (var callback in _callbacks) + { + // Skip over callbacks that have already been called (by an earlier callback that used WaitFor) + if (_finishedCallbacks.Contains(callback.Key)) + continue; + + // TODO: Assert _executingCallbacks should NEVER have this callback already in its set because reasons (explain) + _executingCallbacks.Add(callback.Key); + callback.Value(_currentAction); + _executingCallbacks.Remove(callback.Key); + _finishedCallbacks.Add(callback.Key); + } + + _isDispatching = false; + _currentAction = null; + _finishedCallbacks.Clear(); + } + + /// + /// Registers a callback to receive actions dispatched through this dispatcher. + /// A token is returned to allow other callbacks to wait for this specific callback to be executed before another during a dispatch. + /// + /// The callback; may not be null. + /// A token to allow this callback to be unregistered or waited upon. + /// + /// This method cannot be called while a dispatch is in progress. + /// + public IDispatchToken Register(Action callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + if (_isDispatching) + throw new InvalidOperationException("Cannot register a dispatch token while dispatching."); + + var token = new DispatchToken(); + _callbacks.Add(token, callback); + return token; + } + + /// + /// Unregisters the callback associated with the given token. + /// + /// The dispatch token to unregister; may not be null. + /// + /// This method cannot be called while a dispatch is in progress. + /// + public void Unregister(IDispatchToken token) + { + if (token == null) + throw new ArgumentNullException(nameof(token)); + if (!_callbacks.ContainsKey(token)) + throw new ArgumentException("", nameof(token)); + + if (_isDispatching) + throw new InvalidOperationException("Cannot unregister a dispatch token while dispatching."); + + // TODO: FB Flux throws on a token that isn't registered but I've gone the nice approach here of not erroring if that's the case (since the operation is idempotent) + // Is there value in throwing if the token isn't registered? Just to enforce consumers be strict about their token usage? + _callbacks.Remove(token); + } + + /// + /// Waits for the callbacks associated with the given tokens to be called first during a dispatch operation. + /// + /// The tokens to wait on; may not be null. + /// + /// This method can only be called while a dispatch is in progress. + /// + public void WaitFor(IEnumerable tokens) + { + if (tokens == null) + throw new ArgumentNullException(nameof(tokens)); + if (!tokens.All(token => _callbacks.ContainsKey(token))) + throw new ArgumentException("All given tokens must be registered with this dispatcher.", nameof(tokens)); + + if (!_isDispatching) + throw new InvalidOperationException("Can only call WaitFor while dispatching."); + + // Ensure there isn't a circular dependency of tokens waiting on each other + // This check is done after the _isDispatching check because the state of _executingCallbacks is undefined if not currently dispatching + // TODO: Does this logic hold firm even DURING looping over the tokens in the foreach after this check? Need to visualise the state better. + if (tokens.Any(token => _executingCallbacks.Contains(token))) + throw new ArgumentException("None of the tokens can have its callback already executing.", nameof(tokens)); + + foreach (var token in tokens) + { + // Skip over callbacks that have already been called + if (_finishedCallbacks.Contains(token)) + continue; + + _executingCallbacks.Add(token); + _callbacks[token](_currentAction); + _executingCallbacks.Remove(token); + _finishedCallbacks.Add(token); + } + } + + /// + /// Waits for the callbacks associated with the given tokens to be called first during a dispatch operation. + /// + /// The tokens to wait on; may not be null. + /// + /// This method can only be called while a dispatch is in progress. + /// + public void WaitFor(params IDispatchToken[] tokens) + { + WaitFor((IEnumerable)tokens); + } + + private sealed class DispatchToken : IDispatchToken + { + // No need for any book-keeping here - dispatch tokens will differ based on reference equality + } + } +} diff --git a/Bridge.Flux/IDispatchToken.cs b/Bridge.Flux/IDispatchToken.cs new file mode 100644 index 0000000..a549d9e --- /dev/null +++ b/Bridge.Flux/IDispatchToken.cs @@ -0,0 +1,5 @@ +namespace Bridge.Flux +{ + // TODO: Documentation + public interface IDispatchToken { } +} diff --git a/Bridge.Flux/IDispatcher.cs b/Bridge.Flux/IDispatcher.cs new file mode 100644 index 0000000..433cc20 --- /dev/null +++ b/Bridge.Flux/IDispatcher.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace Bridge.Flux +{ + // TODO: Documentation + public interface IDispatcher + { + /// + /// Dispatches an action that will be sent to all callbacks registered with this dispatcher. + /// + /// The action to dispatch; may not be null. + /// + /// This method cannot be called while a dispatch is in progress. + /// + void Dispatch(IDispatcherAction action); + + /// + /// Registers a callback to receive actions dispatched through this dispatcher. + /// A token is returned to allow other callbacks to wait for this specific callback to be executed before another during a dispatch. + /// + /// The callback; may not be null. + /// A token to allow this callback to be unregistered or waited upon. + /// + /// This method cannot be called while a dispatch is in progress. + /// + IDispatchToken Register(Action callback); + + /// + /// Unregisters the callback associated with the given token. + /// + /// The dispatch token to unregister; may not be null. + /// + /// This method cannot be called while a dispatch is in progress. + /// + void Unregister(IDispatchToken token); + + /// + /// Waits for the callbacks associated with the given tokens to be called first during a dispatch operation. + /// + /// The tokens to wait on; may not be null. + /// + /// This method can only be called while a dispatch is in progress. + /// + void WaitFor(IEnumerable tokens); + + /// + /// Waits for the callbacks associated with the given tokens to be called first during a dispatch operation. + /// + /// The tokens to wait on; may not be null. + /// + /// This method can only be called while a dispatch is in progress. + /// + void WaitFor(params IDispatchToken[] tokens); + } +} diff --git a/Bridge.Flux/IDispatcherAction.cs b/Bridge.Flux/IDispatcherAction.cs new file mode 100644 index 0000000..75dc35a --- /dev/null +++ b/Bridge.Flux/IDispatcherAction.cs @@ -0,0 +1,5 @@ +namespace Bridge.Flux +{ + // TODO: Documentation + public interface IDispatcherAction { } +} diff --git a/Bridge.Flux/Properties/AssemblyInfo.cs b/Bridge.Flux/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6f8ba2b --- /dev/null +++ b/Bridge.Flux/Properties/AssemblyInfo.cs @@ -0,0 +1,45 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Bridge.Flux")] +[assembly: AssemblyDescription("Simple bindings for implementing a Flux architecture with Bridge.NET")] // TODO: These aren't really "bindings" but I can't think of a good word to describe these classes - they're essentially architecture enforcers/guidelines? +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Dion Williams")] +[assembly: AssemblyProduct("Bridge.Flux")] +[assembly: AssemblyCopyright("Copyright © 2017 Dion Williams")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("43fc4667-2824-4880-9be7-ac941a2e2252")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +// TODO: I'm not sure what to do about Assembly and Assembly File Versioning right now. +// I think when v1 hits we'll set AssemblyVersion to 1.0.0.0 and keep that until a +// breaking v2 release changes it to 2.0.0.0. Need to think about assembly conflicts +// and possibly automatic binding redirects could be helpful and not require us to limit +// to one major version number? +[assembly: AssemblyVersion("0.0.0.0")] +[assembly: AssemblyFileVersion("0.0.0.0")] + +// This version string is the official version of the library and is used by the NuGet package. +// It should follow the rules of Semantic Versioning (http://semver.org/). +// Not sure NuGet understands 0.y.z versions as being "experimental", so the "-alpha" suffix is also added. +[assembly: AssemblyInformationalVersion("0.1.0-alpha")] diff --git a/Bridge.Flux/Utils/Container.cs b/Bridge.Flux/Utils/Container.cs new file mode 100644 index 0000000..3817d32 --- /dev/null +++ b/Bridge.Flux/Utils/Container.cs @@ -0,0 +1,85 @@ +using Bridge.React; +using System; +using System.Collections.Generic; + +namespace Bridge.Flux.Utils +{ + /// + /// A Flux Container is a React Component that sits atop a hierarchy of child components and manages coordination + /// by sending UI actions to the dispatcher and retrieving state from stores that it depends on. + /// + /// The type of this container's Props. + /// The type of this container's State. + public abstract class Container : Component + { + /// + /// Constructs the container, assigning the initial component props. + /// + /// The props to assign to this container component. + public Container(TProps props) + : base(props) { } + + /// + /// Calculates the current state of the container. + /// The implementation should only derive its state from the state of its stores. + /// + /// The container's state. + protected abstract TState CalculateState(); + + /// + /// Returns the stores that this container is dependent on. + /// The container will subscribe to the change events of these stores when mounted, and unsubscribe when unmounted. + /// + /// The container's stores. + protected abstract IEnumerable GetStores(); + + /// + /// Compares if two states are equal. The default behaviour uses , which should suffice for immutable types. + /// + /// The first state. + /// The second state. + /// True if both states are equal; false otherwise. + protected virtual bool AreStatesEqual(TState state1, TState state2) + { + return EqualityComparer.Default.Equals(state1, state2); + } + + // Note: I've sealed the lifecycle methods here to avoid inadvertently forgetting to call required base methods in derived classes. This is just an + // experiment; if it turns out derived classes can actually make good use of these lifecycle methods (I can't envision a scenario at the moment), + // they can just be unsealed (a non-breaking change). + + protected sealed override void ComponentWillMount() + { + var stores = GetStores(); + if (stores == null) + throw new Exception("GetStores cannot return null."); + + foreach (var store in stores) + store.Change += StoreChanged; + } + + protected sealed override void ComponentWillUnmount() + { + var stores = GetStores(); + if (stores == null) + throw new Exception("GetStores cannot return null."); + + foreach (var store in stores) + store.Change -= StoreChanged; + } + + protected sealed override TState GetInitialState() + { + return CalculateState(); + } + + private void StoreChanged() + { + // The CalculateState method is free to return any valid value for the State type, this includes null if the state is a reference type. + var newState = CalculateState(); + + if (!AreStatesEqual(state, newState)) + SetState(newState); + } + } +} diff --git a/Bridge.Flux/Utils/ReduceStore.cs b/Bridge.Flux/Utils/ReduceStore.cs new file mode 100644 index 0000000..8a60211 --- /dev/null +++ b/Bridge.Flux/Utils/ReduceStore.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; + +namespace Bridge.Flux.Utils +{ + // TODO: Class documentation (why use this over Store, etc.) + public abstract class ReduceStore : Store + { + /// + /// Constructs the store, setting the initial state to the one returned by GetInitialState and registering with the provided dispatcher. + /// + /// The dispatcher to register with to receive actions. + public ReduceStore(IDispatcher dispatcher) : base(dispatcher) + { + State = GetInitialState(); + } + + /// + /// Gets the current state of the store. + /// + public TState State { get; private set; } + + /// + /// Gets the initial state of the store. + /// + /// Initial state of the store. + protected abstract TState GetInitialState(); + + // TODO: FB Flux passes State as the first parameter to Reduce. While I like the idea that the method should rely solely on the previous and the given action, + // we can't enforce that with class inheritance. It would seem redundant to pass the previous state when the derived class has access to State already. + // It would be ideal if the Reduce method could enforced to be an explicitly pure function, but the only way I see of doing that is to have this class' + // constructor accept a Func Reduce delegate in its constructor (which seems lamer than implementing an abstract method). + + /// + /// Reduces an incoming dispatcher action to a new state for the store. + /// This method should return the existing state if nothing has changed. + /// + /// The incoming dispatcher action. + /// New state for the store after applying the dispatcher action. + protected abstract TState Reduce(IDispatcherAction action); + + /// + /// Compares if two states are equal. The default behaviour uses , which should suffice for immutable types. + /// + /// The first state. + /// The second state. + /// True if both states are equal; false otherwise. + protected virtual bool AreStatesEqual(TState state1, TState state2) + { + return EqualityComparer.Default.Equals(state1, state2); + } + + protected override void HandleAction(IDispatcherAction action) + { + // The Reduce method is free to return any valid value for the State type, this includes null if the state is a reference type. + var newState = Reduce(action); + + if (!AreStatesEqual(State, newState)) + { + State = newState; + OnChange(); + } + } + } +} diff --git a/Bridge.Flux/Utils/Store.cs b/Bridge.Flux/Utils/Store.cs new file mode 100644 index 0000000..7152119 --- /dev/null +++ b/Bridge.Flux/Utils/Store.cs @@ -0,0 +1,40 @@ +using System; + +namespace Bridge.Flux.Utils +{ + // TODO: Class documentation + public abstract class Store + { + /// + /// Constructs the store, registering with the provided dispatcher. + /// + /// The dispatcher to register with to receive actions. + public Store(IDispatcher dispatcher) + { + if (dispatcher == null) + throw new ArgumentNullException(nameof(dispatcher)); + + dispatcher.Register(HandleAction); + } + + /// + /// Provides an event that will be raised when the State of the store changes. + /// + public event Action Change; + + /// + /// Handles an incoming dispatcher action. + /// This method should call OnChange if the state of the store has changed after handling the action. + /// + /// The dispatcher action to handle. + protected abstract void HandleAction(IDispatcherAction action); + + /// + /// Triggers the Change event. + /// + protected void OnChange() + { + Change?.Invoke(); + } + } +} diff --git a/Bridge.Flux/packages.config b/Bridge.Flux/packages.config new file mode 100644 index 0000000..c007d45 --- /dev/null +++ b/Bridge.Flux/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/LICENCE.txt b/LICENCE.txt new file mode 100644 index 0000000..e22e9c9 --- /dev/null +++ b/LICENCE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2017, Dion Williams + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.