Browse Source

Merge branch 'master' into scenegraph

 Conflicts:
	src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
	src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs
	src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
	src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
	src/Windows/Avalonia.Direct2D1/RenderTarget.cs
Steven Kirk 8 years ago
parent
commit
7ce58c92aa
49 changed files with 860 additions and 330 deletions
  1. 1 0
      .gitignore
  2. 20 1
      Avalonia.sln.DotSettings
  3. 5 1
      samples/ControlCatalog/Pages/ImagePage.xaml
  4. 23 0
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  5. 2 4
      src/Android/Avalonia.Android/PlatformIconLoader.cs
  6. 11 7
      src/Avalonia.Base/DirectProperty.cs
  7. 7 2
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  8. 1 1
      src/Avalonia.Base/IDirectPropertyMetadata.cs
  9. 17 0
      src/Avalonia.Controls/Control.cs
  10. 5 0
      src/Avalonia.Controls/IVirtualizingPanel.cs
  11. 1 1
      src/Avalonia.Controls/Platform/IWindowIconImpl.cs
  12. 4 0
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  13. 25 2
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  14. 7 4
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  15. 0 4
      src/Avalonia.Controls/ScrollViewer.cs
  16. 25 2
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  17. 1 1
      src/Avalonia.Controls/WindowIcon.cs
  18. 31 0
      src/Avalonia.Styling/Controls/NameScope.cs
  19. 3 2
      src/Gtk/Avalonia.Gtk/IconImpl.cs
  20. 4 0
      src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
  21. 1 0
      src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs
  22. 5 11
      src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
  23. 1 1
      src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs
  24. 13 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  25. 37 4
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  26. 11 141
      src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs
  27. 4 5
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  28. 12 118
      src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs
  29. 57 0
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs
  30. 1 1
      src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs
  31. 130 0
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs
  32. 136 0
      src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs
  33. 3 5
      src/Windows/Avalonia.Win32/IconImpl.cs
  34. 7 0
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  35. 4 1
      src/Windows/Avalonia.Win32/WindowImpl.cs
  36. 3 5
      src/iOS/Avalonia.iOS/PlatformIconLoader.cs
  37. 29 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  38. 1 1
      tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs
  39. 18 0
      tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs
  40. 18 0
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  41. 16 0
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs
  42. 2 3
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
  43. 18 0
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs
  44. 1 0
      tests/Avalonia.LeakTests/AvaloniaObjectTests.cs
  45. 8 0
      tests/Avalonia.LeakTests/MemberSelectorTests.cs
  46. 23 1
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs
  47. 1 0
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  48. 63 0
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Self.cs
  49. 44 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

+ 1 - 0
.gitignore

@@ -9,6 +9,7 @@
 *.suo
 *.user
 *.sln.docstates
+.vs/
 
 # Build results
 

+ 20 - 1
Avalonia.sln.DotSettings

@@ -2,6 +2,24 @@
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=6417B24E_002D49C2_002D4985_002D8DB2_002D3AB9D898EC91/@EntryIndexedValue">ExplicitlyExcluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=E3A1060B_002D50D0_002D44E8_002D88B6_002DF44EF2E5BD72_002Ff_003Ahtml_002Ehtm/@EntryIndexedValue">ExplicitlyExcluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective/@EntryIndexedValue">HINT</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=DECLSPEC_005FPROPERTY/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=ENUM/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=ENUMERATOR/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=GETTER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=GLOBAL_005FFUNCTION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=GLOBAL_005FVARIABLE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=LOCAL_005FTYPEDEF/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=LOCAL_005FVARIABLE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=NAMESPACE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=PARAMETER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=SETTER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="set_" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=STRUCT/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=STRUCT_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="_" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=STRUCT_005FMETHODS/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=TEMPLATE_005FPARAMETER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=TYPEDEF/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION_005FMEMBER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=EnumMember/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Interfaces/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="I" Suffix="" Style="AaBb" /&gt;</s:String>
@@ -10,8 +28,9 @@
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Other/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Parameters/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /&gt;</s:String>
-	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="s_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=StaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypeParameters/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String></wpf:ResourceDictionary>

+ 5 - 1
samples/ControlCatalog/Pages/ImagePage.xaml

@@ -34,6 +34,10 @@
                Width="100" Height="200"
                Stretch="UniformToFill"/>
       </StackPanel>
-    </StackPanel>    
+    </StackPanel>
+    <StackPanel Orientation="Vertical">
+      <TextBlock>Window Icon as an Image</TextBlock>
+      <Image Name="Icon" Width="100" Height="200" Stretch="None" />
+    </StackPanel>
   </StackPanel>
 </UserControl>

+ 23 - 0
samples/ControlCatalog/Pages/ImagePage.xaml.cs

@@ -1,10 +1,14 @@
+using System.IO;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using Avalonia.Media.Imaging;
 
 namespace ControlCatalog.Pages
 {
     public class ImagePage : UserControl
     {
+        private Image iconImage;
         public ImagePage()
         {
             this.InitializeComponent();
@@ -13,6 +17,25 @@ namespace ControlCatalog.Pages
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
+            iconImage = this.Get<Image>("Icon");
+        }
+
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+            if (iconImage.Source == null)
+            {
+                var windowRoot = e.Root as Window;
+                if (windowRoot != null)
+                {
+                    using (var stream = new MemoryStream())
+                    {
+                        windowRoot.Icon.Save(stream);
+                        stream.Seek(0, SeekOrigin.Begin);
+                        iconImage.Source = new Bitmap(stream);
+                    }
+                }
+            }
         }
     }
 }

+ 2 - 4
src/Android/Avalonia.Android/PlatformIconLoader.cs

@@ -49,11 +49,9 @@ namespace Avalonia.Android
             stream.CopyTo(this.stream);
         }
 
-        public Stream Save()
+        public void Save(Stream outputStream)
         {
-            var returnStream = new MemoryStream();
-            stream.CopyTo(returnStream);
-            return returnStream;
+            stream.CopyTo(outputStream);
         }
     }
 }

+ 11 - 7
src/Avalonia.Base/DirectProperty.cs

@@ -30,7 +30,7 @@ namespace Avalonia
             string name,
             Func<TOwner, TValue> getter,
             Action<TOwner, TValue> setter,
-            PropertyMetadata metadata)
+            DirectPropertyMetadata<TValue> metadata)
             : base(name, typeof(TOwner), metadata)
         {
             Contract.Requires<ArgumentNullException>(getter != null);
@@ -50,7 +50,7 @@ namespace Avalonia
             AvaloniaProperty<TValue> source,
             Func<TOwner, TValue> getter,
             Action<TOwner, TValue> setter,
-            PropertyMetadata metadata)
+            DirectPropertyMetadata<TValue> metadata)
             : base(source, typeof(TOwner), metadata)
         {
             Contract.Requires<ArgumentNullException>(getter != null);
@@ -93,18 +93,22 @@ namespace Avalonia
             Func<TNewOwner, TValue> getter,
             Action<TNewOwner, TValue> setter = null,
             TValue unsetValue = default(TValue),
-            BindingMode defaultBindingMode = BindingMode.OneWay,
+            BindingMode defaultBindingMode = BindingMode.Default,
             bool enableDataValidation = false)
                 where TNewOwner : AvaloniaObject
         {
+            var metadata = new DirectPropertyMetadata<TValue>(
+                unsetValue: unsetValue,
+                defaultBindingMode: defaultBindingMode,
+                enableDataValidation: enableDataValidation);
+
+            metadata.Merge(GetMetadata<TOwner>(), this);
+
             var result = new DirectProperty<TNewOwner, TValue>(
                 this,
                 getter,
                 setter,
-                new DirectPropertyMetadata<TValue>(
-                    unsetValue: unsetValue,
-                    defaultBindingMode: defaultBindingMode,
-                    enableDataValidation: enableDataValidation));
+                metadata);
 
             AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
             return result;

