![]() |
VOOZH | about |
dotnet add package KoKo --version 2.3.0
NuGet\Install-Package KoKo -Version 2.3.0
<PackageReference Include="KoKo" Version="2.3.0" />
<PackageVersion Include="KoKo" Version="2.3.0" />Directory.Packages.props
<PackageReference Include="KoKo" />Project file
paket add KoKo --version 2.3.0
#r "nuget: KoKo, 2.3.0"
#:package KoKo@2.3.0
#addin nuget:?package=KoKo&version=2.3.0Install as a Cake Addin
#tool nuget:?package=KoKo&version=2.3.0Install as a Cake Tool
👁 Package Version
👁 NuGet Gallery Download Count
👁 GitHub Actions Workflow Status
👁 Testspace
👁 Coveralls
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.
dotnet add package KoKo
class to act as your business or view model.KoKo.Property namespace.Property fields, one for each piece of data you want to represent.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:
<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.
StoredPropertyINotifyPropertyChanged yourselfStoredProperty value using Increment(), Decrement(), Add(value), Exchange(value), or CompareExchange(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!
DerivedPropertyStoredPropertyDerivedProperty instances using the DerivedProperty.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.
ConnectablePropertyPassthroughProperty, except the dependent ConnectableProperty has its dependency Property passed to it instead of depending upon it at creation timeProperty, which is more convenient that constructing a whole new StoredProperty instance to hold that constantvar 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
ManuallyRecalculatedPropertyDerivedProperty, except instead of recalculating its value based on changes to dependency properties, you must manually instruct the property to recalculate its valueManuallyRecalculatedProperty<T>, call the parameterless super-constructor, and override the ComputeValue() 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
MultiLevelPropertyPassthroughProperty, except it gets a property value nested at an arbitrary depthvar 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
NativeReadablePropertyINotifyPropertyChanged 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
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 the Text property on the ToolStripStatusLabel class, NativeReadableProperty will listen for TextChanged 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));
propertyName + "Changed" naming convention, you may override it with the optional third nativeEventName 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
NativeWritablePropertyNativeReadableProperty, except you can also change the value// 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
PassthroughPropertyDerivedProperty, except it depends on a single property and does not transform the value at allvar 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
TentativePropertyPassthroughProperty, except you can supply a temporary overriding value, which it will use for a specified duration before reverting to the passthrough valuevar 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.
SynchronizationContextEventSynchronizationContext changed, not just PassthroughPropertyvar backing = new StoredProperty<double>(3.0);
var passthrough = new PassthroughProperty<double>(backing);
passthrough.EventSynchronizationContext = SynchronizationContext.Current;
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 was computed. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 was computed. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 netcoreapp2.0 was computed. netcoreapp2.1 netcoreapp2.1 was computed. netcoreapp2.2 netcoreapp2.2 was computed. netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 netstandard2.0 is compatible. netstandard2.1 netstandard2.1 is compatible. |
| .NET Framework | net461 net461 was computed. net462 net462 is compatible. net463 net463 was computed. net47 net47 was computed. net471 net471 was computed. net472 net472 was computed. net48 net48 was computed. net481 net481 was computed. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen40 tizen40 was computed. tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
Showing the top 2 NuGet packages that depend on KoKo:
| Package | Downloads |
|---|---|
|
BehringerXTouchExtender
Send and receive events with a Behringer X-Touch Extender DAW MIDI control surface over USB. |
|
|
SousVide
Monitor and control an Anova Precision Cooker sous vide over Bluetooth. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.4.0-beta.1 | 50 | 5/6/2026 |
| 2.3.0 | 190 | 3/5/2026 |
| 2.3.0-beta1 | 403 | 8/19/2024 |
| 2.2.0 | 2,347 | 10/7/2022 |
| 2.1.0 | 6,287 | 11/24/2021 |
| 2.0.3 | 764 | 10/11/2020 |
| 2.0.2 | 856 | 6/9/2020 |
| 2.0.1 | 634 | 6/9/2020 |
| 1.0.0 | 1,649 | 5/15/2018 |