From 1b4ebf5b1d361abbe12effc4bbf695bd5abf12d5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 14 Oct 2016 12:32:07 +0200 Subject: [PATCH 1/5] Renamed AsBinding -> ToBinding. --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 8 +++++++- src/Avalonia.Diagnostics/Views/ControlDetailsView.cs | 8 ++++---- .../AvaloniaObjectTests_DataValidation.cs | 4 ++-- tests/Avalonia.Controls.UnitTests/ListBoxTests.cs | 8 ++++---- tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs | 4 ++-- .../Primitives/TemplatedControlTests.cs | 2 +- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 3ca55529e6d..685bf83a75a 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 3cde8bb49df..7cb74ebb33b 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -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(), } }; } @@ -64,7 +64,7 @@ private IEnumerable PropertyTemplate(object i) TextWrapping = TextWrapping.NoWrap, [!ToolTip.TipProperty] = property .WhenAnyValue(x => x.Diagnostic) - .AsBinding(), + .ToBinding(), }; yield return new TextBlock @@ -73,13 +73,13 @@ private IEnumerable 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(), }; } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index c8436c376fd..b12b2e3c31d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -56,7 +56,7 @@ public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() 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 @@ public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation() 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 99e44c8d2c3..f8eea8c4eb4 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -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(), } }); } @@ -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], diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 9999fa53463..c7992fe80f8 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -207,7 +207,7 @@ private Control CreateListBoxTemplate(ITemplatedControl parent) Content = new ItemsPresenter { Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).AsBinding(), + [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), } }; } @@ -217,7 +217,7 @@ private Control CreateScrollViewerTemplate(ITemplatedControl parent) 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 960be2ce0eb..3c2f2e4f5c5 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -224,7 +224,7 @@ public void Nested_Templated_Controls_Have_Correct_TemplatedParent() { Child = new ContentPresenter { - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).AsBinding(), + [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).ToBinding(), } }; }), From d1295bb6c8494f1253d8b4ba2fb0ddcb841212c4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 14 Oct 2016 12:32:43 +0200 Subject: [PATCH 2/5] Updated "from WPF" doc. --- docs/tutorial/from-wpf.md | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/docs/tutorial/from-wpf.md b/docs/tutorial/from-wpf.md index f25de9cde4f..f8bb3defa27 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 From 536b9dbf4ae8c0d96fa80dfe0e97a428489bdbb9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 14 Oct 2016 12:34:05 +0200 Subject: [PATCH 3/5] Add some documentation on bindings. --- docs/spec/binding-from-code.md | 156 +++++++++++++++++++++++++++ docs/spec/binding-from-xaml.md | 99 +++++++++++++++++ docs/spec/toc.yml | 4 + docs/spec/working-with-properties.md | 2 + 4 files changed, 261 insertions(+) create mode 100644 docs/spec/binding-from-code.md create mode 100644 docs/spec/binding-from-xaml.md diff --git a/docs/spec/binding-from-code.md b/docs/spec/binding-from-code.md new file mode 100644 index 00000000000..aa7893762bf --- /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 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 just 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 00000000000..143e3627c86 --- /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 f225084c4e1..24a84ed06be 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 +- name: Binding from Code + href: binding-from-code diff --git a/docs/spec/working-with-properties.md b/docs/spec/working-with-properties.md index 74dd60a9b2b..a8a383b7331 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 From a00fab3b72e1e6f36b0ca9c50292a479f19567c5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 14 Oct 2016 13:10:39 +0200 Subject: [PATCH 4/5] Wording tweaks. --- docs/spec/binding-from-code.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/spec/binding-from-code.md b/docs/spec/binding-from-code.md index aa7893762bf..76f3aa55c9a 100644 --- a/docs/spec/binding-from-code.md +++ b/docs/spec/binding-from-code.md @@ -24,8 +24,8 @@ 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.* +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: @@ -126,7 +126,7 @@ var textBlock = new TextBlock # 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 observables you could bind to a property on `DataContext` like this: +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(); @@ -136,7 +136,7 @@ var viewModelProperty = textBlock.GetObservable(TextBlock.DataContext) textBlock.Bind(TextBlock, viewModelProperty); ``` -However, it might be preferable to just use a XAML binding in this case: +However, it might be preferable to use a XAML binding in this case: ```csharp var textBlock = new TextBlock From bb577d03179d1ae97c01510de4700f8798164e49 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 14 Oct 2016 16:13:06 +0200 Subject: [PATCH 5/5] Added missing file extension. --- docs/spec/toc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/spec/toc.yml b/docs/spec/toc.yml index 24a84ed06be..b0981017e0e 100644 --- a/docs/spec/toc.yml +++ b/docs/spec/toc.yml @@ -9,6 +9,6 @@ - name: Logging href: logging.md - name: Binding from XAML - href: binding-from-xaml + href: binding-from-xaml.md - name: Binding from Code - href: binding-from-code + href: binding-from-code.md