+ 7 - 2
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@@ -23,7 +23,7 @@ namespace Avalonia
         public DirectPropertyMetadata(
             TValue unsetValue = default(TValue),
             BindingMode defaultBindingMode = BindingMode.Default,
-            bool enableDataValidation = false)
+            bool? enableDataValidation = null)
                 : base(defaultBindingMode)
         {
             UnsetValue = unsetValue;
@@ -44,7 +44,7 @@ namespace Avalonia
         /// control (such as a TextBox's Text property) will be interested in recieving data
         /// validation messages so this feature must be explicitly enabled by setting this flag.
         /// </remarks>
-        public bool EnableDataValidation { get; }
+        public bool? EnableDataValidation { get; private set; }
 
         /// <inheritdoc/>
         object IDirectPropertyMetadata.UnsetValue => UnsetValue;
@@ -62,6 +62,11 @@ namespace Avalonia
                 {
                     UnsetValue = src.UnsetValue;
                 }
+
+                if (EnableDataValidation == null)
+                {
+                    EnableDataValidation = src.EnableDataValidation;
+                }
             }
         }
     }

+ 1 - 1
src/Avalonia.Base/IDirectPropertyMetadata.cs

@@ -16,6 +16,6 @@ namespace Avalonia
         /// <summary>
         /// Gets a value indicating whether the property is interested in data validation.
         /// </summary>
-        bool EnableDataValidation { get; }
+        bool? EnableDataValidation { get; }
     }
 }

+ 17 - 0
src/Avalonia.Controls/Control.cs

@@ -671,6 +671,23 @@ namespace Avalonia.Controls
             if (Name != null)
             {
                 _nameScope?.Register(Name, this);
+
+                var visualParent = Parent as Visual;
+
+                if (this is INameScope && visualParent != null)
+                {
+                    // If we have e.g. a named UserControl in a window then we want that control
+                    // to be findable by name from the Window, so register with both name scopes.
+                    // This differs from WPF's behavior in that XAML manually registers controls 
+                    // with name scopes based on the XAML file in which the name attribute appears,
+                    // but we're trying to avoid XAML magic in Avalonia in order to made code-
+                    // created UIs easy. This will cause problems if a UserControl declares a name
+                    // in its XAML and that control is included multiple times in a parent control
+                    // (as the name will be duplicated), however at the moment I'm fine with saying
+                    // "don't do that".
+                    var parentNameScope = NameScope.FindNameScope(visualParent);
+                    parentNameScope?.Register(Name, this);
+                }
             }
         }
 

+ 5 - 0
src/Avalonia.Controls/IVirtualizingPanel.cs

@@ -66,6 +66,11 @@ namespace Avalonia.Controls
         /// </summary>
         double PixelOffset { get; set; }
 
+        /// <summary>
+        /// Gets or sets the current scroll offset in the cross axis.
+        /// </summary>
+        double CrossAxisOffset { get; set; }
+
         /// <summary>
         /// Invalidates the measure of the control and forces a call to 
         /// <see cref="IVirtualizingController.UpdateControls"/> on the next measure.

+ 1 - 1
src/Avalonia.Controls/Platform/IWindowIconImpl.cs

@@ -7,6 +7,6 @@ namespace Avalonia.Platform
 {
     public interface IWindowIconImpl
     {
-        Stream Save();
+        void Save(Stream outputStream);
     }
 }

+ 4 - 0
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -232,6 +232,10 @@ namespace Avalonia.Controls.Presenters
             {
                 DataContext = content;
             }
+            else
+            {
+                ClearValue(DataContextProperty);
+            }
 
             // Update the Child.
             if (newChild == null)

+ 25 - 2
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Collections;
 using System.Collections.Specialized;
+using System.Reactive.Linq;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Utils;
 using Avalonia.Input;
@@ -17,6 +18,7 @@ namespace Avalonia.Controls.Presenters
     internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable
     {
         private double _crossAxisOffset;
+        private IDisposable _subscriptions;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
@@ -27,6 +29,15 @@ namespace Avalonia.Controls.Presenters
             Owner = owner;
             Items = owner.Items;
             ItemCount = owner.Items.Count();
+
+            var panel = VirtualizingPanel;
+
+            if (panel != null)
+            {
+                _subscriptions = panel.GetObservable(Panel.BoundsProperty)
+                    .Skip(1)
+                    .Subscribe(_ => InvalidateScroll());
+            }
         }
 
         /// <summary>
@@ -195,8 +206,17 @@ namespace Avalonia.Controls.Presenters
         /// <returns>The actual size used.</returns>
         public virtual Size ArrangeOverride(Size finalSize)
         {
-            var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset);
-            Owner.Panel.Arrange(new Rect(origin, finalSize));
+            if (VirtualizingPanel != null)
+            {
+                VirtualizingPanel.CrossAxisOffset = _crossAxisOffset;
+                Owner.Panel.Arrange(new Rect(finalSize));
+            }
+            else
+            {
+                var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset);
+                Owner.Panel.Arrange(new Rect(origin, finalSize));
+            }
+
             return finalSize;
         }
 
@@ -240,6 +260,9 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         public virtual void Dispose()
         {
+            _subscriptions?.Dispose();
+            _subscriptions = null;
+
             if (VirtualizingPanel != null)
             {
                 VirtualizingPanel.Controller = null;

+ 7 - 4
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -173,10 +173,13 @@ namespace Avalonia.Controls.Presenters
         {
             if (this.GetVisualParent() != null)
             {
-                _caretBlink = true;
-                _caretTimer.Stop();
-                _caretTimer.Start();
-                InvalidateVisual();
+                if (_caretTimer.IsEnabled)
+                {
+                    _caretBlink = true;
+                    _caretTimer.Stop();
+                    _caretTimer.Start();
+                    InvalidateVisual();
+                }
 
                 if (IsMeasureValid)
                 {

+ 0 - 4
src/Avalonia.Controls/ScrollViewer.cs

@@ -157,10 +157,6 @@ namespace Avalonia.Controls
         /// </summary>
         public ScrollViewer()
         {
-            Observable.CombineLatest(
-                this.GetObservable(ExtentProperty),
-                this.GetObservable(ViewportProperty))
-                .Select(x => new { Extent = x[0], Viewport = x[1] });
         }
 
         /// <summary>

+ 25 - 2
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -19,6 +19,7 @@ namespace Avalonia.Controls
         private double _averageItemSize;
         private int _averageCount;
         private double _pixelOffset;
+        private double _crossAxisOffset;
         private bool _forceRemeasure;
 
         bool IVirtualizingPanel.IsFull
@@ -60,6 +61,20 @@ namespace Avalonia.Controls
             }
         }
 
+        double IVirtualizingPanel.CrossAxisOffset
+        {
+            get { return _crossAxisOffset; }
+
+            set
+            {
+                if (_crossAxisOffset != value)
+                {
+                    _crossAxisOffset = value;
+                    InvalidateArrange();
+                }
+            }
+        }
+
         private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller;
 
         void IVirtualizingPanel.ForceInvalidateMeasure()
@@ -140,7 +155,11 @@ namespace Avalonia.Controls
         {
             if (orientation == Orientation.Vertical)
             {
-                rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height);
+                rect = new Rect(
+                    rect.X - _crossAxisOffset,
+                    rect.Y - _pixelOffset,
+                    rect.Width,
+                    rect.Height);
                 child.Arrange(rect);
 
                 if (rect.Y >= _availableSpace.Height)
@@ -157,7 +176,11 @@ namespace Avalonia.Controls
             }
             else
             {
-                rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height);
+                rect = new Rect(
+                    rect.X - _pixelOffset,
+                    rect.Y - _crossAxisOffset,
+                    rect.Width,
+                    rect.Height);
                 child.Arrange(rect);
 
                 if (rect.X >= _availableSpace.Width)

+ 1 - 1
src/Avalonia.Controls/WindowIcon.cs

@@ -31,6 +31,6 @@ namespace Avalonia.Controls
 
         public IWindowIconImpl PlatformImpl { get; }
 
-        public Stream Save() => PlatformImpl.Save();
+        public void Save(Stream stream) => PlatformImpl.Save(stream);
     }
 }

+ 31 - 0
src/Avalonia.Styling/Controls/NameScope.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using Avalonia.LogicalTree;
 
 namespace Avalonia.Controls
 {
@@ -29,6 +30,32 @@ namespace Avalonia.Controls
         /// </summary>
         public event EventHandler<NameScopeEventArgs> Unregistered;
 
+        /// <summary>
+        /// Finds the containing name scope for a visual.
+        /// </summary>
+        /// <param name="visual">The visual.</param>
+        /// <returns>The containing name scope.</returns>
+        public static INameScope FindNameScope(Visual visual)
+        {
+            Contract.Requires<ArgumentNullException>(visual != null);
+
+            INameScope result;
+
+            while (visual != null)
+            {
+                result = visual as INameScope ?? GetNameScope(visual);
+
+                if (result != null)
+                {
+                    return result;
+                }
+
+                visual = (visual as ILogical).LogicalParent as Visual;
+            }
+
+            return null;
+        }
+
         /// <summary>
         /// Gets the value of the attached <see cref="NameScopeProperty"/> on a visual.
         /// </summary>
@@ -36,6 +63,8 @@ namespace Avalonia.Controls
         /// <returns>The value of the NameScope attached property.</returns>
         public static INameScope GetNameScope(Visual visual)
         {
+            Contract.Requires<ArgumentNullException>(visual != null);
+
             return visual.GetValue(NameScopeProperty);
         }
 
@@ -46,6 +75,8 @@ namespace Avalonia.Controls
         /// <param name="value">The value to set.</param>
         public static void SetNameScope(Visual visual, INameScope value)
         {
+            Contract.Requires<ArgumentNullException>(visual != null);
+
             visual.SetValue(NameScopeProperty, value);
         }
 

+ 3 - 2
src/Gtk/Avalonia.Gtk/IconImpl.cs

@@ -18,9 +18,10 @@ namespace Avalonia.Gtk
 
         public Pixbuf Pixbuf { get; }
 
-        public Stream Save()
+        public void Save(Stream stream)
         {
-            return new MemoryStream(Pixbuf.SaveToBuffer("png"));
+            var buffer = Pixbuf.SaveToBuffer("png");
+            stream.Write(buffer, 0, buffer.Length);
         }
     }
 }

