diff --git a/docs/pages/action.md b/docs/pages/action.md index 4dd84c9..fdc840a 100644 --- a/docs/pages/action.md +++ b/docs/pages/action.md @@ -11,10 +11,15 @@ Actions help you to structure your code better. Actions in Cortex.Net achieve th ## action as a delegate -It takes a delegate and returns a delegate with the same signature, but wrapped with [`Transaction`](xref:Cortex.Net.Api.SharedStateTransactionExtensions.Transaction(Cortex.Net.ISharedState,Action)), [`Untracked`](xref:Cortex.Net.Core.ActionExtensions.Untracked``1(Cortex.Net.ISharedState,Func{``0})), and [`AllowStateChanges`](xref:Cortex.Net.Core.ActionExtensions.Untracked(Cortex.Net.ISharedState,Action)). +It takes a delegate and returns a delegate with the same signature, but wrapped with +[`Transaction`](xref:Cortex.Net.Api.SharedStateTransactionExtensions.Transaction(Cortex.Net.ISharedState,Action)), +[`Untracked`](xref:Cortex.Net.Core.ActionExtensions.Untracked``1(Cortex.Net.ISharedState,Func{``0})), and +[`AllowStateChanges`](xref:Cortex.Net.Core.ActionExtensions.Untracked(Cortex.Net.ISharedState,Action)). + Especially the fact that `Transaction` is applied automatically yields great performance benefits; actions will batch mutations and only notify computed values and reactions after the (outer most) action has finished. -This makes sure intermediate or incomplete values produced during an action are not visible to the rest of the application until the action has finished. +This makes sure intermediate or incomplete values produced during an action are not visible to the rest of the +application until the action has finished. Example: @@ -32,7 +37,8 @@ myAction(); ## action as an atrribute -It is advised to use an [[Action]](xref:Cortex.Net.Api.ActionAttribute) attribute on any method that modifies observables or has side effects. +It is advised to use an [[Action]](xref:Cortex.Net.Api.ActionAttribute) attribute on any method that modifies +observables or has side effects. ```csharp @@ -47,17 +53,23 @@ public void MyAction() MyAction(); ``` -Using the `Action` attributes with property setters is not supported; however, setters of [computed properties are automatically actions](computed.md). +Using the `Action` attributes with property setters is not supported; however, setters of +[computed properties are automatically actions](computed.md). -Note: using `Action` is mandatory when Cortex.Net is configured to require actions to make state changes, see [EnforceActions](xref:Cortex.Net.CortexConfiguration.EnforceActions). +Note: using `Action` is mandatory when Cortex.Net is configured to require actions to make state changes, see +[EnforceActions](xref:Cortex.Net.CortexConfiguration.EnforceActions). ## When to use actions? Actions should only, and always, be used on methods that _modify_ state. -Methods that just perform look-ups, filters etc should _not_ be marked as actions; to allow Cortex.Net to track their invocations. +Methods that just perform look-ups, filters etc should _not_ be marked as actions; to allow Cortex.Net to track their +invocations. -[EnforceActions](xref:Cortex.Net.CortexConfiguration.EnforceActions) enforces that all state modifications are done by an action. This is a useful best practice in larger, long term code bases. +[EnforceActions](xref:Cortex.Net.CortexConfiguration.EnforceActions) enforces that all state modifications are done by +an action. This is a useful best practice in larger, long term code bases. ## RunInAction -[RunInAction](xref:Cortex.Net.Api.ActionExtensions.RunInAction(Cortex.Net.ISharedState,System.String,System.Object,Action))is a simple utility that takes an code block and executes in an (anonymous) action. This is useful to create and execute actions on the fly, for example inside an asynchronous process. `RunInAction(f)` is sugar for `Action(f)()` +[RunInAction](xref:Cortex.Net.Api.ActionExtensions.RunInAction(Cortex.Net.ISharedState,System.String,System.Object,Action)) +is a simple utility that takes an code block and executes in an (anonymous) action. This is useful to create and execute +actions on the fly, for example inside an asynchronous process. `RunInAction(f)` is sugar for `Action(f)()` diff --git a/docs/pages/async.md b/docs/pages/async.md index e97d58d..764dd6e 100644 --- a/docs/pages/async.md +++ b/docs/pages/async.md @@ -1,17 +1,24 @@ # Writing asynchronous actions -The [Action method](action.md) only works on synchronous methods. For asynchronous methods it does not work. To understand why, we have to dive a bit deeper into asynchronous methods and how the compiler generates them. -An asynchronous method in C# is split on the await calls into multiple synchronous parts by the compiler. These synchronous parts are put into a state machine. The method continues after an await with the next part based on the state in the state machine. The transformation by the compiler is described in detail in [this blog post by Sergey Tepliakov](https://devblogs.microsoft.com/premier-developer/dissecting-the-async-methods-in-c/). +The [Action method](action.md) only works on synchronous methods. For asynchronous methods it does not work. To +understand why, we have to dive a bit deeper into asynchronous methods and how the compiler generates them. +An asynchronous method in C# is split on the await calls into multiple synchronous parts by the compiler. These +synchronous parts are put into a state machine. The method continues after an await with the next part based on the +state in the state machine. The transformation by the compiler is described in detail in +[this blog post by Sergey Tepliakov](https://devblogs.microsoft.com/premier-developer/dissecting-the-async-methods-in-c/). -Now it's important to understand that as opposed to a normal method the async method has multiple entry and exit points and that the [Action method](action.md) wound only wrap the first "part" of the async method. +Now it's important to understand that as opposed to a normal method the async method has multiple entry and exit points +and that the [Action method](action.md) wound only wrap the first "part" of the async method. -This means that if you have an `async` method, and in that method some more state is changed, those callbacks should be wrapped in `action` as well! +This means that if you have an `async` method, and in that method some more state is changed, those callbacks should be +wrapped in `action` as well! Let's start with a basic example: ### Using RunInAction -Although this is clean and explicit, it might get a bit verbose with complex async flows. It illustrates how to modify state from an asynchronous method nicely. +Although this is clean and explicit, it might get a bit verbose with complex async flows. It illustrates how to modify +state from an asynchronous method nicely. ```csharp using Cortex.Net; @@ -62,9 +69,10 @@ public class Store { ### Using the action attribute -However, using [Fody weaving](weaving.md) we are able to detect the generated state machine and wrap the right parts with -the [Action method](action.md) automatically. All it takes is to add the [[Action]](xref:Cortex.Net.Api.ActionAttribute) attribute to the asynchronous method and it will be wrapped in multiple actions automatically. -The code below is completely equivalent to the code above: +However, using [Fody weaving](weaving.md) we are able to detect the generated state machine and wrap the right parts +with the [Action method](action.md) automatically. All it takes is to add the +[[Action]](xref:Cortex.Net.Api.ActionAttribute) attribute to the asynchronous method and it will be wrapped in multiple +actions automatically. The code below is completely equivalent to the code above: ```csharp using Cortex.Net; diff --git a/docs/pages/autorun.md b/docs/pages/autorun.md index 81b2185..afad99b 100644 --- a/docs/pages/autorun.md +++ b/docs/pages/autorun.md @@ -1,15 +1,21 @@ # Autorun -[Autorun](xref:Cortex.Net.Api.SharedStateReactionExtensions.Autorun(Cortex.Net.ISharedState,Action{Cortex.Net.Core.Reaction},Cortex.Net.AutorunOptions)) can be used in those cases where you want to create a reactive function that will never have observers itself. -This is usually the case when you need to bridge from reactive to imperative code, for example for logging, persistence, or UI-updating code. -When the `Autorun` extension method on `ISharedState` is used, the provided function will always be triggered once immediately and then again each time one of its dependencies changes. -In contrast, [Computed](computed.md) creates functions that only re-evaluate if it has -observers on its own, otherwise its value is considered to be irrelevant. -As a rule of thumb: use `Autorun` if you have a function that should run automatically but that doesn't result in a new value. +[Autorun](xref:Cortex.Net.Api.SharedStateReactionExtensions.Autorun(Cortex.Net.ISharedState,Action{Cortex.Net.Core.Reaction},Cortex.Net.AutorunOptions)) +can be used in those cases where you want to create a reactive function that will never have observers itself. This is +usually the case when you need to bridge from reactive to imperative code, for example for logging, persistence, or +UI-updating code. When the `Autorun` extension method on `ISharedState` is used, the provided function will always be +triggered once immediately and then again each time one of its dependencies changes. In contrast, +[Computed](computed.md) creates functions that only re-evaluate if it has observers on its own, otherwise its value is +considered to be irrelevant. As a rule of thumb: use `Autorun` if you have a function that should run automatically but +that doesn't result in a new value. Use `Computed` for everything else. Autoruns are about initiating _effects_, not about producing new values. -You can pass an instance of [AutorunOptions](xref:Cortex.Net.AutorunOptions) as an argument to `Autorun`, it will be used to set options like a debug name or a delay. +You can pass an instance of [AutorunOptions](xref:Cortex.Net.AutorunOptions) as an argument to `Autorun`, it will be +used to set options like a debug name or a delay. -The return value from autorun is an `IDisposable` instance, which can be used to dispose of the autorun when you no longer need it. The reaction itself will also be passed as the only argument to the function given to autorun, which allows you to manipulate it from within the autorun function. This means there are two ways you can dispose of the reaction when you no longer need it: +The return value from autorun is an `IDisposable` instance, which can be used to dispose of the autorun when you no +longer need it. The reaction itself will also be passed as the only argument to the function given to autorun, which +allows you to manipulate it from within the autorun function. This means there are two ways you can dispose of the +reaction when you no longer need it: ```csharp using Cortex.Net.Api; @@ -33,7 +39,8 @@ sharedState.Autorun(reaction => }); ``` -Just like the [`Observer` attribute](observer.md), `Autorun` will only observe data that is used during the execution of the provided function. +Just like the [`Observer` attribute](observer.md), `Autorun` will only observe data that is used during the execution +of the provided function. ```csharp using Cortex.Net.Api; @@ -59,12 +66,16 @@ numbers.Add(5); ## Options -Autorun accepts as the second argument an [AutorunOptions](xref:Cortex.Net.AutorunOptions) instance with the following options: +Autorun accepts as the second argument an [AutorunOptions](xref:Cortex.Net.AutorunOptions) instance with the following +options: -- `Delay`: Number in milliseconds that can be used to debounce the effect delegate. If zero (the default), no debouncing will happen. +- `Delay`: Number in milliseconds that can be used to debounce the effect delegate. If zero (the default), no + debouncing will happen. - `Name`: String that is used as name for this reaction in for example [`Spy`](spy.md) events. - `ErrorHandler`: delegate that will handle the errors of this reaction, rather then propagating them. -- `Scheduler`: Set a custom scheduler to determine how re-running the autorun function should be scheduled. It takes a async delegate that should be invoked at some point in the future, for example: `{ Scheduler: async () => { await Task.Delay(1000); }}` +- `Scheduler`: Set a custom scheduler to determine how re-running the autorun function should be scheduled. It takes + a async delegate that should be invoked at some point in the future, for example: + `{ Scheduler: async () => { await Task.Delay(1000); }}` ## The `Delay` option @@ -89,10 +100,11 @@ sharedState.Autorun( ## The `ErrorHandler` option -Exceptions thrown in autorun and all other types reactions are caught and logged to the console or debugger, but not propagated back to the original causing code. -This is to make sure that a reaction in one exception does not prevent the scheduled execution of other, possibly unrelated, reactions. -This also allows reactions to recover from exceptions; throwing an exception does not break the tracking done by MobX, -so as subsequent run of a reaction might complete normally again if the cause for the exception is removed. +Exceptions thrown in autorun and all other types reactions are caught and logged to the console or debugger, but not +propagated back to the original causing code. This is to make sure that a reaction in one exception does not prevent the +scheduled execution of other, possibly unrelated, reactions. This also allows reactions to recover from exceptions; +throwing an exception does not break the tracking done by Cortex.Net, so as subsequent run of a reaction might complete +normally again if the cause for the exception is removed. It is possible to override the default logging behavior of Reactions by providing the `ErrorHandler` option Example: @@ -122,4 +134,6 @@ const dispose = sharedState.Autorun( ); ``` -A global onError handler on the shared state can be set as well, use [ISharedState.UnhandledReactionException](xref:Cortex.Net.ISharedState.UnhandledReactionException). This can be useful in tests or for client side error monitoring. +A global onError handler on the shared state can be set as well, use +[ISharedState.UnhandledReactionException](xref:Cortex.Net.ISharedState.UnhandledReactionException). This can be useful +in tests or for client side error monitoring. diff --git a/docs/pages/box.md b/docs/pages/box.md index 8c8b7ae..2e6c5ee 100644 --- a/docs/pages/box.md +++ b/docs/pages/box.md @@ -10,11 +10,13 @@ For these cases it is possible to create an observable box that manages such a v ### ISharedState.Box(value) -So [ISharedState.Box(value)](xref:Cortex.Net.Api.SharedStateObservableExtensions.Box``1(Cortex.Net.ISharedState,``0,System.String,Cortex.Net.IEnhancer)) accepts any value and stores it inside a box. -The current value can be accessed through `.Value` property and updated using `.Value =`. +So [ISharedState.Box(value)](xref:Cortex.Net.Api.SharedStateObservableExtensions.Box``1(Cortex.Net.ISharedState,``0,System.String,Cortex.Net.IEnhancer)) +accepts any value and stores it inside a box. The current value can be accessed through `.Value` property and updated +using `.Value =`. Furthermore you can register a callback using its `.Observe` method to listen to changes on the stored value. -But since Cortex.Net tracks changes to boxes automatically, in most cases it is better to use a reaction like [autorun](autorun.md) instead. +But since Cortex.Net tracks changes to boxes automatically, in most cases it is better to use a reaction like +[autorun](autorun.md) instead. So the signature of object returned by `observable.box(scalar)` is: @@ -48,4 +50,5 @@ cityName.Value = Amsterdam; ## ISharedState.Box(value, name) -The `name` parameter can be used to give the box a friendly debug name, to be used in for example `spy` or the React dev tools. +The `name` parameter can be used to give the box a friendly debug name, to be used in for example `spy` or the React +dev tools. diff --git a/docs/pages/computed.md b/docs/pages/computed.md index 729bcdc..29b6c1d 100644 --- a/docs/pages/computed.md +++ b/docs/pages/computed.md @@ -19,16 +19,18 @@ In such cases it will be suspended. This automatic suspension is very convenient. If a computed value is no longer observed, for example the UI in which it was used no longer exists, Cortex.Net can automatically garbage collect it. This differs from `autorun`'s values where you must dispose of them yourself. -It sometimes confuses people new to Cortex.Net, that if you create a computed property but don't use it anywhere in a reaction, -it will not cache its value and recompute more often than seems necessary. +It sometimes confuses people new to Cortex.Net, that if you create a computed property but don't use it anywhere in a +eaction, it will not cache its value and recompute more often than seems necessary. However, in real life situations this is by far the best default, and you can always forcefully keep a -computed value awake if you need to, by using either [`observe`](observer.md) or the [`keepAlive`](xref:Cortex.Net.ComputedValueOptions`1.KeepAlive) option. +computed value awake if you need to, by using either [`observe`](observer.md) or the +[`keepAlive`](xref:Cortex.Net.ComputedValueOptions`1.KeepAlive) option. Note that `computed` properties are not enumerable. Nor can they be overwritten in an inheritance chain. ## `ComputedAttribute` -You can use the [ComputedAttribute](xref:Cortex.Net.Api.ComputedAttribute) on any getter of a class property to declaratively create computed properties. +You can use the [ComputedAttribute](xref:Cortex.Net.Api.ComputedAttribute) on any getter of a class property to +declaratively create computed properties. ```csharp using Cortex.Net.Api; @@ -49,8 +51,8 @@ public class OrderLine ## Setters for computed values -It is possible to define a setter for computed values as well. Note that these setters cannot be used to alter the value of the computed property directly, -but they can be used as 'inverse' of the derivation. For example: +It is possible to define a setter for computed values as well. Note that these setters cannot be used to alter the value +of the computed property directly, but they can be used as 'inverse' of the derivation. For example: ```csharp using Cortex.Net.Api; @@ -98,9 +100,12 @@ public class Foo { ## `Computed(expression)` as method. -[Computed](xref:Cortex.Net.Api.SharedStateObservableExtensions.Computed``1(Cortex.Net.ISharedState,Func{``0},System.String)) can also be invoked directly as an extension method to ISharedState. -Use [`.Value`](xref:Cortex.Net.IValue`1.Value) on the returned object to get the current value of the computation, or [`.Observe()`](xref:Cortex.Net.IComputedValue`1.Observe(EventHandler{Cortex.Net.Types.ValueChangedEventArgs{`0}},System.Boolean)) to observe its changes. -This form of `computed` is not used very often, but in some cases where you need to pass a computed value around it might prove useful. +[Computed](xref:Cortex.Net.Api.SharedStateObservableExtensions.Computed``1(Cortex.Net.ISharedState,Func{``0},System.String)) +can also be invoked directly as an extension method to ISharedState. Use [`.Value`](xref:Cortex.Net.IValue`1.Value) on +the returned object to get the current value of the computation, or +[`.Observe()`](xref:Cortex.Net.IComputedValue`1.Observe(EventHandler{Cortex.Net.Types.ValueChangedEventArgs{`0}},System.Boolean)) +to observe its changes. This form of `computed` is not used very often, but in some cases where you need to pass a +computed value around it might prove useful. Example: diff --git a/docs/pages/concepts.md b/docs/pages/concepts.md index 1278179..d507da6 100644 --- a/docs/pages/concepts.md +++ b/docs/pages/concepts.md @@ -2,13 +2,14 @@ ## Concepts -Cortex.Net distinguishes the following concepts in your application. You saw them in the previous gist, but let's dive into them in a bit more detail. +Cortex.Net distinguishes the following concepts in your application. You saw them in the previous gist, but let's dive +into them in a bit more detail. ### 1. State _State_ is the data that drives your application. -Usually there is _domain specific state_ like a list of todo items and there is _view state_ such as the currently selected element. -Remember, state is like spreadsheets cells that hold a value. +Usually there is _domain specific state_ like a list of todo items and there is _view state_ such as the currently +selected element. Remember, state is like spreadsheets cells that hold a value. ### 2. Derivations @@ -21,12 +22,16 @@ Derivations exist in many forms: Cortex.Net distinguishes two kind of derivations: -- _Computed values_. These are values that can always be derived from the current observable state using a pure function. A pure function is a function where its result is dependent on its arguments and does not produce side effects. -- _Reactions_. Reactions are side effects that need to happen automatically if the state changes. These are needed as a bridge between imperative and reactive programming. Or to make it more clear, they are ultimately needed to achieve I/O -People starting with Cortex.Net tend to use reactions too often. -The golden rule is: if you want to create a value based on the current state, use `computed`. +- _Computed values_. These are values that can always be derived from the current observable state using a pure + function. A pure function is a function where its result is dependent on its arguments and does not produce side + effects. +- _Reactions_. Reactions are side effects that need to happen automatically if the state changes. These are needed as + a bridge between imperative and reactive programming. Or to make it more clear, they are ultimately needed to + achieve I/O. People starting with Cortex.Net tend to use reactions too often. + The golden rule is: if you want to create a value based on the current state, use `computed`. -Back to the spreadsheet analogy, formulas are derivations that _compute_ a value. But for you as a user to be able to see it on the screen a _reaction_ is needed that repaints part of the GUI. +Back to the spreadsheet analogy, formulas are derivations that _compute_ a value. But for you as a user to be able to +see it on the screen a _reaction_ is needed that repaints part of the GUI. ### 3. Actions @@ -34,20 +39,24 @@ An _action_ is any piece of code that changes the _state_. User events, backend An action is like a user that enters a new value in a spreadsheet cell. Actions can be defined explicitly in Cortex.Net to help you to structure code more clearly. -If Cortex.Net is used in [strict mode](xref:Cortex.Net.CortexConfiguration.EnforceActions), Cortex.Net will enforce that no state can be modified outside actions. +If Cortex.Net is used in [strict mode](xref:Cortex.Net.CortexConfiguration.EnforceActions), Cortex.Net will enforce that +no state can be modified outside actions. ## Principles -Cortex.Net supports a uni-directional data flow where _actions_ change the _state_, which in turn updates all affected _views_. +Cortex.Net supports a uni-directional data flow where _actions_ change the _state_, which in turn updates all affected +_views_. ![Action, State, View](../images/action-state-view.png) -All _Derivations_ are updated **automatically** and **atomically** when the _state_ changes. As a result it is never possible to observe intermediate values. +All _Derivations_ are updated **automatically** and **atomically** when the _state_ changes. As a result it is never +possible to observe intermediate values. -All _Derivations_ are updated **synchronously** by default. This means that, for example, _actions_ can safely inspect a computed value directly after altering the _state_. +All _Derivations_ are updated **synchronously** by default. This means that, for example, _actions_ can safely inspect +a computed value directly after altering the _state_. -_Computed values_ are updated **lazily**. Any computed value that is not actively in use will not be updated until it is needed for a side effect (I/O). -If a view is no longer in use it will be garbage collected automatically. +_Computed values_ are updated **lazily**. Any computed value that is not actively in use will not be updated until it +is necessary for a side effect (I/O). If a view is no longer in use it will be garbage collected automatically. All _Computed values_ should be **pure**. They are not supposed to change _state_. diff --git a/docs/pages/dependencyinjection.md b/docs/pages/dependencyinjection.md index a73887d..a3d08fc 100644 --- a/docs/pages/dependencyinjection.md +++ b/docs/pages/dependencyinjection.md @@ -1,16 +1,18 @@ # Dependency Injection. -Cortex.Net is a [DI friendly library](https://blog.ploeh.dk/2014/05/19/di-friendly-library/). This means that a lot of code is written to -interfaces, not implementations of those interfaces. It works with any DI Container. There are some tips and tricks to get the most out -of your DI container. +Cortex.Net is a [DI friendly library](https://blog.ploeh.dk/2014/05/19/di-friendly-library/). This means that a lot of +code is written to interfaces, not implementations of those interfaces. It works with any DI Container. There are some +tips and tricks to get the most out of your DI container. ## Register ISharedState in the container. -To be able to access [ISharedState](xref:Cortex.Net.ISharedState), and to be able to pass it as an argument to other objects that take it -on as a dependency, it is customary to register an implementation of [ISharedState](xref:Cortex.Net.ISharedState) inside the container. +To be able to access [ISharedState](xref:Cortex.Net.ISharedState), and to be able to pass it as an argument to other +objects that take it on as a dependency, it is customary to register an implementation of +[ISharedState](xref:Cortex.Net.ISharedState) inside the container. -This can either be a single container registered as singleton, or multiple containers with the appropriate scope, as [described here](sharedstate.md). -An example of a registration of ISharedState in the [DI container from microsoft](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) is below: +This can either be a single container registered as singleton, or multiple containers with the appropriate scope, as +[described here](sharedstate.md). An example of a registration of ISharedState in the +[DI container from microsoft](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) is below: ```csharp using Cortex.Net.Api; diff --git a/docs/pages/gist.md b/docs/pages/gist.md index 9f712f5..3775e15 100644 --- a/docs/pages/gist.md +++ b/docs/pages/gist.md @@ -6,7 +6,8 @@ So far it all might sound a bit fancy, but making an app reactive using Cortex.N Store state in any data structure you like; objects, collections, classes. Cyclic data structures, references, it doesn't matter. -Just make sure that all properties that you want to change over time are marked by the [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) attribute to make them observable. +Just make sure that all properties that you want to change over time are marked by the +[[Observable]](xref:Cortex.Net.Api.ObservableAttribute) attribute to make them observable. ```csharp using Cortex.Net.Api; @@ -26,8 +27,8 @@ you can now create views that automatically update whenever relevant data in the Cortex.Net will find the minimal way to update your views. This single fact saves you tons of boilerplate and is wickedly efficient. -Generally speaking any Action/Func/Method can become a reactive view that observes its data, and Cortex.Net can be applied in any .NET netstandard environment. -But here is an example of a view in the form of a Blazor component. +Generally speaking any Action/Func/Method can become a reactive view that observes its data, and Cortex.Net can be +applied in any .NET netstandard environment. But here is an example of a view in the form of a Blazor component. ```cshtml-razor @using Cortex.Net.Blazor @@ -60,9 +61,9 @@ There are best practices, but the key thing to remember is: **_Cortex.Net helps you do things in a simple straightforward way_**. The following code will alter your data every second, and the UI will update automatically when needed. -No explicit relations are defined in either the controller functions that _change_ the state or in the views that should _update_. -Decorating your _state_ and _views_ with [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) is enough for Cortex.Net to detect all relationships. -Here are two examples of changing the state: +No explicit relations are defined in either the controller functions that _change_ the state or in the views that +should _update_. Decorating your _state_ and _views_ with [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) is +enough for Cortex.Net to detect all relationships. Here are two examples of changing the state: ```csharp @@ -93,5 +94,5 @@ public class AppState ``` The `Action` attribute is only neccessary when using Cortex.Net enforces modification through reactions. -It is recommended to use action though as it will help you to better structure applications and expresses the intention of a function to modify state. -It automatically applies transactions for optimal performance as well. \ No newline at end of file +It is recommended to use action though as it will help you to better structure applications and expresses the intention +of a function to modify state. It automatically applies transactions for optimal performance as well. \ No newline at end of file diff --git a/docs/pages/index.md b/docs/pages/index.md index fce3eae..210a603 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -78,7 +78,8 @@ public class Todo Using `observable` is like turning a property of an object into a spreadsheet cell. But unlike spreadsheets, these values can be not only primitive values, but also references, objects and arrays. -If your environment doesn't support weaving, don't worry, as Cortex.Net can be used fine without attributes, by inheriting from or encapsulating [ObservableObject](xref:Cortex.Net.Types.ObservableObject). +If your environment doesn't support weaving, don't worry, as Cortex.Net can be used fine without attributes, by +inheriting from or encapsulating [ObservableObject](xref:Cortex.Net.Types.ObservableObject). Many Cortex.Net users do prefer the atrribute syntax though, as it is less boilerblate. ```csharp @@ -102,7 +103,8 @@ public class Todo : ObservableObject ### Computed values With Cortex.Net you can define values that will be derived automatically when relevant data is modified. -By using the [[Computed]](xref:Cortex.Net.Api.ComputedAttribute) attribute or by using methods or property getters (or even setters) when using [ObservableObject](xref:Cortex.Net.Types.ObservableObject). +By using the [[Computed]](xref:Cortex.Net.Api.ComputedAttribute) attribute or by using methods or property getters +(or even setters) when using [ObservableObject](xref:Cortex.Net.Types.ObservableObject). ```csharp using Cortex.Net.Api; @@ -119,14 +121,21 @@ public class TodoStore } ``` -Cortex.Net will ensure that `ActiveCount` is updated automatically when a todo is added or when one of the `Completed` properties is modified. Computations like these resemble formulas in spreadsheet programs like MS Excel. They update automatically and only when required. +Cortex.Net will ensure that `ActiveCount` is updated automatically when a todo is added or when one of the `Completed` +properties is modified. Computations like these resemble formulas in spreadsheet programs like MS Excel. +They update automatically and only when required. ### Reactions -Reactions are similar to a computed value, but instead of producing a new value, a reaction produces a side effect for things like printing to the console, making network requests, incrementally updating the React component tree to patch the DOM, etc. -In short, reactions bridge [reactive](https://en.wikipedia.org/wiki/Reactive_programming) and [imperative](https://en.wikipedia.org/wiki/Imperative_programming) programming. +Reactions are similar to a computed value, but instead of producing a new value, a reaction produces a side effect for +things like printing to the console, making network requests, incrementally updating the React component tree to patch +the DOM, etc. In short, reactions bridge [reactive](https://en.wikipedia.org/wiki/Reactive_programming) and +[imperative](https://en.wikipedia.org/wiki/Imperative_programming) programming. -Reactions can simply be created using the [Autorun](xref:Cortex.Net.Api.SharedStateReactionExtensions.Autorun(Cortex.Net.ISharedState,Action{Cortex.Net.Core.Reaction},Cortex.Net.AutorunOptions)), [Reaction](xref:Cortex.Net.Api.SharedStateReactionExtensions.Reaction``1(Cortex.Net.ISharedState,Func{Cortex.Net.Core.Reaction,``0},Action{``0,Cortex.Net.Core.Reaction},Cortex.Net.ReactionOptions{``0})) or [when](http://mobxjs.github.io/mobx/refguide/when.html) methods to fit your specific situations. +Reactions can simply be created using the +[Autorun](xref:Cortex.Net.Api.SharedStateReactionExtensions.Autorun(Cortex.Net.ISharedState,Action{Cortex.Net.Core.Reaction},Cortex.Net.AutorunOptions)), +[Reaction](xref:Cortex.Net.Api.SharedStateReactionExtensions.Reaction``1(Cortex.Net.ISharedState,Func{Cortex.Net.Core.Reaction,``0},Action{``0,Cortex.Net.Core.Reaction},Cortex.Net.ReactionOptions{``0})) +or [when](http://mobxjs.github.io/mobx/refguide/when.html)methods to fit your specific situations. For example the following `Autorun` prints a log message each time `ActiveCount` changes: @@ -140,7 +149,8 @@ sharedState.Autorun(() => { ### Blazor components -If you are using Blazor, you can turn your components into reactive components by simply adding the [[Observer]](xref:Cortex.Net.Blazor.ObserverAttribute) attribute from the `Cortex.Net.Blazor` nuget package onto them. +If you are using Blazor, you can turn your components into reactive components by simply adding the +[[Observer]](xref:Cortex.Net.Blazor.ObserverAttribute) attribute from the `Cortex.Net.Blazor` nuget package onto them. `TodoListView.razor:` ```cshtml-razor @@ -193,16 +203,21 @@ If you are using Blazor, you can turn your components into reactive components b } ``` -`[Observer]` turns Blazor components into derivations of the data they render. Cortex.Net will make sure the components are always re-rendered whenever needed, but also no more than that. So the `onChange` handler in the above example will force the proper `TodoItem` to render, and it will cause the `TodoListView` to render if the number of unfinished tasks has changed. -However, if you would remove the `Tasks left` line (or put it into a separate component), the `TodoListView` will no longer re-render when ticking a box. +`[Observer]` turns Blazor components into derivations of the data they render. Cortex.Net will make sure the components +are always re-rendered whenever needed, but also no more than that. So the `onChange` handler in the above example will +force the proper `TodoItem` to render, and it will cause the `TodoListView` to render if the number of unfinished tasks +has changed. However, if you would remove the `Tasks left` line (or put it into a separate component), the +`TodoListView` will no longer re-render when ticking a box. ### What will Cortex.Net react to? -Why does a new message get printed or the Blazor component rerendered each time the `ActiveCount` is changed? The answer is this rule of thumb: +Why does a new message get printed or the Blazor component rerendered each time the `ActiveCount` is changed? The answer +is this rule of thumb: -Cortex.Net reacts to any existing observable property that is read during the execution of a tracked function._ +_Cortex.Net reacts to any existing observable property that is read during the execution of a tracked function._ -For an in-depth explanation about how Cortex.Net determines to which observables needs to be reacted, check [understanding what Cortex.Net reacts to](react.md). +For an in-depth explanation about how Cortex.Net determines to which observables needs to be reacted, check +[understanding what Cortex.Net reacts to](react.md). ### Actions @@ -210,13 +225,16 @@ Unlike many flux frameworks, Cortex.Net is unopinionated about how user events s - This can be done in a Flux like manner. - Or by processing events using RxJS. -- Or by simply handling events in the most straightforward way possible, as demonstrated in the above `onChanged` handler. +- Or by simply handling events in the most straightforward way possible, as demonstrated in the above `onChanged` + handler. In the end it all boils down to: Somehow the state should be updated. -After updating the state Cortex.Net will take care of the rest in an efficient, glitch-free manner. So simple statements, like below, are enough to automatically update the user interface. +After updating the state Cortex.Net will take care of the rest in an efficient, glitch-free manner. So simple +statements, like below, are enough to automatically update the user interface. -There is no technical need for firing events, calling a dispatcher or what more. A Blazor component in the end is nothing more than a fancy representation of your state. A derivation that will be managed by Cortex.Net. +There is no technical need for firing events, calling a dispatcher or what more. A Blazor component in the end is +nothing more than a fancy representation of your state. A derivation that will be managed by Cortex.Net. ```csharp todos.Add(new Todo() { Title = "Get Coffee" }); @@ -226,7 +244,9 @@ todos[0].Completed = true; Nonetheless, Cortex.Net has an optional built-in concept of [`actions`](action.md). Read this section as well if you want to know more about writing asynchronous actions. It's easy! -Use them to your advantage; they will help you to structure your code better and make wise decisions about when and where state should be modified. The default configuration of Cortex.Net will throw exceptions when observed data is modified outside an action to make sure that you are conscise and do not trigger too many reactions. +Use them to your advantage; they will help you to structure your code better and make wise decisions about when and +where state should be modified. The default configuration of Cortex.Net will throw exceptions when observed data is +modified outside an action to make sure that you are conscise and do not trigger too many reactions. ```csharp @@ -256,37 +276,51 @@ MyAction(); ``` ## Cortex.Net: Simple and scalable -Cortex.Net is one of the least obtrusive libraries you can use for state management. That makes the `Cortex.Net` approach not just simple, but very scalable as well: +Cortex.Net is one of the least obtrusive libraries you can use for state management. That makes the `Cortex.Net` +approach not just simple, but very scalable as well: ### Using classes and real references -With Cortex.Net you don't need to normalize your data. This makes the library very suitable for very complex domain models. +With Cortex.Net you don't need to normalize your data. This makes the library very suitable for very complex domain +models. ### Referential integrity guaranteed. -Since data doesn't need to be normalized, and Cortex.Net automatically tracks the relations between state and derivations, you get referential integrity for free. Rendering something that is accessed through three levels of indirection? +Since data doesn't need to be normalized, and Cortex.Net automatically tracks the relations between state and +derivations, you get referential integrity for free. Rendering something that is accessed through three levels +of indirection? -No problem, Cortex.Net will track them and re-render whenever one of the references changes. As a result staleness bugs are a thing of the past. As a programmer you might forget that changing some data might influence a seemingly unrelated component in a corner case. Cortex.Net won't forget. +No problem, Cortex.Net will track them and re-render whenever one of the references changes. As a result staleness bugs +are a thing of the past. As a programmer you might forget that changing some data might influence a seemingly unrelated +component in a corner case. Cortex.Net won't forget. ### Simpler actions are easier to maintain -As demonstrated above, modifying state when using Cortex.Net is very straightforward. You simply write down your intentions. Cortex.Net will take care of the rest. +As demonstrated above, modifying state when using Cortex.Net is very straightforward. You simply write down your +intentions. Cortex.Net will take care of the rest. ### Fine grained observability is efficient -Cortex.Net builds a graph of all the derivations in your application to find the least number of re-computations that is needed to prevent staleness. "Derive everything" might sound expensive, Cortex.Net builds a virtual derivation graph to minimize the number of recomputations needed to keep derivations in sync with the state. +Cortex.Net builds a graph of all the derivations in your application to find the least number of re-computations that +is needed to prevent staleness. "Derive everything" might sound expensive, Cortex.Net builds a virtual derivation graph +to minimize the number of recomputations needed to keep derivations in sync with the state. -Secondly Cortex.Net sees the causality between derivations so it can order them in such a way that no derivation has to run twice or introduces a glitch. +Secondly Cortex.Net sees the causality between derivations so it can order them in such a way that no derivation has to +run twice or introduces a glitch. -How that works? See this [in-depth explanation of MobX](https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254). +How that works? See this +[in-depth explanation of MobX](https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254). ### Easy interoperability -Cortex.Net works with POCO objects. Due to its unobtrusiveness it works with most libraries out of the box, without needing Cortex.Net specific library flavors. +Cortex.Net works with POCO objects. Due to its unobtrusiveness it works with most libraries out of the box, without +needing Cortex.Net specific library flavors. -For the same reason you can use it out of the box both server and client side, in isomorphic applications and with any IU framework. As long as the runtime supports netstandard2.0, you are good. +For the same reason you can use it out of the box both server and client side, in isomorphic applications and with any +UI framework. As long as the runtime supports netstandard2.0, you are good. -The result of this is that you often need to learn less new concepts when using Cortex.Net in comparison to other state management solutions. +The result of this is that you often need to learn less new concepts when using Cortex.Net in comparison to other state +management solutions. --- @@ -294,13 +328,19 @@ The result of this is that you often need to learn less new concepts when using Credit where credit is due and Cortex.Net is entirely based on MobX. -MobX is inspired by reactive programming principles as found in spreadsheets. It is inspired by MVVM frameworks like in MeteorJS tracker, knockout and Vue.js. But MobX brings Transparent Functional Reactive Programming to the next level and provides a stand alone implementation. It implements TFRP in a glitch-free, synchronous, predictable and efficient manner. +MobX is inspired by reactive programming principles as found in spreadsheets. It is inspired by MVVM frameworks like in +MeteorJS tracker, knockout and Vue.js. But MobX brings Transparent Functional Reactive Programming to the next level +and provides a stand alone implementation. It implements TFRP in a glitch-free, synchronous, predictable and +efficient manner. -A ton of credits for [Mendix](https://github.com/mendix), for providing the flexibility and support to maintain MobX and the chance to proof the philosophy of MobX in a real, complex, performance critical applications. +A ton of credits for [Mendix](https://github.com/mendix), for providing the flexibility and support to maintain MobX +and the chance to proof the philosophy of MobX in a real, complex, performance critical applications. -And finally kudos for all the people that believed in, tried, validated and even [sponsored](https://github.com/mobxjs/mobx/blob/master/sponsors.md) MobX. +And finally kudos for all the people that believed in, tried, +validated and even [sponsored](https://github.com/mobxjs/mobx/blob/master/sponsors.md) MobX. -To make Cortex.Net possible in .NET in an unobtrusive and transparent way we use IL-weaving from [Fody](https://github.com/Fody/Home). +To make Cortex.Net possible in .NET in an unobtrusive and transparent way we use IL-weaving from +[Fody](https://github.com/Fody/Home). ### Attributions @@ -313,20 +353,22 @@ To make Cortex.Net possible in .NET in an unobtrusive and transparent way we use ## License -Cortext.Net is licenced under the [MIT license](https://opensource.org/licenses/MIT). More information about the license can be found on [opensource.org](https://opensource.org/licenses/MIT). The full text of the license is below: +Cortext.Net is licenced under the [MIT license](https://opensource.org/licenses/MIT). More information about the +license can be found on [opensource.org](https://opensource.org/licenses/MIT). The full text of the license is below: ```text Copyright 2019 Michel Weststrate, Jan-Willem Spuij -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: +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 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. +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. ``` \ No newline at end of file diff --git a/docs/pages/inject.md b/docs/pages/inject.md index d0522e1..b0be418 100644 --- a/docs/pages/inject.md +++ b/docs/pages/inject.md @@ -1,7 +1,8 @@ # Blazor Components / inject -Cortex.Net observable models / stores can be easily shared by multiple UI Components by injecting a store into the component using the -[DI container](https://docs.microsoft.com/en-us/aspnet/core/blazor/dependency-injection) provided by Blazor and the Inject attribute. +Cortex.Net observable models / stores can be easily shared by multiple UI Components by injecting a store into the +component using the [DI container](https://docs.microsoft.com/en-us/aspnet/core/blazor/dependency-injection) provided +by Blazor and the Inject attribute. ## Example: diff --git a/docs/pages/observable.md b/docs/pages/observable.md index de6ee3f..b53900d 100644 --- a/docs/pages/observable.md +++ b/docs/pages/observable.md @@ -57,10 +57,10 @@ public class Person } ``` -The [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) attribute is applied to an auto-implemented property to signal -Cortex.Net that it should be converted to an observable value. If you want all your auto-implemented properties to be converted -to observable values, you can apply the [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) attribute to a class as well. -The final code thus will be: +The [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) attribute is applied to an auto-implemented property to +signal Cortex.Net that it should be converted to an observable value. If you want all your auto-implemented properties +to be converted to observable values, you can apply the [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) +attribute to a class as well. The final code thus will be: ```csharp using Cortex.Net.Api; @@ -78,14 +78,19 @@ Not bad for all that functionality right? ## Conventions -There are a few simple rules to remember while working with the [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) attribute: +There are a few simple rules to remember while working with the [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) +attribute: -- It either applies to an [auto-implemented property](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties) +- It either applies to an + [auto-implemented property](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties) or a class. In the latter case all auto-implemented properties will be converted to observables. -- It will convert IList<T>, ICollection<T>, IReadonlyCollection<T> interface properties to [ObservableCollection<T>](xref:Cortex.Net.Types.ObservableCollection`1). +- It will convert IList<T>, ICollection<T>, IReadonlyCollection<T> interface properties to + [ObservableCollection<T>](xref:Cortex.Net.Types.ObservableCollection`1). - It will convert ISet<T> interface properties to [ObservableSet](xref:Cortex.Net.Types.ObservableSet). -- It will convert IDictionary<TKey,TValue> interface properties to [ObservableDictionary](xref:Cortex.Net.Types.ObservableDictionary). -- It cannot convert arrays and it will not convert collection properties that are declared as a concrete type instead of an instance. +- It will convert IDictionary<TKey,TValue> interface properties to + [ObservableDictionary](xref:Cortex.Net.Types.ObservableDictionary). +- It cannot convert arrays and it will not convert collection properties that are declared as a concrete type instead + of an instance. These rules might seem complicated at first sight, but you will notice that in practice they are very intuitive to work with. Some notes: diff --git a/docs/pages/observer.md b/docs/pages/observer.md index 2599ace..d016e06 100644 --- a/docs/pages/observer.md +++ b/docs/pages/observer.md @@ -1,13 +1,17 @@ # Observer -The `Observer` attribute is only implemented in Blazor for now. It is possible to integrate the Attribute in other UI frameworks easily. Create an issue if your favorite UI framework is not supported yet. Feel free to implement it and create a pull request if you want to do it yourself. +The `Observer` attribute is only implemented in Blazor for now. It is possible to integrate the Attribute in other UI +frameworks easily. Create an issue if your favorite UI framework is not supported yet. Feel free to implement it and +create a pull request if you want to do it yourself. ## Blazor -The [Observer](xref:Cortex.Net.Blazor.ObserverAttribute) attribute subscribes Blazor components automatically to _any observables_ that are used _during render_. +The [Observer](xref:Cortex.Net.Blazor.ObserverAttribute) attribute subscribes Blazor components automatically to _any +observables_ that are used _during render_. As a result, components will automatically re-render when relevant observables change. But it also makes sure that components don't re-render when there are _no relevant_ changes. -As a result Cortex.NET applications are in practice much better optimized than Redux-like or vanilla Blazor applications are out of the box. +As a result Cortex.NET applications are in practice much better optimized than Redux-like or vanilla Blazor applications +are out of the box. - `Observer` is provided through the separate [`Cortex.Net.Blazor` package](https://www.nuget.org/packages/Cortex.Net.Blazor/). @@ -55,28 +59,40 @@ Using `Observer` is pretty straight forward: ``` -Because `Observer` automatically tracks any observables that are used (and none more), the `Timer` component above will automatically re-render whever `SecondsPassed` is updated, since it is declared as an observable. +Because `Observer` automatically tracks any observables that are used (and none more), the `Timer` component above will +automatically re-render whever `SecondsPassed` is updated, since it is declared as an observable. -Note that `Observer` _only_ subscribes to observables used during the _own_ render of the component. So if observables are passed to child components, those have to be marked as `Observer` as well. This also holds for any callback based components. +Note that `Observer` _only_ subscribes to observables used during the _own_ render of the component. So if observables +are passed to child components, those have to be marked as `Observer` as well. This also holds for any callback based +components. ## When to apply `Observer`? -The simple rule of thumb is: _all components that render observable data_. That observable data might exist on the component itself, but of course can also come from an injected service or any other place that has the [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) attribute applied. +The simple rule of thumb is: _all components that render observable data_. That observable data might exist on the +component itself, but of course can also come from an injected service or any other place that has the +[[Observable]](xref:Cortex.Net.Api.ObservableAttribute) attribute applied. ## Characteristics of observer components -- Observer enables your components to interact with state that is not managed by Blazor, and still update as efficiently as possible. This is great for decoupling. -- Observers only subscribe to the data structures that were actively used during the last render. This means that you cannot under-subscribe or over-subscribe. You can even use data in your rendering that will only be available at later moment in time. This is ideal for asynchronously loading data. -- You are not required to declare what data a component will use. Instead, dependencies are determined at runtime and tracked in a very fine-grained manner. +- Observer enables your components to interact with state that is not managed by Blazor, and still update as + efficiently as possible. This is great for decoupling. +- Observers only subscribe to the data structures that were actively used during the last render. This means that you + cannot under-subscribe or over-subscribe. You can even use data in your rendering that will only be available at later + moment in time. This is ideal for asynchronously loading data. +- You are not required to declare what data a component will use. Instead, dependencies are determined at runtime and + tracked in a very fine-grained manner. - `Observer` calls 'StateHasChanged` automatically so that children are not re-rendered unnecessary. -- `Observer` based components sideways load data; parent components won't re-render unnecessarily even when child components will. +- `Observer` based components sideways load data; parent components won't re-render unnecessarily even when child + components will. ## Tips #### Use the `` component in cases where you can't use observer -Sometimes it is hard to apply `Observer` to a part of the rendering, for example because you are rendering inside a RenderFragment, and you don't want to extract a new component to be able to mark it as `observer`. -In those cases [``](xref:TBD) comes in handy. It takes a child content that is automatically re-rendered if any referenced observables change: +Sometimes it is hard to apply `Observer` to a part of the rendering, for example because you are rendering inside a +RenderFragment, and you don't want to extract a new component to be able to mark it as `observer`. +In those cases [``](xref:TBD) comes in handy. It takes a child content that is automatically re-rendered if +any referenced observables change: ## Troubleshooting @@ -84,4 +100,5 @@ In those cases [``](xref:TBD) comes in handy. It takes a child conte 1. Make sure you didn't forget `Observer` (yes, this is the most common mistake) 2. Make sure you grok how tracking works in general: [what will Cortex.Net react to](breact.md) 3. Read the [common mistakes](pitfalls.md) section -4. Use [Trace](trace.md) to verify that you are subscribing to the right things or check what MobX is doing in general using [Spy](spy.md). +4. Use [Trace](trace.md) to verify that you are subscribing to the right things or check what MobX is doing in general +using [Spy](spy.md). diff --git a/docs/pages/react.md b/docs/pages/react.md index 1ed0683..e7b8d23 100644 --- a/docs/pages/react.md +++ b/docs/pages/react.md @@ -8,8 +8,11 @@ At that point it is invaluable to understand how Cortex.Net determines what to r > Cortex.Net reacts to any _existing_ **observable** _property_ that is read during the execution of a tracked function. - _"reading"_ is dereferencing an object's property, which can be done through "dotting into" it (eg. `User.Name`). -- _"tracked functions"_ are the expression of `Computed`, the `BuildRenderTree` method of an Observer component, and the functions that are passed as the first param to [`When`](when.md), [`Reaction`](reaction.md) and [`Autorun`](autorun.md). -- _"during"_ means that only those observables that are being read while the function is executing are tracked. It doesn't matter whether these values are used directly or indirectly by the tracked function. +- _"tracked functions"_ are the expression of `Computed`, the `BuildRenderTree` method of an Observer component, and + the functions that are passed as the first param to [`When`](when.md), [`Reaction`](reaction.md) and + [`Autorun`](autorun.md). +- _"during"_ means that only those observables that are being read while the function is executing are tracked. + It doesn't matter whether these values are used directly or indirectly by the tracked function. In other words, Cortex.Net will not react to: @@ -18,7 +21,8 @@ In other words, Cortex.Net will not react to: ## Cortex.Net tracks property access, not values -To elaborate on the above rules with an example, suppose that you have the following observable data structure (`Observable` applies to all properties when applied on the class, so all properties in this example are observable): +To elaborate on the above rules with an example, suppose that you have the following observable data structure +(`Observable` applies to all properties when applied on the class, so all properties in this example are observable): ```csharp using Cortex.Net; @@ -50,11 +54,13 @@ var message = new Message() { ``` -In memory that looks as follows. The green boxes indicate _observable_ properties. Note that the _values_ themselves are not observable! +In memory that looks as follows. The green boxes indicate _observable_ properties. Note that the _values_ themselves +are not observable! ![Cortex.Net reacts to changing references](../images/observed-refs.png) -Now what Cortex.Net basically does is recording which _arrows_ you use in your function. After that, it will re-run whenever one of these _arrows_ changes; when they start to refer to something else. +Now what Cortex.Net basically does is recording which _arrows_ you use in your function. After that, it will re-run +whenever one of these _arrows_ changes; when they start to refer to something else. ## Examples @@ -69,9 +75,11 @@ sharedState.Autorun(r => { message.Title = "Bar"; ``` -This will react as expected, the `.Title` property was dereferenced by the `Autorun`, and changed afterwards, so this change is detected. +This will react as expected, the `.Title` property was dereferenced by the `Autorun`, and changed afterwards, so this +change is detected. -You can verify what Cortex.Net will track by calling [`Trace()`](xref:Cortex.Net.Api.TraceExtensions) inside the tracked function. In the case of the above function it will output the following: +You can verify what Cortex.Net will track by calling [`Trace()`](xref:Cortex.Net.Api.TraceExtensions) inside the tracked +function. In the case of the above function it will output the following: ```csharp var disposable = sharedState.Autorun(r => { @@ -99,8 +107,8 @@ message = new Message() }; ``` -This will **not** react. `message` was changed, but `message` is not an observable, just a variable which _refers to_ an observable, -but the variable (reference) itself is not observable. +This will **not** react. `message` was changed, but `message` is not an observable, just a variable which _refers to_ +an observable, but the variable (reference) itself is not observable. #### Incorrect: dereference outside a tracked function @@ -113,7 +121,8 @@ sharedState.Autorun(r => { message.Title = "Bar"; ``` -This will **not** react. `message.Title` was dereferenced outside the `Autorun`, and just contains the value of `message.Title` at the moment of dereferencing (the string `"Foo"`). +This will **not** react. `message.Title` was dereferenced outside the `Autorun`, and just contains the value of +`message.Title` at the moment of dereferencing (the string `"Foo"`). `Title` is not an observable so `Autorun` will never react. #### Correct: dereference inside the tracked function @@ -126,7 +135,8 @@ Message.Author.Name = "Sara"; Message.Author = new Author() { Name: "John" }; ``` -This will react to both changes. Both `Author` and `Author.Name` are dotted into, allowing Cortex.Net to track these references. +This will react to both changes. Both `Author` and `Author.Name` are dotted into, allowing Cortex.Net to track +these references. #### Incorrect: store a local reference to an observable object without tracking @@ -140,8 +150,9 @@ Message.Author.Name = "Sara"; Message.Author = new Author() { Name: "John" }; ``` -The first change will be picked up, `message.Author` and `author` are the same object, and the `.Name` property is dereferenced in the autorun. -However the second change will **not** be picked up, the `message.Author` relation is not tracked by the `Autorun`. Autorun is still using the "old" `author`. +The first change will be picked up, `message.Author` and `author` are the same object, and the `.Name` property is +dereferenced in the autorun. However the second change will **not** be picked up, the `message.Author` relation is not +tracked by the `Autorun`. Autorun is still using the "old" `author`. #### Correct: access observable collection properties in tracked function @@ -165,9 +176,9 @@ sharedState.Autorun(r => { message.Likes.Add("Jennifer"); ``` -This will react with the above sample data, array indexers count as property access. But **only** if the provided `index < Count`. -Cortex.Net will not track not-yet-existing indices or object properties (except when using Dictionaries). -So always guard your array index based access with a `.Count` check. +This will react with the above sample data, array indexers count as property access. But **only** if the provided +`index < Count`. Cortex.Net will not track not-yet-existing indices or object properties +(except when using Dictionaries). So always guard your array index based access with a `.Count` check. #### Correct: access Collection enumerator in tracked function @@ -244,8 +255,9 @@ In general this is quite obvious and rarely causes issues. ## Cortex.Net only tracks data accessed for `observer` components if they are directly accessed by `render` -A common mistake made with `observer` is that it doesn't track data that syntactically seems parent of the `observer` component, -but in practice is actually rendered out by a different component. This often happens when render callbacks of components are passed in first class to another component. +A common mistake made with `observer` is that it doesn't track data that syntactically seems parent of the `observer` +component, but in practice is actually rendered out by a different component. This often happens when render callbacks +of components are passed in first class to another component. Take for example the following contrived example: @@ -257,10 +269,14 @@ const MyComponent = observer(({ message }) => ( message.title = "Bar" ``` -At first glance everything might seem ok here, except that the `
` is actually not rendered by `MyComponent` (which has a tracked rendering), but by `SomeContainer`. -So to make sure that the title of `SomeContainer` correctly reacts to a new `message.title`, `SomeContainer` should be an `observer` as well. +At first glance everything might seem ok here, except that the `
` is actually not rendered by `MyComponent` +(which has a tracked rendering), but by `SomeContainer`. +So to make sure that the title of `SomeContainer` correctly reacts to a new `message.title`, `SomeContainer` +should be an `observer` as well. -If `SomeContainer` comes from an external lib, this is often not under your own control. In that case you can address this by either wrapping the `div` in its own stateless `observer` based component, or by leveraging the `` component: +If `SomeContainer` comes from an external lib, this is often not under your own control. In that case you can address +this by either wrapping the `div` in its own stateless `observer` based component, or by leveraging the `` +component: ```javascript const MyComponent = observer(({ message }) => @@ -276,7 +292,8 @@ const TitleRenderer = observer(({ message }) => message.title = "Bar" ``` -Alternatively, to avoid creating additional components, it is also possible to use the Cortex.Net-react built-in `Observer` component, which takes no arguments, and a single render function as children: +Alternatively, to avoid creating additional components, it is also possible to use the Cortex.Net-react built-in +`Observer` component, which takes no arguments, and a single render function as children: ```javascript const MyComponent = ({ message }) => ( @@ -288,7 +305,8 @@ message.title = "Bar" ## Avoid caching observables in local fields -A common mistake is to store local variables that dereference observables, and then expect components to react. For example: +A common mistake is to store local variables that dereference observables, and then expect components to react. For +example: ```javascript @observer @@ -351,8 +369,11 @@ const Likes = observer(({ likes }) => ( Notes: -1. \* If the `Author` component was invoked like: ``. Then `Message` would be the dereferencing component and react to changes to `message.author.name`. Nonetheless `` would rerender as well, because it receives a new value. So performance wise it is best to dereference as late as possible. -2. \*\* If likes were objects instead of strings, and if they were rendered by their own `Like` component, the `Likes` component would not rerender for changes happening inside a specific like. +1. \* If the `Author` component was invoked like: ``. Then `Message` would be +the dereferencing component and react to changes to `message.author.name`. Nonetheless `` would rerender as well, +because it receives a new value. So performance wise it is best to dereference as late as possible. +2. \*\* If likes were objects instead of strings, and if they were rendered by their own `Like` component, the `Likes` +component would not rerender for changes happening inside a specific like. ## TL;DR diff --git a/docs/pages/reaction.md b/docs/pages/reaction.md index 6087813..3bc0ffd 100644 --- a/docs/pages/reaction.md +++ b/docs/pages/reaction.md @@ -1,11 +1,13 @@ # Reaction -Declaration: `IDisposable Reaction(this ISharedState sharedState, Func expression, Action effect, ReactionOptions reactionOptions = null);`. +Declaration: `IDisposable Reaction(this ISharedState sharedState, Func expression, Action +effect, ReactionOptions reactionOptions = null);`. A variation on [Autorun](autorun.md) that gives more fine grained control on which observables will be tracked. -It takes two delegates, the first one (the _expression_ function) is tracked and returns data that is used as input for the second one, the _effect_ function. -Unlike `Autorun` the side effect won't be run directly when created, but only after the data expression returns a new value for the first time. -Any observables that are accessed while executing the side effect will not be tracked. +It takes two delegates, the first one (the _expression_ function) is tracked and returns data that is used as input for +the second one, the _effect_ function. +Unlike `Autorun` the side effect won't be run directly when created, but only after the data expression returns a new +value for the first time. Any observables that are accessed while executing the side effect will not be tracked. `Reaction` returns an IDisposable instance. @@ -13,27 +15,36 @@ The second function (the _effect_ function) passed to `Reaction` will receive tw The first argument is the value returned by the _expression_ function. The second argument is the current reaction, which can be used to dispose the `Reaction` during execution. -It is important to notice that the side effect will _only_ react to data that was _accessed_ in the data expression, which might be less than the data that is actually used in the effect. +It is important to notice that the side effect will _only_ react to data that was _accessed_ in the data expression, +which might be less than the data that is actually used in the effect. Also, the side effect will only be triggered when the data returned by the expression has changed. In other words: reaction requires you to produce the things you need in your side effect. ## Options -Reaction accepts a third argument as an [ReactionOptions<T>](xref:Cortex.Net.ReactionOptions-1) with the following optional options: +Reaction accepts a third argument as an [ReactionOptions<T>](xref:Cortex.Net.ReactionOptions-1) with the following +optional options: -- `FireImmediately`: Boolean that indicates that the effect function should immediately be triggered after the first run of the data function. `false` by default. -- `Delay`: Number in milliseconds that can be used to debounce the effect function. If zero (the default), no debouncing will happen. -- `EqualityComparer`: If specified, this comparer function will be used to compare the previous and next values produced by the _expression_ function. The _effect_ function will only be invoked if this function returns false. If specified, this will override the default `Equals()` Comparison. +- `FireImmediately`: Boolean that indicates that the effect function should immediately be triggered after the first + run of the data function. `false` by default. +- `Delay`: Number in milliseconds that can be used to debounce the effect function. If zero (the default), no + debouncing will happen. +- `EqualityComparer`: If specified, this comparer function will be used to compare the previous and next values + produced by the _expression_ function. The _effect_ function will only be invoked if this function returns false. + If specified, this will override the default `Equals()` Comparison. - `Name`: String that is used as name for this reaction in for example [`Spy`](spy.md) events. - `ErrorHandler`: delegate that will handle the errors of this reaction, rather then propagating them. -- `Scheduler`: Set a custom scheduler to determine how running the effect function should be scheduled. It takes a async delegate that should be invoked at some point in the future, for example: `{ Scheduler: async () => { await Task.Delay(1000); }}` +- `Scheduler`: Set a custom scheduler to determine how running the effect function should be scheduled. It takes a + async delegate that should be invoked at some point in the future, for example: `{ Scheduler: async () => + { await Task.Delay(1000); }}` ## Example -In the following example both `reaction1`, `reaction2` and `autorun1` will react to the addition, removal or replacement of todo's in the `todos` collection. -But only `reaction2` and `autorun` will react to the change of a `Title` in one of the todo items, because `Title` is used in the data expression of reaction 2, while it isn't in the data expression of reaction 1. -`Autorun` tracks the complete side effect, hence it will always trigger correctly, but is also more suspectible to accidentally accessing unrelevant data. -See also [what will Cortex.NET React to?](react.md). +In the following example both `reaction1`, `reaction2` and `autorun1` will react to the addition, removal or replacement +of todo's in the `todos` collection. But only `reaction2` and `autorun` will react to the change of a `Title` in one of +the todo items, because `Title` is used in the data expression of reaction 2, while it isn't in the data expression of +reaction 1. `Autorun` tracks the complete side effect, hence it will always trigger correctly, but is also more +suspectible to accidentally accessing unrelevant data. See also [what will Cortex.NET React to?](react.md). ```csharp using Cortex.Net.Api; @@ -73,7 +84,8 @@ var reaction2 = sharedState.Reaction( ); // autorun reacts to just everything that is used in its delegate -var autorun1 = sharedState.Autorun(r => Console.WriteLine("autorun 1:", string.Join(", ", todos.Select(todo => todo.Title)))); +var autorun1 = sharedState.Autorun(r => Console.WriteLine("autorun 1:", string.Join(", ", +todos.Select(todo => todo.Title)))); todos.Add(new Todo { Title = "explain reactions", Done = false }); diff --git a/docs/pages/sharedstate.md b/docs/pages/sharedstate.md index 361956a..9e7300a 100644 --- a/docs/pages/sharedstate.md +++ b/docs/pages/sharedstate.md @@ -84,7 +84,8 @@ var person3 = new Person(); Most DI containers provide a way to retrieve scoped instances from the container that are tied or can be tied to something like the thread context, async context -or a webrequest et cetera. For instance the [DI container from microsoft](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) +or a webrequest et cetera. For instance the +[DI container from microsoft](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) makes it possible to tie a service to a web request like this: ```csharp diff --git a/docs/pages/threading.md b/docs/pages/threading.md index a618cb7..e7169c2 100644 --- a/docs/pages/threading.md +++ b/docs/pages/threading.md @@ -3,8 +3,8 @@ A common myth, like for any event system is that Cortex.Net is multithreaded. It is not, computed values are updated on access, or at the end of an action, after which reactions are executed. This all happens synchronously. -To maintain integrity of the shared state, reads and updates of observable values should happen sequentially, which means -either on the same thread or with a proper locking mechanism to guard against simultaneous access. +To maintain integrity of the shared state, reads and updates of observable values should happen sequentially, which +means either on the same thread or with a proper locking mechanism to guard against simultaneous access. In most applications that have a UI it makes sense to do this on the UI thread. # Scheduling actions on the UI thread. diff --git a/docs/pages/toc.yml b/docs/pages/toc.yml index 8335df9..9a5274a 100644 --- a/docs/pages/toc.yml +++ b/docs/pages/toc.yml @@ -44,7 +44,7 @@ items: - name: Weaving with fody. href: weaving.md - - name: "Threading and async/await" + - name: "Threading" href: threading.md - name: Dependency Injection href: dependencyinjection.md diff --git a/docs/pages/weaving.md b/docs/pages/weaving.md index e69de29..c7a565f 100644 --- a/docs/pages/weaving.md +++ b/docs/pages/weaving.md @@ -0,0 +1,121 @@ +# Weaving + +Using a state library like Cortex.Net, or any other for that matter, will provide you with the benefits of that library, +but it comes at a cost. It will force you to write your state models in a certain way and add boilerplate. The +boilerplate of [INotifyPropertyChanged](https://www.google.com/search?q=inotifypropertychanged+boilerplate) or +[Redux](https://github.com/reduxjs/redux/issues/2295) is well documented for example. + +So the goal is to maximize effectiveness with a minimal amount of code. In the [Introduction](index.md) we have already +seen that we can manually implement observable properties by inheriting from or encapsulating +[ObservableObject](xref:Cortex.Net.Types.ObservableObject). Writing the code for observability for a lot of properties +becomes tedious though: + +```csharp +using Cortex.Net.Types; + +public class Todo : ObservableObject +{ + public string Title + { + get => this.Read(nameof(Title)); + set => this.Write(nameof(Title), value); + } + + public bool Completed + { + get => this.Read(nameof(Completed)); + set => this.Write(nameof(Completed), value); + } +} +``` + +Likewise implementing [actions](action.md) as delegates isn't always what we want. What if we could ask the computer +to write this boilerplate for us? It is impossible to improve upon the pattern above with the standard language features +because it is a [cross cutting concern](https://en.wikipedia.org/wiki/Cross-cutting_concern). We observe the repetition +but it is tied to the name and type of the property, so imposible to further separate or reduce. + +This is where [Aspect oriented Programming](https://en.wikipedia.org/wiki/Aspect-oriented_programming) comes into play. +AOP allows you to add code (an advice) at certain locations (pointcuts) at runtime or at compile time. The combination +of advice and pointcut is called an Aspect. + +Cortex.Net uses Fody to reduce the boilerplate. Fody is an extensible tool for weaving .NET assemblies. +Fody itself doesn’t do much to the code, it mostly integrates itself into the MSBuild pipeline, but it has a collection +of plugins to actually change it. Cortex.Net comes with its own Fody plugin built-in. + +Cortex.Net will add aspects as a post-build-step to the code that will handle tracking property access and executing +actions transparently. Adding the nuget reference to Cortex.Net will also add Fody and the Cortex.Net plugin as a +post-build step. To enable weaving the FodyWeavers.xml file that is automatically created in the project root needs to +be modified: + +## Add to FodyWeavers.xml + +To make life easier Cortex.Net supports weaving to create transparent observable state. To do this you need to create a +FodyWeavers.xml file and add it to your project. Add `` to +[FodyWeavers.xml](https://github.com/Fody/Home/blob/master/pages/usage.md#add-fodyweaversxml) + +```xml + + + +``` + +## Controlling the pointcuts with attributes. + +Pointcuts can be automatically applied, but Cortex.Net has chosen to make this explicit by requiring attributes to be +added to the code at the location where weaving needs to be added: + +### Adding observability. + +[Observability](observable.md) can be applied by applying the [[Observable]](xref:Cortex.Net.Api.ObservableAttribute) +attribute to auto implemented properties or to classes. + +```csharp +using Cortex.Net.Api; + +public class Person +{ + [Observable] + public string FirstName { get; set;} + + [Observable] + public string LastName { get; set; } +} +``` + +Applying the attribute to an auto property that implements a collection interface will instantiate the interface with +an observable version of the collection: + +``` +using Cortex.Net.Api; +using Cortex.Net; + +public class IntegerStore +{ + [Observable] + public ICollection Integers { get; set; } +} + +### Creating an action. + +Applying the [[Action]](xref:Cortex.Net.Api.ActionAttribute) attribute on any method that modifies observables will +batch the modifications and runs the reactions at the end of the method: + +```csharp + +[Action] +public void MyAction() +{ + todos.Add(new Todo() { Title = "Get Coffee" }); + todos.Add(new Todo() { Title = "Write Code" }); + todos[0].Completed = true; +} + +MyAction(); +``` + +## Caveats + +Aspect oriented programming has disadvantages: Control flow is obscured, debugging is harder, point cuts of different +aspects might interfere with each other and so in. Make sure the advantages outweigh the disadvantages when using it. +It is perfectly possible to mix Fody enhanced models with manually implemented IReactiveObject / ObservableObject +instances. \ No newline at end of file diff --git a/docs/pages/when.md b/docs/pages/when.md index 3fb68c4..8f248ba 100644 --- a/docs/pages/when.md +++ b/docs/pages/when.md @@ -4,9 +4,9 @@ Declaration public static IDisposable When(this ISharedState sharedState, Func predicate, Action effect, WhenOptions whenOptions = null) ``` -[When](xref:Cortex.Net.Api.SharedStateWhenExtensions.When(Cortex.Net.ISharedState,Func{System.Boolean},Action,Cortex.Net.WhenOptions))] observes & runs the given `predicate` until it returns true. -Once that happens, the given `effect` is executed and the autorunner is disposed. -The function returns a disposer to cancel the autorunner prematurely. +[When](xref:Cortex.Net.Api.SharedStateWhenExtensions.When(Cortex.Net.ISharedState,Func{System.Boolean},Action,Cortex.Net.WhenOptions))] +observes & runs the given `predicate` until it returns true. Once that happens, the given `effect` is executed and the +autorunner is disposed. The function returns a disposer to cancel the autorunner prematurely. This function is really useful to dispose or cancel stuff in a reactive way. For example: