Bläddra i källkod

Merge remote-tracking branch 'AvaloniaUI/master' into selector-parse-no-sprache

Jeremy Koritzinsky 7 år sedan
förälder
incheckning
547d4dc906
100 ändrade filer med 1379 tillägg och 924 borttagningar
  1. 5 0
      .gitignore
  2. 13 13
      samples/BindingDemo/MainWindow.xaml
  3. 3 3
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  4. 3 3
      samples/ControlCatalog/Pages/BorderPage.xaml
  5. 5 5
      samples/ControlCatalog/Pages/ButtonPage.xaml
  6. 3 3
      samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
  7. 3 3
      samples/ControlCatalog/Pages/CalendarPage.xaml
  8. 2 2
      samples/ControlCatalog/Pages/CanvasPage.xaml
  9. 5 5
      samples/ControlCatalog/Pages/CarouselPage.xaml
  10. 5 5
      samples/ControlCatalog/Pages/CheckBoxPage.xaml
  11. 3 3
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  12. 3 3
      samples/ControlCatalog/Pages/DatePickerPage.xaml
  13. 2 2
      samples/ControlCatalog/Pages/DialogsPage.xaml
  14. 3 3
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  15. 3 3
      samples/ControlCatalog/Pages/DropDownPage.xaml
  16. 3 3
      samples/ControlCatalog/Pages/ExpanderPage.xaml
  17. 2 2
      samples/ControlCatalog/Pages/ImagePage.xaml
  18. 2 2
      samples/ControlCatalog/Pages/MenuPage.xaml
  19. 3 3
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  20. 3 3
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  21. 4 4
      samples/ControlCatalog/Pages/RadioButtonPage.xaml
  22. 2 2
      samples/ControlCatalog/Pages/SliderPage.xaml
  23. 6 5
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  24. 1 1
      samples/ControlCatalog/Pages/ToolTipPage.xaml
  25. 2 2
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  26. 1 1
      samples/VirtualizationDemo/MainWindow.xaml
  27. 71 55
      src/Avalonia.Animation/Animation.cs
  28. 59 7
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  29. 104 87
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  30. 27 53
      src/Avalonia.Animation/Animator`1.cs
  31. 1 1
      src/Avalonia.Animation/Cue.cs
  32. 4 4
      src/Avalonia.Animation/DoubleAnimator.cs
  33. 8 2
      src/Avalonia.Animation/IAnimation.cs
  34. 1 1
      src/Avalonia.Animation/IAnimationSetter.cs
  35. 1 1
      src/Avalonia.Animation/IAnimator.cs
  36. 11 5
      src/Avalonia.Animation/KeyFrame.cs
  37. 4 4
      src/Avalonia.Animation/KeyFramePair`1.cs
  38. 2 1
      src/Avalonia.Base/Avalonia.Base.csproj
  39. 13 38
      src/Avalonia.Base/AvaloniaObject.cs
  40. 3 49
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  41. 49 0
      src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
  42. 17 8
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  43. 3 0
      src/Avalonia.Base/IPriorityValueOwner.cs
  44. 8 3
      src/Avalonia.Base/PriorityValue.cs
  45. 3 3
      src/Avalonia.Base/Utilities/CharacterReader.cs
  46. 34 19
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  47. 3 3
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  48. 58 0
      src/Avalonia.Base/Utilities/SingleOrQueue.cs
  49. 17 7
      src/Avalonia.Base/ValueStore.cs
  50. 58 13
      src/Avalonia.Controls/ContextMenu.cs
  51. 2 4
      src/Avalonia.Controls/Expander.cs
  52. 15 5
      src/Avalonia.Controls/Grid.cs
  53. 5 4
      src/Avalonia.Controls/Image.cs
  54. 5 0
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  55. 22 14
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  56. 9 21
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  57. 6 0
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  58. 13 12
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  59. 7 2
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  60. 1 1
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  61. 39 51
      src/Avalonia.Controls/ProgressBar.cs
  62. 2 3
      src/Avalonia.Controls/Slider.cs
  63. 22 19
      src/Avalonia.Controls/StackPanel.cs
  64. 12 4
      src/Avalonia.Controls/TextBox.cs
  65. 6 6
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  66. 8 2
      src/Avalonia.Controls/Window.cs
  67. 2 2
      src/Avalonia.Controls/WindowCollection.cs
  68. 6 6
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  69. 2 2
      src/Avalonia.Diagnostics/DevTools.xaml
  70. 1 0
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  71. 2 2
      src/Avalonia.Diagnostics/Views/TreePageView.xaml
  72. 1 0
      src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj
  73. 5 12
      src/Avalonia.Styling/Styling/Setter.cs
  74. 5 0
      src/Avalonia.Themes.Default/MenuItem.xaml
  75. 36 9
      src/Avalonia.Themes.Default/ProgressBar.xaml
  76. 50 10
      src/Avalonia.Visuals/Animation/CrossFade.cs
  77. 1 1
      src/Avalonia.Visuals/Animation/IPageTransition.cs
  78. 59 5
      src/Avalonia.Visuals/Animation/PageSlide.cs
  79. 3 3
      src/Avalonia.Visuals/Animation/TransformAnimator.cs
  80. 6 4
      src/Avalonia.Visuals/Media/DrawingContext.cs
  81. 16 3
      src/Avalonia.Visuals/Media/ITileBrush.cs
  82. 31 0
      src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs
  83. 7 4
      src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs
  84. 13 3
      src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs
  85. 9 3
      src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs
  86. 39 0
      src/Avalonia.Visuals/Media/RenderOptions.cs
  87. 19 0
      src/Avalonia.Visuals/Media/TileBrush.cs
  88. 3 1
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  89. 4 3
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  90. 18 5
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  91. 7 0
      src/Gtk/Avalonia.Gtk3/KeyTransform.cs
  92. 1 0
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  93. 31 37
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs
  94. 10 155
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  95. 85 0
      src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs
  96. 2 2
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs
  97. 1 1
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  98. 71 48
      src/Markup/Avalonia.Markup/Data/Binding.cs
  99. 4 2
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  100. 6 5
      src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

+ 5 - 0
.gitignore

@@ -165,6 +165,11 @@ $RECYCLE.BIN/
 #################
 .idea
 
+#################
+## VS Code
+#################
+.vscode/
+
 #################
 ## Cake
 #################

+ 13 - 13
samples/BindingDemo/MainWindow.xaml

@@ -18,18 +18,18 @@
     <TabItem Header="Basic">
       <StackPanel Orientation="Vertical">
         <StackPanel Orientation="Horizontal">
-          <StackPanel Margin="18" Gap="4" Width="200">
+          <StackPanel Margin="18" Spacing="4" Width="200">
             <TextBlock FontSize="16" Text="Simple Bindings"/>
             <TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/>
             <TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
             <TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
           </StackPanel>
-          <StackPanel Margin="18" Gap="4" Width="200">
+          <StackPanel Margin="18" Spacing="4" Width="200">
             <TextBlock FontSize="16" Text="Collection Bindings"/>
             <TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Path=Items[1].StringValue}"/>
             <Button Command="{Binding ShuffleItems}">Shuffle</Button>
           </StackPanel>
-          <StackPanel Margin="18" Gap="4" Width="200">
+          <StackPanel Margin="18" Spacing="4" Width="200">
             <TextBlock FontSize="16" Text="Negated Bindings"/>
             <TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding Path=BooleanString}"/>
             <CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox>
@@ -37,13 +37,13 @@
           </StackPanel>
         </StackPanel>
         <StackPanel Orientation="Horizontal">
-          <StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
+          <StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
             <TextBlock FontSize="16" Text="Numeric Bindings"/>
             <TextBox Watermark="Double" UseFloatingWatermark="True" Text="{Binding Path=DoubleValue, Mode=TwoWay}"/>
             <TextBlock Text="{Binding Path=DoubleValue}"/>
             <ProgressBar Maximum="10" Value="{Binding DoubleValue}"/>
           </StackPanel>
-          <StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
+          <StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
             <TextBlock FontSize="16" Text="Binding Sources"/>
             <TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True" 
                      Text="{Binding #first.Text, Mode=TwoWay}"/>
@@ -52,7 +52,7 @@
             <TextBox Watermark="Value of SharedItem.StringValue (duplicate)" UseFloatingWatermark="True"
                      Text="{Binding StringValue, Source={StaticResource SharedItem}, Mode=TwoWay}"/>
           </StackPanel>
-          <StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
+          <StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
             <TextBlock FontSize="16" Text="Scheduler"/>
             <TextBox Watermark="Background Thread" Text="{Binding CurrentTime, Mode=OneWay}"/>
             <TextBlock FontSize="16" Text="Stream Operator"/>
@@ -68,11 +68,11 @@
             <TextBlock Text="{Binding StringValue}"/>
           </DataTemplate>
         </StackPanel.DataTemplates>
-        <StackPanel Margin="18" Gap="4" Width="200">
+        <StackPanel Margin="18" Spacing="4" Width="200">
           <TextBlock FontSize="16" Text="Multiple"/>
           <ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
         </StackPanel>
-        <StackPanel Margin="18" Gap="4" Width="200">
+        <StackPanel Margin="18" Spacing="4" Width="200">
           <TextBlock FontSize="16" Text="Multiple"/>
           <ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
         </StackPanel>
@@ -87,16 +87,16 @@
     </TabItem>
     <TabItem Header="Property Validation">
       <StackPanel Orientation="Horizontal">
-        <StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding ExceptionDataValidation}">
+        <StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding ExceptionDataValidation}">
           <TextBlock FontSize="16" Text="Exception Validation"/>
           <TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
         </StackPanel>
-        <StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding IndeiDataValidation}">
+        <StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding IndeiDataValidation}">
           <TextBlock FontSize="16" Text="INotifyDataErrorInfo Validation"/>
           <TextBox Watermark="Maximum" UseFloatingWatermark="True" Text="{Binding Path=Maximum}"/>
           <TextBox Watermark="Value" UseFloatingWatermark="True" Text="{Binding Path=Value}"/>
         </StackPanel>
-        <StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}">
+        <StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}">
           <TextBlock FontSize="16" Text="Data Annotations Validation"/>
           <TextBox Watermark="Phone #" UseFloatingWatermark="True" Text="{Binding PhoneNumber}"/>
           <TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
@@ -104,7 +104,7 @@
       </StackPanel>
     </TabItem>
     <TabItem Header="Commands">
-      <StackPanel Margin="18" Gap="4" Width="200">
+      <StackPanel Margin="18" Spacing="4" Width="200">
         <Button Content="Button" Command="{Binding StringValueCommand}" CommandParameter="Button"/>
         <ToggleButton Content="ToggleButton" IsChecked="{Binding BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="ToggleButton"/>
         <CheckBox Content="CheckBox" IsChecked="{Binding !BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="CheckBox"/>
@@ -114,4 +114,4 @@
       </StackPanel>
     </TabItem>
   </TabControl>
-</Window>
+</Window>

+ 3 - 3
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@@ -1,12 +1,12 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">AutoCompleteBox</TextBlock>
     <TextBlock Classes="h2">A control into which the user can input text</TextBlock>
 
     <StackPanel Orientation="Horizontal"
               Margin="0,16,0,0"
               HorizontalAlignment="Center"
-              Gap="8">
+              Spacing="8">
       <StackPanel Orientation="Vertical">
         <TextBlock Text="MinimumPrefixLength: 1"/>
         <AutoCompleteBox Width="200"
@@ -56,4 +56,4 @@
       </StackPanel>
     </StackPanel>
   </StackPanel>
-</UserControl>
+</UserControl>

+ 3 - 3
samples/ControlCatalog/Pages/BorderPage.xaml

@@ -1,12 +1,12 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Border</TextBlock>
     <TextBlock Classes="h2">A control which decorates a child with a border and background</TextBlock>
 
     <StackPanel Orientation="Vertical"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
-                Gap="16">
+                Spacing="16">
       <Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16">
         <TextBlock>Border</TextBlock>
       </Border>
@@ -29,4 +29,4 @@
       </Border>
     </StackPanel>    
   </StackPanel>
-</UserControl>
+</UserControl>

+ 5 - 5
samples/ControlCatalog/Pages/ButtonPage.xaml

@@ -1,14 +1,14 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Button</TextBlock>
     <TextBlock Classes="h2">A button control</TextBlock>
 
     <StackPanel Orientation="Horizontal"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
-                Gap="16">
-      <StackPanel Orientation="Vertical" Gap="8" Width="150">
+                Spacing="16">
+      <StackPanel Orientation="Vertical" Spacing="8" Width="150">
         <Button>Button</Button>
         <Button Foreground="White">Foreground</Button>
         <Button Background="{DynamicResource ThemeAccentBrush}">Background</Button>
@@ -25,7 +25,7 @@
         </Button>
       </StackPanel>
 
-      <StackPanel Orientation="Vertical" Gap="8" Width="150">
+      <StackPanel Orientation="Vertical" Spacing="8" Width="150">
         <Button BorderThickness="0">No Border</Button>
         <Button BorderBrush="{DynamicResource ThemeAccentBrush}">Border Color</Button>
         <Button BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="4">Thick Border</Button>
@@ -33,4 +33,4 @@
       </StackPanel>
     </StackPanel>    
   </StackPanel>
-</UserControl>
+</UserControl>

+ 3 - 3
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml

@@ -1,11 +1,11 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">ButtonSpinner</TextBlock>
     <TextBlock Classes="h2">The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element.</TextBlock>
 
-    <StackPanel Orientation="Vertical" Gap="8" Width="200" Margin="0,20,0,0">
+    <StackPanel Orientation="Vertical" Spacing="8" Width="200" Margin="0,20,0,0">
       <CheckBox Name="allowSpinCheck" IsChecked="True">AllowSpin</CheckBox>
       <CheckBox Name="showSpinCheck" IsChecked="True">ShowButtonSpinner</CheckBox>
       <ButtonSpinner Spin="OnSpin" Height="30"
@@ -21,4 +21,4 @@
     </StackPanel>
   </StackPanel>
 
-</UserControl>
+</UserControl>

+ 3 - 3
samples/ControlCatalog/Pages/CalendarPage.xaml

@@ -1,13 +1,13 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Calendar</TextBlock>
     <TextBlock Classes="h2">A calendar control for selecting dates</TextBlock>
         
     <StackPanel Orientation="Horizontal"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
-                Gap="16">
+                Spacing="16">
       <StackPanel Orientation="Vertical">
         <TextBlock Text="SelectionMode: None"/>
         <Calendar SelectionMode="None"
@@ -44,4 +44,4 @@
       
     </StackPanel> 
   </StackPanel>
-</UserControl>
+</UserControl>

+ 2 - 2
samples/ControlCatalog/Pages/CanvasPage.xaml

@@ -1,5 +1,5 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Canvas</TextBlock>
     <TextBlock Classes="h2">A panel which lays out its children by explicit coordinates</TextBlock>
     <Canvas Background="Yellow" Width="300" Height="400">
@@ -31,4 +31,4 @@
       <Polyline Points="0,0 65,0 78,-26 91,39 104,-39 117,13 130,0 195,0" Stroke="Brown" Canvas.Left="30" Canvas.Top="350"/>
     </Canvas>
   </StackPanel>
-</UserControl>
+</UserControl>

+ 5 - 5
samples/ControlCatalog/Pages/CarouselPage.xaml

@@ -1,9 +1,9 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Carousel</TextBlock>
     <TextBlock Classes="h2">An items control that displays its items as pages that fill the control.</TextBlock>
 
-    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Gap="8">
+    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
       <Button Name="left" VerticalAlignment="Center" Padding="20">
         <Path Data="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" Fill="Black"/>
       </Button>
@@ -20,7 +20,7 @@
       </Button>
     </StackPanel>
 
-    <StackPanel Orientation="Horizontal" Gap="4">
+    <StackPanel Orientation="Horizontal" Spacing="4">
       <TextBlock VerticalAlignment="Center">Transition</TextBlock>
       <DropDown Name="transition" SelectedIndex="1" VerticalAlignment="Center">
         <DropDownItem>None</DropDownItem>
@@ -29,7 +29,7 @@
       </DropDown>
     </StackPanel>
 
-    <StackPanel Orientation="Horizontal" Gap="4">
+    <StackPanel Orientation="Horizontal" Spacing="4">
       <TextBlock VerticalAlignment="Center">Orientation</TextBlock>
       <DropDown Name="orientation" SelectedIndex="1" VerticalAlignment="Center">
         <DropDownItem>Horizontal</DropDownItem>
@@ -38,4 +38,4 @@
     </StackPanel>
     
   </StackPanel>
-</UserControl>
+</UserControl>

+ 5 - 5
samples/ControlCatalog/Pages/CheckBoxPage.xaml

@@ -1,15 +1,15 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">CheckBox</TextBlock>
     <TextBlock Classes="h2">A check box control</TextBlock>
 
     <StackPanel Orientation="Horizontal"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
-                Gap="16">
+                Spacing="16">
       <StackPanel Orientation="Vertical"
-                  Gap="16">
+                  Spacing="16">
         <CheckBox>Unchecked</CheckBox>
         <CheckBox IsChecked="True">Checked</CheckBox>
         <CheckBox IsChecked="{x:Null}">Indeterminate</CheckBox>
@@ -17,7 +17,7 @@
       </StackPanel>
       <StackPanel Orientation="Vertical"
                   HorizontalAlignment="Center"
-                  Gap="16">
+                  Spacing="16">
         <CheckBox IsChecked="False" IsThreeState="True">Three State: Unchecked</CheckBox>
         <CheckBox IsChecked="True" IsThreeState="True">Three State: Checked</CheckBox>
         <CheckBox IsChecked="{x:Null}" IsThreeState="True">Three State: Indeterminate</CheckBox>
@@ -25,4 +25,4 @@
       </StackPanel>
     </StackPanel>
   </StackPanel>
-</UserControl>
+</UserControl>

+ 3 - 3
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@@ -1,12 +1,12 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-    <StackPanel Orientation="Vertical" Gap="4">
+    <StackPanel Orientation="Vertical" Spacing="4">
         <TextBlock Classes="h1">Context Menu</TextBlock>
         <TextBlock Classes="h2">A right click menu that can be applied to any control.</TextBlock>
 
         <StackPanel Orientation="Horizontal"
               Margin="0,16,0,0"
               HorizontalAlignment="Center"
-              Gap="16">
+              Spacing="16">
             <Border Background="{DynamicResource ThemeAccentBrush}"
               Padding="48,48,48,48">
                 <Border.ContextMenu>
@@ -33,4 +33,4 @@
             </Border>
         </StackPanel>
     </StackPanel>
-</UserControl>
+</UserControl>

+ 3 - 3
samples/ControlCatalog/Pages/DatePickerPage.xaml

@@ -1,13 +1,13 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">DatePicker</TextBlock>
     <TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
         
     <StackPanel Orientation="Horizontal"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
-                Gap="16">
+                Spacing="16">
       <StackPanel Orientation="Vertical"
                   Width="200">
         <TextBlock Text="SelectedDateFormat: Short"/>
@@ -43,4 +43,4 @@
 
     </StackPanel> 
   </StackPanel>
-</UserControl>
+</UserControl>

+ 2 - 2
samples/ControlCatalog/Pages/DialogsPage.xaml

@@ -1,5 +1,5 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4" Margin="4">
+  <StackPanel Orientation="Vertical" Spacing="4" Margin="4">
       <Button Name="OpenFile">Open File</Button>
       <Button Name="SaveFile">Save File</Button>
       <Button Name="SelectFolder">Select Folder</Button>
@@ -9,4 +9,4 @@
       </StackPanel>
       <Button Name="DecoratedWindow">Decorated window</Button>
   </StackPanel>
-</UserControl>
+</UserControl>

+ 3 - 3
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@@ -1,12 +1,12 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-    <StackPanel Orientation="Vertical" Gap="4">
+    <StackPanel Orientation="Vertical" Spacing="4">
         <TextBlock Classes="h1">Drag+Drop</TextBlock>
         <TextBlock Classes="h2">Example of Drag+Drop capabilities</TextBlock>
 
         <StackPanel Orientation="Horizontal"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
-                Gap="16">
+                Spacing="16">
             <Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe">
                 <TextBlock Name="DragState">Drag Me</TextBlock>
             </Border>
@@ -16,4 +16,4 @@
             </Border>
         </StackPanel>
     </StackPanel>
-</UserControl>
+</UserControl>

+ 3 - 3
samples/ControlCatalog/Pages/DropDownPage.xaml

@@ -1,9 +1,9 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">DropDown</TextBlock>
     <TextBlock Classes="h2">A drop-down list.</TextBlock>
 
-    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Gap="8">
+    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
       <DropDown SelectedIndex="0">
         <DropDownItem>Inline Items</DropDownItem>
         <DropDownItem>Inline Item 2</DropDownItem>
@@ -28,4 +28,4 @@
     </StackPanel>
 
   </StackPanel>
-</UserControl>
+</UserControl>

+ 3 - 3
samples/ControlCatalog/Pages/ExpanderPage.xaml

@@ -1,12 +1,12 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Expander</TextBlock>
     <TextBlock Classes="h2">Expands to show nested content</TextBlock>
 
     <StackPanel Orientation="Vertical"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
-                Gap="16">
+                Spacing="16">
       <Expander Header="Expand Up" ExpandDirection="Up">
         <StackPanel>
           <TextBlock>Expanded content</TextBlock>
@@ -29,4 +29,4 @@
       </Expander>
     </StackPanel>    
   </StackPanel>
-</UserControl>
+</UserControl>

+ 2 - 2
samples/ControlCatalog/Pages/ImagePage.xaml

@@ -1,12 +1,12 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Image</TextBlock>
     <TextBlock Classes="h2">Displays an image</TextBlock>
 
     <StackPanel Orientation="Horizontal"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
-                Gap="16">
+                Spacing="16">
       <StackPanel Orientation="Vertical">
         <TextBlock>No Stretch</TextBlock>
         <Image Source="resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg"

+ 2 - 2
samples/ControlCatalog/Pages/MenuPage.xaml

@@ -1,12 +1,12 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Menu</TextBlock>
     <TextBlock Classes="h2">A window menu</TextBlock>
 
         <StackPanel Orientation="Horizontal"
               Margin="0,16,0,0"
               HorizontalAlignment="Center"
-              Gap="16">
+              Spacing="16">
             <Menu>
                 <MenuItem Header="_First">
                     <MenuItem Header="Standard _Menu Item"/>

+ 3 - 3
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@@ -1,6 +1,6 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock>
     <TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock>
 
@@ -26,7 +26,7 @@
                   VerticalAlignment="Center" Margin="2">
           <DropDown.ItemTemplate>
             <DataTemplate>
-              <StackPanel Orientation="Horizontal" Gap="2">
+              <StackPanel Orientation="Horizontal" Spacing="2">
                 <TextBlock Text="{Binding Name}"/>
                 <TextBlock Text="-"/>
                 <TextBlock Text="{Binding Value}"/>
@@ -69,7 +69,7 @@
       </Grid>
     </Grid>
 
-    <StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10">
+    <StackPanel Margin="2,10,2,2" Orientation="Horizontal" Spacing="10">
       <TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
       <NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
                      CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100"

+ 3 - 3
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@@ -1,5 +1,5 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">ProgressBar</TextBlock>
     <TextBlock Classes="h2">A progress bar control</TextBlock>
 
@@ -7,8 +7,8 @@
       <StackPanel Orientation="Horizontal"
                   Margin="0,16,0,0"
                   HorizontalAlignment="Center"
-                  Gap="16">
-        <StackPanel Gap="16">
+                  Spacing="16">
+        <StackPanel Spacing="16">
           <ProgressBar Value="{Binding #hprogress.Value}" />
           <ProgressBar IsIndeterminate="True"/>
         </StackPanel>

+ 4 - 4
samples/ControlCatalog/Pages/RadioButtonPage.xaml

@@ -1,22 +1,22 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">RadioButton</TextBlock>
     <TextBlock Classes="h2">Allows the selection of a single option of many</TextBlock>
 
     <StackPanel Orientation="Horizontal"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
-                Gap="16">
+                Spacing="16">
       <StackPanel Orientation="Vertical"
-                  Gap="16">
+                  Spacing="16">
         <RadioButton IsChecked="True">Option 1</RadioButton>
         <RadioButton>Option 2</RadioButton>
         <RadioButton IsChecked="{x:Null}">Option 3</RadioButton>
         <RadioButton IsEnabled="False">Disabled</RadioButton>
       </StackPanel>
       <StackPanel Orientation="Vertical"
-                  Gap="16">
+                  Spacing="16">
         <RadioButton IsChecked="True" IsThreeState="True">Three States: Option 1</RadioButton>
         <RadioButton IsChecked="False" IsThreeState="True">Three States: Option 2</RadioButton>
         <RadioButton IsChecked="{x:Null}" IsThreeState="True">Three States: Option 3</RadioButton>

+ 2 - 2
samples/ControlCatalog/Pages/SliderPage.xaml

@@ -1,9 +1,9 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">Slider</TextBlock>
     <TextBlock Classes="h2">A control that lets the user select from a range of values by moving a Thumb control along a Track.</TextBlock>
 
-    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Gap="16">
+    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="16">
       <Slider Value="0"
               Minimum="0"
               Maximum="100"

+ 6 - 5
samples/ControlCatalog/Pages/TextBoxPage.xaml

@@ -1,14 +1,15 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">TextBox</TextBlock>
     <TextBlock Classes="h2">A control into which the user can input text</TextBlock>
 
     <StackPanel Orientation="Horizontal"
               Margin="0,16,0,0"
               HorizontalAlignment="Center"
-              Gap="16">
-      <StackPanel Orientation="Vertical" Gap="8">
+              Spacing="16">
+      <StackPanel Orientation="Vertical" Spacing="8">
         <TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" />
+        <TextBox Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
         <TextBox Width="200" Watermark="Watermark" />
         <TextBox Width="200"
                  Watermark="Floating Watermark"
@@ -25,13 +26,13 @@
         <TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
       </StackPanel>
 
-      <StackPanel Orientation="Vertical" Gap="8">
+      <StackPanel Orientation="Vertical" Spacing="8">
         <TextBox AcceptsReturn="True" TextWrapping="Wrap" Width="200" Height="125"
                  Text="Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
         <TextBox AcceptsReturn="True" Width="200" Height="125"
                  Text="Multiline TextBox with no TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
       </StackPanel>
-        <StackPanel Orientation="Vertical" Gap="8">
+        <StackPanel Orientation="Vertical" Spacing="8">
             <TextBox Width="200" Text="Custom font regular" FontWeight="Normal" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/>
                 <TextBox Width="200" Text="Custom font bold" FontWeight="Bold" FontStyle="Normal" FontFamily="resm:ControlCatalog.Assets.Fonts?assembly=ControlCatalog#Source Sans Pro"/>
                 <TextBox Width="200" Text="Custom font italic" FontWeight="Normal" FontStyle="Italic" FontFamily="resm:ControlCatalog.Assets.Fonts.SourceSansPro-Italic.ttf?assembly=ControlCatalog#Source Sans Pro"/>

+ 1 - 1
samples/ControlCatalog/Pages/ToolTipPage.xaml

@@ -1,6 +1,6 @@
 <UserControl xmlns="https://github.com/avaloniaui">
     <StackPanel Orientation="Vertical"
-                Gap="4">
+                Spacing="4">
         <TextBlock Classes="h1">ToolTip</TextBlock>
         <TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock>
 

+ 2 - 2
samples/ControlCatalog/Pages/TreeViewPage.xaml

@@ -1,12 +1,12 @@
 <UserControl xmlns="https://github.com/avaloniaui">
-  <StackPanel Orientation="Vertical" Gap="4">
+  <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">TreeView</TextBlock>
     <TextBlock Classes="h2">Displays a hierachical tree of data.</TextBlock>
 
     <StackPanel Orientation="Horizontal"
               Margin="0,16,0,0"
               HorizontalAlignment="Center"
-              Gap="16">
+              Spacing="16">
       <TreeView Items="{Binding}" Width="250" Height="350">
         <TreeView.ItemTemplate>
           <TreeDataTemplate ItemsSource="{Binding Children}">

+ 1 - 1
samples/VirtualizationDemo/MainWindow.xaml

@@ -6,7 +6,7 @@
         <StackPanel DockPanel.Dock="Right" 
                     Margin="16 0 0 0" 
                     MinWidth="150"
-                    Gap="4">
+                    Spacing="4">
             <DropDown Items="{Binding VirtualizationModes}"
                       SelectedItem="{Binding VirtualizationMode}"/>
             <DropDown Items="{Binding Orientations}"

+ 71 - 55
src/Avalonia.Animation/Animation.cs

@@ -10,13 +10,16 @@ using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Reflection;
 using System.Linq;
+using System.Threading.Tasks;
+using System.Reactive.Linq;
+using System.Reactive.Disposables;
 
 namespace Avalonia.Animation
 {
     /// <summary>
     /// Tracks the progress of an animation.
     /// </summary>
-    public class Animation : AvaloniaList<KeyFrame>, IDisposable, IAnimation
+    public class Animation : AvaloniaList<KeyFrame>, IAnimation
     {
         private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
         {
@@ -24,7 +27,7 @@ namespace Avalonia.Animation
         };
 
         public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
-            where TAnimator: IAnimator
+            where TAnimator : IAnimator
         {
             Animators.Insert(0, (condition, typeof(TAnimator)));
         }
@@ -41,8 +44,6 @@ namespace Avalonia.Animation
             return null;
         }
 
-        private bool _isChildrenChanged = false;
-        private List<IDisposable> _subscription = new List<IDisposable>();
         public AvaloniaList<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>();
 
         /// <summary>
@@ -68,22 +69,18 @@ namespace Avalonia.Animation
         /// <summary>
         /// The value fill mode for this animation.
         /// </summary>
-        public FillMode FillMode { get; set; } 
+        public FillMode FillMode { get; set; }
 
         /// <summary>
         /// Easing function to be used.
-        /// </summary> 
+        /// </summary>
         public Easing Easing { get; set; } = new LinearEasing();
 
-        public Animation()
-        {
-            this.CollectionChanged += delegate { _isChildrenChanged = true; };
-        }
- 
-        private void InterpretKeyframes()
+        private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
         {
-            var handlerList = new List<(Type, AvaloniaProperty)>();
-            var kfList = new List<AnimatorKeyFrame>();
+            var handlerList = new List<(Type type, AvaloniaProperty property)>();
+            var animatorKeyFrames = new List<AnimatorKeyFrame>();
+            var subscriptions = new List<IDisposable>();
 
             foreach (var keyframe in this)
             {
@@ -99,68 +96,87 @@ namespace Avalonia.Animation
                     if (!handlerList.Contains((handler, setter.Property)))
                         handlerList.Add((handler, setter.Property));
 
-                    var newKF = new AnimatorKeyFrame()
+                    var cue = keyframe.Cue;
+
+                    if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
                     {
-                        Handler = handler,
-                        Property = setter.Property,
-                        Cue = keyframe.Cue,
-                        KeyTime = keyframe.KeyTime,
-                        timeSpanSet = keyframe.timeSpanSet,
-                        cueSet = keyframe.cueSet,
-                        Value = setter.Value
-                    };
-
-                    kfList.Add(newKF);
+                        cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
+                    }
+
+                    var newKF = new AnimatorKeyFrame(handler, cue);
+
+                    subscriptions.Add(newKF.BindSetter(setter, control));
+
+                    animatorKeyFrames.Add(newKF);
                 }
             }
 
-            var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>();
+            var newAnimatorInstances = new List<IAnimator>();
 
-            foreach (var handler in handlerList)
+            foreach (var (handlerType, property) in handlerList)
             {
-                var newInstance = (IAnimator)Activator.CreateInstance(handler.Item1);
-                newInstance.Property = handler.Item2;
-                newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance));
+                var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
+                newInstance.Property = property;
+                newAnimatorInstances.Add(newInstance);
             }
 
-            foreach (var kf in kfList)
+            foreach (var keyframe in animatorKeyFrames)
             {
-                var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler &&
-                                                             p.prop == kf.Property)
-                                                 .First();
-                parent.inst.Add(kf);
+                var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType &&
+                                                             a.Property == keyframe.Property);
+                animator.Add(keyframe);
             }
 
-            foreach(var instance in newAnimatorInstances)
-                _animators.Add(instance.inst);
-
+            return (newAnimatorInstances, subscriptions);
         }
 
-        /// <summary>
-        /// Cancels the animation.
-        /// </summary>
-        public void Dispose()
+        /// <inheritdocs/>
+        public IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete)
         {
-            foreach (var sub in _subscription)
+            var (animators, subscriptions) = InterpretKeyframes(control);
+            if (animators.Count == 1)
+            {
+                subscriptions.Add(animators[0].Apply(this, control, match, onComplete));
+            }
+            else
             {
-                sub.Dispose();
+                var completionTasks = onComplete != null ? new List<Task>() : null;
+                foreach (IAnimator animator in animators)
+                {
+                    Action animatorOnComplete = null;
+                    if (onComplete != null)
+                    {
+                        var tcs = new TaskCompletionSource<object>();
+                        animatorOnComplete = () => tcs.SetResult(null);
+                        completionTasks.Add(tcs.Task);
+                    }
+                    subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete));
+                }
+
+                if (onComplete != null)
+                {
+                    Task.WhenAll(completionTasks).ContinueWith(_ => onComplete());
+                }
             }
+            return new CompositeDisposable(subscriptions);
         }
 
         /// <inheritdocs/>
-        public IDisposable Apply(Animatable control, IObservable<bool> matchObs)
+        public Task RunAsync(Animatable control)
         {
-            if (_isChildrenChanged)
-            {
-                InterpretKeyframes();
-                _isChildrenChanged = false;
-            }
+            var run = new TaskCompletionSource<object>();
 
-            foreach (IAnimator keyframes in _animators)
+            if (this.RepeatCount == RepeatCount.Loop)
+                run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
+
+            IDisposable subscriptions = null;
+            subscriptions = this.Apply(control, Observable.Return(true), () =>
             {
-                _subscription.Add(keyframes.Apply(this, control, matchObs));
-            }
-            return this;
+                run.SetResult(null);
+                subscriptions?.Dispose();
+            });
+
+            return run.Task;
         }
     }
-}
+}

+ 59 - 7
src/Avalonia.Animation/AnimatorKeyFrame.cs

@@ -4,6 +4,8 @@ using System.Text;
 using System.ComponentModel;
 using Avalonia.Metadata;
 using Avalonia.Collections;
+using Avalonia.Data;
+using Avalonia.Reactive;
 
 namespace Avalonia.Animation
 {
@@ -11,13 +13,63 @@ namespace Avalonia.Animation
     /// Defines a KeyFrame that is used for
     /// <see cref="Animator{T}"/> objects.
     /// </summary>
-    public class AnimatorKeyFrame
+    public class AnimatorKeyFrame : AvaloniaObject
     {
-        public Type Handler;
-        public Cue Cue;
-        public TimeSpan KeyTime;
-        internal bool timeSpanSet, cueSet;
-        public AvaloniaProperty Property;
-        public object Value;
+        public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
+            AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
+
+        public AnimatorKeyFrame()
+        {
+
+        }
+
+        public AnimatorKeyFrame(Type animatorType, Cue cue)
+        {
+            AnimatorType = animatorType;
+            Cue = cue;
+        }
+
+        public Type AnimatorType { get; }
+        public Cue Cue { get; }
+        public AvaloniaProperty Property { get; private set; }
+
+        private object _value;
+
+        public object Value
+        {
+            get => _value;
+            set => SetAndRaise(ValueProperty, ref _value, value);
+        }
+
+        public IDisposable BindSetter(IAnimationSetter setter, Animatable targetControl)
+        {
+            Property = setter.Property;
+            var value = setter.Value;
+
+            if (value is IBinding binding)
+            {
+                return this.Bind(ValueProperty, binding, targetControl);
+            }
+            else
+            {
+                return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl);
+            }
+        }
+
+        public T GetTypedValue<T>()
+        {
+            var typeConv = TypeDescriptor.GetConverter(typeof(T));
+
+            if (Value == null)
+            {
+                throw new ArgumentNullException($"KeyFrame value can't be null.");
+            }
+            if (!typeConv.CanConvertTo(Value.GetType()))
+            {
+                throw new InvalidCastException($"KeyFrame value doesnt match property type.");
+            }
+
+            return (T)typeConv.ConvertTo(Value, typeof(T));
+        }
     }
 }

+ 104 - 87
src/Avalonia.Animation/AnimatorStateMachine`1.cs

@@ -35,6 +35,7 @@ namespace Avalonia.Animation
         private T _neutralValue;
         internal bool _unsubscribe = false;
         private IObserver<object> _targetObserver;
+        private readonly Action _onComplete;
 
         [Flags]
         private enum KeyFramesStates
@@ -51,9 +52,9 @@ namespace Avalonia.Animation
             Disposed
         }
 
-        public void Initialize(Animation animation, Animatable control, Animator<T> keyframes)
+        public AnimatorStateMachine(Animation animation, Animatable control, Animator<T> animator, Action onComplete)
         {
-            _parent = keyframes;
+            _parent = animator;
             _targetAnimation = animation;
             _targetControl = control;
             _neutralValue = (T)_targetControl.GetValue(_parent.Property);
@@ -82,6 +83,7 @@ namespace Avalonia.Animation
                 _currentState = KeyFramesStates.DoDelay;
             else
                 _currentState = KeyFramesStates.DoRun;
+            _onComplete = onComplete;
         }
 
         public void Step(PlayState _playState, Func<double, T, T> Interpolator)
@@ -123,121 +125,136 @@ namespace Avalonia.Animation
 
             double _tempDuration = 0d, _easedTime;
 
-        checkstate:
-            switch (_currentState)
+            bool handled = false;
+
+            while (!handled)
             {
-                case KeyFramesStates.DoDelay:
+                switch (_currentState)
+                {
+                    case KeyFramesStates.DoDelay:
 
-                    if (_fillMode == FillMode.Backward
-                     || _fillMode == FillMode.Both)
-                    {
-                        if (_currentIteration == 0)
+                        if (_fillMode == FillMode.Backward
+                         || _fillMode == FillMode.Both)
                         {
-                            _targetObserver.OnNext(_firstKFValue);
+                            if (_currentIteration == 0)
+                            {
+                                _targetObserver.OnNext(_firstKFValue);
+                            }
+                            else
+                            {
+                                _targetObserver.OnNext(_lastInterpValue);
+                            }
+                        }
+
+                        if (_delayFrameCount > _delayTotalFrameCount)
+                        {
+                            _currentState = KeyFramesStates.DoRun;
                         }
                         else
                         {
-                            _targetObserver.OnNext(_lastInterpValue);
+                            handled = true;
+                            _delayFrameCount++;
                         }
-                    }
-
-                    if (_delayFrameCount > _delayTotalFrameCount)
-                    {
-                        _currentState = KeyFramesStates.DoRun;
-                        goto checkstate;
-                    }
-                    _delayFrameCount++;
-                    break;
-
-                case KeyFramesStates.DoRun:
-
-                    if (_isReversed)
-                        _currentState = KeyFramesStates.RunBackwards;
-                    else
-                        _currentState = KeyFramesStates.RunForwards;
-
-                    goto checkstate;
-
-                case KeyFramesStates.RunForwards:
-
-                    if (_durationFrameCount > _durationTotalFrameCount)
-                    {
-                        _currentState = KeyFramesStates.RunComplete;
-                        goto checkstate;
-                    }
+                        break;
 
-                    _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
-                    _currentState = KeyFramesStates.RunApplyValue;
+                    case KeyFramesStates.DoRun:
 
-                    goto checkstate;
+                        if (_isReversed)
+                            _currentState = KeyFramesStates.RunBackwards;
+                        else
+                            _currentState = KeyFramesStates.RunForwards;
 
-                case KeyFramesStates.RunBackwards:
+                        break;
 
-                    if (_durationFrameCount > _durationTotalFrameCount)
-                    {
-                        _currentState = KeyFramesStates.RunComplete;
-                        goto checkstate;
-                    }
+                    case KeyFramesStates.RunForwards:
 
-                    _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
-                    _currentState = KeyFramesStates.RunApplyValue;
+                        if (_durationFrameCount > _durationTotalFrameCount)
+                        {
+                            _currentState = KeyFramesStates.RunComplete;
+                        }
+                        else
+                        {
+                            _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
+                            _currentState = KeyFramesStates.RunApplyValue;
 
-                    goto checkstate;
+                        }
+                        break;
 
-                case KeyFramesStates.RunApplyValue:
+                    case KeyFramesStates.RunBackwards:
 
-                    _easedTime = _targetAnimation.Easing.Ease(_tempDuration);
+                        if (_durationFrameCount > _durationTotalFrameCount)
+                        {
+                            _currentState = KeyFramesStates.RunComplete;
+                        }
+                        else
+                        {
+                            _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
+                            _currentState = KeyFramesStates.RunApplyValue;
+                        }
+                        break;
 
-                    _durationFrameCount++;
-                    _lastInterpValue = Interpolator(_easedTime, _neutralValue);
-                    _targetObserver.OnNext(_lastInterpValue);
-                    _currentState = KeyFramesStates.DoRun;
+                    case KeyFramesStates.RunApplyValue:
 
-                    break;
+                        _easedTime = _targetAnimation.Easing.Ease(_tempDuration);
 
-                case KeyFramesStates.RunComplete:
+                        _durationFrameCount++;
+                        _lastInterpValue = Interpolator(_easedTime, _neutralValue);
+                        _targetObserver.OnNext(_lastInterpValue);
+                        _currentState = KeyFramesStates.DoRun;
+                        handled = true;
+                        break;
 
-                    if (_checkLoopAndRepeat)
-                    {
-                        _delayFrameCount = 0;
-                        _durationFrameCount = 0;
+                    case KeyFramesStates.RunComplete:
 
-                        if (_isLooping)
-                        {
-                            _currentState = KeyFramesStates.DoRun;
-                        }
-                        else if (_isRepeating)
+                        if (_checkLoopAndRepeat)
                         {
-                            if (_currentIteration >= _repeatCount)
+                            _delayFrameCount = 0;
+                            _durationFrameCount = 0;
+
+                            if (_isLooping)
                             {
-                                _currentState = KeyFramesStates.Stop;
+                                _currentState = KeyFramesStates.DoRun;
                             }
-                            else
+                            else if (_isRepeating)
                             {
-                                _currentState = KeyFramesStates.DoRun;
+                                if (_currentIteration >= _repeatCount)
+                                {
+                                    _currentState = KeyFramesStates.Stop;
+                                }
+                                else
+                                {
+                                    _currentState = KeyFramesStates.DoRun;
+                                }
+                                _currentIteration++;
                             }
-                            _currentIteration++;
-                        }
 
-                        if (_animationDirection == PlaybackDirection.Alternate
-                         || _animationDirection == PlaybackDirection.AlternateReverse)
-                            _isReversed = !_isReversed;
+                            if (_animationDirection == PlaybackDirection.Alternate
+                             || _animationDirection == PlaybackDirection.AlternateReverse)
+                                _isReversed = !_isReversed;
+
+                            break;
+                        }
 
+                        _currentState = KeyFramesStates.Stop;
                         break;
-                    }
 
-                    _currentState = KeyFramesStates.Stop;
-                    goto checkstate;
+                    case KeyFramesStates.Stop:
 
-                case KeyFramesStates.Stop:
+                        if (_fillMode == FillMode.Forward
+                         || _fillMode == FillMode.Both)
+                        {
+                            _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
+                        }
 
-                    if (_fillMode == FillMode.Forward
-                     || _fillMode == FillMode.Both)
-                    {
-                        _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
-                    }
-                    _targetObserver.OnCompleted();
-                    break;
+                        _targetObserver.OnCompleted();
+                        _onComplete?.Invoke();
+                        Dispose();
+                        handled = true;
+                        break;
+                    default:
+                        handled = true;
+                        break;
+                }
             }
         }
 
@@ -253,4 +270,4 @@ namespace Avalonia.Animation
             _currentState = KeyFramesStates.Disposed;
         }
     }
-}
+}

+ 27 - 53
src/Avalonia.Animation/Animator`1.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Animation
         /// <summary>
         /// List of type-converted keyframes.
         /// </summary>
-        private Dictionary<double, (T, bool isNeutral)> _convertedKeyframes = new Dictionary<double, (T, bool)>();
+        private readonly SortedList<double, (AnimatorKeyFrame, bool isNeutral)> _convertedKeyframes = new SortedList<double, (AnimatorKeyFrame, bool)>();
 
         private bool _isVerfifiedAndConverted;
 
@@ -35,18 +35,17 @@ namespace Avalonia.Animation
         }
 
         /// <inheritdoc/>
-        public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
+        public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete)
         {
             if (!_isVerfifiedAndConverted)
-                VerifyConvertKeyFrames(animation, typeof(T));
+                VerifyConvertKeyFrames();
 
             return obsMatch
-                .Where(p => p == true)
                 // Ignore triggers when global timers are paused.
-                .Where(p => Timing.GetGlobalPlayState() != PlayState.Pause)
+                .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
                 .Subscribe(_ =>
                 {
-                    var timerObs = RunKeyFrames(animation, control);
+                    var timerObs = RunKeyFrames(animation, control, onComplete);
                 });
         }
 
@@ -60,8 +59,8 @@ namespace Avalonia.Animation
         /// <param name="t">The time parameter, relative to the total animation time</param>
         protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
         {
-            KeyValuePair<double, (T, bool)> firstCue, lastCue;
-            int kvCount = _convertedKeyframes.Count();
+            KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue;
+            int kvCount = _convertedKeyframes.Count;
             if (kvCount > 2)
             {
                 if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
@@ -76,8 +75,8 @@ namespace Avalonia.Animation
                 }
                 else
                 {
-                    firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last();
-                    lastCue = _convertedKeyframes.Where(j => j.Key >= t).First();
+                    firstCue = _convertedKeyframes.Last(j => j.Key <= t);
+                    lastCue = _convertedKeyframes.First(j => j.Key >= t);
                 }
             }
             else
@@ -89,26 +88,24 @@ namespace Avalonia.Animation
             double t0 = firstCue.Key;
             double t1 = lastCue.Key;
             var intraframeTime = (t - t0) / (t1 - t0);
-            return (intraframeTime, new KeyFramePair<T>(firstCue, lastCue));
+            var firstFrameData = (firstCue.Value.frame.GetTypedValue<T>(), firstCue.Value.isNeutral);
+            var lastFrameData = (lastCue.Value.frame.GetTypedValue<T>(), lastCue.Value.isNeutral);
+            return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
         }
 
 
         /// <summary>
         /// Runs the KeyFrames Animation.
         /// </summary>
-        private IDisposable RunKeyFrames(Animation animation, Animatable control)
+        private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete)
         {
-            var _kfStateMach = new AnimatorStateMachine<T>();
-            _kfStateMach.Initialize(animation, control, this);
+            var stateMachine = new AnimatorStateMachine<T>(animation, control, this, onComplete);
 
             Timing.AnimationStateTimer
-                        .TakeWhile(_ => !_kfStateMach._unsubscribe)
-                        .Subscribe(p =>
-                        {
-                            _kfStateMach.Step(p, DoInterpolation);
-                        });
+                        .TakeWhile(_ => !stateMachine._unsubscribe)
+                        .Subscribe(p => stateMachine.Step(p, DoInterpolation));
 
-            return control.Bind(Property, _kfStateMach, BindingPriority.Animation);
+            return control.Bind(Property, stateMachine, BindingPriority.Animation);
         }
 
         /// <summary>
@@ -119,39 +116,19 @@ namespace Avalonia.Animation
         /// <summary>
         /// Verifies and converts keyframe values according to this class's target type.
         /// </summary>
-        private void VerifyConvertKeyFrames(Animation animation, Type type)
+        private void VerifyConvertKeyFrames()
         {
-            var typeConv = TypeDescriptor.GetConverter(type);
-
-            foreach (AnimatorKeyFrame k in this)
+            foreach (AnimatorKeyFrame keyframe in this)
             {
-                if (k.Value == null)
-                {
-                    throw new ArgumentNullException($"KeyFrame value can't be null.");
-                }
-                if (!typeConv.CanConvertTo(k.Value.GetType()))
-                {
-                    throw new InvalidCastException($"KeyFrame value doesnt match property type.");
-                }
-
-                T convertedValue = (T)typeConv.ConvertTo(k.Value, type);
-
-                Cue _normalizedCue = k.Cue;
-
-                if (k.timeSpanSet)
-                {
-                    _normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks);
-                }
-
-                _convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false));
+                _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false));
             }
 
-            SortKeyFrameCues(_convertedKeyframes);
+            AddNeutralKeyFramesIfNeeded();
             _isVerfifiedAndConverted = true;
 
         }
 
-        private void SortKeyFrameCues(Dictionary<double, (T, bool)> convertedValues)
+        private void AddNeutralKeyFramesIfNeeded()
         {
             bool hasStartKey, hasEndKey;
             hasStartKey = hasEndKey = false;
@@ -170,23 +147,20 @@ namespace Avalonia.Animation
             }
 
             if (!hasStartKey || !hasEndKey)
-                AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes);
-
-            _convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key)
-                                                     .ToDictionary((k) => k.Key, (v) => v.Value);
+                AddNeutralKeyFrames(hasStartKey, hasEndKey);
         }
 
-        private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary<double, (T, bool)> convertedKeyframes)
+        private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
         {
             if (!hasStartKey)
             {
-                convertedKeyframes.Add(0.0d, (default(T), true));
+                _convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
             }
 
             if (!hasEndKey)
             {
-                convertedKeyframes.Add(1.0d, (default(T), true));
+                _convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
             }
         }
     }
-}
+}

+ 1 - 1
src/Avalonia.Animation/Cue.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Animation
     /// A Cue object for <see cref="KeyFrame"/>. 
     /// </summary>
     [TypeConverter(typeof(CueTypeConverter))]
-    public struct Cue : IEquatable<Cue>, IEquatable<double>
+    public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
     {
         /// <summary>
         /// The normalized percent value, ranging from 0.0 to 1.0

+ 4 - 4
src/Avalonia.Animation/DoubleAnimator.cs

@@ -24,15 +24,15 @@ namespace Avalonia.Animation
             var firstKF = pair.KFPair.FirstKeyFrame;
             var secondKF = pair.KFPair.SecondKeyFrame;
 
-            if (firstKF.Value.isNeutral)
+            if (firstKF.isNeutral)
                 y0 = neutralValue;
             else
-                y0 = firstKF.Value.TargetValue;
+                y0 = firstKF.TargetValue;
 
-            if (secondKF.Value.isNeutral)
+            if (secondKF.isNeutral)
                 y1 = neutralValue;
             else
-                y1 = secondKF.Value.TargetValue;
+                y1 = secondKF.TargetValue;
 
             // Do linear parametric interpolation 
             return y0 + (pair.IntraKFTime) * (y1 - y0);

+ 8 - 2
src/Avalonia.Animation/IAnimation.cs

@@ -1,6 +1,7 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Text;
+using System.Threading.Tasks;
 
 namespace Avalonia.Animation
 {
@@ -12,6 +13,11 @@ namespace Avalonia.Animation
         /// <summary>
         /// Apply the animation to the specified control
         /// </summary>
-        IDisposable Apply(Animatable control, IObservable<bool> match);
+        IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete = null);
+
+        /// <summary>
+        /// Run the animation to the specified control
+        /// </summary>
+        Task RunAsync(Animatable control);
     }
 }

+ 1 - 1
src/Avalonia.Animation/IAnimationSetter.cs

@@ -5,4 +5,4 @@ namespace Avalonia.Animation
         AvaloniaProperty Property { get; set; }
         object Value { get; set; }
     }
-}
+}

+ 1 - 1
src/Avalonia.Animation/IAnimator.cs

@@ -17,6 +17,6 @@ namespace Avalonia.Animation
         /// <summary>
         /// Applies the current KeyFrame group to the specified control.
         /// </summary>
-        IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch);
+        IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete);
     }
 }

+ 11 - 5
src/Avalonia.Animation/KeyFrame.cs

@@ -7,6 +7,11 @@ using Avalonia.Collections;
 
 namespace Avalonia.Animation
 {
+    internal enum KeyFrameTimingMode
+    {
+        TimeSpan = 1,
+        Cue
+    }
 
     /// <summary>
     /// Stores data regarding a specific key
@@ -14,7 +19,6 @@ namespace Avalonia.Animation
     /// </summary>
     public class KeyFrame : AvaloniaList<IAnimationSetter>
     {
-        internal bool timeSpanSet, cueSet;
         private TimeSpan _ktimeSpan;
         private Cue _kCue;
 
@@ -30,6 +34,8 @@ namespace Avalonia.Animation
         {
         }
 
+        internal KeyFrameTimingMode TimingMode { get; private set; }
+
         /// <summary>
         /// Gets or sets the key time of this <see cref="KeyFrame"/>.
         /// </summary>
@@ -42,11 +48,11 @@ namespace Avalonia.Animation
             }
             set
             {
-                if (cueSet)
+                if (TimingMode == KeyFrameTimingMode.Cue)
                 {
                     throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
                 }
-                timeSpanSet = true;
+                TimingMode = KeyFrameTimingMode.TimeSpan;
                 _ktimeSpan = value;
             }
         }
@@ -63,11 +69,11 @@ namespace Avalonia.Animation
             }
             set
             {
-                if (timeSpanSet)
+                if (TimingMode == KeyFrameTimingMode.TimeSpan)
                 {
                     throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
                 }
-                cueSet = true;
+                TimingMode = KeyFrameTimingMode.Cue;
                 _kCue = value;
             }
         }