+ 4 - 0
src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs

@@ -115,6 +115,10 @@ namespace Avalonia.Markup.Xaml.Data
                     anchor,
                     enableDataValidation);
             }
+            else if (RelativeSource.Mode == RelativeSourceMode.Self)
+            {
+                observer = CreateSourceObserver(target, pathInfo.Path, enableDataValidation);
+            }
             else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
             {
                 observer = CreateTemplatedParentObserver(target, pathInfo.Path);

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs

@@ -5,6 +5,7 @@ namespace Avalonia.Markup.Xaml.Data
 {
     public enum RelativeSourceMode
     {
+        Self,
         DataContext,
         TemplatedParent,
     }

+ 5 - 11
src/Markup/Avalonia.Markup/Data/ExpressionNode.cs

@@ -131,20 +131,14 @@ namespace Avalonia.Markup.Data
             }
             else
             {
-                if (notification.Error != null)
+                if (Next != null)
                 {
-                    _observer.OnNext(notification);
+                    Next.Target = new WeakReference(notification.Value);
                 }
-                else if (notification.HasValue)
+                
+                if (Next == null || notification.Error != null)
                 {
-                    if (Next != null)
-                    {
-                        Next.Target = new WeakReference(notification.Value);
-                    }
-                    else
-                    {
-                        _observer.OnNext(value);
-                    }
+                    _observer.OnNext(value);
                 }
             }
         }

+ 1 - 1
src/Markup/Avalonia.Markup/Data/MarkupBindingChainException.cs

@@ -26,7 +26,7 @@ namespace Avalonia.Markup.Data
             _nodes = null;
         }
 
-        public bool HasNodes => _nodes.Count > 0;
+        public bool HasNodes => _nodes?.Count > 0;
         public void AddNode(string node) => _nodes.Add(node);
 
         public void Commit(string expression)

+ 13 - 1
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@@ -53,6 +53,15 @@
       <HintPath>..\..\..\packages\SharpDX.DXGI.3.1.1\lib\net45\SharpDX.DXGI.dll</HintPath>
       <Private>True</Private>
     </Reference>
+    <Reference Include="SharpDX.Direct2D1, Version=3.1.1.0, Culture=neutral, PublicKeyToken=b4dcf0f35e5521f1, processorArchitecture=MSIL">
+      <HintPath>..\..\..\packages\SharpDX.Direct2D1.3.1.1\lib\net45\SharpDX.Direct2D1.dll</HintPath>
+    </Reference>
+    <Reference Include="SharpDX.Direct3D11, Version=3.1.1.0, Culture=neutral, PublicKeyToken=b4dcf0f35e5521f1, processorArchitecture=MSIL">
+      <HintPath>..\..\..\packages\SharpDX.Direct3D11.3.1.1\lib\net45\SharpDX.Direct3D11.dll</HintPath>
+    </Reference>
+    <Reference Include="SharpDX.DXGI, Version=3.1.1.0, Culture=neutral, PublicKeyToken=b4dcf0f35e5521f1, processorArchitecture=MSIL">
+      <HintPath>..\..\..\packages\SharpDX.DXGI.3.1.1\lib\net45\SharpDX.DXGI.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="Microsoft.CSharp" />
@@ -66,12 +75,15 @@
     </Compile>
     <Compile Include="Direct2D1Platform.cs" />
     <Compile Include="Disposable.cs" />
+    <Compile Include="HwndRenderTarget.cs" />
     <Compile Include="Media\BrushImpl.cs" />
     <Compile Include="Media\BrushWrapper.cs" />
     <Compile Include="Media\DrawingContextImpl.cs" />
     <Compile Include="Media\GeometryImpl.cs" />
     <Compile Include="Media\Imaging\RenderTargetBitmapImpl.cs" />
     <Compile Include="Media\Imaging\BitmapImpl.cs" />
+    <Compile Include="Media\Imaging\D2DBitmapImpl.cs" />
+    <Compile Include="Media\Imaging\WicBitmapImpl.cs" />
     <Compile Include="Media\RadialGradientBrushImpl.cs" />
     <Compile Include="Media\LinearGradientBrushImpl.cs" />
     <Compile Include="Media\AvaloniaTextRenderer.cs" />
@@ -83,8 +95,8 @@
     <Compile Include="Media\FormattedTextImpl.cs" />
     <Compile Include="PrimitiveExtensions.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="HwndRenderTarget.cs" />
     <Compile Include="RenderTarget.cs" />
+    <Compile Include="SwapChainRenderTarget.cs" />
   </ItemGroup>
   <ItemGroup>
     <None Include="app.config" />

+ 37 - 4
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -47,13 +47,46 @@ namespace Avalonia.Direct2D1
                 .Bind<SharpDX.Direct2D1.Factory>().ToConstant(s_d2D1Factory)
                 .Bind<SharpDX.Direct2D1.Factory1>().ToConstant(s_d2D1Factory)
                 .BindToSelf(s_dwfactory)
-                .BindToSelf(s_imagingFactory);
+                .BindToSelf(s_imagingFactory)
+                .BindToSelf(s_dxgiDevice)
+                .BindToSelf(s_d2D1Device);
             SharpDX.Configuration.EnableReleaseOnFinalizer = true;
         }
 
