Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Binding tweaks and docs #766

Merged
merged 6 commits into from
Oct 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions docs/spec/binding-from-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Binding from Code

Avalonia binding from code works somewhat differently to WPF/UWP. At the low level, Avalonia's
binding system is based on Reactive Extensions' `IObservable` which is then built upon by XAML
bindings (which can also be instantiated in code).

## Binding to an observable

You can bind a property to an observable using the `AvaloniaObject.Bind` method:

```csharp
// We use an Rx Subject here so we can push new values using OnNext
var source = new Subject<string>();
var textBlock = new TextBlock();

// Bind TextBlock.Text to source
textBlock.Bind(TextBlock.TextProperty, source);

// Set textBlock.Text to "hello"
source.OnNext("hello");
// Set textBlock.Text to "world!"
source.OnNext("world!");
```

## Binding priorities

You can also pass a priority to a binding. *Note: Priorities only apply to styled properties: they*
*are ignored for direct properties.*

The priority is passed using the `BindingPriority` enum, which looks like this:

```csharp
/// <summary>
/// The priority of a binding.
/// </summary>
public enum BindingPriority
{
/// <summary>
/// A value that comes from an animation.
/// </summary>
Animation = -1,

/// <summary>
/// A local value: this is the default.
/// </summary>
LocalValue = 0,

/// <summary>
/// A triggered style binding.
/// </summary>
/// <remarks>
/// A style trigger is a selector such as .class which overrides a
/// <see cref="TemplatedParent"/> binding. In this way, a basic control can have
/// for example a Background from the templated parent which changes when the
/// control has the :pointerover class.
/// </remarks>
StyleTrigger,

/// <summary>
/// A binding to a property on the templated parent.
/// </summary>
TemplatedParent,

/// <summary>
/// A style binding.
/// </summary>
Style,

/// <summary>
/// The binding is uninitialized.
/// </summary>
Unset = int.MaxValue,
}
```

Bindings with a priority with a smaller number take precedence over bindings with a higher value
priority, and bindings added more recently take precedence over other bindings with the same
priority. Whenever the binding produces `AvaloniaProperty.UnsetValue` then the next binding in the
priority order is selected.

## Setting a binding in an object initializer

It is often useful to set up bindings in object initializers. You can do this using the indexer:

```csharp
var source = new Subject<string>();
var textBlock = new TextBlock
{
Foreground = Brushes.Red,
MaxWidth = 200,
[!TextBlock.TextProperty] = source.ToBinding(),
};
```

Using this method you can also easily bind a property on one control to a property on another:

```csharp
var textBlock1 = new TextBlock();
var textBlock2 = new TextBlock
{
Foreground = Brushes.Red,
MaxWidth = 200,
[!TextBlock.TextProperty] = textBlock1[!TextBlock.TextProperty],
};
```

Of course the indexer can be used outside object initializers too:

```csharp
textBlock2[!TextBlock.TextProperty] = textBlock1[!TextBlock.TextProperty];
```

# Transforming binding values

Because we're working with observables, we can easily transform the values we're binding!

```csharp
var source = new Subject<string>();
var textBlock = new TextBlock
{
Foreground = Brushes.Red,
MaxWidth = 200,
[!TextBlock.TextProperty] = source.Select(x => "Hello " + x).ToBinding(),
};
```

# Using XAML bindings from code

Sometimes when you want the additional features that XAML bindings provide, it's easier to use XAML bindings from code. For example, using only observables you could bind to a property on `DataContext` like this:

```csharp
var textBlock = new TextBlock();
var viewModelProperty = textBlock.GetObservable(TextBlock.DataContext)
.OfType<MyViewModel>()
.Select(x => x?.Name);
textBlock.Bind(TextBlock, viewModelProperty);
```

However, it might be preferable to use a XAML binding in this case:

```csharp
var textBlock = new TextBlock
{
[!TextBlock.TextProperty] = new Binding("Name")
};
```

By using XAML binding objects, you get access to binding to named controls and [all the other features that XAML bindings bring](binding-from.xaml.md):

