Knockout for Cocoa, for C#
KoKo lets you create Property
objects as members of your model classes.
Unlike native C# properties, KoKo Property
objects automatically fire change events. They can be composed from multiple other Properties without the dependencies being aware of the dependents, and without writing any boilerplate event handling code. They are compatible with native C# properties and events, as well as with WPF and Windows Forms databinding.
These properties are very similar to what you would find in Knockout, MobX, and WPF's DependencyProperty
. They do not rely on a presentation layer like WPF, and they do not require you to import and understand a large, overblown, confusing library like .NET Reactive Extensions/Rx.NET.
This library was ported from an open-source Swift library by @abrindam called KoKo (which means "Knockout for Cocoa"), which was later renamed to Yoyo because "KoKo" and "Cocoa" are homophones and thus verbally indistinguishable.
Table of Contents
- Any of the following runtimes
- .NET Core 2.0 or later, including .NET 5 or later
- .NET Framework 4.6.2 or later
- Any other runtime that supports .NET Standard 2.0 or later
- Right-click on your C# project and go to Manage NuGet Packages.
- Under Browse, search for
KoKo
. - Click the down arrow button for
KoKo
.
# Alternately, using .NET CLI
dotnet add package KoKo
# Alternately, using Package Manager PowerShell
Install-Package KoKo
- Create a
class
to act as your business or view model. - Import the
KoKo.Property
namespace. - Add some
Property
fields, one for each piece of data you want to represent. - Get and set their
Value
.
This is a just a silly, simple example. Don't actually represent people's names this way.
using KoKo.Property;
namespace MyProject {
public class Person {
private StoredProperty<string> FirstName { get; }
private StoredProperty<string> LastName { get; }
public Property<string> FullName { get; }
public Person(string firstName, string lastName) {
FirstName = new StoredProperty<string>(firstName);
LastName = new StoredProperty<string>(lastName);
FullName = DerivedProperty<string>.Create(FirstName, LastName, (first, last) => $"{first} {last}");
}
public void SetFirstName(string firstName) {
FirstName.Value = firstName;
}
}
}
Now you can get a person
object's autogenerated full name,
var person = new Person("Alice", "Smith");
Console.WriteLine(person.FullName.Value); // Alice Smith
and if you change a dependency value, the dependent full name will be automatically updated.
person.SetFirstName("Bob");
Console.WriteLine(person.FullName.Value); // Bob Smith
You can also use Properties with databinding:
<!-- WPF -->
<Label Content="{Binding FullName.Value}" />
// Windows Forms
private void Form1_Load(object sender, System.EventArgs e) {
personNameLabel.DataBindings.Add("Text", model.FullName, "Value");
}
Remember to use the Value
property in your databinding declarations, otherwise you won't get the property's value.
- Stores a single value in memory
- Value can be get or set imperatively
- Similar to a native C# property, except you don't have to implement
INotifyPropertyChanged
yourself - You can perform atomic operations on a
StoredProperty
value usingIncrement()
,Decrement()
,Add(value)
,Exchange(value)
, orCompareExchange(possibleNewValue, assignIfOldValueEquals)
var a = new StoredProperty<string>("world");
Console.WriteLine($"Hello {a.Value}"); // Hello world
a.Value = "world!";
Console.WriteLine($"Hello {a.Value}"); // Hello world!
- Automatically calculates its value from one or more other properties (dependencies), which can be any type of KoKo property
- You don't need to manually invoke event handlers on the dependencies when their values change, this property will automatically recalculate its value instead
- You cannot set its value directly, but you can get the value just like a
StoredProperty
- Unlike other KoKo property classes, you construct
DerivedProperty
instances using theDerivedProperty.Create()
factory method instead of a constructor. This allows you to pass a type-safe calculator function that doesn't capture the dependency properties.
var b = new StoredProperty<int>(8);
DerivedProperty<int> absoluteValueB = DerivedProperty<int>.Create(b, (bValue) => System.Math.Abs(bValue));
Console.WriteLine($"The absolute value of {b.Value} is {absoluteValueB.Value}."); // The absolute value of 8 is 8.
b.Value = -9;
Console.WriteLine($"The absolute value of {b.Value} is {absoluteValueB.Value}."); // The absolute value of -9 is 9.
- Like a
PassthroughProperty
, except the dependentConnectableProperty
has its dependencyProperty
passed to it instead of depending upon it at creation time - You can also pass a constant value instead of a
Property
, which is more convenient that constructing a whole newStoredProperty
instance to hold that constant - Useful for inversion of control where the dependent must not be aware of how to obtain its dependencies
- Can be reconnected and disconnected multiple times, to allow reconfiguration
var connectable = new ConnectableProperty<int>(0);
var a = new StoredProperty<int>(8);
var b = 9;
Console.WriteLine(connectable.Value); // 0
connectable.Connect(a);
Console.WriteLine(connectable.Value); // 8
connectable.Connect(b);
Console.WriteLine(connectable.Value); // 9
- Like a
DerivedProperty
, except instead of recalculating its value based on changes to dependency properties, you must manually instruct the property to recalculate its value - Useful when the dependencies do not have a way to expose change events
- Useful when the dependencies are constantly changing and you don't care about every change
- Useful when the recalculation is very expensive and you want to reduce how often it is run
- Alternatively, if you want to reuse the same calculator, expose a parameterless constructor, or parameterize multiple instances, you can subclass
ManuallyRecalculatedProperty<T>
, call the parameterless super-constructor, and override theComputeValue()
method with your calculator logic, rather than passing a calculator function to the super-constructor.
var manuallyRecalculated = new ManuallyRecalculatedProperty<long>(() => DateTimeOffset.Now.ToUnixTimeMilliseconds());
Console.WriteLine(manuallyRecalculated); // 1591651725420
Thread.Sleep(1000);
manuallyRecalculated.Recalculate();
Console.WriteLine(manuallyRecalculated); // 1591651726420
- Like a
PassthroughProperty
, except it gets a property value nested at an arbitrary depth - Useful if you have an object graph with properties whose values are classes with other properties
- Useful if the top-level reference may change to a different instance (which person is currently logged in), but the property chain you want to refer to is always the same (the full name of the currently logged-in person)
var person = new Person("FirstName", "LastName");
var currentUser = new StoredProperty<Person>(person);
var currentUserFullName = new MultiLevelProperty<string>(() => currentUser.Value.fullName);
Console.WriteLine($"Welcome, {currentUserFullName.Value}"); // Welcome, FirstName LastName
- Useful for interoperation with C# classes, whether they expose property value changes using
INotifyPropertyChanged
or other events
// MyNativePropertyClass implements INotifyPropertyChanged and fires PropertyChanged events
var nativePropertyObject = new MyNativePropertyClass { NativeProperty = 8 };
var kokoProperty = new NativeReadableProperty<int>(nativePropertyObject, nameof(NativePropertyClass.NativeProperty));
Console.WriteLine(kokoProperty.Value); // 8
- If the class does not implement
INotifyPropertyChanged
, this class assumes that it should listen for*Changed
events that are named after the native property, so that it can tell when the native property value changes. For example, given theText
property on theToolStripStatusLabel
class,NativeReadableProperty
will listen forTextChanged
events.
// ToolStripStatusLabel does not implement INotifyPropertyChanged, and instead fires TextChanged events
var toolStripStatusLabel = new ToolStripStatusLabel { Text = "ready" };
// The event name TextChanged is inferred from the Text property name
var kokoProperty = new NativeReadableProperty<string>(toolStripStatusLabel, nameof(toolStripStatusLabel.Text));
- If the event name does not follow the
propertyName + "Changed"
naming convention, you may override it with the optional thirdnativeEventName
parameter.
// MyNativePropertyClass2 does not implement INotifyPropertyChanged, and instead fires custom NativePropertyChanged events
var nativePropertyObject = new MyNativePropertyClass2 { NativeProperty = 8 };
var kokoProperty = new NativeReadableProperty<int>(nativePropertyObject, nameof(NativePropertyClass2.NativeProperty),
nameof(NativePropertyClass2.NativePropertyChanged));
Console.WriteLine(kokoProperty.Value); // 8
- Like a
NativeReadableProperty
, except you can also change the value - Useful when the native C# property has an accessible setter
// MyNativePropertyClass implements INotifyPropertyChanged and fires PropertyChanged events
var nativePropertyObject = new MyNativePropertyClass { NativeProperty = 8 };
var kokoProperty = new NativeWritableProperty<int>(nativePropertyObject, nameof(NativePropertyClass.NativeProperty));
Console.WriteLine(kokoProperty.Value); // 8
kokoProperty.Value = 9;
Console.WriteLine(nativePropertyObject.NativeProperty); // 9
// MyNativePropertyClass2 does not implement INotifyPropertyChanged, and instead fires custom NativePropertyChanged events
var nativePropertyObject = new MyNativePropertyClass2 { nativeProperty = 8 };
var kokoProperty = new NativeWritableProperty<int>(nativePropertyObject, nameof(NativePropertyClass2.NativeProperty)); // Implicit event name NativePropertyChanged
Console.WriteLine(kokoProperty.Value); // 8
kokoProperty.Value = 9;
Console.WriteLine(nativePropertyObject.NativeProperty); // 9
// MyNativePropertyClass2 does not implement INotifyPropertyChanged, and instead fires custom NativePropertyChanged events
var nativePropertyObject = new MyNativePropertyClass2 { nativeProperty = 8 };
var kokoProperty = new NativeWritableProperty<int>(nativePropertyObject, nameof(NativePropertyClass2.NativeProperty),
nameof(NativePropertyClass2.NativePropertyChanged)); // Explicit event name NativePropertyChanged
Console.WriteLine(kokoProperty.Value); // 8
kokoProperty.Value = 9;
Console.WriteLine(nativePropertyObject.NativeProperty); // 9
- Like a
DerivedProperty
, except it depends on a single property and does not transform the value at all - Useful for organizing your class structure and defining boundaries of knowledge or responsibility
var backing = new StoredProperty<double>(3.0);
var passthrough = new PassthroughProperty<double>(a);
Console.WriteLine($"{passthrough.Value} liters"); // 3 liters
backing.Value = 5.0;
Console.WriteLine($"{passthrough.Value} liters"); // 5 liters
- Like a
PassthroughProperty
, except you can supply a temporary overriding value, which it will use for a specified duration before reverting to the passthrough value - Useful for dealing with changes which require a long time to take effect, but you want to make it look like it took effect immediately, while still allowing it to be eventually consistent if the change fails.
- For example, clicks on a button that performs a slow remote operation which changes the button's state, but you want the button's state to update early, before the operation completes, so that the user doesn't think their click was ignored and knows what state the system is trying to reach.
var backing = new StoredProperty<int>(8);
var tentative = new TentativeProperty<int>(backing, TimeSpan.FromMilliseconds(500));
Console.WriteLine(tentative.Value); // 8
backing.Value = 9;
Console.WriteLine(tentative.Value); // 9
tentative.Value = 10;
backing.Value = 11;
Console.WriteLine(tentative.Value); // 10
Thread.Sleep(1000);
Console.WriteLine(tentative.Value); // 11
You can subscribe to events that are fired when any Property
's value changes.
If you want to react to a property changing by changing some other data, you may want to use a DerivedProperty
or similar, because all value change dependencies will be consistent and probably simpler to understand.
On the other hand, if you want to react to a property changing by taking some action, then you can listen for the PropertyChanged
event:
var property = new StoredProperty<int>(1);
property.PropertyChanged += (object sender, KoKoPropertyChangedEventArgs<int> args) => {
Console.WriteLine($"Property value changed from {args.OldValue} to {args.NewValue}.");
};
property.Value = 2; // Property value changed from 1 to 2.
If you need to pass a KoKo Property
to a consumer that only accepts INotifyPropertyChanged
, this interface is also implemented.
var property = new StoredProperty<int>(1);
((INotifyPropertyChanged property).PropertyChanged += (object sender, PropertyChangedEventArgs args) => {
Console.WriteLine($"Property value changed to {property.Value}.");
};
property.Value = 2; // Property value changed to 2.
You may want the property changed event handlers to run on a different thread than the one that caused the property value to change in the first place. This is especially important for updating UI controls, since Windows Forms and WPF only allow UI updates on the main thread, whether the update is imperative or declarative (data binding).
To accomplish this, set EventSynchronizationContext
on your KoKo Property
to SynchronizationContext.Current
.
Now, whenever the Property value changes, even if the change happened on a background thread, the event handlers will run in that SynchronizationContext
, so if you have a WPF control bound to that Property value, it will run in the correct WPF Dispatcher
.
- All KoKo event handlers run synchronously, whether on the original thread or through a
SynchronizationContext
- All KoKo Properties can have their
EventSynchronizationContext
changed, not justPassthroughProperty
var backing = new StoredProperty<double>(3.0);
var passthrough = new PassthroughProperty<double>(backing);
passthrough.EventSynchronizationContext = SynchronizationContext.Current;