-
Notifications
You must be signed in to change notification settings - Fork 10
Documentation
The SmartReactives API is divided into three layers:
- Common: the bread and butter of SmartReactives. The central classes are ReactiveVariable and ReactiveExpression.
- Core: the lowest level API on which the other API's are based. The central class here is ReactiveManager. In 99% of the cases you won't have a reason for using the Core API.
- Postsharp: an API of attributes that when put on properties will enhance them with SmartReactive capabilities. This API provides the most concise code.
Everything from the common API can be accessed from the class Reactive
, which features the following methods:
ReactiveVariable<T> Variable<T>(T value);
ReactiveExpression<T> Expression<T>(Func<T> expression);
ReactiveCache<T> Cache<T>(Func<T> expression);
IList<T> ToReactive<T>(this IList<T> original);
ISet<T> ToReactive<T>(this ISet<T> original);
IDictionary<TKey, TValue> ToReactive<TKey, TValue>(this IDictionary<TKey, TValue> original);
The common API is straightforward to use. Some examples can be found on the ReadMe.
The basic premise on which SmartReactives is built is quite simple: if we're evaluating A and suddenly B is read from, then A must depend on B. The tricky part is correctly dealing with multiple threads, not leaking any memory, being performant, and making sure we don't notify too often or too little.
The Core API of SmartReactives can be accessed from ReactiveManager, which exposes these methods:
Evaluate<T>(IListener dependent, Func<T> func)
void WasRead(object source)
void WasChanged(object source)
where IListener
contains the following method:
void Notify()
Each of the methods from ReactiveManager requires you to pass an object to ReactiveManager. We call these reactive objects. By correctly calling Evaluate and WasRead, ReactiveManager will build a dependency graph of your reactive objects. When you call WasChanged
, ReactiveManager will call Notify
on all objects that depend on the changed object.
An important rule of ReactiveManager is the following: after an object is notified by the ReactiveManager, it will not be notified again until the object is evaluated again. Every time an object changes its value and you're still interesting in this object, then you must evaluate it again to indicate this interest. Simply put, if you've lost interest in an object, then we won't bother you when it changes. Internally this behavior is required because an object's dependencies my change after the object changes its value, and we must re-evaluate the object to again correctly establish its dependencies. To prevent doing any unnecessary work, we leave it up to the user to re-evaluate the object.
ReactiveManager only stores weak references to object you pass to it, so it will never cause a memory leak. Compare this to events in .NET, where you can easily leak memory when you forget to remove an event handler.
A general rule for event handlers is that they should not throw exception. For more information on this subject look at Should event handlers in C# ever raise exceptions?. In the context of ReactiveManager, event handlers are the implementations of IListener.Notify
. However, if an implementation of Notify still throws an exception, ReactiveManager does the best it can. When WasChanged
is called, then Notify
is called on all the dependent objects, even if one or more of them throw an exception. These exceptions are collected and will throw an AggregateException
on the call to WasChanged
.
Previously we have defined reactive objects as objects that you pass to ReactiveManager. However, sometimes you want to track changes of something which is not an actual .NET object. For example you have a class X with a property A, and you want to know when A changes for a particular instance of X. The thing you want to track is neither the instance of X, since that would track changes in all properties of X, nor the PropertyInfo of A, since that would track changes in A's for all instances of X. Actually the thing you want to track is a tuple of both the instance of X, and a reference to A. Using a CompositeReactiveObject
, as shown in the following example, you can get the desired behavior.
class ReactivePropertiesThroughCore
{
int input = 1;
public int Input
{
get
{
ReactiveManager.WasRead(new CompositeReactiveObject(this, nameof(Input)));
return input;
}
set
{
input = value;
ReactiveManager.WasChanged(new CompositeReactiveObject(this, nameof(Input)));
}
}
public void Test()
{
var inputSquared = Reactive.Expression(() => Input * Input);
inputSquared.Subscribe(getSquare => Console.WriteLine("square = " + getSquare())); //Prints 'square = 1'
Input = 2; //Prints 'square = 2'
}
}