```csharp
var textBlock = new TextBlock
{
[!TextBlock.TextProperty] = new Binding("Text") { ElementName = "other" }
};
```

99 changes: 99 additions & 0 deletions docs/spec/binding-from-xaml.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Binding from XAML

Binding from XAML works on the whole the same as in other XAML frameworks: you use the `{Binding}`
markup extension. Avalonia does have some extra syntacic niceties however. Here's an overview of
what you can currently do in Avalonia:

## Binding to a property on the DataContext

By default a binding binds to a property on the `DataContext`, e.g.:

```xml
<!-- Binds to the tb.DataContext.Name property -->
<TextBlock Name="tb" Text="{Binding Name}"/>
<!-- Which is the same as ('Path' is optional) -->
<TextBlock Name="tb" Text="{Binding Path=Name}"/>
```

An empty binding binds to DataContext itself

```xml
<!-- Binds to the tb.DataContext property -->
<TextBlock Name="tb" Text="{Binding}"/>
<!-- Which is the same as -->
<TextBlock Name="tb" Text="{Binding .}"/>
```

This usage is identical to WPF/UWP etc.

## Two way bindings and more

You can also specify a binding `Mode`:

```xml
<!-- Bind two-way to the property (although this is actually the default binding mode for
TextBox.Text) so strictly speaking it's unnecessary here) -->
<TextBox Name="tb" Text="{Binding Name, Mode=TwoWay}"/>
```

This usage is identical to WPF/UWP etc.

## Binding to a property on the templated parent

When you're creating a control template and you want to bind to the templated parent you can use:

```xml
<TextBlock Name="tb" Text="{TemplateBinding Caption}"/>
<!-- Which is short for -->
<TextBlock Name="tb" Text="{Binding Caption, RelativeSource={RelativeSource TemplatedParent}}"/>
```

This usage is identical to WPF/UWP etc.

## Binding to a named control

If you want to bind to a property on another (named) control, you can use `ElementName` as in
WPF/UWP:

```xml
<!-- Binds to the Tag property of a control named "other" -->
<TextBlock Text="{Binding Tag, ElementName=other}"/>
```

However Avalonia also introduces a shorthand syntax for this:

```xml
<TextBlock Text="{Binding #other.Tag}"/>
```

## Negating bindings

You can also negate the value of a binding using the `!` operator:

```xml
<TextBox IsEnabled="{Binding !HasErrors}"/>
```

Here, the `TextBox` will only be enabled when the view model signals that it has no errors. Behind
the scenes, Avalonia tries to convert the incoming value to a boolean, and if it can be converted
it negates the value. If the incoming value cannot be converted to a boolean then no value will be
pushed to the binding target.

This syntax is specific to Avalonia.

## Binding to tasks and observables

You can subscribe to the result of a task or an observable by using the `^` stream binding operator.

```xml
<!-- If DataContext.Name is an IObservable<string> then this will bind to the length of each
string produced by the observable as each value is produced -->
<TextBlock Text="{Binding Name^.Length}"/>
```

This syntax is specific to Avalonia.

