Skip to content

jsoldi/ReactiveProperties

Repository files navigation

ReactiveProperties

Observable properties for easy data binding. It includes a tiny sample WinForms application to give you a quick idea of what it does.

Quick start

ReactiveProperties consists of two interfaces: IPropertySource<T> and IProperty<T>.

IPropertySource<T> represents a readonly property that can be subscribed to, just like you'd subscribe to IObservable<T> if you were using Reactive Extensions:

public interface IPropertySource<out T>
{
    T Value { get; }
    IDisposable RawSubscribe(Action rawObserver);
}

IProperty<T> implements IPropertySource<T>, but it also has a setter:

public interface IProperty<T> : IPropertySource<T>
{
    new T Value { get; set; }
}

(To subscribe, don't use RawSubstribe but the Subscribe extension method.)

It's easy to create a property from any standard property that either has an associated [PropertyName]Changed event, or whose declaring class implements INotifyPropertyChanged. For instance, to create a property given a TextBox that represents its text, just use the FromProperty method:

IProperty<string> textBoxTextProperty = Property.FromProperty(() => textBox1.Text);

This property can be then observed or it's value retrieved:

string value = textBoxTextProperty.Value;
IDisposable subscription = textBoxTextProperty.Subscribe(val => { /* Do something with the value */ });

Every subscription method returns an IDisposable that must be disposed as soon as we're done observing it. An easy way to deal with all the disposables is to use the DisposableSet utility class and add all the disposables in a single call to the AddRange method (see the sample application).

Extension Methods

Since IPropertySource<T> is a monad, some Linq operators can be implemented (although I've implemented just a few of them), all of which return another IPropertySource<T>. For instance, say we have a ComboBox that contains a list of items of class Person, which in turn have a property called NameProperty. If so, we could create an IPropertySource<T> that represents the name of the selected person using the As, SelectMany and Return methods:

IPropertySource<string> currentName = Property
    .FromProperty(() => comboBox1.SelectedValue)
    .As<Person>()
    .SelectMany(person => person == null ? PropertySource.Return("") : person.NameProperty);

In this example, currentName will contain the name of the currently selected person, or an empty string if no person is selected, and will notify whenever either the current person or the current person's name changes. By convention, I like to call the property version of a C# property [Name]Property, such as in this sample class:

public class Person
{
	public readonly IProperty<string> NameProperty;

	public string Name
	{
		get { return NameProperty.Value; }
		set { NameProperty.Value = value; }
	}
}

Properties can also be merged using the Merge method to make composite properties:

public class Vector
{
	public readonly IProperty<double> XProperty;
	public readonly IProperty<double> YProperty;
	public readonly IPropertySource<double> MagnitudeProperty;
	
	public double X
	{
	    get { return XProperty.Value; }
	    set { XProperty.Value = value; }
	}

	public double Y
	{
	    get { return YProperty.Value; }
	    set { YProperty.Value = value; }
	}
	
	public double Magnitude
	{
	    get { return MagnitudeProperty.Value; }
	}
	
	public Vector()
	{
	    XProperty = Property.FromValue(0.0);
	    YProperty = Property.FromValue(0.0);
	    MagnitudeProperty = XProperty.Merge(YProperty, (x, y) => Math.Sqrt(x * x + y * y));
	}
}

In this example, changing any of the X or Y properties will cause (if the actual value of the magnitude changes) MagnitudeProperty to notify a subscriber, if any. Alternatively, the Magnitude property can also have a setter if MagnitudeProperty is an IProperty<double> defined using Property's Create method:

MagnitudeProperty = Property.Create(
	XProperty.Merge(YProperty, (x, y) => Math.Sqrt(x * x + y * y)), 
	newMagnitude =>
	{
		var scale = newMagnitude / Magnitude;
		X *= scale;
		Y *= scale;
	}
);

IProperty<T> also has a SelectMany and a variation of the Select method (which also take a backwards selector), but since it cannot have a Return method (it wouldn't make any sense), it is not a monad and therefore not as extensible as IPropertySource<T>.

About

Observable properties for easy data binding

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages