Kinetic is an alternative implementation of the Reactive framework focused on performance and lesser memory allocations.
To achive the goal Kinetic doesn't support the INotifyPropertyChanged
interface and fully relies on IObservable<T>
. To make it work an observable property should via Property<T>
or ReadOnlyProperty<T>
structures which bundle a getter, a setter and an observable. Calling the Set
method on a property sets the corresponind field and notifyies observers about the change. The Changed
property returns an observable for the property which is a cached and reused, and no LINQ expressions allocated as it happens when WhenAnyValue
is used from Reactive.
private sealed class Some : ObservableObject
{
private int _number;
private string _text = string.Empty;
public Some() => Number.Changed.Subscribe(value => Set(Text, value.ToString()));
public Property<int> Number => Property(ref _number);
public ReadOnlyProperty<string> Text => Property(ref _text);
public static void Usage()
{
var some = new Some();
var numberExplicit = some.Number.Get();
int numberImplicit = some.Number;
some.Number.Set(42);
}
}
Instead of the standard ObservableCollection<T>
and other custom types implementing INotifyCollectionChanged
Kinetic has two built-in collections, ObservableList<T>
and ReadOnlyObservableList<T>
which use the same notification model as the rest of the framework. Both of them expose an IObservable<ListChange<T>>
through the Changed
property.
In contradiction to .NET and DynamicData, there are no multiple element changes to avoid memory allocations on notifications and code complexity for views. Therefore, each ListChange<T>
can represent one of the following actions:
RemoveAll
Remove
Insert
Replace
Move
Other than that Kinetic collections looks pretty similar to what the .NET ecosystem has.
For possible scenarios of making views using LINQ operators please refer to tests. Currently there are three operators, but more options and operators should come to Kinetic:
OrderBy
Select
Where
Kinetic provides an implementation of the ICommand
interface which produces a result on completion by implementing IObservable<T>
too. Any command can have a state object passed to it on creation or received from the provided observable object.
Kinetic commands in contradistinction to Reactive support parameter validation on CanExecute
and Execute
, and even does nullable reference type validation.
// Nullable reference parameter
var command = Command<string?>.Create(p => p);
command.CanExecute(null); // returns true
command.CanExecute("text"); // returns true
// Non-nullable reference parameter
var command = Command<string>.Create(p => p);
command.CanExecute(null); // returns false
command.CanExecute("text"); // returns true
The project provides a subset of LINQ extensions, which are contained by Kinetic.Linq
package. The key idea of it is to build a single state machine for a chain of extension method call to minimize memory occupied by the resulting observer, and to avoid many interface calls which happen in Reactive.
Since all observable properties should be defined as Property<T>
or ReadOnlyProperty<T>
, there's a limitation of Kinetic usage. It's supported only by Avalonia at the moment thanfully to the extensible binding system, but a general solution to support any XAML framework will come later.
To make Avalonia recognize Kinetic properties, the Kinetic.Avalonia
package should be added and one line of code at startup as well:
Collection bindings automaticlly produce a proxy which translates ListChange<T>
to INotifyCollectionChanged
events.
using Avalonia.Data.Core;
using Kinetic.Data;
// Adds the accessor for Kinetic properties before the CLR property accessor
BindingPlugins.PropertyAccessors.Insert(2, new KineticPropertyAccessor());
This approach is incompatible with compiled bindings since XAMLIL has no idea about Kinetic properties and treats them as usual properties. Ability to create compiled bindings in XAML will come in one of next releases, but it's already available in code behind using
OneWay
andTwoWay
methods ofBinding
type inKinetic.Data
Kinetic assembles a chain of LINQ calls into a single state machine, so it's possible to inspect it in the debugger as a single list. No more diving into internal fields, checking subscribers of each operator to find what you want.
As an example, the observer
variable can be seen in the debugger as a list of 4 state machines:
var source = new PublishSubject<int>();
var observer = source // observable
// ├ [0] ObserverStateMachine
.Where(x => x > 0) // ├ [1] WhereStateMachind
.Select(x => x + 1) // ├ [2] SelectStateMachineelectStateMachine
.ToObservable(); // └ [3] ObservableStateMachine
Properties are also have debugger views allowing to explore a value and observers subscribed for changes:
var person = new Person("John Doe", "Somewhere in the Universe");
var addressChange = person.Address.Subscribe(a => Console.WriteLine(a));
class Person : ObservableObject
{
private string _name;
private string _address;
public Person(string name, string address) =>
(_name, _address) = (name, address);
public Property<string> Name => Property(ref _name);
public Property<string> Address => Property(ref _address);
}
// person
// ├ Address
// │ ├ Observers Observers = 1
// │ │ └ [0] SubscribeBox
// │ └ Value "Somewhere in the Universe"
// └ Name
// ├ Observers Observers = 0
// └ Value "John Doe"