+        private static readonly SharpDX.DXGI.Device s_dxgiDevice;
+
+        private static readonly SharpDX.Direct2D1.Device s_d2D1Device;
+
+        static Direct2D1Platform()
+        {
+            var featureLevels = new[]
+            {
+                SharpDX.Direct3D.FeatureLevel.Level_11_1,
+                SharpDX.Direct3D.FeatureLevel.Level_11_0,
+                SharpDX.Direct3D.FeatureLevel.Level_10_1,
+                SharpDX.Direct3D.FeatureLevel.Level_10_0,
+                SharpDX.Direct3D.FeatureLevel.Level_9_3,
+                SharpDX.Direct3D.FeatureLevel.Level_9_2,
+                SharpDX.Direct3D.FeatureLevel.Level_9_1,
+            };
+
+            using (var d3dDevice = new SharpDX.Direct3D11.Device(
+                SharpDX.Direct3D.DriverType.Hardware,
+                SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport | SharpDX.Direct3D11.DeviceCreationFlags.VideoSupport,
+                featureLevels))
+            {
+                s_dxgiDevice = d3dDevice.QueryInterface<SharpDX.DXGI.Device>();
+            }
+
+            using (var factory1 = s_d2D1Factory.QueryInterface<SharpDX.Direct2D1.Factory1>())
+            {
+                s_d2D1Device = new SharpDX.Direct2D1.Device(factory1, s_dxgiDevice);
+            }
+        }
+
         public IBitmapImpl CreateBitmap(int width, int height)
         {
-            return new BitmapImpl(s_imagingFactory, width, height);
+            return new WicBitmapImpl(s_imagingFactory, width, height);
         }
 
         public IFormattedTextImpl CreateFormattedText(
@@ -126,12 +159,12 @@ namespace Avalonia.Direct2D1
 
         public IBitmapImpl LoadBitmap(string fileName)
         {
-            return new BitmapImpl(s_imagingFactory, fileName);
+            return new WicBitmapImpl(s_imagingFactory, fileName);
         }
 
         public IBitmapImpl LoadBitmap(Stream stream)
         {
-            return new BitmapImpl(s_imagingFactory, stream);
+            return new WicBitmapImpl(s_imagingFactory, stream);
         }
     }
 }

+ 11 - 141
src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs

@@ -1,159 +1,29 @@
-// 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.Direct2D1.Media;
-using Avalonia.Media;
-using Avalonia.Platform;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
 using Avalonia.Win32.Interop;
 using SharpDX;
