2 changed files with 278 additions and 0 deletions
@ -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<Brush> BackgroundProperty = |
||||
|
PerspexProperty.Register<Border, Brush>(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<TOwner, TValue, TValue>`. 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<Brush> BackgroundProperty = |
||||
|
Border.BackgroundProperty.AddOwner<Panel>(); |
||||
|
|
||||
|
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<int> ColumnProperty = |
||||
|
PerspexProperty.RegisterAttached<Grid, Control, int>("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<Visual, Rect> BoundsProperty = |
||||
|
PerspexProperty.RegisterDirect<Visual, Rect>( |
||||
|
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<ItemsControl, IEnumerable> ItemsProperty = |
||||
|
PerspexProperty.RegisterDirect<ItemsControl, IEnumerable>( |
||||
|
nameof(Items), |
||||
|
o => o.Items, |
||||
|
(o, v) => o.Items = v); |
||||
|
|
||||
|
private IEnumerable _items = new PerspexList<object>(); |
||||
|
|
||||
|
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 |
||||
@ -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<T>` 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<MyControl>(x => x.FooChanged); |
||||
|
} |
||||
|
|
||||
|
private void FooChanged(PerspexPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
// The 'e' parameter describes what's changed. |
||||
|
} |
||||
|
``` |
||||
Loading…
Reference in new issue