Skip to content

Commit

Permalink
Weaving documentation and doc formatting.
Browse files Browse the repository at this point in the history
  • Loading branch information
jspuij committed Feb 18, 2020
1 parent 51a8444 commit aaf3577
Show file tree
Hide file tree
Showing 19 changed files with 459 additions and 185 deletions.
28 changes: 20 additions & 8 deletions docs/pages/action.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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

Expand All @@ -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)()`
24 changes: 16 additions & 8 deletions docs/pages/async.md
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down
48 changes: 31 additions & 17 deletions docs/pages/autorun.md
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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.
11 changes: 7 additions & 4 deletions docs/pages/box.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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.
23 changes: 14 additions & 9 deletions docs/pages/computed.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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:

Expand Down
Loading

0 comments on commit aaf3577

Please sign in to comment.