-using SharpDX.Direct2D1;
+using SharpDX.DXGI;
 
 namespace Avalonia.Direct2D1
 {
-    public class HwndRenderTarget : IRenderTarget
+    class HwndRenderTarget : SwapChainRenderTarget
     {
         private readonly IntPtr _hwnd;
-        private readonly SharpDX.Direct3D11.Device _d3dDevice;
-        private DeviceContext _deviceContext;
-        private SharpDX.DXGI.SwapChain1 _swapChain;
-        private Size2 _size;
-        private Size2F _dpi;
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="HwndRenderTarget"/> class.
-        /// </summary>
-        /// <param name="hwnd">The window handle.</param>
         public HwndRenderTarget(IntPtr hwnd)
         {
             _hwnd = hwnd;
-            Direct2DFactory = AvaloniaLocator.Current.GetService<Factory1>();
-            DirectWriteFactory = AvaloniaLocator.Current.GetService<SharpDX.DirectWrite.Factory>();
-
-            var featureLevels = new[]
-            {
-                SharpDX.Direct3D.FeatureLevel.Level_12_1,
-                SharpDX.Direct3D.FeatureLevel.Level_12_0,
-                SharpDX.Direct3D.FeatureLevel.Level_11_1,
-                SharpDX.Direct3D.FeatureLevel.Level_11_0,
-                SharpDX.Direct3D.FeatureLevel.Level_10_1,
-                SharpDX.Direct3D.FeatureLevel.Level_10_0,
-                SharpDX.Direct3D.FeatureLevel.Level_9_3,
-                SharpDX.Direct3D.FeatureLevel.Level_9_2,
-                SharpDX.Direct3D.FeatureLevel.Level_9_1,
-            };
-
-            _d3dDevice = new SharpDX.Direct3D11.Device(
-                SharpDX.Direct3D.DriverType.Hardware,
-                SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport,
-                featureLevels);
-
-            var size = GetWindowSize();
-            var dpi = GetWindowDpi();
-            UpdateSwapChain(size, dpi);
         }
 
-        /// <summary>
-        /// Gets the Direct2D factory.
-        /// </summary>
-        public Factory1 Direct2DFactory
+        protected override SwapChain1 CreateSwapChain(Factory2 dxgiFactory, SwapChainDescription1 swapChainDesc)
         {
-            get;
+            return new SwapChain1(dxgiFactory, DxgiDevice, _hwnd, ref swapChainDesc);
         }
 
-        /// <summary>
-        /// Gets the DirectWrite factory.
-        /// </summary>
-        public SharpDX.DirectWrite.Factory DirectWriteFactory
-        {
-            get;
-        }
-
-        /// <summary>
-        /// Creates a drawing context for a rendering session.
-        /// </summary>
-        /// <returns>An <see cref="Avalonia.Media.DrawingContext"/>.</returns>
-        public IDrawingContextImpl CreateDrawingContext()
-        {
-            var size = GetWindowSize();
-            var dpi = GetWindowDpi();
-
-            if (size != _size || _dpi != dpi)
-            {
-                UpdateSwapChain(size, dpi);
-            }
-
-            return new DrawingContextImpl(_deviceContext, DirectWriteFactory, _swapChain);
-        }
-
-        public void Dispose()
-        {
-            _deviceContext.Dispose();
-            _swapChain.Dispose();
-            _d3dDevice.Dispose();
-        }
-
-        private void UpdateSwapChain(Size2 size, Size2F dpi)
-        {
-            _size = size;
-            _dpi = dpi;
-
-            using (var dxgiDevice = _d3dDevice.QueryInterface<SharpDX.DXGI.Device>())
-            using (var d2dDevice = new Device(Direct2DFactory, dxgiDevice))
-            using (var dxgiAdaptor = dxgiDevice.Adapter)
-            using (var dxgiFactory = dxgiAdaptor.GetParent<SharpDX.DXGI.Factory2>())
-            {
-                _deviceContext?.Dispose();
-
-                if (_swapChain == null)
-                {
-                    var swapChainDesc = new SharpDX.DXGI.SwapChainDescription1
-                    {
-                        Width = 0,
-                        Height = 0,
-                        Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm,
-                        Stereo = false,
-                        SampleDescription = new SharpDX.DXGI.SampleDescription
-                        {
-                            Count = 1,
-                            Quality = 0,
-                        },
-                        Usage = SharpDX.DXGI.Usage.RenderTargetOutput,
-                        BufferCount = 2,
-                        Scaling = SharpDX.DXGI.Scaling.None,
-                        SwapEffect = SharpDX.DXGI.SwapEffect.FlipSequential,
-                        Flags = 0,
-                    };
-
-                    _swapChain = new SharpDX.DXGI.SwapChain1(dxgiFactory, _d3dDevice, _hwnd, ref swapChainDesc);
-                }
-                else
-                {
-                    _swapChain.ResizeBuffers(2, 0, 0, SharpDX.DXGI.Format.B8G8R8A8_UNorm, 0);
-                }
-
-                _deviceContext = new DeviceContext(d2dDevice, DeviceContextOptions.None);
-                _deviceContext.DotsPerInch = dpi;
-
-                using (var dxgiBackBuffer = _swapChain.GetBackBuffer<SharpDX.DXGI.Surface>(0))
-                using (var d2dBackBuffer = new Bitmap1(
-                    _deviceContext,
-                    dxgiBackBuffer,
-                    new BitmapProperties1(
-                        new PixelFormat
-                        {
-                            AlphaMode = AlphaMode.Ignore,
-                            Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm
-                        },
-                        dpi.Width,
-                        dpi.Height,
-                        BitmapOptions.Target | BitmapOptions.CannotDraw)))
-                {
-                    _deviceContext.Target = d2dBackBuffer;
-                }
-            }
-        }
-        private Size2F GetWindowDpi()
+        protected override Size2F GetWindowDpi()
         {
             if (UnmanagedMethods.ShCoreAvailable)
             {
@@ -176,7 +46,7 @@ namespace Avalonia.Direct2D1
             return new Size2F(96, 96);
         }
 
-        private Size2 GetWindowSize()
+        protected override Size2 GetWindowSize()
         {
             UnmanagedMethods.RECT rc;
             UnmanagedMethods.GetClientRect(_hwnd, out rc);

+ 4 - 5
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -27,6 +27,7 @@ namespace Avalonia.Direct2D1.Media
         /// </summary>
         /// <param name="renderTarget">The render target to draw to.</param>
         /// <param name="directWriteFactory">The DirectWrite factory.</param>
+        /// <param name="swapChain">An optional swap chain associated with this drawing context.</param>
         public DrawingContextImpl(
             SharpDX.Direct2D1.RenderTarget renderTarget,
             SharpDX.DirectWrite.Factory directWriteFactory,
@@ -35,6 +36,7 @@ namespace Avalonia.Direct2D1.Media
             _renderTarget = renderTarget;
             _swapChain = swapChain;
             _directWriteFactory = directWriteFactory;
+            _swapChain = swapChain;
             _renderTarget.BeginDraw();
         }
 
@@ -63,11 +65,8 @@ namespace Avalonia.Direct2D1.Media
             try
             {
                 _renderTarget.EndDraw();
-
-                if (_swapChain != null)
-                {
-                    _swapChain.Present(1, SharpDX.DXGI.PresentFlags.None);
-                }
+                
+                _swapChain?.Present(1, SharpDX.DXGI.PresentFlags.None);
             }
             catch (SharpDXException ex) when((uint)ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET
             {

+ 12 - 118
src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs

@@ -1,130 +1,24 @@
-// 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 System;
+using System.Collections.Generic;
 using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
 using Avalonia.Platform;
-using SharpDX.WIC;
+using SharpDX.Direct2D1;
 
 namespace Avalonia.Direct2D1.Media
 {
-    /// <summary>
-    /// A Direct2D implementation of a <see cref="Avalonia.Media.Imaging.Bitmap"/>.
-    /// </summary>
-    public class BitmapImpl : IBitmapImpl
+    public abstract class BitmapImpl : IBitmapImpl, IDisposable
     {
-        private readonly ImagingFactory _factory;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="BitmapImpl"/> class.
-        /// </summary>
-        /// <param name="factory">The WIC imaging factory to use.</param>
-        /// <param name="fileName">The filename of the bitmap to load.</param>
-        public BitmapImpl(ImagingFactory factory, string fileName)
-        {
-            _factory = factory;
-
-            using (BitmapDecoder decoder = new BitmapDecoder(factory, fileName, DecodeOptions.CacheOnDemand))
-            {
-                WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand);
-            }
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="BitmapImpl"/> class.
-        /// </summary>
-        /// <param name="factory">The WIC imaging factory to use.</param>
-        /// <param name="stream">The stream to read the bitmap from.</param>
-        public BitmapImpl(ImagingFactory factory, Stream stream)
-        {
-            _factory = factory;
-
-            using (BitmapDecoder decoder = new BitmapDecoder(factory, stream, DecodeOptions.CacheOnLoad))
-            {
-                WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad);
-            }
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="BitmapImpl"/> class.
-        /// </summary>
-        /// <param name="factory">The WIC imaging factory to use.</param>
-        /// <param name="width">The width of the bitmap.</param>
-        /// <param name="height">The height of the bitmap.</param>
-        public BitmapImpl(ImagingFactory factory, int width, int height)
-        {
-            _factory = factory;
-            WicImpl = new Bitmap(
-                factory,
-                width,
-                height,
-                PixelFormat.Format32bppPBGRA,
-                BitmapCreateCacheOption.CacheOnLoad);
-        }
-
-        /// <summary>
-        /// Gets the width of the bitmap, in pixels.
-        /// </summary>
-        public int PixelWidth => WicImpl.Size.Width;
-
-        /// <summary>
-        /// Gets the height of the bitmap, in pixels.
-        /// </summary>
-        public int PixelHeight => WicImpl.Size.Height;
+        public abstract Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target);
+        public abstract int PixelWidth { get; }
+        public abstract int PixelHeight { get; }
+        public abstract void Save(string fileName);
+        public abstract void Save(Stream stream);
 
         public virtual void Dispose()
         {
-            WicImpl.Dispose();
-        }
-
-        /// <summary>
-        /// Gets the WIC implementation of the bitmap.
-        /// </summary>
-        public Bitmap WicImpl
-        {
-            get;
-        }
-
-        /// <summary>
-        /// Gets a Direct2D bitmap to use on the specified render target.
-        /// </summary>
-        /// <param name="renderTarget">The render target.</param>
-        /// <returns>The Direct2D bitmap.</returns>
-        public SharpDX.Direct2D1.Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget)
-        {
-            FormatConverter converter = new FormatConverter(_factory);
-            converter.Initialize(WicImpl, PixelFormat.Format32bppPBGRA);
-            return SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter);
-        }
-
-        /// <summary>
-        /// Saves the bitmap to a file.
-        /// </summary>
-        /// <param name="fileName">The filename.</param>
-        public void Save(string fileName)
-        {
-            if (Path.GetExtension(fileName) != ".png")
-            {
-                // Yeah, we need to support other formats.
-                throw new NotSupportedException("Use PNG, stoopid.");
-            }
-
-            using (FileStream s = new FileStream(fileName, FileMode.Create))
-            {
-                Save(s);
-            }
-        }
-
-        public void Save(Stream stream)
-        {
-            PngBitmapEncoder encoder = new PngBitmapEncoder(_factory);
-            encoder.Initialize(stream);
-
-            BitmapFrameEncode frame = new BitmapFrameEncode(encoder);
-            frame.Initialize();
-            frame.WriteSource(WicImpl);
-            frame.Commit();
-            encoder.Commit();
         }
     }
 }

+ 57 - 0
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs

@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Platform;
+using SharpDX.Direct2D1;
+
+namespace Avalonia.Direct2D1.Media
+{
+    /// <summary>
+    /// A Direct2D Bitmap implementation that uses a GPU memory bitmap as its image.
+    /// </summary>
+    public class D2DBitmapImpl : BitmapImpl
+    {
+        private Bitmap _direct2D;
+
+        /// <summary>
+        /// Initialize a new instance of the <see cref="BitmapImpl"/> class
+        /// with a bitmap backed by GPU memory.
+        /// </summary>
+        /// <param name="d2DBitmap">The GPU bitmap.</param>
+        /// <remarks>
+        /// This bitmap must be either from the same render target,
+        /// or if the render target is a <see cref="SharpDX.Direct2D1.DeviceContext"/>,
+        /// the device associated with this context, to be renderable.
+        /// </remarks>
+        public D2DBitmapImpl(Bitmap d2DBitmap)
+        {
+            if (d2DBitmap == null) throw new ArgumentNullException(nameof(d2DBitmap));
+
+            _direct2D = d2DBitmap;
+        }
+
+        public override Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target) => _direct2D;
+               
+        public override int PixelWidth => _direct2D.PixelSize.Width;
+        public override int PixelHeight => _direct2D.PixelSize.Height;
+
+        public override void Save(string fileName)
+        {
+            throw new NotImplementedException();
+        }
+
+        public override void Save(Stream stream)
+        {
+            throw new NotImplementedException();
+        }
+
+        public override void Dispose()
+        {
+            base.Dispose();
+            _direct2D.Dispose();
+        }
+    }
+}

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs

@@ -10,7 +10,7 @@ using DirectWriteFactory = SharpDX.DirectWrite.Factory;
 
 namespace Avalonia.Direct2D1.Media
 {
-    public class RenderTargetBitmapImpl : BitmapImpl, IRenderTargetBitmapImpl, IDisposable
+    public class RenderTargetBitmapImpl : WicBitmapImpl, IRenderTargetBitmapImpl
     {
         private readonly DirectWriteFactory _dwriteFactory;
         private readonly WicRenderTarget _target;

+ 130 - 0
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs

@@ -0,0 +1,130 @@
+// 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 System.IO;
+using Avalonia.Platform;
+using SharpDX.WIC;
+
+namespace Avalonia.Direct2D1.Media
+{
+    /// <summary>
+    /// A WIC implementation of a <see cref="Avalonia.Media.Imaging.Bitmap"/>.
+    /// </summary>
+    public class WicBitmapImpl : BitmapImpl
+    {
+        private readonly ImagingFactory _factory;
+
+        private SharpDX.Direct2D1.Bitmap _direct2D;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="WicBitmapImpl"/> class.
+        /// </summary>
+        /// <param name="factory">The WIC imaging factory to use.</param>
+        /// <param name="fileName">The filename of the bitmap to load.</param>
+        public WicBitmapImpl(ImagingFactory factory, string fileName)
+        {
+            _factory = factory;
+
+            using (BitmapDecoder decoder = new BitmapDecoder(factory, fileName, DecodeOptions.CacheOnDemand))
+            {
+                WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand);
+            }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="WicBitmapImpl"/> class.
+        /// </summary>
+        /// <param name="factory">The WIC imaging factory to use.</param>
+        /// <param name="stream">The stream to read the bitmap from.</param>
+        public WicBitmapImpl(ImagingFactory factory, Stream stream)
+        {
+            _factory = factory;
+
+            using (BitmapDecoder decoder = new BitmapDecoder(factory, stream, DecodeOptions.CacheOnLoad))
+            {
+                WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad);
+            }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="WicBitmapImpl"/> class.
+        /// </summary>
+        /// <param name="factory">The WIC imaging factory to use.</param>
+        /// <param name="width">The width of the bitmap.</param>
+        /// <param name="height">The height of the bitmap.</param>
+        public WicBitmapImpl(ImagingFactory factory, int width, int height)
+        {
+            _factory = factory;
+            WicImpl = new Bitmap(
+                factory,
+                width,
+                height,
+                PixelFormat.Format32bppPBGRA,
+                BitmapCreateCacheOption.CacheOnLoad);
+        }
+
+        /// <summary>
+        /// Gets the width of the bitmap, in pixels.
+        /// </summary>
+        public override int PixelWidth => WicImpl.Size.Width;
+
+        /// <summary>
+        /// Gets the height of the bitmap, in pixels.
+        /// </summary>
+        public override int PixelHeight => WicImpl.Size.Height;
+
+        public override void Dispose()
+        {
+            WicImpl.Dispose();
+            _direct2D?.Dispose();
+        }
+
+        /// <summary>
+        /// Gets the WIC implementation of the bitmap.
+        /// </summary>
+        public Bitmap WicImpl { get; }
+
+        /// <summary>
+        /// Gets a Direct2D bitmap to use on the specified render target.
+        /// </summary>
+        /// <param name="renderTarget">The render target.</param>
+        /// <returns>The Direct2D bitmap.</returns>
+        public override SharpDX.Direct2D1.Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget)
+        {
+            FormatConverter converter = new FormatConverter(_factory);
+            converter.Initialize(WicImpl, PixelFormat.Format32bppPBGRA);
+            return SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter);
+        }
+
+        /// <summary>
+        /// Saves the bitmap to a file.
+        /// </summary>
+        /// <param name="fileName">The filename.</param>
+        public override void Save(string fileName)
+        {
+            if (Path.GetExtension(fileName) != ".png")
+            {
+                // Yeah, we need to support other formats.
+                throw new NotSupportedException("Use PNG, stoopid.");
+            }
+
+            using (FileStream s = new FileStream(fileName, FileMode.Create))
+            {
+                Save(s);
+            }
+        }
+
+        public override void Save(Stream stream)
+        {
+            PngBitmapEncoder encoder = new PngBitmapEncoder(_factory);
+            encoder.Initialize(stream);
+
+            BitmapFrameEncode frame = new BitmapFrameEncode(encoder);
+            frame.Initialize();
+            frame.WriteSource(WicImpl);
+            frame.Commit();
+            encoder.Commit();
+        }
+    }
+}

+ 136 - 0
src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs

@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Direct2D1.Media;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Win32.Interop;
+using SharpDX;
+using SharpDX.Direct2D1;
+using SharpDX.DXGI;
+using AlphaMode = SharpDX.Direct2D1.AlphaMode;
+using Device = SharpDX.Direct2D1.Device;
+using Factory = SharpDX.Direct2D1.Factory;
+using Factory2 = SharpDX.DXGI.Factory2;
+
+namespace Avalonia.Direct2D1
+{
+    public abstract class SwapChainRenderTarget : IRenderTarget
+    {
+        private Size2 _savedSize;
+        private Size2F _savedDpi;
+        private DeviceContext _deviceContext;
+        private SwapChain1 _swapChain;
+
+        protected SwapChainRenderTarget()
+        {
+            DxgiDevice = AvaloniaLocator.Current.GetService<SharpDX.DXGI.Device>();
+            D2DDevice = AvaloniaLocator.Current.GetService<Device>();
+            Direct2DFactory = AvaloniaLocator.Current.GetService<Factory>();
+            DirectWriteFactory = AvaloniaLocator.Current.GetService<SharpDX.DirectWrite.Factory>();
+        }
+
+
+        /// <summary>
+        /// Gets the Direct2D factory.
+        /// </summary>
+        public Factory Direct2DFactory
+        {
+            get;
+        }
+
+        /// <summary>
+        /// Gets the DirectWrite factory.
+        /// </summary>
+        public SharpDX.DirectWrite.Factory DirectWriteFactory
+        {
+            get;
+        }
+
+        protected SharpDX.DXGI.Device DxgiDevice { get; }
+        
+        public Device D2DDevice { get; }
+
+        /// <summary>
+        /// Creates a drawing context for a rendering session.
+        /// </summary>
+        /// <returns>An <see cref="Avalonia.Media.DrawingContext"/>.</returns>
+        public IDrawingContextImpl CreateDrawingContext()
+        {
+            var size = GetWindowSize();
+            var dpi = GetWindowDpi();
+
+            if (size != _savedSize || dpi != _savedDpi)
+            {
+                _savedSize = size;
+                _savedDpi = dpi;
+                CreateSwapChain();
+            }
+
+            return new DrawingContextImpl(_deviceContext, DirectWriteFactory, _swapChain);
+        }
+
+        public void Dispose()
+        {
+            _deviceContext?.Dispose();
+            _swapChain?.Dispose();
+        }
+
+        private void CreateSwapChain()
+        {
+            using (var dxgiAdaptor = DxgiDevice.Adapter)
+            using (var dxgiFactory = dxgiAdaptor.GetParent<Factory2>())
+            {
+                _deviceContext?.Dispose();
+                _deviceContext = new DeviceContext(D2DDevice, DeviceContextOptions.None) {DotsPerInch = _savedDpi};
+
+
+                var swapChainDesc = new SwapChainDescription1
+                {
+                    Width = _savedSize.Width,
+                    Height = _savedSize.Height,
+                    Format = Format.B8G8R8A8_UNorm,
+                    Stereo = false,
+                    SampleDescription = new SampleDescription
+                    {
+                        Count = 1,
+                        Quality = 0,
+                    },
+                    Usage = Usage.RenderTargetOutput,
+                    BufferCount = 2,
+                    Scaling = Scaling.None,
+                    SwapEffect = SwapEffect.FlipSequential,
+                    Flags = 0,
+                };
+
+                _swapChain?.Dispose();
+                _swapChain = CreateSwapChain(dxgiFactory, swapChainDesc);
+
+                using (var dxgiBackBuffer = _swapChain.GetBackBuffer<Surface>(0))
+                using (var d2dBackBuffer = new Bitmap1(
+                    _deviceContext,
+                    dxgiBackBuffer,
+                    new BitmapProperties1(
+                        new PixelFormat
+                        {
+                            AlphaMode = AlphaMode.Ignore,
+                            Format = Format.B8G8R8A8_UNorm
+                        },
+                        _savedDpi.Width,
+                        _savedDpi.Height,
+                        BitmapOptions.Target | BitmapOptions.CannotDraw)))
+                {
+                    _deviceContext.Target = d2dBackBuffer;
+                }
+            }
+        }
+
+        protected abstract SwapChain1 CreateSwapChain(Factory2 dxgiFactory, SwapChainDescription1 swapChainDesc);
+
+        protected abstract Size2F GetWindowDpi();
+
+        protected abstract Size2 GetWindowSize();
+    }
+}

+ 3 - 5
src/Windows/Avalonia.Win32/IconImpl.cs

@@ -27,18 +27,16 @@ namespace Avalonia.Win32
 
         public IntPtr HIcon => icon?.Handle ?? bitmap.GetHicon();
 
-        public Stream Save()
+        public void Save(Stream outputStream)
         {
-            var stream = new MemoryStream();
             if (icon != null)
             {
-                icon.Save(stream);
+                icon.Save(outputStream);
             }
             else
             {
-                bitmap.Save(stream, ImageFormat.Png);
+                bitmap.Save(outputStream, ImageFormat.Png);
             }
-            return stream;
         }
     }
 }

