diff --git a/docs/spec/binding-from-code.md b/docs/spec/binding-from-code.md new file mode 100644 index 0000000000..76f3aa55c9 --- /dev/null +++ b/docs/spec/binding-from-code.md @@ -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(); +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 +/// +/// The priority of a binding. +/// +public enum BindingPriority +{ + /// + /// A value that comes from an animation. + /// + Animation = -1, + + /// + /// A local value: this is the default. + /// + LocalValue = 0, + + /// + /// A triggered style binding. + /// + /// + /// A style trigger is a selector such as .class which overrides a + /// 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. + /// + StyleTrigger, + + /// + /// A binding to a property on the templated parent. + /// + TemplatedParent, + + /// + /// A style binding. + /// + Style, + + /// + /// The binding is uninitialized. + /// + 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(); +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(); +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() + .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" } +}; +``` + diff --git a/docs/spec/binding-from-xaml.md b/docs/spec/binding-from-xaml.md new file mode 100644 index 0000000000..143e3627c8 --- /dev/null +++ b/docs/spec/binding-from-xaml.md @@ -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 + + + + +``` + +An empty binding binds to DataContext itself + +```xml + + + + +``` + +This usage is identical to WPF/UWP etc. + +## Two way bindings and more + +You can also specify a binding `Mode`: + +```xml + + +``` + +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 + + + +``` + +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 + + +``` + +However Avalonia also introduces a shorthand syntax for this: + +```xml + +``` + +## Negating bindings + +You can also negate the value of a binding using the `!` operator: + +```xml + +``` + +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 + + +``` + +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.* diff --git a/docs/spec/toc.yml b/docs/spec/toc.yml index f225084c4e..b0981017e0 100644 --- a/docs/spec/toc.yml +++ b/docs/spec/toc.yml @@ -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 diff --git a/docs/spec/working-with-properties.md b/docs/spec/working-with-properties.md index 74dd60a9b2..a8a383b733 100644 --- a/docs/spec/working-with-properties.md +++ b/docs/spec/working-with-properties.md @@ -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 diff --git a/docs/tutorial/from-wpf.md b/docs/tutorial/from-wpf.md index f25de9cde4..f8bb3defa2 100644 --- a/docs/tutorial/from-wpf.md +++ b/docs/tutorial/from-wpf.md @@ -40,17 +40,6 @@ placed in a `DataTemplates` collection on each control (and on `Application`): -`ItemsControl`s don't currently have an `ItemTemplate` property: instead just -place the template for your items into the control's `DataTemplates`, e.g. - - - - - - - - - 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 @@ -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 diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 3ca55529e6..685bf83a75 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -16,7 +16,13 @@ namespace Avalonia /// public static class AvaloniaObjectExtensions { - public static IBinding AsBinding(this IObservable source) + /// + /// Converts an to an . + /// + /// The type produced by the observable. + /// The observable + /// An . + public static IBinding ToBinding(this IObservable source) { return new BindingAdaptor(source.Select(x => (object)x)); } diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs index 3cde8bb49d..7cb74ebb33 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -49,7 +49,7 @@ namespace Avalonia.Diagnostics.Views }, }, [GridRepeater.TemplateProperty] = pt, - [!GridRepeater.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Properties).AsBinding(), + [!GridRepeater.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Properties).ToBinding(), } }; } @@ -64,7 +64,7 @@ namespace Avalonia.Diagnostics.Views TextWrapping = TextWrapping.NoWrap, [!ToolTip.TipProperty] = property .WhenAnyValue(x => x.Diagnostic) - .AsBinding(), + .ToBinding(), }; yield return new TextBlock @@ -73,13 +73,13 @@ namespace Avalonia.Diagnostics.Views [!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(), }; } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index c8436c376f..b12b2e3c31 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -56,7 +56,7 @@ namespace Avalonia.Base.UnitTests var source = new Subject(); var target = new Class1 { - [!Class1.NonValidatedProperty] = source.AsBinding(), + [!Class1.NonValidatedProperty] = source.ToBinding(), }; source.OnNext(new BindingNotification(6)); @@ -73,7 +73,7 @@ namespace Avalonia.Base.UnitTests var source = new Subject(); var target = new Class1 { - [!Class1.ValidatedDirectProperty] = source.AsBinding(), + [!Class1.ValidatedDirectProperty] = source.ToBinding(), }; source.OnNext(new BindingNotification(6)); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 99e44c8d2c..f8eea8c4eb 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -163,9 +163,9 @@ namespace Avalonia.Controls.UnitTests 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(), } }); } @@ -187,7 +187,7 @@ namespace Avalonia.Controls.UnitTests 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], diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 9999fa5346..c7992fe80f 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -207,7 +207,7 @@ namespace Avalonia.Controls.UnitTests Content = new ItemsPresenter { Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).AsBinding(), + [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), } }; } @@ -217,7 +217,7 @@ namespace Avalonia.Controls.UnitTests return new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).AsBinding(), + [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).ToBinding(), }; } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 960be2ce0e..3c2f2e4f5c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = new ContentPresenter { - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).AsBinding(), + [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).ToBinding(), } }; }),