diff --git a/docs/defining-properties.md b/docs/defining-properties.md new file mode 100644 index 0000000000..532a2ede71 --- /dev/null +++ b/docs/defining-properties.md @@ -0,0 +1,178 @@ +# Defining Properties + +If you are creating a control, you will want to define properties on your +control. The process in Perspex is broadly similar to other XAML languages +with a few differences - the main one being that Perpsex's equivalent of +`DependencyProperty` is called `StyledProperty`. + +## Registering Styled Properties + +A styled property is analogous to a `DependencyProperty` in other XAML +frameworks. + +You register a styled property by calling `PerspexProperty.Register` and +storing the result in a `static readonly` field. You then create a standard C# +property to access it. + +Here's how the `Border` control defines its `Background` property: + +```c# + public static readonly StyledProperty BackgroundProperty = + PerspexProperty.Register(nameof(Background)); + + public Brush Background + { + get { return GetValue(BackgroundProperty); } + set { SetValue(BackgroundProperty, value); } + } +``` + +The `PerspexProperty.Register` method also accepts a number of other parameters: + +- `defaultValue`: This gives the property a default value. Be sure to only pass +value types and immutable types here as passing a reference type will cause the +same object to be used on all instances on which the property is registered. +- `inherits`: Specified that the property's default value should come from +the parent control. +- `defaultBindingMode`: The default binding mode for the property. Can be set to +`OneWay`, `TwoWay`, `OneTime` or `OneWayToSource`. +- `validate`: A validation/coercion function of type +`Func`. The function accepts the instance of the class +on which the property is being set and the value and returns the coerced value +or throws an exception for an invalid value. + +## Using a StyledProperty from Another Class + +Sometimes the property you want to add to your control already exists on another +control, `Background` being a good example. To register a property defined on +another control, you call `StyledProperty.AddOwner`: + + +```c# + public static readonly StyledProperty BackgroundProperty = + Border.BackgroundProperty.AddOwner(); + + public Brush Background + { + get { return GetValue(BackgroundProperty); } + set { SetValue(BackgroundProperty, value); } + } +``` + +*Note: Unlike WPF, a property must be registered on a class otherwise it cannot +be set on an object of that class.* + +## Attached Properties + +Attached properties are defined almost identically to styled properties except +that they are registered using the `RegisterAttached` method and their accessors +are defined as static methods. + +Here's how `Grid` defines its `Grid.Column` attached property: + +```c# + public static readonly AttachedProperty ColumnProperty = + PerspexProperty.RegisterAttached("Column"); + + public static int GetColumn(Control element) + { + return element.GetValue(ColumnProperty); + } + + public static void SetColumn(Control element, int value) + { + element.SetValue(ColumnProperty, value); + } +``` + +## Readonly PerspexProperties + +To create a readonly property you use the `PerspexProperty.RegisterDirect` +method. Here is how `Visual` registers the readonly `Bounds` property: + +```c# + public static readonly DirectProperty BoundsProperty = + PerspexProperty.RegisterDirect( + nameof(Bounds), + o => o.Bounds); + + private Rect _bounds; + + public Rect Bounds + { + get { return _bounds; } + private set { SetAndRaise(BoundsProperty, ref _bounds, value); } + } +``` + +As can be seen, readonly properties are stored as a field on the object. When +registering the property, a getter is passed which is used to access the +property value through `GetValue` and then `SetAndRaise` is used to notify +listeners to changes to the property. + +## Direct PerspexProperties + +As its name suggests, `RegisterDirect` isn't just used for registering readonly +properties. You can also pass a *setter* to `RegisterDirect` to expose a +standard C# property as a Perspex property. + +A `StyledProperty` which is registered using `PerspexProperty.Register` +maintains a prioritized list of values and bindings that allow styles to work. +However, this is overkill for many properties, such as `ItemsControl.Items` - +this will never be styled and the overhead involved with styled properties is +unnecessary. + +Here is how `ItemsControl.Items` is registered: + +```c# + public static readonly DirectProperty ItemsProperty = + PerspexProperty.RegisterDirect( + nameof(Items), + o => o.Items, + (o, v) => o.Items = v); + + private IEnumerable _items = new PerspexList(); + + public IEnumerable Items + { + get { return _items; } + set { SetAndRaise(ItemsProperty, ref _items, value); } + } +``` + + +Direct properties are a lightweight version of styled properties that support +the following: + +- PerspexObject.GetValue +- PerspexObject.SetValue for non-readonly properties +- PropertyChanged +- Binding (only with LocalValue priority) +- GetObservable +- AddOwner +- Metadata + +They don't support the following: + +- Validation/Coercion (although this could be done in the property setter) +- Overriding default values. +- Inherited values + +## When to use a Direct vs a Styled Property + +Direct properties have advantages and disadvantages: + +Pros: +- No additional object is allocated per-instance for the property +- Property getter is a standard C# property getter +- Property setter is is a standard C# property setter that raises an event. + +Cons: +- Cannot inherit value from parent control +- Cannot take advantage of Perspex's styling system +- Property value is a field and as such is allocated whether the property is +set on the object or not + +So use direct properties when you have the following requirements: +- Property will not need to be styled +- Property will usually or always have a value diff --git a/docs/working-with-properties.md b/docs/working-with-properties.md new file mode 100644 index 0000000000..bde12b50f2 --- /dev/null +++ b/docs/working-with-properties.md @@ -0,0 +1,100 @@ +# Working with Properties + +Perspex controls expose their properties as standard CLR properties, so for +reading and writing values there's no surprises: + +```c# + // Create a TextBlock and set its Text property. + var textBlock = new TextBlock(); + textBlock.Text = "Hello World!"; +``` + + +However there's a lot more you can do with properties such as subscribing to +changes on the property, and binding. + +# Subscribing to Changes to a Property + +You can subscribe to changes on a property by calling the `GetObservable` +method. This returns an `IObservable` which can be used to listen for changes +to the property: + +```c# + var textBlock = new TextBlock(); + var text = textBlock.GetObservable(TextBlock.TextProperty); +``` + +Each property that can be subscribed to has a static readonly field called +`[PropertyName]Property` which is passed to `GetObservable` in order to +subscribe to the property's changes. + +`IObservable` (part of Reactive Extensions, or rx for short) is out of scope +for this guide, but here's an example which uses the returned observable to +print a message with the changing property values to the console: + +```c# + var textBlock = new TextBlock(); + var text = textBlock.GetObservable(TextBlock.TextProperty); + text.Subscribe(value => Console.WriteLine(value + " Changed")); +``` + +When the returned observable is subscribed, it will return the current value +of the property immediately and then push a new value each time the property +changes. If you don't want the current value, you can use the rx `Skip` +operator: + +```c# + var text = textBlock.GetObservable(TextBlock.TextProperty).Skip(1); +``` + +# Binding a property + +Observables don't just go one way! You can also use them to bind properties. +For example here we create two `TextBlock`s and bind the second's `Text` +property to the first: + +```c# + var textBlock1 = new TextBlock(); + var textBlock2 = new TextBlock(); + + // Get an observable for the first text block's Text property. + var source = textBlock1.GetObservable(TextBlock.TextProperty); + + // And bind it to the second. + textBlock2.Bind(TextBlock.TextProperty, source); + + // Changes to the first TextBlock's Text property will now be propagated + // to the second. + textBlock1.Text = "Goodbye Cruel World"; + + // Prints "Goodbye Cruel World" + Console.WriteLine(textBlock2.Text); +``` + +# Subscribing to a Property on Any Object + +The `GetObservable` method returns an observable that tracks changes to a +property on a single instance. However, if you're writing a control you may +want to implement an `OnPropertyChanged` method. In WPF this is done by passing +a static `PropertyChangedCallback` to the `DependencyProperty` registration +method, but in Perspex it's slightly different (and hopefully easier!) + +The field which defines the property is derived from `PerspexProperty` and this +has a `Changed` observable which is fired every time the property changes on +*any* object. In addition there is an `AddClassHandler` extension method which +can automatically route the event to a method on your control. + +For example if you want to listen to changes to your control's `Foo` property +you'd do it like this: + +```c# + static MyControl() + { + FooProperty.Changed.AddClassHandler(x => x.FooChanged); + } + + private void FooChanged(PerspexPropertyChangedEventArgs e) + { + // The 'e' parameter describes what's changed. + } +```