+ 7 - 0
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@@ -88,9 +88,16 @@ namespace Avalonia.Win32
 
                     var pofn = &ofn;
 
+                    // We should save the current directory to restore it later.
+                    var currentDirectory = Environment.CurrentDirectory;
+
                     var res = dialog is OpenFileDialog
                         ? UnmanagedMethods.GetOpenFileName(new IntPtr(pofn))
                         : UnmanagedMethods.GetSaveFileName(new IntPtr(pofn));
+
+                    // Restore the old current directory, since GetOpenFileName and GetSaveFileName change it after they're called
+                    Environment.CurrentDirectory = currentDirectory;
+
                     if (!res)
                         return null;
                     if (dialog?.Filters.Count > 0)

+ 4 - 1
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -709,7 +709,10 @@ namespace Avalonia.Win32
                 MaximizeWithoutCoveringTaskbar();
             }
 
-            SetFocus(_hwnd);
+            if (!Design.IsDesignMode)
+            {
+                SetFocus(_hwnd);
+            }
         }
 
         private void MaximizeWithoutCoveringTaskbar()

+ 3 - 5
src/iOS/Avalonia.iOS/PlatformIconLoader.cs

@@ -31,18 +31,16 @@ namespace Avalonia.iOS
     // Stores the icon created as a stream to support saving even though an icon is never shown
     public class FakeIcon : IWindowIconImpl
     {
-        private Stream stream = new MemoryStream();
+        private readonly Stream stream = new MemoryStream();
 
         public FakeIcon(Stream stream)
         {
             stream.CopyTo(this.stream);
         }
 
-        public Stream Save()
+        public void Save(Stream outputStream)
         {
-            var returnStream = new MemoryStream();
-            stream.CopyTo(returnStream);
-            return returnStream;
+            stream.CopyTo(outputStream);
         }
     }
 }