+ 4 - 4
src/Avalonia.Animation/KeyFramePair`1.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Animation
         /// </summary>
         /// <param name="FirstKeyFrame"></param>
         /// <param name="LastKeyFrame"></param>
-        public KeyFramePair(KeyValuePair<double, (T, bool)> FirstKeyFrame, KeyValuePair<double, (T, bool)> LastKeyFrame) : this()
+        public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this()
         {
             this.FirstKeyFrame = FirstKeyFrame;
             this.SecondKeyFrame = LastKeyFrame;
@@ -31,11 +31,11 @@ namespace Avalonia.Animation
         /// <summary>
         /// First <see cref="KeyFrame"/> object.
         /// </summary>
-        public KeyValuePair<double, (T TargetValue, bool isNeutral)> FirstKeyFrame { get; private set; }
+        public (T TargetValue, bool isNeutral) FirstKeyFrame { get; }
 
         /// <summary>
         /// Second <see cref="KeyFrame"/> object.
         /// </summary>
-        public KeyValuePair<double, (T TargetValue, bool isNeutral)> SecondKeyFrame { get; private set; }
+        public (T TargetValue, bool isNeutral) SecondKeyFrame { get; }
     }
-}
+}

+ 2 - 1
src/Avalonia.Base/Avalonia.Base.csproj

@@ -8,4 +8,5 @@
   <Import Project="..\..\build\Binding.props" />
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\JetBrains.Annotations.props" />
-</Project>
+  <Import Project="..\..\build\System.Memory.props" />
+</Project>

+ 13 - 38
src/Avalonia.Base/AvaloniaObject.cs

@@ -22,7 +22,7 @@ namespace Avalonia
     /// <remarks>
     /// This class is analogous to DependencyObject in WPF.
     /// </remarks>
-    public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner
+    public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
     {
         /// <summary>
         /// The parent object that inherited values are inherited from.
@@ -45,21 +45,8 @@ namespace Avalonia
         /// </summary>
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
 
-        private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
         private ValueStore _values;
-
-        /// <summary>
-        /// Delayed setter helper for direct properties. Used to fix #855.
-        /// </summary>
-        private DeferredSetter<AvaloniaProperty, object> DirectPropertyDeferredSetter
-        {
-            get
-            {
-                return _directDeferredSetter ??
-                    (_directDeferredSetter = new DeferredSetter<AvaloniaProperty, object>());
-            }
-        }
-
+        private ValueStore Values => _values ?? (_values = new ValueStore(this));
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@@ -225,7 +212,7 @@ namespace Avalonia
             }
             else if (_values != null)
             {
-                var result = _values.GetValue(property);
+                var result = Values.GetValue(property);
 
                 if (result == AvaloniaProperty.UnsetValue)
                 {
@@ -376,12 +363,7 @@ namespace Avalonia
                     description,
                     priority);
 
-                if (_values == null)
-                {
-                    _values = new ValueStore(this);
-                }
-
-                return _values.AddBinding(property, source, priority);
+                return Values.AddBinding(property, source, priority);
             }
         }
 
@@ -414,9 +396,8 @@ namespace Avalonia
             VerifyAccess();
             _values?.Revalidate(property);
         }
-
-        /// <inheritdoc/>
-        void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
+        
+        internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
         {
             oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
                 GetDefaultValue(property) :
@@ -439,9 +420,8 @@ namespace Avalonia
                     (BindingPriority)priority);
             }
         }
-
-        /// <inheritdoc/>
-        void IPriorityValueOwner.BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
+        
+        internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
         {
             UpdateDataValidation(property, notification);
         }
@@ -456,7 +436,7 @@ namespace Avalonia
         /// Gets all priority values set on the object.
         /// </summary>
         /// <returns>A collection of property/value tuples.</returns>
-        internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => _values?.GetSetValues();
+        internal IDictionary<AvaloniaProperty, object> GetSetValues() => Values?.GetSetValues();
 
         /// <summary>
         /// Forces revalidation of properties when a property value changes.
@@ -566,12 +546,12 @@ namespace Avalonia
             T value)
         {
             Contract.Requires<ArgumentNullException>(setterCallback != null);
-            return DirectPropertyDeferredSetter.SetAndNotify(
+            return Values.Setter.SetAndNotify(
                 property,
                 ref field,
-                (object val, ref T backing, Action<Action> notify) =>
+                (object update, ref T backing, Action<Action> notify) =>
                 {
-                    setterCallback((T)val, ref backing, notify);
+                    setterCallback((T)update, ref backing, notify);
                     return true;
                 },
                 value);
@@ -737,13 +717,8 @@ namespace Avalonia
                     originalValue?.GetType().FullName ?? "(null)"));
             }
 
-            if (_values == null)
-            {
-                _values = new ValueStore(this);
-            }
-
             LogPropertySet(property, value, priority);
-            _values.AddValue(property, value, (int)priority);
+            Values.AddValue(property, value, (int)priority);
         }
 
         /// <summary>

+ 3 - 49
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -106,7 +106,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Finds a registered non-attached property on a type by name.
+        /// Finds a registered property on a type by name.
         /// </summary>
         /// <param name="type">The type.</param>
         /// <param name="name">The property name.</param>
@@ -130,7 +130,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Finds a registered non-attached property on a type by name.
+        /// Finds a registered property on an object by name.
         /// </summary>
         /// <param name="o">The object.</param>
         /// <param name="name">The property name.</param>
@@ -148,52 +148,6 @@ namespace Avalonia
             return FindRegistered(o.GetType(), name);
         }
 
-        /// <summary>
-        /// Finds a registered attached property on a type by name.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <param name="ownerType">The owner type.</param>
-        /// <param name="name">The property name.</param>
-        /// <returns>
-        /// The registered property or null if no matching property found.
-        /// </returns>
-        /// <exception cref="InvalidOperationException">
-        /// The property name contains a '.'.
-        /// </exception>
-        public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name)
-        {
-            Contract.Requires<ArgumentNullException>(type != null);
-            Contract.Requires<ArgumentNullException>(ownerType != null);
-            Contract.Requires<ArgumentNullException>(name != null);
-
-            if (name.Contains('.'))
-            {
-                throw new InvalidOperationException("Attached properties not supported.");
-            }
-
-            return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name);
-        }
-
-        /// <summary>
-        /// Finds a registered non-attached property on a type by name.
-        /// </summary>
-        /// <param name="o">The object.</param>
-        /// <param name="ownerType">The owner type.</param>
-        /// <param name="name">The property name.</param>
-        /// <returns>
-        /// The registered property or null if no matching property found.
-        /// </returns>
-        /// <exception cref="InvalidOperationException">
-        /// The property name contains a '.'.
-        /// </exception>
-        public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name)
-        {
-            Contract.Requires<ArgumentNullException>(o != null);
-            Contract.Requires<ArgumentNullException>(name != null);
-
-            return FindRegisteredAttached(o.GetType(), ownerType, name);
-        }
-
         /// <summary>
         /// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type.
         /// </summary>
@@ -287,4 +241,4 @@ namespace Avalonia
             _attachedCache.Clear();
         }
     }
-}
+}

+ 49 - 0
src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs

@@ -0,0 +1,49 @@
+using System;
+using System.Globalization;
+
+namespace Avalonia.Data.Converters
+{
+    /// <summary>
+    /// A value converter which calls <see cref="string.Format(string, object)"/>
+    /// </summary>
+    public class StringFormatValueConverter : IValueConverter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StringFormatValueConverter"/> class.
+        /// </summary>
+        /// <param name="format">The format string.</param>
+        /// <param name="inner">
+        /// An optional inner converter to be called before the format takes place.
+        /// </param>
+        public StringFormatValueConverter(string format, IValueConverter inner)
+        {
+            Contract.Requires<ArgumentNullException>(format != null);
+
+            Format = format;
+            Inner = inner;
+        }
+
+        /// <summary>
+        /// Gets an inner value converter which will be called before the string format takes place.
+        /// </summary>
+        public IValueConverter Inner { get; }
+
+        /// <summary>
+        /// Gets the format string.
+        /// </summary>
+        public string Format { get; }
+
+        /// <inheritdoc/>
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            value = Inner?.Convert(value, targetType, parameter, culture) ?? value;
+            return string.Format(culture, Format, value);
+        }
+
+        /// <inheritdoc/>
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotSupportedException("Two way bindings are not supported with a string format");
+        }
+    }
+}

+ 17 - 8
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

@@ -23,15 +23,24 @@ namespace Avalonia.Diagnostics
         {
             var set = o.GetSetValues();
 
-            PriorityValue value;
-
-            if (set.TryGetValue(property, out value))
+            if (set.TryGetValue(property, out var obj))
             {
-                return new AvaloniaPropertyValue(
-                    property,
-                    o.GetValue(property),
-                    (BindingPriority)value.ValuePriority,
-                    value.GetDiagnostic());
+                if (obj is PriorityValue value)
+                {
+                    return new AvaloniaPropertyValue(
+                        property,
+                        o.GetValue(property),
+                        (BindingPriority)value.ValuePriority,
+                        value.GetDiagnostic());
+                }
+                else
+                {
+                    return new AvaloniaPropertyValue(
+                        property,
+                        obj,
+                        BindingPriority.LocalValue,
+                        "Local value");
+                }
             }
             else
             {

+ 3 - 0
src/Avalonia.Base/IPriorityValueOwner.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using Avalonia.Data;
+using Avalonia.Utilities;
 
 namespace Avalonia
 {
@@ -31,5 +32,7 @@ namespace Avalonia
         /// Ensures that the current thread is the UI thread.
         /// </summary>
         void VerifyAccess();
+
+        DeferredSetter<object> Setter { get; }
     }
 }

+ 8 - 3
src/Avalonia.Base/PriorityValue.cs

@@ -21,7 +21,7 @@ namespace Avalonia
     /// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
     /// are multiple bindings registered with the same priority, the most recently added binding
     /// has a higher priority. Each time the value changes, the 
-    /// <see cref="IPriorityValueOwner.Changed(PriorityValue, object, object)"/> method on the 
+    /// <see cref="IPriorityValueOwner.Changed"/> method on the 
     /// owner object is fired with the old and new values.
     /// </remarks>
     internal class PriorityValue
@@ -30,7 +30,6 @@ namespace Avalonia
         private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
 
         private readonly Func<object, object> _validate;
-        private static readonly DeferredSetter<PriorityValue, (object value, int priority)> delayedSetter = new DeferredSetter<PriorityValue, (object, int)>();
         private (object value, int priority) _value;
 
         /// <summary>
@@ -243,12 +242,18 @@ namespace Avalonia
         /// <param name="priority">The priority level that the value came from.</param>
         private void UpdateValue(object value, int priority)
         {
-            delayedSetter.SetAndNotify(this,
+            Owner.Setter.SetAndNotify(Property,
                 ref _value,
                 UpdateCore,
                 (value, priority));
         }
 
+        private bool UpdateCore(
+            object update,
+            ref (object value, int priority) backing,
+            Action<Action> notify)
+            => UpdateCore(((object, int))update, ref backing, notify);
+
         private bool UpdateCore(
             (object value, int priority) update,
             ref (object value, int priority) backing,

+ 3 - 3
src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs → src/Avalonia.Base/Utilities/CharacterReader.cs

@@ -5,13 +5,13 @@ using System;
 using System.Globalization;
 using System.Text;
 
-namespace Avalonia.Markup.Parsers
+namespace Avalonia.Utilities
 {
-    internal ref struct Reader
+    public ref struct CharacterReader
     {
         private ReadOnlySpan<char> _s;
 
-        public Reader(ReadOnlySpan<char> s)
+        public CharacterReader(ReadOnlySpan<char> s)
             :this()
         {
             _s = s;

+ 34 - 19
src/Avalonia.Base/Utilities/DeferredSetter.cs

@@ -8,11 +8,10 @@ namespace Avalonia.Utilities
 {
     /// <summary>
     /// A utility class to enable deferring assignment until after property-changed notifications are sent.
+    /// Used to fix #855.
     /// </summary>
-    /// <typeparam name="TProperty">The type of the object that represents the property.</typeparam>
     /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
-    class DeferredSetter<TProperty, TSetRecord>
-        where TProperty: class
+    class DeferredSetter<TSetRecord>
     {
         private struct NotifyDisposable : IDisposable
         {
@@ -37,29 +36,44 @@ namespace Avalonia.Utilities
         {
             public bool Notifying { get; set; }
 
-            private Queue<TSetRecord> pendingValues;
+            private SingleOrQueue<TSetRecord> pendingValues;
             
-            public Queue<TSetRecord> PendingValues
+            public SingleOrQueue<TSetRecord> PendingValues
             {
                 get
                 {
-                    return pendingValues ?? (pendingValues = new Queue<TSetRecord>());
+                    return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
                 }
             }
         }
 
-        private readonly ConditionalWeakTable<TProperty, SettingStatus> setRecords = new ConditionalWeakTable<TProperty, SettingStatus>();
+        private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
+        private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
+            => _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
+
+        private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
+        {
+            if (!SetRecords.TryGetValue(property, out var status))
+            {
+                status = new SettingStatus();
+                SetRecords.Add(property, status);
+            }
+
+            return status;
+        }
 
         /// <summary>
         /// Mark the property as currently notifying.
         /// </summary>
         /// <param name="property">The property to mark as notifying.</param>
         /// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
-        private NotifyDisposable MarkNotifying(TProperty property)
+        private NotifyDisposable MarkNotifying(AvaloniaProperty property)
         {
             Contract.Requires<InvalidOperationException>(!IsNotifying(property));
-            
-            return new NotifyDisposable(setRecords.GetOrCreateValue(property));
+
+            SettingStatus status = GetOrCreateStatus(property);
+
+            return new NotifyDisposable(status);
         }
 
         /// <summary>
@@ -67,19 +81,19 @@ namespace Avalonia.Utilities
         /// </summary>
         /// <param name="property">The property.</param>
         /// <returns>If the property is currently notifying listeners.</returns>
-        private bool IsNotifying(TProperty property)
-            => setRecords.TryGetValue(property, out var value) && value.Notifying;
+        private bool IsNotifying(AvaloniaProperty property)
+            => SetRecords.TryGetValue(property, out var value) && value.Notifying;
 
         /// <summary>
         /// Add a pending assignment for the property.
         /// </summary>
         /// <param name="property">The property.</param>
         /// <param name="value">The value to assign.</param>
-        private void AddPendingSet(TProperty property, TSetRecord value)
+        private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
         {
             Contract.Requires<InvalidOperationException>(IsNotifying(property));
 
-            setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value);
+            GetOrCreateStatus(property).PendingValues.Enqueue(value);
         }
 
         /// <summary>
@@ -87,9 +101,9 @@ namespace Avalonia.Utilities
         /// </summary>
         /// <param name="property">The property to check.</param>
         /// <returns>If the property has any pending assignments.</returns>
-        private bool HasPendingSet(TProperty property)
+        private bool HasPendingSet(AvaloniaProperty property)
         {
-            return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0;
+            return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
         }
 
         /// <summary>
@@ -97,9 +111,9 @@ namespace Avalonia.Utilities
         /// </summary>
         /// <param name="property">The property to check.</param>
         /// <returns>The first pending assignment for the property.</returns>
-        private TSetRecord GetFirstPendingSet(TProperty property)
+        private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
         {
-            return setRecords.GetOrCreateValue(property).PendingValues.Dequeue();
+            return GetOrCreateStatus(property).PendingValues.Dequeue();
         }
 
         public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
@@ -115,7 +129,7 @@ namespace Avalonia.Utilities
         /// </param>
         /// <param name="value">The value to try to set.</param>
         public bool SetAndNotify<TValue>(
-            TProperty property,
+            AvaloniaProperty property,
             ref TValue backing,
             SetterDelegate<TValue> setterCallback,
             TSetRecord value)
@@ -144,6 +158,7 @@ namespace Avalonia.Utilities
                         }
                     });
                 }
+
                 return updated;
             }
             else if(!object.Equals(value, backing))

+ 3 - 3
src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs → src/Avalonia.Base/Utilities/IdentifierParser.cs

@@ -6,11 +6,11 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Text;
 
-namespace Avalonia.Markup.Parsers
+namespace Avalonia.Utilities
 {
-    internal static class IdentifierParser
+    public static class IdentifierParser
     {
-        public static ReadOnlySpan<char> ParseIdentifier(this ref Reader r)
+        public static ReadOnlySpan<char> ParseIdentifier(this ref CharacterReader r)
         {
             if (IsValidIdentifierStart(r.Peek))
             {

+ 58 - 0
src/Avalonia.Base/Utilities/SingleOrQueue.cs

@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Utilities
+{
+    /// <summary>
+    /// FIFO Queue optimized for holding zero or one items.
+    /// </summary>
+    /// <typeparam name="T">The type of items held in the queue.</typeparam>
+    public class SingleOrQueue<T>
+    {
+        private T _head;
+        private Queue<T> _tail;
+
+        private Queue<T> Tail => _tail ?? (_tail = new Queue<T>());
+
+        private bool HasTail => _tail != null;
+
+        public bool Empty { get; private set; } = true;
+
+        public void Enqueue(T value)
+        {
+            if (Empty)
+            {
+                _head = value;
+            }
+            else
+            {
+                Tail.Enqueue(value);
+            }
+
+            Empty = false;
+        }
+
+        public T Dequeue()
+        {
+            if (Empty)
+            {
+                throw new InvalidOperationException("Cannot dequeue from an empty queue!");
+            }
+
+            var result = _head;
+
+            if (HasTail && Tail.Count != 0)
+            {
+                _head = Tail.Dequeue();
+            }
+            else
+            {
+                _head = default;
+                Empty = true;
+            }
+
+            return result;
+        }
+    }
+}

+ 17 - 7
src/Avalonia.Base/ValueStore.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Data;
+using Avalonia.Utilities;
 
 namespace Avalonia
 {
@@ -91,15 +92,15 @@ namespace Avalonia
 
         public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
         {
-            ((IPriorityValueOwner)_owner).BindingNotificationReceived(property, notification);
+            _owner.BindingNotificationReceived(property, notification);
         }
 
         public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
         {
-            ((IPriorityValueOwner)_owner).Changed(property, priority, oldValue, newValue);
+            _owner.PriorityValueChanged(property, priority, oldValue, newValue);
         }
 
-        public IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => throw new NotImplementedException();
+        public IDictionary<AvaloniaProperty, object> GetSetValues() => _values;
 
         public object GetValue(AvaloniaProperty property)
         {
@@ -115,7 +116,7 @@ namespace Avalonia
 
         public bool IsAnimating(AvaloniaProperty property)
         {
-            return _values.TryGetValue(property, out var value) ? (value as PriorityValue)?.IsAnimating ?? false : false;
+            return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
         }
 
         public bool IsSet(AvaloniaProperty property)
@@ -148,13 +149,11 @@ namespace Avalonia
                 validate2 = v => validate(_owner, v);
             }
 
-            PriorityValue result = new PriorityValue(
+            return new PriorityValue(
                 this,
                 property,
                 property.PropertyType,
                 validate2);
-
-            return result;
         }
 
         private object Validate(AvaloniaProperty property, object value)
@@ -168,5 +167,16 @@ namespace Avalonia
 
             return value;
         }
+
+        private DeferredSetter<object> _defferedSetter;
+
+        public DeferredSetter<object> Setter
+        {
+            get
+            {
+                return _defferedSetter ??
+                    (_defferedSetter = new DeferredSetter<object>());
+            }
+        }
     }
 }

+ 58 - 13
src/Avalonia.Controls/ContextMenu.cs

@@ -7,11 +7,20 @@ namespace Avalonia.Controls
     using System;
     using System.Reactive.Linq;
     using System.Linq;
+    using System.ComponentModel;
+
     public class ContextMenu : SelectingItemsControl
     {
         private bool _isOpen;
         private Popup _popup;
 
+        /// <summary>
+        /// Defines the <see cref="IsOpen"/> property.
+        /// </summary>
+        public static readonly DirectProperty<ContextMenu, bool> IsOpenProperty =
+                            AvaloniaProperty.RegisterDirect<ContextMenu, bool>(nameof(IsOpen), o => o.IsOpen);
+
+
         /// <summary>
         /// Initializes static members of the <see cref="ContextMenu"/> class.
         /// </summary>
@@ -22,6 +31,26 @@ namespace Avalonia.Controls
             MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true);
         }
 
+        /// <summary>
+        /// Gets a value indicating whether the popup is open
+        /// </summary>
+        public bool IsOpen => _isOpen;
+
+        /// <summary>
+        /// Occurs when the value of the
+        /// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
+        /// property is changing from false to true.
+        /// </summary>
+        public event CancelEventHandler ContextMenuOpening;
+
+        /// <summary>
+        /// Occurs when the value of the
+        /// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
+        /// property is changing from true to false.
+        /// </summary>
+        public event CancelEventHandler ContextMenuClosing;
+
+
         /// <summary>
         /// Called when the <see cref="Control.ContextMenu"/> property changes on a control.
         /// </summary>
@@ -59,12 +88,12 @@ namespace Avalonia.Controls
         {
             if (_popup != null && _popup.IsVisible)
             {
-                _popup.Close();
+                _popup.IsOpen = false;
             }
 
             SelectedIndex = -1;
 
-            _isOpen = false;
+            SetAndRaise(IsOpenProperty, ref _isOpen, false);
         }
 
         /// <summary>
@@ -89,11 +118,11 @@ namespace Avalonia.Controls
                 }
 
                 ((ISetLogicalParent)_popup).SetParent(control);
-                _popup.Child = control.ContextMenu;
+                _popup.Child = this;
 
-                _popup.Open();
+                _popup.IsOpen = true;
 
-                control.ContextMenu._isOpen = true;
+                SetAndRaise(IsOpenProperty, ref _isOpen, true);
             }
         }
 
@@ -118,21 +147,37 @@ namespace Avalonia.Controls
             var control = (Control)sender;
             var contextMenu = control.ContextMenu;
 
-            if (e.MouseButton == MouseButton.Right)
+            if (control.ContextMenu._isOpen)
             {
-                if (control.ContextMenu._isOpen)
-                {
-                    control.ContextMenu.Hide();
-                }
+                if (contextMenu.CancelClosing())
+                    return;
 
-                contextMenu.Show(control);
+                control.ContextMenu.Hide();
                 e.Handled = true;
             }
-            else if (contextMenu._isOpen)
+
+            if (e.MouseButton == MouseButton.Right)
             {
-                control.ContextMenu.Hide();
+                if (contextMenu.CancelOpening())
+                    return;
+
+                contextMenu.Show(control);
                 e.Handled = true;
             }
         }
+
+        private bool CancelClosing()
+        {
+            var eventArgs = new CancelEventArgs();
+            ContextMenuClosing?.Invoke(this, eventArgs);
+            return eventArgs.Cancel;
+        }
+
+        private bool CancelOpening()
+        {
+            var eventArgs = new CancelEventArgs();
+            ContextMenuOpening?.Invoke(this, eventArgs);
+            return eventArgs.Cancel;
+        }
     }
 }

+ 2 - 4
src/Avalonia.Controls/Expander.cs

@@ -66,9 +66,7 @@ namespace Avalonia.Controls
 
         protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
         {
-            IVisual visualContent = Presenter;
-
-            if (Content != null && ContentTransition != null && visualContent != null)
+            if (Content != null && ContentTransition != null && Presenter is Visual visualContent)
             {
                 bool forward = ExpandDirection == ExpandDirection.Left ||
                                 ExpandDirection == ExpandDirection.Up;
@@ -87,4 +85,4 @@ namespace Avalonia.Controls
         private ExpandDirection _expandDirection;
         private bool _isExpanded;
     }
-}
+}

+ 15 - 5
src/Avalonia.Controls/Grid.cs

@@ -194,6 +194,16 @@ namespace Avalonia.Controls
         /// </summary>
         private GridLayout.MeasureResult _rowMeasureCache;
 
+        /// <summary>
+        /// Gets the row layout as of the last measure.
+        /// </summary>
+        private GridLayout _rowLayoutCache;
+
+        /// <summary>
+        /// Gets the column layout as of the last measure.
+        /// </summary>
+        private GridLayout _columnLayoutCache;
+
         /// <summary>
         /// Measures the grid.
         /// </summary>
@@ -253,6 +263,9 @@ namespace Avalonia.Controls
             // Cache the measure result and return the desired size.
             _columnMeasureCache = columnResult;
             _rowMeasureCache = rowResult;
+            _rowLayoutCache = rowLayout;
+            _columnLayoutCache = columnLayout;
+
             return new Size(columnResult.DesiredLength, rowResult.DesiredLength);
 
             // Measure each child only once.
@@ -299,13 +312,11 @@ namespace Avalonia.Controls
             //       arrow back to any statements and re-run them without any side-effect.
 
             var (safeColumns, safeRows) = GetSafeColumnRows();
-            var columnLayout = new GridLayout(ColumnDefinitions);
-            var rowLayout = new GridLayout(RowDefinitions);
-
+            var columnLayout = _columnLayoutCache;
+            var rowLayout = _rowLayoutCache;
             // Calculate for arrange result.
             var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache);
             var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache);
-
             // Arrange the children.
             foreach (var child in Children.OfType<Control>())
             {
@@ -315,7 +326,6 @@ namespace Avalonia.Controls
                 var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]);
                 var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]);
                 var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]);
-
                 child.Arrange(new Rect(x, y, width, height));
             }
 

+ 5 - 4
src/Avalonia.Controls/Image.cs

@@ -1,12 +1,11 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
 
 namespace Avalonia.Controls
-{
+{   
     /// <summary>
     /// Displays a <see cref="Bitmap"/> image.
     /// </summary>
@@ -68,7 +67,9 @@ namespace Avalonia.Controls
                 Rect sourceRect = new Rect(sourceSize)
                     .CenterRect(new Rect(destRect.Size / scale));
 
-                context.DrawImage(source, 1, sourceRect, destRect);
+                var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this);
+
+                context.DrawImage(source, 1, sourceRect, destRect, interpolationMode);
             }
         }
 
@@ -100,4 +101,4 @@ namespace Avalonia.Controls
             }
         }
     }
-}
+}

+ 5 - 0
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@@ -161,6 +161,11 @@ namespace Avalonia.Controls.Presenters
         /// <returns>An <see cref="ItemVirtualizer"/>.</returns>
         public static ItemVirtualizer Create(ItemsPresenter owner)
         {
+            if (owner.Panel == null)
+            {
+                return null;
+            }
+
             var virtualizingPanel = owner.Panel as IVirtualizingPanel;
             var scrollable = (ILogicalScrollable)owner;
             ItemVirtualizer result = null;

+ 22 - 14
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@@ -22,7 +22,6 @@ namespace Avalonia.Controls.Presenters
                 nameof(VirtualizationMode),
                 defaultValue: ItemVirtualizationMode.None);
 
-        private ItemVirtualizer _virtualizer;
         private bool _canHorizontallyScroll;
         private bool _canVerticallyScroll;
 
@@ -76,21 +75,27 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         bool ILogicalScrollable.IsLogicalScrollEnabled
         {
-            get { return _virtualizer?.IsLogicalScrollEnabled ?? false; }
+            get { return Virtualizer?.IsLogicalScrollEnabled ?? false; }
         }
 
         /// <inheritdoc/>
-        Size IScrollable.Extent => _virtualizer.Extent;
+        Size IScrollable.Extent => Virtualizer?.Extent ?? Size.Empty;
 
         /// <inheritdoc/>
         Vector IScrollable.Offset
         {
-            get { return _virtualizer.Offset; }
-            set { _virtualizer.Offset = CoerceOffset(value); }
+            get { return Virtualizer?.Offset ?? new Vector(); }
+            set
+            {
+                if (Virtualizer != null)
+                {
+                    Virtualizer.Offset = CoerceOffset(value);
+                }
+            }
         }
 
         /// <inheritdoc/>
-        Size IScrollable.Viewport => _virtualizer.Viewport;
+        Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size;
 
         /// <inheritdoc/>
         Action ILogicalScrollable.InvalidateScroll { get; set; }
@@ -101,6 +106,8 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         Size ILogicalScrollable.PageScrollSize => new Size(0, 1);
 
+        internal ItemVirtualizer Virtualizer { get; private set; }
+
         /// <inheritdoc/>
         bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect)
         {
@@ -110,29 +117,30 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from)
         {
-            return _virtualizer?.GetControlInDirection(direction, from);
+            return Virtualizer?.GetControlInDirection(direction, from);
         }
 
         public override void ScrollIntoView(object item)
         {
-            _virtualizer?.ScrollIntoView(item);
+            Virtualizer?.ScrollIntoView(item);
         }
 
         /// <inheritdoc/>
         protected override Size MeasureOverride(Size availableSize)
         {
-            return _virtualizer?.MeasureOverride(availableSize) ?? Size.Empty;
+            return Virtualizer?.MeasureOverride(availableSize) ?? Size.Empty;
         }
 
         protected override Size ArrangeOverride(Size finalSize)
         {
-            return _virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty;
+            return Virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty;
         }
 
         /// <inheritdoc/>
         protected override void PanelCreated(IPanel panel)
         {
-            _virtualizer = ItemVirtualizer.Create(this);
+            Virtualizer?.Dispose();
+            Virtualizer = ItemVirtualizer.Create(this);
             ((ILogicalScrollable)this).InvalidateScroll?.Invoke();
 
             if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
@@ -149,7 +157,7 @@ namespace Avalonia.Controls.Presenters
 
         protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
         {
-            _virtualizer?.ItemsChanged(Items, e);
+            Virtualizer?.ItemsChanged(Items, e);
         }
 
         private Vector CoerceOffset(Vector value)
@@ -162,8 +170,8 @@ namespace Avalonia.Controls.Presenters
 
         private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e)
         {
-            _virtualizer?.Dispose();
-            _virtualizer = ItemVirtualizer.Create(this);
+            Virtualizer?.Dispose();
+            Virtualizer = ItemVirtualizer.Create(this);
             ((ILogicalScrollable)this).InvalidateScroll?.Invoke();
         }
     }

+ 9 - 21
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Collections;
 using System.Collections.Specialized;
+using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Templates;
 using Avalonia.Styling;
@@ -40,6 +41,7 @@ namespace Avalonia.Controls.Presenters
             ItemsControl.MemberSelectorProperty.AddOwner<ItemsPresenterBase>();
 
         private IEnumerable _items;
+        private IDisposable _itemsSubscription;
         private bool _createdPanel;
         private IItemContainerGenerator _generator;
 
@@ -63,24 +65,12 @@ namespace Avalonia.Controls.Presenters
 
             set
             {
-                if (_createdPanel)
-                {
-                    INotifyCollectionChanged incc = _items as INotifyCollectionChanged;
-
-                    if (incc != null)
-                    {
-                        incc.CollectionChanged -= ItemsCollectionChanged;
-                    }
-                }
+                _itemsSubscription?.Dispose();
+                _itemsSubscription = null;
 
-                if (_createdPanel && value != null)
+                if (_createdPanel && value is INotifyCollectionChanged incc)
                 {
-                    INotifyCollectionChanged incc = value as INotifyCollectionChanged;
-
-                    if (incc != null)
-                    {
-                        incc.CollectionChanged += ItemsCollectionChanged;
-                    }
+                    _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
                 }
 
                 SetAndRaise(ItemsProperty, ref _items, value);
@@ -233,11 +223,9 @@ namespace Avalonia.Controls.Presenters
 
             _createdPanel = true;
 
-            INotifyCollectionChanged incc = Items as INotifyCollectionChanged;
-
-            if (incc != null)
+            if (_itemsSubscription == null && Items is INotifyCollectionChanged incc)
             {
-                incc.CollectionChanged += ItemsCollectionChanged;
+                _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
             }
 
             PanelCreated(Panel);
@@ -263,4 +251,4 @@ namespace Avalonia.Controls.Presenters
             (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this);
         }
     }
-}
+}

+ 6 - 0
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -189,6 +189,12 @@ namespace Avalonia.Controls.Presenters
                     _caretTimer.Start();
                     InvalidateVisual();
                 }
+                else
+                {
+                    _caretTimer.Start();
+                    InvalidateVisual();
+                    _caretTimer.Stop();
+                }
 
                 if (IsMeasureValid)
                 {

+ 13 - 12
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -83,21 +83,22 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public void SnapInsideScreenEdges()
         {
-            var window = this.GetSelfAndLogicalAncestors().OfType<Window>().First();
-            
-            var screen = window.Screens.ScreenFromPoint(Position);
+            var screen = Application.Current.MainWindow?.Screens.ScreenFromPoint(Position);
 
-            var screenX = Position.X + Bounds.Width - screen.Bounds.X;
-            var screenY = Position.Y + Bounds.Height - screen.Bounds.Y;
-
-            if (screenX > screen.Bounds.Width)
+            if (screen != null)
             {
-                Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
-            }
+                var screenX = Position.X + Bounds.Width - screen.Bounds.X;
+                var screenY = Position.Y + Bounds.Height - screen.Bounds.Y;
 
-            if (screenY > screen.Bounds.Height)
-            {
-                Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
+                if (screenX > screen.Bounds.Width)
+                {
+                    Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
+                }
+
+                if (screenY > screen.Bounds.Height)
+                {
+                    Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
+                }
             }
         }
 

+ 7 - 2
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -360,7 +360,7 @@ namespace Avalonia.Controls.Primitives
                     {
                         if (!AlwaysSelected)
                         {
-                            SelectedIndex = -1;
+                            selectedIndex = SelectedIndex = -1;
                         }
                         else
                         {
@@ -368,10 +368,15 @@ namespace Avalonia.Controls.Primitives
                         }
                     }
 
+                    var items = Items?.Cast<object>();
+                    if (selectedIndex >= items.Count())
+                    {
+                        selectedIndex = SelectedIndex = items.Count() - 1;
+                    }
                     break;
 
                 case NotifyCollectionChangedAction.Reset:
-                    SelectedIndex = IndexOf(e.NewItems, SelectedItem);
+                    SelectedIndex = IndexOf(Items, SelectedItem);
                     break;
             }
         }

+ 1 - 1
src/Avalonia.Controls/Primitives/ToggleButton.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives
                 nameof(IsChecked),
                 o => o.IsChecked,
                 (o, v) => o.IsChecked = v,
-                unsetValue: false,
+                unsetValue: null,
                 defaultBindingMode: BindingMode.TwoWay);
 
         public static readonly StyledProperty<bool> IsThreeStateProperty =

+ 39 - 51
src/Avalonia.Controls/ProgressBar.cs

@@ -21,18 +21,27 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<Orientation> OrientationProperty =
             AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
 
+        private static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty =
+            AvaloniaProperty.RegisterDirect<ProgressBar, double>(
+                nameof(IndeterminateStartingOffset),
+                p => p.IndeterminateStartingOffset,
+                (p, o) => p.IndeterminateStartingOffset = o);
+
+        private static readonly DirectProperty<ProgressBar, double> IndeterminateEndingOffsetProperty =
+            AvaloniaProperty.RegisterDirect<ProgressBar, double>(
+                nameof(IndeterminateEndingOffset),
+                p => p.IndeterminateEndingOffset,
+                (p, o) => p.IndeterminateEndingOffset = o);
+
         private Border _indicator;
-        private IndeterminateAnimation _indeterminateAnimation;
 
         static ProgressBar()
         {
             PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
             PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
+            PseudoClass(IsIndeterminateProperty, ":indeterminate");
 
             ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged);
-
-            IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(
-                (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); });
         }
 
         public bool IsIndeterminate
@@ -46,6 +55,19 @@ namespace Avalonia.Controls
             get => GetValue(OrientationProperty);
             set => SetValue(OrientationProperty, value);
         }
+        private double _indeterminateStartingOffset;
+        private double IndeterminateStartingOffset
+        {
+            get => _indeterminateStartingOffset;
+            set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
+        }
+
+        private double _indeterminateEndingOffset;
+        private double IndeterminateEndingOffset
+        {
+            get => _indeterminateEndingOffset;
+            set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
+        }
 
         /// <inheritdoc/>
         protected override Size ArrangeOverride(Size finalSize)
@@ -60,7 +82,6 @@ namespace Avalonia.Controls
             _indicator = e.NameScope.Get<Border>("PART_Indicator");
 
             UpdateIndicator(Bounds.Size);
-            UpdateIsIndeterminate(IsIndeterminate);
         }
 
         private void UpdateIndicator(Size bounds)
@@ -70,9 +91,20 @@ namespace Avalonia.Controls
                 if (IsIndeterminate)
                 {
                     if (Orientation == Orientation.Horizontal)
-                        _indicator.Width = bounds.Width / 5.0;
+                    {
+                        var width = bounds.Width / 5.0;
+                        IndeterminateStartingOffset = -width;
+                        _indicator.Width = width;
+                        IndeterminateEndingOffset = bounds.Width;
+
+                    }
                     else
-                        _indicator.Height = bounds.Height / 5.0;
+                    {
+                        var height = bounds.Height / 5.0;
+                        IndeterminateStartingOffset = -bounds.Height;
+                        _indicator.Height = height;
+                        IndeterminateEndingOffset = height;
+                    }
                 }
                 else
                 {
@@ -86,53 +118,9 @@ namespace Avalonia.Controls
             }
         }
 
-        private void UpdateIsIndeterminate(bool isIndeterminate)
-        {
-            if (isIndeterminate)
-            {
-                if (_indeterminateAnimation == null || _indeterminateAnimation.Disposed)
-                    _indeterminateAnimation = IndeterminateAnimation.StartAnimation(this);
-            }
-            else
-                _indeterminateAnimation?.Dispose();
-        }
-
         private void ValueChanged(AvaloniaPropertyChangedEventArgs e)
         {
             UpdateIndicator(Bounds.Size);
         }
-
-        // TODO: Implement Indeterminate Progress animation
-        //       in xaml (most ideal) or if it's not possible
-        //       then on this class.
-        private class IndeterminateAnimation : IDisposable
-        {
-            private WeakReference<ProgressBar> _progressBar;
-
-            private bool _disposed;
-
-            public bool Disposed => _disposed;
-
-            private IndeterminateAnimation(ProgressBar progressBar)
-            {
-                _progressBar = new WeakReference<ProgressBar>(progressBar);
-
-            }
-
-            public static IndeterminateAnimation StartAnimation(ProgressBar progressBar)
-            {
-                return new IndeterminateAnimation(progressBar);
-            }
-
-            private Rect GetAnimationRect(TimeSpan time)
-            {
-                return Rect.Empty;
-            }
-
-            public void Dispose()
-            {
-                _disposed = true;
-            }
-        }
     }
 }

+ 2 - 3
src/Avalonia.Controls/Slider.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Controls
         /// Defines the <see cref="Orientation"/> property.
         /// </summary>
         public static readonly StyledProperty<Orientation> OrientationProperty =
-            AvaloniaProperty.Register<Slider, Orientation>(nameof(Orientation), Orientation.Horizontal);
+            ScrollBar.OrientationProperty.AddOwner<Slider>();
 
         /// <summary>
         /// Defines the <see cref="IsSnapToTickEnabled"/> property.
@@ -41,8 +41,7 @@ namespace Avalonia.Controls
         /// </summary>
         static Slider()
         {
-            PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
-            PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
+            OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
             Thumb.DragStartedEvent.AddClassHandler<Slider>(x => x.OnThumbDragStarted, RoutingStrategies.Bubble);
             Thumb.DragDeltaEvent.AddClassHandler<Slider>(x => x.OnThumbDragDelta, RoutingStrategies.Bubble);
             Thumb.DragCompletedEvent.AddClassHandler<Slider>(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble);

+ 22 - 19
src/Avalonia.Controls/StackPanel.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Linq;
 using Avalonia.Input;
 
 namespace Avalonia.Controls
@@ -12,10 +13,10 @@ namespace Avalonia.Controls
     public class StackPanel : Panel, INavigableContainer
     {
         /// <summary>
-        /// Defines the <see cref="Gap"/> property.
+        /// Defines the <see cref="Spacing"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> GapProperty =
-            AvaloniaProperty.Register<StackPanel, double>(nameof(Gap));
+        public static readonly StyledProperty<double> SpacingProperty =
+            AvaloniaProperty.Register<StackPanel, double>(nameof(Spacing));
 
         /// <summary>
         /// Defines the <see cref="Orientation"/> property.
@@ -28,17 +29,17 @@ namespace Avalonia.Controls
         /// </summary>
         static StackPanel()
         {
-            AffectsMeasure(GapProperty);
+            AffectsMeasure(SpacingProperty);
             AffectsMeasure(OrientationProperty);
         }
 
         /// <summary>
-        /// Gets or sets the size of the gap to place between child controls.
+        /// Gets or sets the size of the spacing to place between child controls.
         /// </summary>
-        public double Gap
+        public double Spacing
         {
-            get { return GetValue(GapProperty); }
-            set { SetValue(GapProperty, value); }
+            get { return GetValue(SpacingProperty); }
+            set { SetValue(SpacingProperty, value); }
         }
 
         /// <summary>
@@ -151,7 +152,8 @@ namespace Avalonia.Controls
 
             double measuredWidth = 0;
             double measuredHeight = 0;
-            double gap = Gap;
+            double spacing = Spacing;
+            bool hasVisibleChild = Children.Any(c => c.IsVisible);
 
             foreach (Control child in Children)
             {
@@ -160,23 +162,23 @@ namespace Avalonia.Controls
 
                 if (Orientation == Orientation.Vertical)
                 {
-                    measuredHeight += size.Height + gap;
+                    measuredHeight += size.Height + (child.IsVisible ? spacing : 0);
                     measuredWidth = Math.Max(measuredWidth, size.Width);
                 }
                 else
                 {
-                    measuredWidth += size.Width + gap;
+                    measuredWidth += size.Width + (child.IsVisible ? spacing : 0);   
                     measuredHeight = Math.Max(measuredHeight, size.Height);
                 }
             }
 
             if (Orientation == Orientation.Vertical)
             {
-                measuredHeight -= gap;
+                measuredHeight -= (hasVisibleChild ? spacing : 0);
             }
             else
             {
-                measuredWidth -= gap;
+                measuredWidth -= (hasVisibleChild ? spacing : 0);
             }
 
             return new Size(measuredWidth, measuredHeight);
@@ -192,7 +194,8 @@ namespace Avalonia.Controls
             var orientation = Orientation;
             double arrangedWidth = finalSize.Width;
             double arrangedHeight = finalSize.Height;
-            double gap = Gap;
+            double spacing = Spacing;
+            bool hasVisibleChild = Children.Any(c => c.IsVisible);
 
             if (Orientation == Orientation.Vertical)
             {
@@ -214,25 +217,25 @@ namespace Avalonia.Controls
                     Rect childFinal = new Rect(0, arrangedHeight, width, childHeight);
                     ArrangeChild(child, childFinal, finalSize, orientation);
                     arrangedWidth = Math.Max(arrangedWidth, childWidth);
-                    arrangedHeight += childHeight + gap;
+                    arrangedHeight += childHeight + (child.IsVisible ? spacing : 0);
                 }
                 else
                 {
                     double height = Math.Max(childHeight, arrangedHeight);
                     Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height);
                     ArrangeChild(child, childFinal, finalSize, orientation);
-                    arrangedWidth += childWidth + gap;
+                    arrangedWidth += childWidth + (child.IsVisible ? spacing : 0);
                     arrangedHeight = Math.Max(arrangedHeight, childHeight);
                 }
             }
 
             if (orientation == Orientation.Vertical)
             {
-                arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height);
+                arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? spacing : 0), finalSize.Height);
             }
             else
             {
-                arrangedWidth = Math.Max(arrangedWidth - gap, finalSize.Width);
+                arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? spacing : 0), finalSize.Width);
             }
 
             return new Size(arrangedWidth, arrangedHeight);
@@ -247,4 +250,4 @@ namespace Avalonia.Controls
             child.Arrange(rect);
         }
     }
-}
+}

+ 12 - 4
src/Avalonia.Controls/TextBox.cs

@@ -124,7 +124,7 @@ namespace Avalonia.Controls
                 ScrollViewer.HorizontalScrollBarVisibilityProperty,
                 horizontalScrollBarVisibility,
                 BindingPriority.Style);
-            _undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);            
+            _undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);
         }
 
         public bool AcceptsReturn
@@ -262,7 +262,7 @@ namespace Avalonia.Controls
 
             if (IsFocused)
             {
-                _presenter.ShowCaret();
+                DecideCaretVisibility();
             }
         }
 
@@ -282,12 +282,20 @@ namespace Avalonia.Controls
             }
             else
             {
-                _presenter?.ShowCaret();
+                DecideCaretVisibility();
             }
 
             e.Handled = true;
         }
 
+        private void DecideCaretVisibility()
+        {
+            if (!IsReadOnly)
+                _presenter?.ShowCaret();
+            else
+                _presenter?.HideCaret();
+        }
+
         protected override void OnLostFocus(RoutedEventArgs e)
         {
             base.OnLostFocus(e);
@@ -557,7 +565,7 @@ namespace Avalonia.Controls
             var index = CaretIndex = _presenter.GetCaretIndex(point);
             var text = Text;
 
-            if (text != null)
+            if (text != null && e.MouseButton == MouseButton.Left)
             {
                 switch (e.ClickCount)
                 {

+ 6 - 6
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -200,7 +200,7 @@ namespace Avalonia.Controls
         private void UpdateAdd(IControl child)
         {
             var bounds = Bounds;
-            var gap = Gap;
+            var spacing = Spacing;
 
             child.Measure(_availableSpace);
             ++_averageCount;
@@ -208,13 +208,13 @@ namespace Avalonia.Controls
             if (Orientation == Orientation.Vertical)
             {
                 var height = child.DesiredSize.Height;
-                _takenSpace += height + gap;
+                _takenSpace += height + spacing;
                 AddToAverageItemSize(height);
             }
             else
             {
                 var width = child.DesiredSize.Width;
-                _takenSpace += width + gap;
+                _takenSpace += width + spacing;
                 AddToAverageItemSize(width);
             }
         }
@@ -222,18 +222,18 @@ namespace Avalonia.Controls
         private void UpdateRemove(IControl child)
         {
             var bounds = Bounds;
-            var gap = Gap;
+            var spacing = Spacing;
 
             if (Orientation == Orientation.Vertical)
             {
                 var height = child.DesiredSize.Height;
-                _takenSpace -= height + gap;
+                _takenSpace -= height + spacing;
                 RemoveFromAverageItemSize(height);
             }
             else
             {
                 var width = child.DesiredSize.Width;
-                _takenSpace -= width + gap;
+                _takenSpace -= width + spacing;
                 RemoveFromAverageItemSize(width);
             }
 

+ 8 - 2
src/Avalonia.Controls/Window.cs

@@ -302,17 +302,23 @@ namespace Avalonia.Controls
 
         internal void Close(bool ignoreCancel)
         {
+            bool close = true;
+
             try
             {
                 if (!ignoreCancel && HandleClosing())
                 {
+                    close = false;
                     return;
                 }
             }
             finally
             {
-                PlatformImpl?.Dispose();
-                HandleClosed();
+                if (close)
+                {
+                    PlatformImpl?.Dispose();
+                    HandleClosed();
+                }
             }
         }
 

+ 2 - 2
src/Avalonia.Controls/WindowCollection.cs

@@ -96,7 +96,7 @@ namespace Avalonia
         {
             while (_windows.Count > 0)
             {
-                _windows[0].Close();
+                _windows[0].Close(true);
             }
         }
 
@@ -131,4 +131,4 @@ namespace Avalonia
             }
         }
     }
-}
+}

+ 6 - 6
src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj

@@ -1,7 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
-    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <!-- WARNING! The designer support version number needs to be frozen 
+         To allow projects that implement designer functionality to still
+         work with newer versions of Avalonia. This version number only
+         need change when there are breaking changes to designer support api.
+    -->
+    <Version>0.7.0</Version>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
     <DebugSymbols>true</DebugSymbols>
@@ -37,11 +42,6 @@
     <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
     <ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
   </ItemGroup>
-  <ItemGroup>
-    <Compile Include="..\Shared\SharedAssemblyInfo.cs">
-      <Link>Properties\SharedAssemblyInfo.cs</Link>
-    </Compile>
-  </ItemGroup>
   <Import Project="..\..\build\Microsoft.CSharp.props" />
   <Import Project="..\..\build\Rx.props" />
 </Project>

+ 2 - 2
src/Avalonia.Diagnostics/DevTools.xaml

@@ -7,7 +7,7 @@
 
     <ContentControl Content="{Binding Content}" Grid.Row="1"/> 
     
-    <StackPanel Gap="4" Orientation="Horizontal" Grid.Row="2">
+    <StackPanel Spacing="4" Orientation="Horizontal" Grid.Row="2">
       <TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
       <Separator Width="8"/>
       <TextBlock>Focused:</TextBlock>
@@ -17,4 +17,4 @@
       <TextBlock Text="{Binding PointerOverElement}"/>
     </StackPanel>
   </Grid>
-</UserControl>
+</UserControl>

+ 1 - 0
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@@ -31,6 +31,7 @@ namespace Avalonia.Diagnostics.ViewModels
                 }
             };
 
+            SelectedTab = 0;
             root.GetObservable(TopLevel.PointerOverElementProperty)
                 .Subscribe(x => PointerOverElement = x?.GetType().Name);
         }

+ 2 - 2
src/Avalonia.Diagnostics/Views/TreePageView.xaml

@@ -5,7 +5,7 @@
       <TreeView.DataTemplates>
         <TreeDataTemplate DataType="vm:TreeNode"
                           ItemsSource="{Binding Children}">
-          <StackPanel Orientation="Horizontal" Gap="8">
+          <StackPanel Orientation="Horizontal" Spacing="8">
             <TextBlock Text="{Binding Type}"/>
             <TextBlock Text="{Binding Classes}"/>
           </StackPanel>
@@ -21,4 +21,4 @@
     <GridSplitter Width="4" Grid.Column="1"/>
     <ContentControl Content="{Binding Details}" Grid.Column="2"/>
   </Grid>
-</UserControl>
+</UserControl>

+ 1 - 0
src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj

@@ -10,6 +10,7 @@
     <ProjectReference Include="..\Gtk\Avalonia.Gtk3\Avalonia.Gtk3.csproj" />
     <ProjectReference Include="..\OSX\Avalonia.MonoMac\Avalonia.MonoMac.csproj" />
     <ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
+    <ProjectReference Include="..\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj" />
     <ProjectReference Include="..\Windows\Avalonia.Win32\Avalonia.Win32.csproj" />
   </ItemGroup>  
   <Import Project="..\..\build\NetCore.props" />

+ 5 - 12
src/Avalonia.Styling/Styling/Setter.cs

@@ -126,7 +126,7 @@ namespace Avalonia.Styling
 
                 if (source != null)
                 {
-                    var cloned = Clone(source, style, activator);
+                    var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator);
                     return BindingOperations.Apply(control, Property, cloned, null);
                 }
             }
@@ -134,13 +134,13 @@ namespace Avalonia.Styling
             return Disposable.Empty;
         }
 
-        private InstancedBinding Clone(InstancedBinding sourceInstance, IStyle style, IObservable<bool> activator)
+        private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable<bool> activator)
         {
             if (activator != null)
             {
                 var description = style?.ToString();
 
-                switch (sourceInstance.Mode)
+                switch (mode)
                 {
                     case BindingMode.OneTime:
                         if (sourceInstance.Observable != null)
@@ -158,18 +158,11 @@ namespace Avalonia.Styling
                             var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
                             return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger);
                         }
-                    case BindingMode.OneWayToSource:
-                        {
-                            var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
-                            return InstancedBinding.OneWayToSource(activated, BindingPriority.StyleTrigger);
-                        }
-                    case BindingMode.TwoWay:
+                    default:
                         {
                             var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
-                            return InstancedBinding.TwoWay(activated, BindingPriority.StyleTrigger);
+                            return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger);
                         }
-                    default:
-                        throw new NotSupportedException("Unsupported BindingMode.");
                 }
 
             }

+ 5 - 0
src/Avalonia.Themes.Default/MenuItem.xaml

@@ -122,6 +122,11 @@
     </Setter>
   </Style>
 
+  <Style Selector="MenuItem:selected /template/ Border#root">
+    <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>
+  </Style>
+
   <Style Selector="MenuItem:pointerover /template/ Border#root">
     <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

+ 36 - 9
src/Avalonia.Themes.Default/ProgressBar.xaml

@@ -7,14 +7,9 @@
         <Border Background="{TemplateBinding Background}"
                 BorderBrush="{TemplateBinding BorderBrush}"
                 BorderThickness="{TemplateBinding BorderThickness}">
-          <Grid>
-            <Border Name="PART_Track"
-                    BorderThickness="1"
-                    BorderBrush="{TemplateBinding Background}"/>
-            <Border Name="PART_Indicator"
-                    BorderThickness="1"
-                    Background="{TemplateBinding Foreground}" />
-          </Grid>
+          <Border Name="PART_Indicator"
+                  BorderThickness="1"
+                  Background="{TemplateBinding Foreground}"/>
         </Border>
       </ControlTemplate>
     </Setter>
@@ -35,4 +30,36 @@
     <Setter Property="MinWidth" Value="14"/>
     <Setter Property="MinHeight" Value="200"/>
   </Style>
-</Styles>
+  <Style Selector="ProgressBar:horizontal:indeterminate /template/ Border#PART_Indicator">
+      <Style.Animations>
+          <Animation Duration="0:0:3"
+                     RepeatCount="Loop"
+                     Easing="LinearEasing">
+              <KeyFrame Cue="0%">
+                  <Setter Property="TranslateTransform.X"
+                          Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+              </KeyFrame>
+              <KeyFrame Cue="100%">
+                  <Setter Property="TranslateTransform.X"
+                          Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+              </KeyFrame>
+      </Animation>
+      </Style.Animations>
+  </Style>
+  <Style Selector="ProgressBar:vertical:indeterminate /template/ Border#PART_Indicator">
+      <Style.Animations>
+          <Animation Duration="0:0:3"
+                     RepeatCount="Loop"
+                     Easing="LinearEasing">
+              <KeyFrame Cue="0%">
+                  <Setter Property="TranslateTransform.Y"
+                          Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+              </KeyFrame>
+              <KeyFrame Cue="100%">
+                  <Setter Property="TranslateTransform.Y"
+                          Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+              </KeyFrame>
+      </Animation>
+      </Style.Animations>
+  </Style>
+</Styles>

+ 50 - 10
src/Avalonia.Visuals/Animation/CrossFade.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Reactive.Threading.Tasks;
 using System.Threading.Tasks;
+using Avalonia.Styling;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Animation
@@ -14,10 +15,14 @@ namespace Avalonia.Animation
     /// </summary>
     public class CrossFade : IPageTransition
     {
+        private Animation _fadeOutAnimation;
+        private Animation _fadeInAnimation;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="CrossFade"/> class.
         /// </summary>
         public CrossFade()
+            :this(TimeSpan.Zero)
         {
         }
 
@@ -27,13 +32,51 @@ namespace Avalonia.Animation
         /// <param name="duration">The duration of the animation.</param>
         public CrossFade(TimeSpan duration)
         {
-            Duration = duration;
+            _fadeOutAnimation = new Animation
+            {
+                new KeyFrame
+                (
+                    new Setter
+                    {
+                        Property = Visual.OpacityProperty,
+                        Value = 0.0
+                    }
+                )
+                {
+                    Cue = new Cue(1.0)
+                }
+            };
+            _fadeInAnimation = new Animation
+            {
+                new KeyFrame
+                (
+                    new Setter
+                    {
+                        Property = Visual.OpacityProperty,
+                        Value = 0.0
+                    }
+                )
+                {
+                    Cue = new Cue(0.0)
+                }
+            };
+            _fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration;
         }
 
         /// <summary>
         /// Gets the duration of the animation.
         /// </summary>
-        public TimeSpan Duration { get; set; }
+        public TimeSpan Duration
+        {
+            get
+            {
+                return _fadeOutAnimation.Duration;
+            }
+            set
+            {
+                _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value;
+            }
+        }
 
         /// <summary>
         /// Starts the animation.
@@ -47,12 +90,10 @@ namespace Avalonia.Animation
         /// <returns>
         /// A <see cref="Task"/> that tracks the progress of the animation.
         /// </returns>
-        public async Task Start(IVisual from, IVisual to)
+        public async Task Start(Visual from, Visual to)
         {
             var tasks = new List<Task>();
 
-            // TODO: Implement relevant transition logic here (or discard this class)
-            // in favor of XAML based transition for pages
             if (to != null)
             {
                 to.Opacity = 0;
@@ -60,22 +101,21 @@ namespace Avalonia.Animation
 
             if (from != null)
             {
+                tasks.Add(_fadeOutAnimation.RunAsync(from));
             }
 
             if (to != null)
             {
-                to.Opacity = 0;
                 to.IsVisible = true;
+                tasks.Add(_fadeInAnimation.RunAsync(to));
 
             }
 
-            // FIXME: This is temporary until animations are fixed.
-            await Task.Delay(1);
+            await Task.WhenAll(tasks);
 
             if (from != null)
             {
                 from.IsVisible = false;
-                from.Opacity = 1;
             }
 
             if (to != null)
@@ -99,7 +139,7 @@ namespace Avalonia.Animation
         /// <returns>
         /// A <see cref="Task"/> that tracks the progress of the animation.
         /// </returns>
-        Task IPageTransition.Start(IVisual from, IVisual to, bool forward)
+        Task IPageTransition.Start(Visual from, Visual to, bool forward)
         {
             return Start(from, to);
         }

+ 1 - 1
src/Avalonia.Visuals/Animation/IPageTransition.cs

@@ -26,6 +26,6 @@ namespace Avalonia.Animation
         /// <returns>
         /// A <see cref="Task"/> that tracks the progress of the animation.
         /// </returns>
-        Task Start(IVisual from, IVisual to, bool forward);
+        Task Start(Visual from, Visual to, bool forward);
     }
 }

+ 59 - 5
src/Avalonia.Visuals/Animation/PageSlide.cs

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Reactive.Threading.Tasks;
 using System.Threading.Tasks;
 using Avalonia.Media;
+using Avalonia.Styling;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Animation
@@ -67,7 +68,7 @@ namespace Avalonia.Animation
         /// <returns>
         /// A <see cref="Task"/> that tracks the progress of the animation.
         /// </returns>
-        public async Task Start(IVisual from, IVisual to, bool forward)
+        public async Task Start(Visual from, Visual to, bool forward)
         {
             var tasks = new List<Task>();
             var parent = GetVisualParent(from, to);
@@ -79,16 +80,69 @@ namespace Avalonia.Animation
             // in favor of XAML based transition for pages
             if (from != null)
             {
-
+                var animation = new Animation
+                {
+                    new KeyFrame
+                    (
+                        new Setter
+                        {
+                            Property = translateProperty,
+                            Value = 0
+                        }
+                    )
+                    {
+                        Cue = new Cue(0.0)
+                    },
+                    new KeyFrame
+                    (
+                        new Setter
+                        {
+                            Property = translateProperty,
+                            Value = forward ? -distance : distance
+                        }
+                    )
+                    {
+                        Cue = new Cue(1.0)
+                    }
+                };
+                animation.Duration = Duration;
+                tasks.Add(animation.RunAsync(from));
             }
 
             if (to != null)
             {
-
+                to.IsVisible = true;
+                var animation = new Animation
+                {
+
+                    new KeyFrame
+                    (
+                        new Setter
+                        {
+                            Property = translateProperty,
+                            Value = forward ? -distance : distance
+                        }
+                    )
+                    {
+                        Cue = new Cue(0.0)
+                    },
+                    new KeyFrame
+                    (
+                        new Setter
+                        {
+                            Property = translateProperty,
+                            Value = 0
+                        }
+                    )
+                    {
+                        Cue = new Cue(1.0)
+                    },
+                };
+                animation.Duration = Duration;
+                tasks.Add(animation.RunAsync(to));
             }
 
-            // FIXME: This is temporary until animations are fixed.
-            await Task.Delay(1);
+            await Task.WhenAll(tasks);
 
             if (from != null)
             {

+ 3 - 3
src/Avalonia.Visuals/Animation/TransformAnimator.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Animation
         DoubleAnimator childKeyFrames;
 
         /// <inheritdoc/>
-        public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
+        public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete)
         {
             var ctrl = (Visual)control;
 
@@ -51,7 +51,7 @@ namespace Avalonia.Animation
                 // It's a transform object so let's target that.
                 if (renderTransformType == Property.OwnerType)
                 {
-                    return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch);
+                    return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch, onComplete);
                 }
                 // It's a TransformGroup and try finding the target there.
                 else if (renderTransformType == typeof(TransformGroup))
@@ -60,7 +60,7 @@ namespace Avalonia.Animation
                     {
                         if (transform.GetType() == Property.OwnerType)
                         {
-                            return childKeyFrames.Apply(animation, transform, obsMatch);
+                            return childKeyFrames.Apply(animation, transform, obsMatch, onComplete);
                         }
                     }
                 }

+ 6 - 4
src/Avalonia.Visuals/Media/DrawingContext.cs

@@ -2,9 +2,10 @@ using System;
 using System.Collections.Generic;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
+using Avalonia.Visuals.Media.Imaging;
 
 namespace Avalonia.Media
-{
+{   
     public sealed class DrawingContext : IDisposable
     {
         private int _currentLevel;
@@ -68,11 +69,12 @@ namespace Avalonia.Media
         /// <param name="opacity">The opacity to draw with.</param>
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
-        public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect)
+        /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
+        public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default)
         {
             Contract.Requires<ArgumentNullException>(source != null);
 
-            PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect);
+            PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode);
         }
 
         /// <summary>
@@ -309,4 +311,4 @@ namespace Avalonia.Media
             return pen?.Brush != null && pen.Thickness > 0;
         }
     }
-}
+}

+ 16 - 3
src/Avalonia.Visuals/Media/ITileBrush.cs

@@ -1,5 +1,10 @@
-namespace Avalonia.Media
-{
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Visuals.Media.Imaging;
+
+namespace Avalonia.Media
+{  
     /// <summary>
     /// A brush which displays a repeating image.
     /// </summary>
@@ -35,5 +40,13 @@
         /// Gets the brush's tile mode.
         /// </summary>
         TileMode TileMode { get; }
+
+        /// <summary>
+        /// Gets the bitmap interpolation mode.
+        /// </summary>
+        /// <value>
+        /// The bitmap interpolation mode.
+        /// </value>
+        BitmapInterpolationMode BitmapInterpolationMode { get; }
     }
-}
+}

+ 31 - 0
src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs

@@ -0,0 +1,31 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Visuals.Media.Imaging
+{
+    /// <summary>
+    /// Controls the performance and quality of bitmap scaling.
+    /// </summary>
+    public enum BitmapInterpolationMode
+    {
+        /// <summary>
+        /// Uses the default behavior of the underling render backend.
+        /// </summary>
+        Default,
+
+        /// <summary>
+        /// The best performance but worst image quality.
+        /// </summary>
+        LowQuality,
+
+        /// <summary>
+        /// Good performance and decent image quality.
+        /// </summary>
+        MediumQuality,      
+
+        /// <summary>
+        /// Highest quality but worst performance.
+        /// </summary>
+        HighQuality
+    }
+}

+ 7 - 4
src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs

@@ -1,5 +1,5 @@
-using System;
-using Avalonia.Media.Imaging;
+using Avalonia.Media.Imaging;
+using Avalonia.Visuals.Media.Imaging;
 
 namespace Avalonia.Media.Immutable
 {
@@ -21,6 +21,7 @@ namespace Avalonia.Media.Immutable
         /// How the source rectangle will be stretched to fill the destination rect.
         /// </param>
         /// <param name="tileMode">The tile mode.</param>
+        /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
         public ImmutableImageBrush(
             IBitmap source,
             AlignmentX alignmentX = AlignmentX.Center,
@@ -29,7 +30,8 @@ namespace Avalonia.Media.Immutable
             double opacity = 1,
             RelativeRect? sourceRect = null,
             Stretch stretch = Stretch.Uniform,
-            TileMode tileMode = TileMode.None)
+            TileMode tileMode = TileMode.None,
+            BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
             : base(
                   alignmentX,
                   alignmentY,
@@ -37,7 +39,8 @@ namespace Avalonia.Media.Immutable
                   opacity,
                   sourceRect ?? RelativeRect.Fill,
                   stretch,
-                  tileMode)
+                  tileMode,
+                  bitmapInterpolationMode)
         {
             Source = source;
         }

+ 13 - 3
src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs

@@ -1,4 +1,7 @@
-using System;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Visuals.Media.Imaging;
 
 namespace Avalonia.Media.Immutable
 {
@@ -19,6 +22,7 @@ namespace Avalonia.Media.Immutable
         /// How the source rectangle will be stretched to fill the destination rect.
         /// </param>
         /// <param name="tileMode">The tile mode.</param>
+        /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
         protected ImmutableTileBrush(
             AlignmentX alignmentX,
             AlignmentY alignmentY,
@@ -26,7 +30,8 @@ namespace Avalonia.Media.Immutable
             double opacity,
             RelativeRect sourceRect,
             Stretch stretch,
-            TileMode tileMode)
+            TileMode tileMode,
+            BitmapInterpolationMode bitmapInterpolationMode)
         {
             AlignmentX = alignmentX;
             AlignmentY = alignmentY;
@@ -35,6 +40,7 @@ namespace Avalonia.Media.Immutable
             SourceRect = sourceRect;
             Stretch = stretch;
             TileMode = tileMode;
+            BitmapInterpolationMode = bitmapInterpolationMode;
         }
 
         /// <summary>
@@ -49,7 +55,8 @@ namespace Avalonia.Media.Immutable
                   source.Opacity,
                   source.SourceRect,
                   source.Stretch,
-                  source.TileMode)
+                  source.TileMode,
+                  source.BitmapInterpolationMode)
         {
         }
 
@@ -73,5 +80,8 @@ namespace Avalonia.Media.Immutable
 
         /// <inheritdoc/>
         public TileMode TileMode { get; }
+
+        /// <inheritdoc/>
+        public BitmapInterpolationMode BitmapInterpolationMode { get; }
     }
 }

+ 9 - 3
src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs

@@ -1,4 +1,7 @@
-using System;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Visuals.Media.Imaging;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Media.Immutable
@@ -21,6 +24,7 @@ namespace Avalonia.Media.Immutable
         /// How the source rectangle will be stretched to fill the destination rect.
         /// </param>
         /// <param name="tileMode">The tile mode.</param>
+        /// <param name="bitmapInterpolationMode">Controls the quality of interpolation.</param>
         public ImmutableVisualBrush(
             IVisual visual,
             AlignmentX alignmentX = AlignmentX.Center,
@@ -29,7 +33,8 @@ namespace Avalonia.Media.Immutable
             double opacity = 1,
             RelativeRect? sourceRect = null,
             Stretch stretch = Stretch.Uniform,
-            TileMode tileMode = TileMode.None)
+            TileMode tileMode = TileMode.None,
+            BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
             : base(
                   alignmentX,
                   alignmentY,
@@ -37,7 +42,8 @@ namespace Avalonia.Media.Immutable
                   opacity,
                   sourceRect ?? RelativeRect.Fill,
                   stretch,
-                  tileMode)
+                  tileMode,
+                  bitmapInterpolationMode)
         {
             Visual = visual;
         }

+ 39 - 0
src/Avalonia.Visuals/Media/RenderOptions.cs

@@ -0,0 +1,39 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Visuals.Media.Imaging;
+
+namespace Avalonia.Media
+{ 
+    public class RenderOptions
+    {
+        /// <summary>
+        /// Defines the <see cref="BitmapInterpolationMode"/> property.
+        /// </summary>
+        public static readonly StyledProperty<BitmapInterpolationMode> BitmapInterpolationModeProperty =
+            AvaloniaProperty.RegisterAttached<RenderOptions, AvaloniaObject, BitmapInterpolationMode>(
+                "BitmapInterpolationMode", 
+                BitmapInterpolationMode.MediumQuality,
+                inherits: true);
+
+        /// <summary>
+        /// Gets the value of the BitmapInterpolationMode attached property for a control.
+        /// </summary>
+        /// <param name="element">The control.</param>
+        /// <returns>The control's left coordinate.</returns>
+        public static BitmapInterpolationMode GetBitmapInterpolationMode(AvaloniaObject element)
+        {
+            return element.GetValue(BitmapInterpolationModeProperty);
+        }
+
+        /// <summary>
+        /// Sets the value of the BitmapInterpolationMode attached property for a control.
+        /// </summary>
+        /// <param name="element">The control.</param>
+        /// <param name="value">The left value.</param>
+        public static void SetBitmapInterpolationMode(AvaloniaObject element, BitmapInterpolationMode value)
+        {
+            element.SetValue(BitmapInterpolationModeProperty, value);
+        }
+    }
+}

+ 19 - 0
src/Avalonia.Visuals/Media/TileBrush.cs

@@ -1,6 +1,8 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Visuals.Media.Imaging;
+
 namespace Avalonia.Media
 {
     /// <summary>
@@ -75,6 +77,11 @@ namespace Avalonia.Media
         public static readonly StyledProperty<TileMode> TileModeProperty =
             AvaloniaProperty.Register<TileBrush, TileMode>(nameof(TileMode));
 
+        static TileBrush()
+        {
+            RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue<TileBrush>(BitmapInterpolationMode.Default);
+        }
+
         /// <summary>
         /// Gets or sets the horizontal alignment of a tile in the destination.
         /// </summary>
@@ -129,5 +136,17 @@ namespace Avalonia.Media
             get { return (TileMode)GetValue(TileModeProperty); }
             set { SetValue(TileModeProperty, value); }
         }
+
+        /// <summary>
+        /// Gets or sets the bitmap interpolation mode.
+        /// </summary>
+        /// <value>
+        /// The bitmap interpolation mode.
+        /// </value>
+        public BitmapInterpolationMode BitmapInterpolationMode
+        {
+            get { return RenderOptions.GetBitmapInterpolationMode(this); }
+            set { RenderOptions.SetBitmapInterpolationMode(this, value); }
+        }
     }
 }

+ 3 - 1
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@@ -4,6 +4,7 @@
 using System;
 using Avalonia.Media;
 using Avalonia.Utilities;
+using Avalonia.Visuals.Media.Imaging;
 
 namespace Avalonia.Platform
 {
@@ -30,7 +31,8 @@ namespace Avalonia.Platform
         /// <param name="opacity">The opacity to draw with.</param>
         /// <param name="sourceRect">The rect in the image to draw.</param>
         /// <param name="destRect">The rect in the output to draw to.</param>
-        void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect);
+        /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
+        void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
 
         /// <summary>
         /// Draws a bitmap image.

+ 4 - 3
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -7,6 +7,7 @@ using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Utilities;
 using Avalonia.VisualTree;
+using Avalonia.Visuals.Media.Imaging;
 
 namespace Avalonia.Rendering.SceneGraph
 {
@@ -114,13 +115,13 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
+        public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
         {
             var next = NextDrawAs<ImageNode>();
 
-            if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect))
+            if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode))
             {
-                Add(new ImageNode(Transform, source, opacity, sourceRect, destRect));
+                Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode));
             }
             else
             {

+ 18 - 5
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

@@ -4,6 +4,7 @@
 using System;
 using Avalonia.Platform;
 using Avalonia.Utilities;
+using Avalonia.Visuals.Media.Imaging;
 
 namespace Avalonia.Rendering.SceneGraph
 {
@@ -20,7 +21,8 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="opacity">The draw opacity.</param>
         /// <param name="sourceRect">The source rect.</param>
         /// <param name="destRect">The destination rect.</param>
-        public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
+        /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
+        public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
             : base(destRect, transform, null)
         {
             Transform = transform;
@@ -28,7 +30,8 @@ namespace Avalonia.Rendering.SceneGraph
             Opacity = opacity;
             SourceRect = sourceRect;
             DestRect = destRect;
-        }
+            BitmapInterpolationMode = bitmapInterpolationMode;
+        }        
 
         /// <summary>
         /// Gets the transform with which the node will be drawn.
@@ -55,6 +58,14 @@ namespace Avalonia.Rendering.SceneGraph
         /// </summary>
         public Rect DestRect { get; }
 
+        /// <summary>
+        /// Gets the bitmap interpolation mode.
+        /// </summary>
+        /// <value>
+        /// The scaling mode.
+        /// </value>
+        public BitmapInterpolationMode BitmapInterpolationMode { get; }
+
         /// <summary>
         /// Determines if this draw operation equals another.
         /// </summary>
@@ -63,18 +74,20 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="opacity">The opacity of the other draw operation.</param>
         /// <param name="sourceRect">The source rect of the other draw operation.</param>
         /// <param name="destRect">The dest rect of the other draw operation.</param>
+        /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
         /// <returns>True if the draw operations are the same, otherwise false.</returns>
         /// <remarks>
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
-        public bool Equals(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
+        public bool Equals(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
         {
             return transform == Transform &&
                 Equals(source.Item, Source.Item) &&
                 opacity == Opacity &&
                 sourceRect == SourceRect &&
-                destRect == DestRect;
+                destRect == DestRect &&
+                bitmapInterpolationMode == BitmapInterpolationMode;
         }
 
         /// <inheritdoc/>
@@ -83,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph
             // TODO: Probably need to introduce some kind of locking mechanism in the case of
             // WriteableBitmap.
             context.Transform = Transform;
-            context.DrawImage(Source, Opacity, SourceRect, DestRect);
+            context.DrawImage(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode);
         }
 
         /// <inheritdoc/>

+ 7 - 0
src/Gtk/Avalonia.Gtk3/KeyTransform.cs

@@ -33,17 +33,24 @@ namespace Avalonia.Gtk.Common
             { GdkKey.Prior, Key.Prior },
             //{ GdkKey.?, Key.PageDown }
             { GdkKey.End, Key.End },
+            { GdkKey.KP_End, Key.End },
             { GdkKey.Home, Key.Home },
+            { GdkKey.KP_Home, Key.Home },
             { GdkKey.Left, Key.Left },
+            { GdkKey.KP_Left, Key.Left },
             { GdkKey.Up, Key.Up },
+            { GdkKey.KP_Up, Key.Up },
             { GdkKey.Right, Key.Right },
+            { GdkKey.KP_Right, Key.Right },
             { GdkKey.Down, Key.Down },
+            { GdkKey.KP_Down, Key.Down },
             { GdkKey.Select, Key.Select },
             { GdkKey.Print, Key.Print },
             { GdkKey.Execute, Key.Execute },
             //{ GdkKey.?, Key.Snapshot }
             { GdkKey.Insert, Key.Insert },
             { GdkKey.Delete, Key.Delete },
+            { GdkKey.KP_Delete, Key.Delete },
             { GdkKey.Help, Key.Help },
             //{ GdkKey.?, Key.D0 }
             //{ GdkKey.?, Key.D1 }

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -33,6 +33,7 @@
         <Compile Include="MarkupExtensions\ResourceInclude.cs" />
         <Compile Include="MarkupExtensions\StaticResourceExtension.cs" />
         <Compile Include="MarkupExtensions\StyleIncludeExtension.cs" />
+        <Compile Include="Parsers\PropertyParser.cs" />
         <Compile Include="PortableXaml\AvaloniaXamlContext.cs" />
         <Compile Include="PortableXaml\AttributeExtensions.cs" />
         <Compile Include="PortableXaml\AvaloniaMemberAttributeProvider.cs" />

+ 31 - 37
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs

@@ -4,19 +4,19 @@
 using System;
 using System.ComponentModel;
 using System.Globalization;
-using System.Text.RegularExpressions;
+using Avalonia.Controls;
+using Avalonia.Logging;
+using Avalonia.Markup.Parsers;
+using Avalonia.Markup.Xaml.Parsers;
 using Avalonia.Markup.Xaml.Templates;
 using Avalonia.Styling;
-using Portable.Xaml;
+using Avalonia.Utilities;
 using Portable.Xaml.ComponentModel;
-using Portable.Xaml.Markup;
 
 namespace Avalonia.Markup.Xaml.Converters
 {
     public class AvaloniaPropertyTypeConverter : TypeConverter
     {
-        private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$");
-
         public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
         {
             return sourceType == typeof(string);
@@ -24,39 +24,47 @@ namespace Avalonia.Markup.Xaml.Converters
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
-            var (owner, propertyName) = ParseProperty((string)value);
-            var ownerType = TryResolveOwnerByName(context, owner) ??
-                context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
-                context.GetFirstAmbientValue<Style>()?.Selector?.TargetType;
+            var registry = AvaloniaPropertyRegistry.Instance;
+            var parser = new PropertyParser();
+            var (ns, owner, propertyName) = parser.Parse(new CharacterReader(((string)value).AsSpan()));
+            var ownerType = TryResolveOwnerByName(context, ns, owner);
+            var targetType = context.GetFirstAmbientValue<ControlTemplate>()?.TargetType ??
+                context.GetFirstAmbientValue<Style>()?.Selector?.TargetType ??
+                typeof(Control);
+            var effectiveOwner = ownerType ?? targetType;
+            var property = registry.FindRegistered(effectiveOwner, propertyName);
 
-            if (ownerType == null)
+            if (property == null)
             {
-                throw new XamlLoadException(
-                    $"Could not determine the owner type for property '{propertyName}'. " +
-                    "Please fully qualify the property name or specify a target type on " +
-                    "the containing template.");
+                throw new XamlLoadException($"Could not find property '{effectiveOwner.Name}.{propertyName}'.");
             }
 
-            var property = AvaloniaPropertyRegistry.Instance.FindRegistered(ownerType, propertyName);
-
-            if (property == null)
+            if (effectiveOwner != targetType &&
+                !property.IsAttached &&
+                !registry.IsRegistered(targetType, property))
             {
-                throw new XamlLoadException($"Could not find AvaloniaProperty '{ownerType.Name}.{propertyName}'.");
+                Logger.Warning(
+                    LogArea.Property,
+                    this,
+                    "Property '{Owner}.{Name}' is not registered on '{Type}'.",
+                    effectiveOwner,
+                    propertyName,
+                    targetType);
             }
 
             return property;
         }
 
-        private Type TryResolveOwnerByName(ITypeDescriptorContext context, string owner)
+        private Type TryResolveOwnerByName(ITypeDescriptorContext context, string ns, string owner)
         {
             if (owner != null)
             {
-                var resolver = context.GetService<IXamlTypeResolver>();
-                var result = resolver.Resolve(owner);
+                var result = context.ResolveType(ns, owner);
 
                 if (result == null)
                 {
-                    throw new XamlLoadException($"Could not find type '{owner}'.");
+                    var name = string.IsNullOrEmpty(ns) ? owner : $"{ns}:{owner}";
+                    throw new XamlLoadException($"Could not find type '{name}'.");
                 }
 
                 return result;
@@ -64,19 +72,5 @@ namespace Avalonia.Markup.Xaml.Converters
 
             return null;
         }
-
-        private (string owner, string property) ParseProperty(string s)
-        {
-            var result = regex.Match(s);
-
-            if (result.Groups[1].Success)
-            {
-                return (result.Groups[1].Value, result.Groups[2].Value);
-            }
-            else
-            {
-                return (null, result.Groups[3].Value);
-            }
-        }
     }
-}
+}

+ 10 - 155
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@@ -32,176 +32,29 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
         {
             var descriptorContext = (ITypeDescriptorContext)serviceProvider;
 
-            var pathInfo = ParsePath(Path, descriptorContext);
-            ValidateState(pathInfo);
-
             return new Binding
             {
                 TypeResolver = descriptorContext.ResolveType,
                 Converter = Converter,
                 ConverterParameter = ConverterParameter,
-                ElementName = pathInfo.ElementName ?? ElementName,
+                ElementName = ElementName,
                 FallbackValue = FallbackValue,
                 Mode = Mode,
-                Path = pathInfo.Path,
+                Path = Path,
                 Priority = Priority,
                 Source = Source,
-                RelativeSource = pathInfo.RelativeSource ?? RelativeSource,
-                DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
+                StringFormat = StringFormat,
+                RelativeSource = RelativeSource,
+                DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext))
             };
         }
 
-        private class PathInfo
-        {
-            public string Path { get; set; }
-            public string ElementName { get; set; }
-            public RelativeSource RelativeSource { get; set; }
-        }
-
-        private void ValidateState(PathInfo pathInfo)
-        {
-            if (pathInfo.ElementName != null && ElementName != null)
-            {
-                throw new InvalidOperationException(
-                    "ElementName property cannot be set when an #elementName path is provided.");
-            }
-
-            if (pathInfo.RelativeSource != null && RelativeSource != null)
-            {
-                throw new InvalidOperationException(
-                    "ElementName property cannot be set when a $self or $parent path is provided.");
-            }
-
-            if ((pathInfo.ElementName != null || ElementName != null) &&
-                (pathInfo.RelativeSource != null || RelativeSource != null))
-            {
-                throw new InvalidOperationException(
-                    "ElementName property cannot be set with a RelativeSource.");
-            }
-        }
-
-        private static PathInfo ParsePath(string path, ITypeDescriptorContext context)
-        {
-            var result = new PathInfo();
-
-            if (string.IsNullOrWhiteSpace(path) || path == ".")
-            {
-                result.Path = string.Empty;
-            }
-            else if (path.StartsWith("#"))
-            {
-                var dot = path.IndexOf('.');
-
-                if (dot != -1)
-                {
-                    result.Path = path.Substring(dot + 1);
-                    result.ElementName = path.Substring(1, dot - 1);
-                }
-                else
-                {
-                    result.Path = string.Empty;
-                    result.ElementName = path.Substring(1);
-                }
-            }
-            else if (path.StartsWith("$"))
-            {
-                var relativeSource = new RelativeSource
-                {
-                    Tree = TreeType.Logical
-                };
-                result.RelativeSource = relativeSource;
-                var dot = path.IndexOf('.');
-                string relativeSourceMode;
-                if (dot != -1)
-                {
-                    result.Path = path.Substring(dot + 1);
-                    relativeSourceMode = path.Substring(1, dot - 1);
-                }
-                else
-                {
-                    result.Path = string.Empty;
-                    relativeSourceMode = path.Substring(1);
-                }
-
-                if (relativeSourceMode == "self")
-                {
-                    relativeSource.Mode = RelativeSourceMode.Self;
-                }
-                else if (relativeSourceMode == "parent")
-                {
-                    relativeSource.Mode = RelativeSourceMode.FindAncestor;
-                    relativeSource.AncestorLevel = 1;
-                }
-                else if (relativeSourceMode.StartsWith("parent["))
-                {
-                    relativeSource.Mode = RelativeSourceMode.FindAncestor;
-                    var parentConfigStart = relativeSourceMode.IndexOf('[');
-                    if (!relativeSourceMode.EndsWith("]"))
-                    {
-                        throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['.");
-                    }
-                    var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(';');
-                    if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0)
-                    {
-                        throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax");
-                    }
-                    else if (parentConfigParams.Length == 1)
-                    {
-                        if (int.TryParse(parentConfigParams[0], out int level))
-                        {
-                            relativeSource.AncestorType = null;
-                            relativeSource.AncestorLevel = level + 1;
-                        }
-                        else
-                        {
-                            relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
-                        }
-                    }
-                    else
-                    {
-                        relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
-                        relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1;
-                    }
-                }
-                else
-                {
-                    throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}");
-                }
-            }
-            else
-            {
-                result.Path = path;
-            }
-
-            return result;
-        }
-
-        private static Type LookupAncestorType(string ancestorTypeName, ITypeDescriptorContext context)
-        {
-            var parts = ancestorTypeName.Split(':');
-            if (parts.Length == 0 || parts.Length > 2)
-            {
-                throw new InvalidOperationException("Invalid type name");
-            }
-
-            if (parts.Length == 1)
-            {
-                return context.ResolveType(string.Empty, parts[0]);
-            }
-            else
-            {
-                return context.ResolveType(parts[0], parts[1]);
-            }
-        }
-
         private static object GetDefaultAnchor(ITypeDescriptorContext context)
         {
-            object anchor = null;
-
-            // The target is not a control, so we need to find an anchor that will let us look
+            // If the target is not a control, so we need to find an anchor that will let us look
             // up named controls and style resources. First look for the closest IControl in
             // the context.
-            anchor = context.GetFirstAmbientValue<IControl>();
+            object anchor = context.GetFirstAmbientValue<IControl>();
 
             // If a control was not found, then try to find the highest-level style as the XAML
             // file could be a XAML file containing only styles.
@@ -227,6 +80,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
 
         public object Source { get; set; }
 
+        public string StringFormat { get; set; }
+
         public RelativeSource RelativeSource { get; set; }
     }
-}
+}

+ 85 - 0
src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs

@@ -0,0 +1,85 @@
+using System;
+using Avalonia.Data.Core;
+using Avalonia.Markup.Parsers;
+using Avalonia.Utilities;
+
+namespace Avalonia.Markup.Xaml.Parsers
+{
+    internal class PropertyParser
+    {
+        public (string ns, string owner, string name) Parse(CharacterReader r)
+        {
+            if (r.End)
+            {
+                throw new ExpressionParseException(0, "Expected property name.");
+            }
+
+            var openParens = r.TakeIf('(');
+            bool closeParens = false;
+            string ns = null;
+            string owner = null;
+            string name = null;
+
+            do
+            {
+                var token = r.ParseIdentifier();
+
+                if (token == null)
+                {
+                    if (r.End)
+                    {
+                        break;
+                    }
+                    else
+                    {
+                        if (openParens && !r.End && (closeParens = r.TakeIf(')')))
+                        {
+                            break;
+                        }
+                        else if (openParens)
+                        {
+                            throw new ExpressionParseException(r.Position, $"Expected ')'.");
+                        }
+
+                        throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'.");
+                    }
+                }
+                else if (!r.End && r.TakeIf(':'))
+                {
+                    ns = ns == null ?
+                        token.ToString() :
+                        throw new ExpressionParseException(r.Position, "Unexpected ':'.");
+                }
+                else if (!r.End && r.TakeIf('.'))
+                {
+                    owner = owner == null ?
+                        token.ToString() :
+                        throw new ExpressionParseException(r.Position, "Unexpected '.'.");
+                }
+                else
+                {
+                    name = token.ToString();
+                }
+            } while (!r.End);
+
+            if (name == null)
+            {
+                throw new ExpressionParseException(0, "Expected property name.");
+            }
+            else if (openParens && owner == null)
+            {
+                throw new ExpressionParseException(1, "Expected property owner.");
+            }
+            else if (openParens && !closeParens)
+            {
+                throw new ExpressionParseException(r.Position, "Expected ')'.");
+            }
+            else if (!r.End)
+            {
+                throw new ExpressionParseException(r.Position, "Expected end of expression.");
+            }
+
+            return (ns, owner, name);
+        }
+    }
+}

+ 2 - 2
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs

@@ -7,7 +7,6 @@ using System.Linq;
 using System.Reflection;
 using Avalonia.Controls;
 using Avalonia.Data;
-using Avalonia.Markup.Data;
 using Avalonia.Markup.Xaml.Templates;
 using Avalonia.Media;
 using Avalonia.Metadata;
@@ -33,6 +32,7 @@ namespace Avalonia.Markup.Xaml.Context
         private static readonly IEnumerable<Assembly> ForcedAssemblies = new[]
         {
             typeof(AvaloniaObject).GetTypeInfo().Assembly,
+            typeof(Animation.Animation).GetTypeInfo().Assembly,
             typeof(Control).GetTypeInfo().Assembly,
             typeof(Style).GetTypeInfo().Assembly,
             typeof(DataTemplate).GetTypeInfo().Assembly,
@@ -146,4 +146,4 @@ namespace Avalonia.Markup.Xaml.Context
             return null;
         }
     }
-}
+}

+ 1 - 1
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@@ -10,4 +10,4 @@
   </ItemGroup>
   <Import Project="..\..\..\build\Rx.props" />
   <Import Project="..\..\..\build\System.Memory.props" />
-</Project>
+</Project>

+ 71 - 48
src/Markup/Avalonia.Markup/Data/Binding.cs

@@ -84,6 +84,11 @@ namespace Avalonia.Data
         /// </summary>
         public object Source { get; set; }
 
+        /// <summary>
+        /// Gets or sets the string format.
+        /// </summary>
+        public string StringFormat { get; set; }
+
         public WeakReference DefaultAnchor { get; set; }
 
         /// <summary>
@@ -105,34 +110,53 @@ namespace Avalonia.Data
             
             ExpressionObserver observer;
 
+            var (node, mode)  = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver);
+
             if (ElementName != null)
             {
                 observer = CreateElementObserver(
                     (target as IStyledElement) ?? (anchor as IStyledElement),
                     ElementName,
-                    Path,
-                    enableDataValidation);
+                    node);
             }
             else if (Source != null)
             {
-                observer = CreateSourceObserver(Source, Path, enableDataValidation);
+                observer = CreateSourceObserver(Source, node);
             }
-            else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
+            else if (RelativeSource == null)
+            {
+                if (mode == SourceMode.Data)
+                {
+                    observer = CreateDataContextObserver(
+                        target,
+                        node,
+                        targetProperty == StyledElement.DataContextProperty,
+                        anchor); 
+                }
+                else
+                {
+                    observer = new ExpressionObserver(
+                        (target as IStyledElement) ?? (anchor as IStyledElement),
+                        node);
+                }
+            }
+            else if (RelativeSource.Mode == RelativeSourceMode.DataContext)
             {
                 observer = CreateDataContextObserver(
                     target,
-                    Path,
+                    node,
                     targetProperty == StyledElement.DataContextProperty,
-                    anchor,
-                    enableDataValidation);
+                    anchor);
             }
             else if (RelativeSource.Mode == RelativeSourceMode.Self)
             {
-                observer = CreateSourceObserver(target, Path, enableDataValidation);
+                observer = CreateSourceObserver(target, node);
             }
             else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
             {
-                observer = CreateTemplatedParentObserver(target, Path, enableDataValidation);
+                observer = CreateTemplatedParentObserver(
+                    (target as IStyledElement) ?? (anchor as IStyledElement),
+                    node);
             }
             else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
             {
@@ -144,8 +168,7 @@ namespace Avalonia.Data
                 observer = CreateFindAncestorObserver(
                     (target as IStyledElement) ?? (anchor as IStyledElement),
                     RelativeSource,
-                    Path,
-                    enableDataValidation);
+                    node);
             }
             else
             {
@@ -163,11 +186,23 @@ namespace Avalonia.Data
                 fallback = null;
             }
 
+            var converter = Converter;
+            var targetType = targetProperty?.PropertyType ?? typeof(object);
+
+            // We only respect `StringFormat` if the type of the property we're assigning to will
+            // accept a string. Note that this is slightly different to WPF in that WPF only applies
+            // `StringFormat` for target type `string` (not `object`).
+            if (!string.IsNullOrWhiteSpace(StringFormat) && 
+                (targetType == typeof(string) || targetType == typeof(object)))
+            {
+                converter = new StringFormatValueConverter(StringFormat, converter);
+            }
+
             var subject = new BindingExpression(
                 observer,
-                targetProperty?.PropertyType ?? typeof(object),
+                targetType,
                 fallback,
-                Converter ?? DefaultValueConverter.Instance,
+                converter ?? DefaultValueConverter.Instance,
                 ConverterParameter,
                 Priority);
 
@@ -176,10 +211,9 @@ namespace Avalonia.Data
 
         private ExpressionObserver CreateDataContextObserver(
             IAvaloniaObject target,
-            string path,
+            ExpressionNode node,
             bool targetIsDataContext,
-            object anchor,
-            bool enableDataValidation)
+            object anchor)
         {
             Contract.Requires<ArgumentNullException>(target != null);
 
@@ -195,48 +229,41 @@ namespace Avalonia.Data
 
             if (!targetIsDataContext)
             {
-                var result = ExpressionObserverBuilder.Build(
+                var result = new ExpressionObserver(
                     () => target.GetValue(StyledElement.DataContextProperty),
-                    path,
+                    node,
                     new UpdateSignal(target, StyledElement.DataContextProperty),
-                    enableDataValidation,
-                    typeResolver: TypeResolver);
+                    null);
 
                 return result;
             }
             else
             {
-                return ExpressionObserverBuilder.Build(
+                return new ExpressionObserver(
                     GetParentDataContext(target),
-                    path,
-                    enableDataValidation,
-                    typeResolver: TypeResolver);
+                    node,
+                    null);
             }
         }
 
         private ExpressionObserver CreateElementObserver(
             IStyledElement target,
             string elementName,
-            string path,
-            bool enableDataValidation)
+            ExpressionNode node)
         {
             Contract.Requires<ArgumentNullException>(target != null);
-
-            var description = $"#{elementName}.{path}";
-            var result = ExpressionObserverBuilder.Build(
+            
+            var result = new ExpressionObserver(
                 ControlLocator.Track(target, elementName),
-                path,
-                enableDataValidation,
-                description,
-                typeResolver: TypeResolver);
+                node,
+                null);
             return result;
         }
 
         private ExpressionObserver CreateFindAncestorObserver(
             IStyledElement target,
             RelativeSource relativeSource,
-            string path,
-            bool enableDataValidation)
+            ExpressionNode node)
         {
             Contract.Requires<ArgumentNullException>(target != null);
 
@@ -260,36 +287,32 @@ namespace Avalonia.Data
                     throw new InvalidOperationException("Invalid tree to traverse.");
             }
 
-            return ExpressionObserverBuilder.Build(
+            return new ExpressionObserver(
                 controlLocator,
-                path,
-                enableDataValidation,
-                typeResolver: TypeResolver);
+                node,
+                null);
         }
 
         private ExpressionObserver CreateSourceObserver(
             object source,
-            string path,
-            bool enableDataValidation)
+            ExpressionNode node)
         {
             Contract.Requires<ArgumentNullException>(source != null);
 
-            return ExpressionObserverBuilder.Build(source, path, enableDataValidation, typeResolver: TypeResolver);
+            return new ExpressionObserver(source, node);
         }
 
         private ExpressionObserver CreateTemplatedParentObserver(
             IAvaloniaObject target,
-            string path,
-            bool enableDataValidation)
+            ExpressionNode node)
         {
             Contract.Requires<ArgumentNullException>(target != null);
             
-            var result = ExpressionObserverBuilder.Build(
+            var result = new ExpressionObserver(
                 () => target.GetValue(StyledElement.TemplatedParentProperty),
-                path,
+                node,
                 new UpdateSignal(target, StyledElement.TemplatedParentProperty),
-                enableDataValidation,
-                typeResolver: TypeResolver);
+                null);
 
             return result;
         }

+ 4 - 2
src/Markup/Avalonia.Markup/Data/TemplateBinding.cs

@@ -100,7 +100,9 @@ namespace Avalonia.Data
                         CultureInfo.CurrentCulture);
                 }
 
-                _target.TemplatedParent.SetValue(Property, value, BindingPriority.TemplatedParent);
+                // Use LocalValue priority here, as TemplatedParent doesn't make sense on controls
+                // that aren't template children.
+                _target.TemplatedParent.SetValue(Property, value, BindingPriority.LocalValue);
             }
         }
 
@@ -171,7 +173,7 @@ namespace Avalonia.Data
         {
             if (e.Property == Property)
             {
-                PublishNext(_target.TemplatedParent.GetValue(Property));
+                PublishValue();
             }
         }
     }

+ 6 - 5
src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using Avalonia.Data.Core;
+using Avalonia.Utilities;
 using System;
 using System.Collections.Generic;
 using System.Text;
@@ -10,7 +11,7 @@ namespace Avalonia.Markup.Parsers
 {
     internal static class ArgumentListParser
     {
-        public static IList<string> ParseArguments(this ref Reader r, char open, char close)
+        public static IList<string> ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',')
         {
             if (r.Peek == open)
             {
@@ -20,7 +21,7 @@ namespace Avalonia.Markup.Parsers
 
                 while (!r.End)
                 {
-                    var argument = r.TakeWhile(c => c != ',' && c != close);
+                    var argument = r.TakeWhile(c => c != delimiter && c != close && !char.IsWhiteSpace(c));
                     if (argument.IsEmpty)
                     {
                         throw new ExpressionParseException(r.Position, "Expected indexer argument.");
@@ -32,7 +33,7 @@ namespace Avalonia.Markup.Parsers
 
                     if (r.End)
                     {
-                        throw new ExpressionParseException(r.Position, "Expected ','.");
+                        throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
                     }
                     else if (r.TakeIf(close))
                     {
@@ -40,9 +41,9 @@ namespace Avalonia.Markup.Parsers
                     }
                     else
                     {
-                        if (r.Take() != ',')
+                        if (r.Take() != delimiter)
                         {
-                            throw new ExpressionParseException(r.Position, "Expected ','.");
+                            throw new ExpressionParseException(r.Position, $"Expected '{delimiter}'.");
                         }
 
                         r.SkipWhitespace();

Vissa filer visades inte eftersom för många filer har ändrats