*Note: the stream operator is actually extensible, see
[here](https://github.com/AvaloniaUI/Avalonia/blob/master/src/Markup/Avalonia.Markup/Data/Plugins/IStreamPlugin.cs)
for the interface to implement and [here](https://github.com/AvaloniaUI/Avalonia/blob/master/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs#L47)
for the registration.*
4 changes: 4 additions & 0 deletions docs/spec/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@
href: working-with-properties.md
- name: Logging
href: logging.md
- name: Binding from XAML
href: binding-from-xaml.md
- name: Binding from Code
href: binding-from-code.md
2 changes: 2 additions & 0 deletions docs/spec/working-with-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ property to the first:
Console.WriteLine(textBlock2.Text);
```

To read more about creating bindings from code, see [Binding from Code](binding-from-code.md).

# Subscribing to a Property on Any Object

The `GetObservable` method returns an observable that tracks changes to a
Expand Down
20 changes: 2 additions & 18 deletions docs/tutorial/from-wpf.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,6 @@ placed in a `DataTemplates` collection on each control (and on `Application`):
<ContentControl Content="{Binding Foo}"/>
<UserControl>

`ItemsControl`s don't currently have an `ItemTemplate` property: instead just
place the template for your items into the control's `DataTemplates`, e.g.

<ListBox Items="ItemsSource">
<ListBox.DataTemplates>
<DataTemplate>
<TextBlock Text="{Binding Caption}"/>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>

Data templates in Avalonia can also target interfaces and derived classes (which
cannot be done in WPF) and so the order of `DataTemplate`s can be important:
`DataTemplate`s within the same collection are evaluated in declaration order
Expand Down Expand Up @@ -92,13 +81,8 @@ referred to using the `{StyleResource}` markup extension both inside and outside
styles.

For non-style-related resources, we suggest defining them in code and referring
to them in markup using the `{Static}` markup extension. There are [various
reasons](http://www.codemag.com/article/1501091) for this, but briefly:

- Resources have to be parsed
- The tree has to be traversed to find them
- XAML doesn't handle immutable objects
- XAML syntax can be long-winded compared to C#
to them in markup using the `{Static}` markup extension. To read more about the reasoning for this,
see [this issue comment](https://github.com/AvaloniaUI/Avalonia/issues/462#issuecomment-191849723).

## Grid

Expand Down
8 changes: 7 additions & 1 deletion src/Avalonia.Base/AvaloniaObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ namespace Avalonia
/// </summary>
public static class AvaloniaObjectExtensions
{
public static IBinding AsBinding<T>(this IObservable<T> source)
/// <summary>
/// Converts an <see cref="IObservable{T}"/> to an <see cref="IBinding"/>.
/// </summary>
/// <typeparam name="T">The type produced by the observable.</typeparam>
/// <param name="source">The observable</param>
/// <returns>An <see cref="IBinding"/>.</returns>
public static IBinding ToBinding<T>(this IObservable<T> source)
{
return new BindingAdaptor(source.Select(x => (object)x));
}
Expand Down
8 changes: 4 additions & 4 deletions src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private void InitializeComponent()
},
},
[GridRepeater.TemplateProperty] = pt,
[!GridRepeater.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Properties).AsBinding(),
[!GridRepeater.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Properties).ToBinding(),
}
};
}
Expand All @@ -64,7 +64,7 @@ private IEnumerable<Control> PropertyTemplate(object i)
TextWrapping = TextWrapping.NoWrap,
[!ToolTip.TipProperty] = property
.WhenAnyValue(x => x.Diagnostic)
.AsBinding(),
.ToBinding(),
};

yield return new TextBlock
Expand All @@ -73,13 +73,13 @@ private IEnumerable<Control> PropertyTemplate(object i)
[!TextBlock.TextProperty] = property
.WhenAnyValue(v => v.Value)
.Select(v => v?.ToString())
.AsBinding(),
.ToBinding(),
};

yield return new TextBlock
{
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.WhenAnyValue(x => x.Priority).AsBinding(),
[!TextBlock.TextProperty] = property.WhenAnyValue(x => x.Priority).ToBinding(),
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
var source = new Subject<object>();
var target = new Class1
{
[!Class1.NonValidatedProperty] = source.AsBinding(),
[!Class1.NonValidatedProperty] = source.ToBinding(),
};

source.OnNext(new BindingNotification(6));
Expand All @@ -73,7 +73,7 @@ public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation()
var source = new Subject<object>();
var target = new Class1
{
[!Class1.ValidatedDirectProperty] = source.AsBinding(),
[!Class1.ValidatedDirectProperty] = source.ToBinding(),
};

source.OnNext(new BindingNotification(6));
Expand Down
8 changes: 4 additions & 4 deletions tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ private FuncControlTemplate ListBoxTemplate()
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).AsBinding(),
[~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).AsBinding(),
[~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).AsBinding(),
[~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(),
[~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(),
[~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).ToBinding(),
}
});
}
Expand All @@ -187,7 +187,7 @@ private FuncControlTemplate ScrollViewerTemplate()
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).AsBinding(),
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
Expand Down
Loading