+ 29 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Collections.Generic;
 using System.Reactive.Subjects;
+using Avalonia;
 using Avalonia.Data;
 using Avalonia.Logging;
 using Avalonia.UnitTests;
@@ -410,6 +411,34 @@ namespace Avalonia.Base.UnitTests
             Assert.True(called);
         }
 
+        [Fact]
+        public void AddOwner_Should_Inherit_DefaultBindingMode()
+        {
+            var foo = new DirectProperty<Class1, string>(
+                "foo",
+                o => "foo",
+                null,
+                new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay));
+            var bar = foo.AddOwner<Class2>(o => "bar");
+
+            Assert.Equal(BindingMode.TwoWay, bar.GetMetadata<Class1>().DefaultBindingMode);
+            Assert.Equal(BindingMode.TwoWay, bar.GetMetadata<Class2>().DefaultBindingMode);
+        }
+
+        [Fact]
+        public void AddOwner_Can_Override_DefaultBindingMode()
+        {
+            var foo = new DirectProperty<Class1, string>(
+                "foo",
+                o => "foo",
+                null,
+                new DirectPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay));
+            var bar = foo.AddOwner<Class2>(o => "bar", defaultBindingMode: BindingMode.OneWayToSource);
+
+            Assert.Equal(BindingMode.TwoWay, bar.GetMetadata<Class1>().DefaultBindingMode);
+            Assert.Equal(BindingMode.OneWayToSource, bar.GetMetadata<Class2>().DefaultBindingMode);
+        }
+
         private class Class1 : AvaloniaObject
         {
             public static readonly DirectProperty<Class1, string> FooProperty =

+ 1 - 1
tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs

@@ -34,7 +34,7 @@ namespace Avalonia.Base.UnitTests
                 "test", 
                 o => null, 
                 null, 
-                new PropertyMetadata());
+                new DirectPropertyMetadata<string>());
 
             Assert.True(target.IsDirect);
         }

+ 18 - 0
tests/Avalonia.Controls.UnitTests/ControlTests_NameScope.cs

@@ -70,5 +70,23 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Null(NameScope.GetNameScope((Control)root.Presenter).Find("foo"));
         }
+
+        [Fact]
+        public void Control_That_Is_NameScope_Should_Register_With_Parent_NameScope()
+        {
+            UserControl userControl;
+            var root = new TestTemplatedRoot
+            {
+                Content = userControl = new UserControl
+                {
+                    Name = "foo",
+                }
+            };
+
+            root.ApplyTemplate();
+
+            Assert.Same(userControl, root.FindControl<UserControl>("foo"));
+            Assert.Same(userControl, userControl.FindControl<UserControl>("foo"));
+        }
     }
 }

+ 18 - 0
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -153,6 +153,23 @@ namespace Avalonia.Controls.UnitTests
             Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
         }
 
+        [Fact]
+        public void ScrollViewer_Should_Have_Correct_Extent_And_Viewport()
+        {
+            var target = new ListBox
+            {
+                Template = ListBoxTemplate(),
+                Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
+                ItemTemplate = new FuncDataTemplate<string>(x => new TextBlock { Width = 20, Height = 10 }),
+                SelectedIndex = 0,
+            };
+
+            Prepare(target);
+
+            Assert.Equal(new Size(20, 20), target.Scroll.Extent);
+            Assert.Equal(new Size(100, 10), target.Scroll.Viewport);
+        }
+
         private FuncControlTemplate ListBoxTemplate()
         {
             return new FuncControlTemplate<ListBox>(parent => 
@@ -233,6 +250,7 @@ namespace Avalonia.Controls.UnitTests
                 i.InvalidateMeasure();
             }
 
+            target.Measure(new Size(100, 100));
             target.Arrange(new Rect(0, 0, 100, 100));
         }
 

+ 16 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs

@@ -171,6 +171,22 @@ namespace Avalonia.Controls.UnitTests.Presenters
             Assert.Equal("foo", target.DataContext);
         }
 
+        [Fact]
+        public void Assigning_Control_To_Content_After_NonControl_Should_Clear_DataContext()
+        {
+            var target = new ContentPresenter();
+
+            target.Content = "foo";
+            target.UpdateChild();
+
+            Assert.True(target.IsSet(Control.DataContextProperty));
+
+            target.Content = new Border();
+            target.UpdateChild();
+
+            Assert.False(target.IsSet(Control.DataContextProperty));
+        }
+
         [Fact]
         public void Tries_To_Recycle_DataTemplate()
         {

+ 2 - 3
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@@ -4,7 +4,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
@@ -113,7 +112,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
 
             var scroll = (ScrollContentPresenter)target.Parent;
             Assert.Equal(new Size(10, 20), scroll.Extent);
-            Assert.Equal(new Size(0, 10), scroll.Viewport);
+            Assert.Equal(new Size(100, 10), scroll.Viewport);
         }
 
         [Fact]
@@ -255,7 +254,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
 
             Assert.Equal(10, target.Panel.Children.Count);
             Assert.Equal(new Size(10, 20), scroll.Extent);
-            Assert.Equal(new Size(0, 10), scroll.Viewport);
+            Assert.Equal(new Size(100, 10), scroll.Viewport);
 
             target.VirtualizationMode = ItemVirtualizationMode.None;
             target.Measure(new Size(100, 100));

+ 18 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

@@ -786,6 +786,24 @@ namespace Avalonia.Controls.UnitTests.Presenters
                 Assert.Equal(new Size(10, 20), ((ILogicalScrollable)target).Extent);
                 Assert.Equal(new Size(5, 10), ((ILogicalScrollable)target).Viewport);
             }
+
+            [Fact]
+            public void Horizontal_Scroll_Should_Update_Item_Position()
+            {
+                var target = CreateTarget();
+
+                target.ApplyTemplate();
+
+                target.Measure(new Size(5, 100));
+                target.Arrange(new Rect(0, 0, 5, 100));
+
+                ((ILogicalScrollable)target).Offset = new Vector(5, 0);
+
+                target.Measure(new Size(5, 100));
+                target.Arrange(new Rect(0, 0, 5, 100));
+
+                Assert.Equal(new Rect(-5, 0, 10, 10), target.Panel.Children[0].Bounds);
+            }
         }
 
         public class Horizontal

+ 1 - 0
tests/Avalonia.LeakTests/AvaloniaObjectTests.cs

@@ -6,6 +6,7 @@ using Xunit.Abstractions;
 
 namespace Avalonia.LeakTests
 {
+    [DotMemoryUnit(FailIfRunWithoutSupport = false)]
     public class AvaloniaObjectTests
     {
         public AvaloniaObjectTests(ITestOutputHelper atr)

+ 8 - 0
tests/Avalonia.LeakTests/MemberSelectorTests.cs

@@ -4,12 +4,20 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using JetBrains.dotMemoryUnit;
 using Xunit;
+using Xunit.Abstractions;
 
 namespace Avalonia.LeakTests
 {
+    [DotMemoryUnit(FailIfRunWithoutSupport = false)]
     public class MemberSelectorTests
     {
+        public MemberSelectorTests(ITestOutputHelper atr)
+        {
+            DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
+        }
+
         [Fact]
         public void Should_Not_Hold_Reference_To_Object()
         {

+ 23 - 1
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs

@@ -2,8 +2,8 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Collections.Generic;
 using System.Reactive.Linq;
+using System.Reactive.Subjects;
 using Avalonia.Markup.Data;
 using Avalonia.UnitTests;
 using Xunit;
@@ -54,6 +54,28 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.False(target.SetValue("foo"));
         }
 
+        /// <summary>
+        /// Test for #831 - Bound properties are incorrectly updated when changing tab items.
+        /// </summary>
+        /// <remarks>
+        /// There was a bug whereby pushing a null as the ExpressionObserver root didn't update
+        /// the leaf node, cauing a subsequent SetValue to update an object that should have become
+        /// unbound.
+        /// </remarks>
+        [Fact]
+        public void Pushing_Null_To_RootObservable_Updates_Leaf_Node()
+        {
+            var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
+            var rootObservable = new BehaviorSubject<Class1>(data);
+            var target = new ExpressionObserver(rootObservable, "Foo.Bar");
+
+            target.Subscribe(_ => { });
+            rootObservable.OnNext(null);
+            target.SetValue("baz");
+
+            Assert.Equal("bar", data.Foo.Bar);
+        }
+
         private class Class1 : NotifyingBase
         {
             private Class2 _foo;

+ 1 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@@ -96,6 +96,7 @@
     <Compile Include="Data\BindingTests_DataValidation.cs" />
     <Compile Include="Data\BindingTests_Source.cs" />
     <Compile Include="Data\BindingTests_ElementName.cs" />
+    <Compile Include="Data\BindingTests_Self.cs" />
     <Compile Include="Data\MultiBindingTests.cs" />
     <Compile Include="Data\BindingTests_TemplatedParent.cs" />
     <Compile Include="Data\BindingTests.cs" />

+ 63 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Self.cs

@@ -0,0 +1,63 @@
+// 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 Moq;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.Data;
+using Avalonia.Styling;
+using Xunit;
+using System.Reactive.Disposables;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Data
+{
+    public class BindingTests_Self
+    {
+        [Fact]
+        public void Binding_To_Property_On_Self_Should_Work()
+        {
+            var target = new TextBlock
+            {
+                Tag = "Hello World!",
+                [!TextBlock.TextProperty] = new Binding("Tag")
+                {
+                    RelativeSource = new RelativeSource(RelativeSourceMode.Self)
+                },
+            };
+
+            Assert.Equal("Hello World!", target.Text);
+        }
+
+        [Fact]
+        public void TwoWay_Binding_To_Property_On_Self_Should_Work()
+        {
+            var target = new TextBlock
+            {
+                Tag = "Hello World!",
+                [!TextBlock.TextProperty] = new Binding("Tag", BindingMode.TwoWay)
+                {
+                    RelativeSource = new RelativeSource(RelativeSourceMode.Self)
+                },
+            };
+
+            Assert.Equal("Hello World!", target.Text);
+            target.Text = "Goodbye cruel world :(";
+            Assert.Equal("Goodbye cruel world :(", target.Text);
+        }
+
+        private Mock<IControl> CreateTarget(
+            ITemplatedControl templatedParent = null,
+            string text = null)
+        {
+            var result = new Mock<IControl>();
+
+            result.Setup(x => x.GetValue(Control.TemplatedParentProperty)).Returns(templatedParent);
+            result.Setup(x => x.GetValue((AvaloniaProperty)Control.TemplatedParentProperty)).Returns(templatedParent);
+            result.Setup(x => x.GetValue((AvaloniaProperty)TextBox.TextProperty)).Returns(text);
+            result.Setup(x => x.Bind(It.IsAny<AvaloniaProperty>(), It.IsAny<IObservable<object>>(), It.IsAny<BindingPriority>()))
+                .Returns(Disposable.Empty);
+            return result;
+        }
+    }
+}

+ 44 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@@ -145,5 +145,49 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
                 Assert.Equal("foo", border.DataContext);
             }
         }
+
+        [Fact(Skip = "OmniXaml doesn't support nested markup extensions. #119")]
+        public void Binding_To_Self_Works()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <TextBlock Name='textblock' Text='{Binding Tag, RelativeSource={RelativeSource Self}}'/>
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var textBlock = (TextBlock)window.Content;
+
+                window.ApplyTemplate();
+
+                Assert.Equal("foo", textBlock.Text);
+            }
+        }
+
+        [Fact]
+        public void Longform_Binding_To_Self_Works()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <TextBlock Name='textblock' Tag='foo'>
+        <TextBlock.Text>
+            <Binding RelativeSource='{RelativeSource Self}' Path='Tag'/>
+        </TextBlock.Text>
+    </TextBlock>
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var textBlock = (TextBlock)window.Content;
+
+                window.ApplyTemplate();
+
+                Assert.Equal("foo", textBlock.Text);
+            }
+        }
     }
 }