Browse Source

Merge branch 'master' into refactor/new-value-store

Dan Walmsley 3 years ago
parent
commit
2e32945455
50 changed files with 1007 additions and 209 deletions
  1. 1 1
      .github/PULL_REQUEST_TEMPLATE.md
  2. 1 1
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  3. 1 0
      samples/IntegrationTestApp/MainWindow.axaml.cs
  4. 2 1
      samples/IntegrationTestApp/ShowWindowTest.axaml
  5. 4 2
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  6. 17 8
      src/Android/Avalonia.Android/AndroidPlatform.cs
  7. 20 1
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  8. 6 3
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  9. 46 10
      src/Avalonia.Base/Controls/ResourceDictionary.cs
  10. 5 0
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  11. 8 0
      src/Avalonia.Base/Controls/Templates/ITemplateResult.cs
  12. 2 1
      src/Avalonia.Base/Controls/Templates/TemplateResult.cs
  13. 2 0
      src/Avalonia.Base/Media/DrawingGroup.cs
  14. 14 0
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  15. 1 1
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  16. 12 1
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  17. 2 0
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  18. 3 0
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  19. 7 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  20. 1 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  21. 2 0
      src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  22. 10 7
      src/Avalonia.Controls/Converters/StringFormatConverter.cs
  23. 1 1
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  24. 8 2
      src/Avalonia.Controls/TransitioningContentControl.cs
  25. 5 0
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  26. 7 0
      src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs
  27. 15 0
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  28. 0 80
      src/Avalonia.ReactiveUI/TransitioningContentControl.cs
  29. 18 0
      src/Avalonia.ReactiveUI/ViewModelViewHost.cs
  30. 4 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  31. 126 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs
  32. 20 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
  33. 9 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  34. 118 34
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs
  35. 1 1
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  36. 18 0
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  37. 4 1
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  38. 88 9
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  39. 7 5
      src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs
  40. 0 15
      src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs
  41. 20 0
      src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs
  42. 1 0
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  43. 2 1
      src/Windows/Avalonia.Win32/WindowImpl.cs
  44. 4 1
      tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
  45. 44 5
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  46. 41 1
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  47. 29 0
      tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
  48. 43 0
      tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs
  49. 207 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs
  50. 0 8
      tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs

+ 1 - 1
.github/PULL_REQUEST_TEMPLATE.md

@@ -21,7 +21,7 @@
 - [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Documentation with user documentation
 
 ## Breaking changes
-<!--- List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes -->
+<!--- List any breaking changes here. -->
 
 ## Obsoletions / Deprecations
 <!--- Obsolete and Deprecated attributes on APIs MUST only be included when discussed with Core team. @grokys, @kekekeks & @danwalmsley -->

+ 1 - 1
native/Avalonia.Native/src/OSX/WindowImpl.mm

@@ -121,7 +121,7 @@ void WindowImpl::BringToFront()
 {
     if(Window != nullptr)
     {
-        if (![Window isMiniaturized])
+        if ([Window isVisible] && ![Window isMiniaturized])
         {
             if(IsDialog())
             {

+ 1 - 0
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -99,6 +99,7 @@ namespace IntegrationTestApp
 
             foreach (var window in lifetime.Windows)
             {
+                window.Show();
                 if (window.WindowState == WindowState.Minimized)
                     window.WindowState = WindowState.Normal;
             }

+ 2 - 1
samples/IntegrationTestApp/ShowWindowTest.axaml

@@ -3,7 +3,7 @@
         x:Class="IntegrationTestApp.ShowWindowTest"
         Name="SecondaryWindow"
         Title="Show Window Test">
-  <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
+  <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
     <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
     <TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
              Text="{Binding ClientSize, Mode=OneWay}"/>
@@ -31,5 +31,6 @@
       <ComboBoxItem>Maximized</ComboBoxItem>
       <ComboBoxItem>Fullscreen</ComboBoxItem>
     </ComboBox>
+    <Button Name="HideButton" Grid.Row="8" Command="{Binding $parent[Window].Hide}">Hide</Button>
   </Grid>
 </Window>

+ 4 - 2
samples/RenderDemo/Pages/CustomSkiaPage.cs

@@ -40,14 +40,16 @@ namespace RenderDemo.Pages
             static Stopwatch St = Stopwatch.StartNew();
             public void Render(IDrawingContextImpl context)
             {
-                var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
-                if (canvas == null)
+                var leaseFeature = context.GetFeature<ISkiaSharpApiLeaseFeature>();
+                if (leaseFeature == null)
                     using (var c = new DrawingContext(context, false))
                     {
                         c.DrawText(_noSkia, new Point());
                     }
                 else
                 {
+                    using var lease = leaseFeature.Lease();
+                    var canvas = lease.SkCanvas;
                     canvas.Save();
                     // create the first shader
                     var colors = new SKColor[] {

+ 17 - 8
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -8,7 +8,8 @@ using Avalonia.Input.Platform;
 using Avalonia.OpenGL.Egl;
 using Avalonia.Platform;
 using Avalonia.Rendering;
-using Avalonia.Skia;
+using Avalonia.Rendering.Composition;
+using Avalonia.OpenGL;
 
 namespace Avalonia
 {
@@ -16,10 +17,8 @@ namespace Avalonia
     {
         public static T UseAndroid<T>(this T builder) where T : AppBuilderBase<T>, new()
         {
-            var options = AvaloniaLocator.Current.GetService<AndroidPlatformOptions>() ?? new AndroidPlatformOptions();
-
             return builder
-                .UseWindowingSubsystem(() => AndroidPlatform.Initialize(options), "Android")
+                .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android")
                 .UseSkia();
         }
     }
@@ -42,9 +41,11 @@ namespace Avalonia.Android
 
         public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500);
 
-        public static void Initialize(AndroidPlatformOptions options)
+        internal static Compositor Compositor { get; private set; }
+
+        public static void Initialize()
         {
-            Options = options;
+            Options = AvaloniaLocator.Current.GetService<AndroidPlatformOptions>() ?? new AndroidPlatformOptions();
 
             AvaloniaLocator.CurrentMutable
                 .Bind<IClipboard>().ToTransient<ClipboardImpl>()
@@ -58,16 +59,24 @@ namespace Avalonia.Android
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
 
-            if (options.UseGpu)
+            if (Options.UseGpu)
             {
                 EglPlatformOpenGlInterface.TryInitialize();
             }
+            
+            if (Options.UseCompositor)
+            {
+                Compositor = new Compositor(
+                    AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
+                    AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
+            }
         }
     }
 
     public sealed class AndroidPlatformOptions
     {
-        public bool UseDeferredRendering { get; set; } = true;
+        public bool UseDeferredRendering { get; set; } = false;
         public bool UseGpu { get; set; } = true;
+        public bool UseCompositor { get; set; } = true;
     }
 }

+ 20 - 1
src/Android/Avalonia.Android/AndroidThreadingInterface.cs

@@ -14,6 +14,7 @@ namespace Avalonia.Android
     internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface
     {
         private Handler _handler;
+        private static Thread s_uiThread;
 
         public AndroidThreadingInterface()
         {
@@ -76,7 +77,25 @@ namespace Avalonia.Android
             EnsureInvokeOnMainThread(() => Signaled?.Invoke(null));
         }
 
-        public bool CurrentThreadIsLoopThread => Looper.MainLooper.Thread.Equals(Java.Lang.Thread.CurrentThread());
+        public bool CurrentThreadIsLoopThread
+        {
+            get
+            {
+                if (s_uiThread != null)
+                    return s_uiThread == Thread.CurrentThread;
+
+                var isOnMainThread = OperatingSystem.IsAndroidVersionAtLeast(23)
+                    ? Looper.MainLooper.IsCurrentThread
+                    : Looper.MainLooper.Thread.Equals(Java.Lang.Thread.CurrentThread());
+                if (isOnMainThread)
+                {
+                    s_uiThread = Thread.CurrentThread;
+                    return true;
+                }
+
+                return false;
+            }
+        }
         public event Action<DispatcherPriority?> Signaled;
     }
 }

+ 6 - 3
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -19,6 +19,7 @@ using Avalonia.OpenGL.Surfaces;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
 using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
@@ -84,9 +85,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer, Handle };
 
         public IRenderer CreateRenderer(IRenderRoot root) =>
-            AndroidPlatform.Options.UseDeferredRendering
-            ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
-            : new ImmediateRenderer(root);
+            AndroidPlatform.Options.UseCompositor
+                ? new CompositingRenderer(root, AndroidPlatform.Compositor)
+                : AndroidPlatform.Options.UseDeferredRendering
+                    ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
+                    : new ImmediateRenderer(root);
 
         public virtual void Hide()
         {

+ 46 - 10
src/Avalonia.Base/Controls/ResourceDictionary.cs

@@ -3,6 +3,7 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Collections;
+using Avalonia.Controls.Templates;
 
 namespace Avalonia.Controls
 {
@@ -29,7 +30,11 @@ namespace Avalonia.Controls
 
         public object? this[object key]
         {
-            get => _inner?[key];
+            get
+            {
+                TryGetValue(key, out var value);
+                return value;
+            }
             set
             {
                 Inner[key] = value;
@@ -119,6 +124,12 @@ namespace Avalonia.Controls
             Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
         }
 
+        public void AddDeferred(object key, Func<IServiceProvider?, object?> factory)
+        {
+            Inner.Add(key, new DeferredItem(factory));
+            Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
+        }
+
         public void Clear()
         {
             if (_inner?.Count > 0)
@@ -143,10 +154,8 @@ namespace Avalonia.Controls
 
         public bool TryGetResource(object key, out object? value)
         {
-            if (_inner is not null && _inner.TryGetValue(key, out value))
-            {
+            if (TryGetValue(key, out value))
                 return true;
-            }
 
             if (_mergedDictionaries != null)
             {
@@ -165,12 +174,28 @@ namespace Avalonia.Controls
 
         public bool TryGetValue(object key, out object? value)
         {
-            if (_inner is not null)
-                return _inner.TryGetValue(key, out value);
+            if (_inner is not null && _inner.TryGetValue(key, out value))
+            {
+                if (value is DeferredItem deffered)
+                {
+                    _inner[key] = value = deffered.Factory(null) switch
+                    {
+                        ITemplateResult t => t.Result,
+                        object v => v,
+                        _ => null,
+                    };
+                }
+                return true;
+            }
+
             value = null;
             return false;
         }
 
+        public IEnumerator<KeyValuePair<object, object?>> GetEnumerator()
+        {
+            return _inner?.GetEnumerator() ?? Enumerable.Empty<KeyValuePair<object, object?>>().GetEnumerator();
+        }
 
         void ICollection<KeyValuePair<object, object?>>.Add(KeyValuePair<object, object?> item)
         {
@@ -198,12 +223,17 @@ namespace Avalonia.Controls
             return false;
         }
 
-        public IEnumerator<KeyValuePair<object, object?>> GetEnumerator()
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+        internal bool ContainsDeferredKey(object key)
         {
-            return _inner?.GetEnumerator() ?? Enumerable.Empty<KeyValuePair<object, object?>>().GetEnumerator();
-        }
+            if (_inner is not null && _inner.TryGetValue(key, out var result))
+            {
+                return result is DeferredItem;
+            }
 
-        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+            return false;
+        }
 
         void IResourceProvider.AddOwner(IResourceHost owner)
         {
@@ -258,5 +288,11 @@ namespace Avalonia.Controls
                 }
             }
         }
+
+        private class DeferredItem
+        {
+            public DeferredItem(Func<IServiceProvider?, object?> factory) => Factory = factory;
+            public Func<IServiceProvider?, object?> Factory { get; }
+        }
     }
 }

+ 5 - 0
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

@@ -132,6 +132,11 @@ namespace Avalonia.Controls
             {
                 _target.OwnerChanged += OwnerChanged;
                 _owner = _target.Owner;
+
+                if (_owner is object)
+                {
+                    _owner.ResourcesChanged += ResourcesChanged;
+                }
             }
 
             protected override void Deinitialize()

+ 8 - 0
src/Avalonia.Base/Controls/Templates/ITemplateResult.cs

@@ -0,0 +1,8 @@
+namespace Avalonia.Controls.Templates
+{
+    public interface ITemplateResult
+    {
+        public object? Result { get; }
+        public INameScope NameScope { get; }
+    }
+}

+ 2 - 1
src/Avalonia.Controls/Templates/TemplateResult.cs → src/Avalonia.Base/Controls/Templates/TemplateResult.cs

@@ -1,9 +1,10 @@
 namespace Avalonia.Controls.Templates
 {
-    public class TemplateResult<T>
+    public class TemplateResult<T> : ITemplateResult
     {
         public T Result { get; }
         public INameScope NameScope { get; }
+        object? ITemplateResult.Result => Result;
 
         public TemplateResult(T result, INameScope nameScope)
         {

+ 2 - 0
src/Avalonia.Base/Media/DrawingGroup.cs

@@ -228,6 +228,8 @@ namespace Avalonia.Media
                 throw new NotImplementedException();
             }
 
+            public object? GetFeature(Type t) => null;
+
             public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
             {
                 throw new NotImplementedException();

+ 14 - 0
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@@ -172,6 +172,20 @@ namespace Avalonia.Platform
         /// </summary>
         /// <param name="custom">Custom draw operation</param>
         void Custom(ICustomDrawOperation custom);
+
+        /// <summary>
+        /// Attempts to get an optional feature from the drawing context implementation
+        /// </summary>
+        object? GetFeature(Type t);
+    }
+
+    public static class DrawingContextImplExtensions
+    {
+        /// <summary>
+        /// Attempts to get an optional feature from the drawing context implementation
+        /// </summary>
+        public static T? GetFeature<T>(this IDrawingContextImpl context) where T : class =>
+            (T?)context.GetFeature(typeof(T));
     }
 
     public interface IDrawingContextLayerImpl : IRenderTargetBitmapImpl

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@@ -71,7 +71,7 @@ public class CompositingRenderer : IRendererWithCompositor
         if(_queuedUpdate)
             return;
         _queuedUpdate = true;
-        Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition);
+        _compositor.InvokeWhenReadyForNextCommit(_update);
     }
     
     /// <inheritdoc/>

+ 12 - 1
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@@ -33,6 +33,7 @@ namespace Avalonia.Rendering.Composition
         internal IEasing DefaultEasing { get; }
         private List<Action>? _invokeOnNextCommit;
         private readonly Stack<List<Action>> _invokeListPool = new();
+        private Task? _lastBatchCompleted;
 
         /// <summary>
         /// Creates a new compositor on a specified render loop that would use a particular GPU
@@ -86,7 +87,7 @@ namespace Avalonia.Rendering.Composition
             if (_invokeOnNextCommit != null) 
                 ScheduleCommitCallbacks(batch.Completed);
             
-            return batch.Completed;
+            return _lastBatchCompleted = batch.Completed;
         }
 
         async void ScheduleCommitCallbacks(Task task)
@@ -139,5 +140,15 @@ namespace Avalonia.Rendering.Composition
             _invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new();
             _invokeOnNextCommit.Add(action);
         }
+
+        public void InvokeWhenReadyForNextCommit(Action action)
+        {
+            if (_lastBatchCompleted == null || _lastBatchCompleted.IsCompleted)
+                Dispatcher.UIThread.Post(action, DispatcherPriority.Composition);
+            else
+                _lastBatchCompleted.ContinueWith(
+                    static (_, state) => Dispatcher.UIThread.Post((Action)state!, DispatcherPriority.Composition),
+                    action);
+        }
     }
 }

+ 2 - 0
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@@ -156,6 +156,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
             ++_drawOperationIndex;
     }
 
+    public object? GetFeature(Type t) => null;
+
     /// <inheritdoc/>
     public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
     {

+ 3 - 0
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Numerics;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
@@ -155,6 +156,8 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
         _impl.Custom(custom);
     }
 
+    public object? GetFeature(Type t) => _impl.GetFeature(t);
+
     public class VisualBrushRenderer : IVisualBrushRenderer
     {
         public CompositionDrawList? VisualBrushDrawList { get; set; }

+ 7 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@@ -78,6 +78,13 @@ namespace Avalonia.Rendering.Composition.Server
 
             if (Root == null) 
                 return;
+
+            if ((_renderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true)
+            {
+                _renderTarget!.Dispose();
+                _renderTarget = null;
+            }
+
             _renderTarget ??= _renderTargetFactory();
 
             Compositor.UpdateServerTime();

+ 1 - 2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@@ -108,6 +108,7 @@ namespace Avalonia.Rendering.Composition.Server
         private void RenderCore()
         {
             ApplyPendingBatches();
+            CompletePendingBatches();
             
             foreach(var animation in _activeAnimations)
                 _animationsToUpdate.Add(animation);
@@ -119,8 +120,6 @@ namespace Avalonia.Rendering.Composition.Server
             
             foreach (var t in _activeTargets)
                 t.Render();
-            
-            CompletePendingBatches();
         }
 
         public void AddCompositionTarget(ServerCompositionTarget target)

+ 2 - 0
src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -203,6 +203,8 @@ namespace Avalonia.Rendering.SceneGraph
                 ++_drawOperationindex;
         }
 
+        public object? GetFeature(Type t) => null;
+
         /// <inheritdoc/>
         public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
         {

+ 10 - 7
src/Avalonia.Controls/Converters/StringFormatConverter.cs

@@ -2,7 +2,6 @@
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
-using Avalonia.Data;
 using Avalonia.Data.Converters;
 
 namespace Avalonia.Controls.Converters;
@@ -15,13 +14,17 @@ public class StringFormatConverter : IMultiValueConverter
 {
     public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
     {
-        try
+        if (values[0] is string format)
         {
-            return string.Format((string)values[0]!, values.Skip(1).ToArray());
-        }
-        catch (Exception e)
-        {
-            return new BindingNotification(e, BindingErrorType.Error);
+            try
+            {
+                return string.Format(format, values.Skip(1).ToArray());
+            }
+            catch
+            {
+                return AvaloniaProperty.UnsetValue;
+            }
         }
+        return AvaloniaProperty.UnsetValue;
     }
 }

+ 1 - 1
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@@ -187,7 +187,7 @@ namespace Avalonia.Controls.Presenters
                         break;
 
                     case NotifyCollectionChangedAction.Remove:
-                        if ((e.OldStartingIndex >= FirstIndex && e.OldStartingIndex < NextIndex) ||
+                        if (e.OldStartingIndex < NextIndex ||
                             panel.Children.Count > ItemCount)
                         {
                             RecycleContainersOnRemove();

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

@@ -84,13 +84,19 @@ public class TransitioningContentControl : ContentControl
 
         _lastTransitionCts?.Cancel();
         _lastTransitionCts = new CancellationTokenSource();
+        var localToken = _lastTransitionCts.Token;
 
         if (PageTransition != null)
-            await PageTransition.Start(this, null, true, _lastTransitionCts.Token);
+            await PageTransition.Start(this, null, true, localToken);
+
+        if (localToken.IsCancellationRequested)
+        {
+            return;
+        }
 
         CurrentContent = content;
 
         if (PageTransition != null)
-            await PageTransition.Start(null, this, true, _lastTransitionCts.Token);
+            await PageTransition.Start(null, this, true, localToken);
     }
 }

+ 5 - 0
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -416,6 +416,11 @@ namespace Avalonia.Headless
 
             }
 
+            public object GetFeature(Type t)
+            {
+                return null;
+            }
+
             public void DrawLine(IPen pen, Point p1, Point p2)
             {
             }

+ 7 - 0
src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs

@@ -146,6 +146,13 @@ namespace Avalonia.OpenGL.Controls
                 return false;
             }
 
+            if (_context == null)
+            {
+                Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase",
+                    "Unable to initialize OpenGL: unable to create additional OpenGL context.");
+                return false;
+            }
+
             GlVersion = _context.Version;
             try
             {

+ 15 - 0
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@@ -64,6 +64,12 @@ namespace Avalonia.ReactiveUI
         public static readonly StyledProperty<string?> ViewContractProperty =
             AvaloniaProperty.Register<RoutedViewHost, string?>(nameof(ViewContract));
 
+        /// <summary>
+        /// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
+        /// </summary>
+        public static readonly StyledProperty<object?> DefaultContentProperty =
+            ViewModelViewHost.DefaultContentProperty.AddOwner<RoutedViewHost>();
+
         /// <summary>
         /// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
         /// </summary>
@@ -106,6 +112,15 @@ namespace Avalonia.ReactiveUI
             set => SetValue(ViewContractProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets the content displayed whenever there is no page currently routed.
+        /// </summary>
+        public object? DefaultContent
+        {
+            get => GetValue(DefaultContentProperty);
+            set => SetValue(DefaultContentProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the ReactiveUI view locator used by this router.
         /// </summary>

+ 0 - 80
src/Avalonia.ReactiveUI/TransitioningContentControl.cs

@@ -1,80 +0,0 @@
-using System;
-using System.Threading;
-
-using Avalonia.Animation;
-using Avalonia.Controls;
-using Avalonia.Styling;
-
-namespace Avalonia.ReactiveUI
-{
-    /// <summary>
-    /// A ContentControl that animates the transition when its content is changed.
-    /// </summary>
-    [Obsolete("Use TransitioningContentControl in Avalonia.Controls namespace")]
-    public class TransitioningContentControl : ContentControl, IStyleable
-    {
-        /// <summary>
-        /// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property.
-        /// </summary>
-        public static readonly StyledProperty<IPageTransition?> PageTransitionProperty =
-            AvaloniaProperty.Register<TransitioningContentControl, IPageTransition?>(nameof(PageTransition),
-                new CrossFade(TimeSpan.FromSeconds(0.5)));
-
-        /// <summary>
-        /// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
-        /// </summary>
-        public static readonly StyledProperty<object?> DefaultContentProperty =
-            AvaloniaProperty.Register<TransitioningContentControl, object?>(nameof(DefaultContent));
-
-        private CancellationTokenSource? _lastTransitionCts;
-
-        /// <summary>
-        /// Gets or sets the animation played when content appears and disappears.
-        /// </summary>
-        public IPageTransition? PageTransition
-        {
-            get => GetValue(PageTransitionProperty);
-            set => SetValue(PageTransitionProperty, value);
-        }
-
-        /// <summary>
-        /// Gets or sets the content displayed whenever there is no page currently routed.
-        /// </summary>
-        public object? DefaultContent
-        {
-            get => GetValue(DefaultContentProperty);
-            set => SetValue(DefaultContentProperty, value);
-        }
-
-        /// <summary>
-        /// Gets or sets the content with animation.
-        /// </summary>
-        public new object? Content
-        {
-            get => base.Content;
-            set => UpdateContentWithTransition(value);
-        }
-        
-        /// <summary>
-        /// TransitioningContentControl uses the default ContentControl 
-        /// template from Avalonia default theme.
-        /// </summary>
-        Type IStyleable.StyleKey => typeof(ContentControl);
-
-        /// <summary>
-        /// Updates the content with transitions.
-        /// </summary>
-        /// <param name="content">New content to set.</param>
-        private async void UpdateContentWithTransition(object? content)
-        {
-            _lastTransitionCts?.Cancel();
-            _lastTransitionCts = new CancellationTokenSource();
-
-            if (PageTransition != null)
-                await PageTransition.Start(this, null, true, _lastTransitionCts.Token);
-            base.Content = content;
-            if (PageTransition != null)
-                await PageTransition.Start(null, this, true, _lastTransitionCts.Token);
-        }
-    }
-}

+ 18 - 0
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@@ -1,5 +1,8 @@
 using System;
 using System.Reactive.Disposables;
+
+using Avalonia.Controls;
+
 using ReactiveUI;
 using Splat;
 
@@ -24,6 +27,12 @@ namespace Avalonia.ReactiveUI
         public static readonly StyledProperty<string?> ViewContractProperty =
             AvaloniaProperty.Register<ViewModelViewHost, string?>(nameof(ViewContract));
 
+        /// <summary>
+        /// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
+        /// </summary>
+        public static readonly StyledProperty<object?> DefaultContentProperty =
+            AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(DefaultContent));
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
         /// </summary>
@@ -55,6 +64,15 @@ namespace Avalonia.ReactiveUI
             set => SetValue(ViewContractProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets the content displayed whenever there is no page currently routed.
+        /// </summary>
+        public object? DefaultContent
+        {
+            get => GetValue(DefaultContentProperty);
+            set => SetValue(DefaultContentProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the view locator.
         /// </summary>

+ 4 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@@ -60,6 +60,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             InsertAfter<TypeReferenceResolver>(
                 new XDataTypeTransformer());
 
+            InsertBefore<DeferredContentTransformer>(
+                new AvaloniaXamlIlDeferredResourceTransformer()
+            );
+
             // After everything else
             InsertBefore<NewObjectTransformer>(
                 new AddNameScopeRegistration(),

+ 126 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDeferredResourceTransformer.cs

@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using XamlX.Ast;
+using XamlX.Emit;
+using XamlX.IL;
+using XamlX.Transform;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
+{
+    internal class AvaloniaXamlIlDeferredResourceTransformer : IXamlAstTransformer
+    {
+        public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+        {
+            if (!(node is XamlPropertyAssignmentNode pa) || pa.Values.Count != 2)
+                return node;
+
+            if (!ShouldBeDeferred(pa.Values[1]))
+                return node;
+
+            var types = context.GetAvaloniaTypes();
+
+            if (pa.Property.DeclaringType == types.ResourceDictionary && pa.Property.Name == "Content")
+            {
+                pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration);
+                pa.PossibleSetters = new List<IXamlPropertySetter>
+                {
+                    new XamlDirectCallPropertySetter(types.ResourceDictionaryDeferredAdd),
+                };
+            }
+            else if (pa.Property.Name == "Resources" && pa.Property.Getter.ReturnType.Equals(types.IResourceDictionary))
+            {
+                pa.Values[1] = new XamlDeferredContentNode(pa.Values[1], types.XamlIlTypes.Object, context.Configuration);
+                pa.PossibleSetters = new List<IXamlPropertySetter>
+                {
+                    new AdderSetter(pa.Property.Getter, types.ResourceDictionaryDeferredAdd),
+                };
+            }
+
+            return node;
+        }
+
+        private static bool ShouldBeDeferred(IXamlAstValueNode node)
+        {
+            // XAML compiler is currently strict about value types, allowing them to be created only through converters.
+            // At the moment it should be safe to not defer structs.
+            return !node.Type.GetClrType().IsValueType;
+        }
+        
+        class AdderSetter : IXamlILOptimizedEmitablePropertySetter, IEquatable<AdderSetter>
+        {
+            private readonly IXamlMethod _getter;
+            private readonly IXamlMethod _adder;
+
+            public AdderSetter(IXamlMethod getter, IXamlMethod adder)
+            {
+                _getter = getter;
+                _adder = adder;
+                TargetType = getter.DeclaringType;
+                Parameters = adder.ParametersWithThis().Skip(1).ToList();
+
+                bool allowNull = Parameters.Last().AcceptsNull();
+                BinderParameters = new PropertySetterBinderParameters
+                {
+                    AllowMultiple = true,
+                    AllowXNull = allowNull,
+                    AllowRuntimeNull = allowNull
+                };
+            }
+
+            public IXamlType TargetType { get; }
+
+            public PropertySetterBinderParameters BinderParameters { get; }
+
+            public IReadOnlyList<IXamlType> Parameters { get; }
+
+            public void Emit(IXamlILEmitter emitter)
+            {
+                var locals = new Stack<XamlLocalsPool.PooledLocal>();
+                // Save all "setter" parameters
+                for (var c = Parameters.Count - 1; c >= 0; c--)
+                {
+                    var loc = emitter.LocalsPool.GetLocal(Parameters[c]);
+                    locals.Push(loc);
+                    emitter.Stloc(loc.Local);
+                }
+
+                emitter.EmitCall(_getter);
+                while (locals.Count>0)
+                    using (var loc = locals.Pop())
+                        emitter.Ldloc(loc.Local);
+                emitter.EmitCall(_adder, true);
+            }
+
+            public void EmitWithArguments(
+                XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context,
+                IXamlILEmitter emitter,
+                IReadOnlyList<IXamlAstValueNode> arguments)
+            {
+                emitter.EmitCall(_getter);
+
+                for (var i = 0; i < arguments.Count; ++i)
+                    context.Emit(arguments[i], emitter, Parameters[i]);
+
+                emitter.EmitCall(_adder, true);
+            }
+
+            public bool Equals(AdderSetter other)
+            {
+                if (ReferenceEquals(null, other))
+                    return false;
+                if (ReferenceEquals(this, other))
+                    return true;
+
+                return _getter.Equals(other._getter) && _adder.Equals(other._adder);
+            }
+
+            public override bool Equals(object obj)
+                => Equals(obj as AdderSetter);
+
+            public override int GetHashCode()
+                => (_getter.GetHashCode() * 397) ^ _adder.GetHashCode();
+        }
+    }
+}

+ 20 - 6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs

@@ -75,17 +75,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
             {
                 Getter = setterType.Methods.First(m => m.Name == "get_Value");
                 var method = setterType.Methods.First(m => m.Name == "set_Value");
-                Setters.Add(new XamlIlDirectCallPropertySetter(method, types.IBinding));
-                Setters.Add(new XamlIlDirectCallPropertySetter(method, types.UnsetValueType));
-                Setters.Add(new XamlIlDirectCallPropertySetter(method, targetType));
+                Setters.Add(new XamlIlDirectCallPropertySetter(method, types.IBinding, false));
+                Setters.Add(new XamlIlDirectCallPropertySetter(method, types.UnsetValueType, false));
+                Setters.Add(new XamlIlDirectCallPropertySetter(method, targetType, targetType.AcceptsNull()));
             }
             
-            class XamlIlDirectCallPropertySetter : IXamlPropertySetter, IXamlEmitablePropertySetter<IXamlILEmitter>
+            sealed class XamlIlDirectCallPropertySetter : IXamlPropertySetter, IXamlEmitablePropertySetter<IXamlILEmitter>
             {
                 private readonly IXamlMethod _method;
                 private readonly IXamlType _type;
                 public IXamlType TargetType { get; }
-                public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters();
+                public PropertySetterBinderParameters BinderParameters { get; }
                 public IReadOnlyList<IXamlType> Parameters { get; }
                 public void Emit(IXamlILEmitter codegen)
                 {
@@ -94,13 +94,27 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
                     codegen.EmitCall(_method, true);
                 }
 
-                public XamlIlDirectCallPropertySetter(IXamlMethod method, IXamlType type)
+                public XamlIlDirectCallPropertySetter(IXamlMethod method, IXamlType type, bool allowNull)
                 {
                     _method = method;
                     _type = type;
                     Parameters = new[] {type};
                     TargetType = method.ThisOrFirstParameter();
+                    BinderParameters = new PropertySetterBinderParameters
+                    {
+                        AllowXNull = allowNull,
+                        AllowRuntimeNull = allowNull
+                    };
                 }
+
+                private bool Equals(XamlIlDirectCallPropertySetter other) 
+                    => Equals(_method, other._method) && Equals(_type, other._type);
+
+                public override bool Equals(object obj) 
+                    => Equals(obj as XamlIlDirectCallPropertySetter);
+
+                public override int GetHashCode() 
+                    => (_method.GetHashCode() * 397) ^ _type.GetHashCode();
             }
         }
     }

+ 9 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@@ -98,6 +98,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
         public IXamlType TextDecorations { get; }
         public IXamlType TextTrimming { get; }
         public IXamlType ISetter { get; }
+        public IXamlType IResourceDictionary { get; }
+        public IXamlType ResourceDictionary { get; }
+        public IXamlMethod ResourceDictionaryDeferredAdd { get; }
 
         public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
         {
@@ -218,6 +221,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
             TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations");
             TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming");
             ISetter = cfg.TypeSystem.GetType("Avalonia.Styling.ISetter");
+            IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary");
+            ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary");
+            ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object,
+                cfg.TypeSystem.GetType("System.Func`2").MakeGenericType(
+                    cfg.TypeSystem.GetType("System.IServiceProvider"),
+                    XamlIlTypes.Object));
         }
     }
 

+ 118 - 34
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs

@@ -206,38 +206,64 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             Setters.Insert(0, new UnsetValueSetter(types, original.DeclaringType, field));
         }
 
-        abstract class AvaloniaPropertyCustomSetter : IXamlPropertySetter, IXamlEmitablePropertySetter<IXamlILEmitter>
+        abstract class AvaloniaPropertyCustomSetter : IXamlILOptimizedEmitablePropertySetter, IEquatable<AvaloniaPropertyCustomSetter>
         {
-            protected AvaloniaXamlIlWellKnownTypes Types;
-            protected IXamlField AvaloniaProperty;
+            protected readonly AvaloniaXamlIlWellKnownTypes Types;
+            protected readonly IXamlField AvaloniaProperty;
 
-            public AvaloniaPropertyCustomSetter(AvaloniaXamlIlWellKnownTypes types,
+            protected AvaloniaPropertyCustomSetter(
+                AvaloniaXamlIlWellKnownTypes types,
                 IXamlType declaringType,
-                IXamlField avaloniaProperty)
+                IXamlField avaloniaProperty,
+                bool allowNull)
             {
                 Types = types;
                 AvaloniaProperty = avaloniaProperty;
                 TargetType = declaringType;
+                BinderParameters = new PropertySetterBinderParameters
+                {
+                    AllowXNull = allowNull,
+                    AllowRuntimeNull = allowNull
+                };
             }
 
             public IXamlType TargetType { get; }
 
-            public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters
-            {
-                AllowXNull = false
-            };
+            public PropertySetterBinderParameters BinderParameters { get; }
 
             public IReadOnlyList<IXamlType> Parameters { get; set; }
-            public abstract void Emit(IXamlILEmitter codegen);
+
+            public abstract void Emit(IXamlILEmitter emitter);
+
+            public abstract void EmitWithArguments(
+                XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context,
+                IXamlILEmitter emitter,
+                IReadOnlyList<IXamlAstValueNode> arguments);
+
+            public bool Equals(AvaloniaPropertyCustomSetter other)
+            {
+                if (ReferenceEquals(null, other))
+                    return false;
+                if (ReferenceEquals(this, other))
+                    return true;
+
+                return GetType() == other.GetType() && AvaloniaProperty.Equals(other.AvaloniaProperty);
+            }
+
+            public override bool Equals(object obj)
+                => Equals(obj as AvaloniaPropertyCustomSetter);
+
+            public override int GetHashCode() 
+                => AvaloniaProperty.GetHashCode();
         }
 
         class BindingSetter : AvaloniaPropertyCustomSetter
         {
             public BindingSetter(AvaloniaXamlIlWellKnownTypes types,
                 IXamlType declaringType,
-                IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty)
+                IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty, false)
             {
-                Parameters = new[] {types.IBinding};
+                Parameters = new[] { types.IBinding };
             }
 
             public override void Emit(IXamlILEmitter emitter)
@@ -246,10 +272,25 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                     emitter
                         .Stloc(bloc.Local)
                         .Ldsfld(AvaloniaProperty)
-                        .Ldloc(bloc.Local)
-                        // TODO: provide anchor?
-                        .Ldnull();
-                emitter.EmitCall(Types.AvaloniaObjectBindMethod, true);
+                        .Ldloc(bloc.Local);
+                EmitAnchorAndBind(emitter);
+            }
+
+            public override void EmitWithArguments(
+                XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context,
+                IXamlILEmitter emitter,
+                IReadOnlyList<IXamlAstValueNode> arguments)
+            {
+                emitter.Ldsfld(AvaloniaProperty);
+                context.Emit(arguments[0], emitter, Parameters[0]);
+                EmitAnchorAndBind(emitter);
+            }
+
+            private void EmitAnchorAndBind(IXamlILEmitter emitter)
+            {
+                emitter
+                    .Ldnull() // TODO: provide anchor?
+                    .EmitCall(Types.AvaloniaObjectBindMethod, true);
             }
         }
 
@@ -257,7 +298,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
         {
             public BindingWithPrioritySetter(AvaloniaXamlIlWellKnownTypes types,
                 IXamlType declaringType,
-                IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty)
+                IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty, false)
             {
                 Parameters = new[] { types.BindingPriority, types.IBinding };
             }
@@ -265,15 +306,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             public override void Emit(IXamlILEmitter emitter)
             {
                 using (var bloc = emitter.LocalsPool.GetLocal(Types.IBinding))
-                using (var priorityLocal = emitter.LocalsPool.GetLocal(Types.Int))
                     emitter
                         .Stloc(bloc.Local)
-                        .Stloc(priorityLocal.Local)
+                        .Pop() // ignore priority
                         .Ldsfld(AvaloniaProperty)
-                        .Ldloc(bloc.Local)
-                        // TODO: provide anchor?
-                        .Ldnull();
-                emitter.EmitCall(Types.AvaloniaObjectBindMethod, true);
+                        .Ldloc(bloc.Local);
+                EmitAnchorAndBind(emitter);
+            }
+
+            public override void EmitWithArguments(
+                XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context,
+                IXamlILEmitter emitter,
+                IReadOnlyList<IXamlAstValueNode> arguments)
+            {
+                emitter.Ldsfld(AvaloniaProperty);
+                context.Emit(arguments[1], emitter, Parameters[1]);
+                EmitAnchorAndBind(emitter);
+            }
+
+            private void EmitAnchorAndBind(IXamlILEmitter emitter)
+            {
+                emitter
+                    .Ldnull() // TODO: provide anchor?
+                    .EmitCall(Types.AvaloniaObjectBindMethod, true);
             }
         }
 
@@ -281,7 +336,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
         {
             public SetValueWithPrioritySetter(AvaloniaXamlIlWellKnownTypes types, IXamlType declaringType, IXamlField avaloniaProperty,
                 IXamlType propertyType)
-                : base(types, declaringType, avaloniaProperty)
+                : base(types, declaringType, avaloniaProperty, propertyType.AcceptsNull())
             {
                 Parameters = new[] { types.BindingPriority, propertyType };
             }
@@ -295,9 +350,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                    - value
                 */
 
-                var method = Types.AvaloniaObjectSetStyledPropertyValue
-                    .MakeGenericMethod(new[] { Parameters[1] });
-
                 using (var valueLocal = emitter.LocalsPool.GetLocal(Parameters[1]))
                 using (var priorityLocal = emitter.LocalsPool.GetLocal(Types.Int))
                     emitter
@@ -305,25 +357,57 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                         .Stloc(priorityLocal.Local)
                         .Ldsfld(AvaloniaProperty)
                         .Ldloc(valueLocal.Local)
-                        .Ldloc(priorityLocal.Local)
-                        .EmitCall(method, true);
+                        .Ldloc(priorityLocal.Local);
+
+                EmitSetStyledPropertyValue(emitter);
+            }
+
+            public override void EmitWithArguments(
+                XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context,
+                IXamlILEmitter emitter,
+                IReadOnlyList<IXamlAstValueNode> arguments)
+            {
+                emitter.Ldsfld(AvaloniaProperty);
+                context.Emit(arguments[1], emitter, Parameters[1]);
+                context.Emit(arguments[0], emitter, Parameters[0]);
+                EmitSetStyledPropertyValue(emitter);
+            }
+
+            private void EmitSetStyledPropertyValue(IXamlILEmitter emitter)
+            {
+                var method = Types.AvaloniaObjectSetStyledPropertyValue.MakeGenericMethod(new[] { Parameters[1] });
+                emitter.EmitCall(method, true);
             }
         }
 
         class UnsetValueSetter : AvaloniaPropertyCustomSetter
         {
             public UnsetValueSetter(AvaloniaXamlIlWellKnownTypes types, IXamlType declaringType, IXamlField avaloniaProperty) 
-                : base(types, declaringType, avaloniaProperty)
+                : base(types, declaringType, avaloniaProperty, false)
             {
-                Parameters = new[] {types.UnsetValueType};
+                Parameters = new[] { types.UnsetValueType };
             }
 
             public override void Emit(IXamlILEmitter codegen)
             {
+                codegen.Pop();
+                EmitSetValue(codegen);
+            }
+
+            public override void EmitWithArguments(
+                XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context,
+                IXamlILEmitter emitter,
+                IReadOnlyList<IXamlAstValueNode> arguments)
+            {
+                EmitSetValue(emitter);
+            }
+
+            private void EmitSetValue(IXamlILEmitter emitter)
+            {
+                // Ignore the instance and load one from the static field to avoid extra local variable
                 var unsetValue = Types.AvaloniaProperty.Fields.First(f => f.Name == "UnsetValue");
-                codegen
-                    // Ignore the instance and load one from the static field to avoid extra local variable
-                    .Pop()
+
+                emitter
                     .Ldsfld(AvaloniaProperty)
                     .Ldsfld(unsetValue)
                     .Ldc_I4(0)

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@@ -1 +1 @@
-Subproject commit a4e6be2d1407abec4f35fcb208848830ce513ead
+Subproject commit c1c0594ec2c35b08988183b1a5b3e34dfa19179d

+ 18 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs

@@ -39,6 +39,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
                 targetType = setter.Property.PropertyType;
             }
 
+            var previousWasControlTheme = false;
+
             // Look upwards though the ambient context for IResourceNodes
             // which might be able to give us the resource.
             foreach (var parent in stack.Parents)
@@ -47,6 +49,21 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
                 {
                     return ColorToBrushConverter.Convert(value, targetType);
                 }
+
+                // HACK: Temporary fix for #8678. Hard-coded to only work for the DevTools main
+                // window as we don't want 3rd parties to start relying on this hack.
+                //
+                // We need to implement compile-time merging of resource dictionaries and this
+                // hack can be removed.
+                if (previousWasControlTheme &&
+                    parent is ResourceDictionary hack &&
+                    hack.Owner?.GetType().FullName == "Avalonia.Diagnostics.Views.MainWindow" &&
+                    hack.Owner.TryGetResource(ResourceKey, out value))
+                {
+                    return ColorToBrushConverter.Convert(value, targetType);
+                }
+
+                previousWasControlTheme = parent is ControlTheme;
             }
 
             if (provideTarget.TargetObject is IControl target &&
@@ -69,3 +86,4 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
         }
     }
 }
+

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

@@ -19,6 +19,7 @@ namespace Avalonia.Data
         private bool _isSetterValue;
         private IStyledElement _target = default!;
         private Type? _targetType;
+        private bool _hasProducedValue;
 
         public TemplateBinding()
         {
@@ -143,10 +144,12 @@ namespace Avalonia.Data
                 }
 
                 PublishNext(value);
+                _hasProducedValue = true;
             }
-            else
+            else if (_hasProducedValue)
             {
                 PublishNext(AvaloniaProperty.UnsetValue);
+                _hasProducedValue = false;
             }
         }
 

+ 88 - 9
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -10,6 +10,7 @@ using Avalonia.Rendering.SceneGraph;
 using Avalonia.Rendering.Utilities;
 using Avalonia.Utilities;
 using Avalonia.Media.Imaging;
+using JetBrains.Annotations;
 using SkiaSharp;
 
 namespace Avalonia.Skia
@@ -17,7 +18,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// Skia based drawing context.
     /// </summary>
-    internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
+    internal class DrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
     {
         private IDisposable[] _disposables;
         private readonly Vector _dpi;
@@ -38,7 +39,8 @@ namespace Avalonia.Skia
         private readonly SKPaint _fillPaint = new SKPaint();
         private readonly SKPaint _boxShadowPaint = new SKPaint();
         private static SKShader s_acrylicNoiseShader;
-        private readonly ISkiaGpuRenderSession _session; 
+        private readonly ISkiaGpuRenderSession _session;
+        private bool _leased = false;
 
         /// <summary>
         /// Context create info.
@@ -83,6 +85,47 @@ namespace Avalonia.Skia
             public ISkiaGpuRenderSession CurrentSession;
         }
 
+        class SkiaLeaseFeature : ISkiaSharpApiLeaseFeature
+        {
+            private readonly DrawingContextImpl _context;
+
+            public SkiaLeaseFeature(DrawingContextImpl context)
+            {
+                _context = context;
+            }
+
+            public ISkiaSharpApiLease Lease()
+            {
+                _context.CheckLease();
+                return new ApiLease(_context);
+            }
+
+            class ApiLease : ISkiaSharpApiLease
+            {
+                private DrawingContextImpl _context;
+                private readonly SKMatrix _revertTransform;
+
+                public ApiLease(DrawingContextImpl context)
+                {
+                    _revertTransform = context.Canvas.TotalMatrix;
+                    _context = context;
+                    _context._leased = true;
+                }
+
+                public SKCanvas SkCanvas => _context.Canvas;
+                public GRContext GrContext => _context.GrContext;
+                public SKSurface SkSurface => _context.Surface;
+                public double CurrentOpacity => _context._currentOpacity;
+                
+                public void Dispose()
+                {
+                    _context.Canvas.SetMatrix(_revertTransform);
+                    _context._leased = false;
+                    _context = null;
+                }
+            }
+        }
+        
         /// <summary>
         /// Create new drawing context.
         /// </summary>
@@ -123,20 +166,23 @@ namespace Avalonia.Skia
         public SKCanvas Canvas { get; }
         public SKSurface Surface { get; }
 
-        SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas;
-        SKSurface ISkiaDrawingContextImpl.SkSurface => Surface;
-        GRContext ISkiaDrawingContextImpl.GrContext => _grContext;
-        double ISkiaDrawingContextImpl.CurrentOpacity => _currentOpacity;
-
+        private void CheckLease()
+        {
+            if (_leased)
+                throw new InvalidOperationException("The underlying graphics API is currently leased");
+        }
+        
         /// <inheritdoc />
         public void Clear(Color color)
         {
+            CheckLease();
             Canvas.Clear(color.ToSKColor());
         }
 
         /// <inheritdoc />
         public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
         {
+            CheckLease();
             var drawableImage = (IDrawableBitmapImpl)source.Item;
             var s = sourceRect.ToSKRect();
             var d = destRect.ToSKRect();
@@ -157,6 +203,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
         {
+            CheckLease();
             PushOpacityMask(opacityMask, opacityMaskRect);
             DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default);
             PopOpacityMask();
@@ -165,6 +212,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawLine(IPen pen, Point p1, Point p2)
         {
+            CheckLease();
             using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
             {
                 if (paint.Paint is object)
@@ -177,6 +225,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
         {
+            CheckLease();
             var impl = (GeometryImpl) geometry;
             var size = geometry.Bounds.Size;
 
@@ -260,6 +309,7 @@ namespace Avalonia.Skia
         {
             if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0)
                 return;
+            CheckLease();
             
             var rc = rect.Rect.ToSKRect();
             var isRounded = rect.IsRounded;
@@ -296,6 +346,7 @@ namespace Avalonia.Skia
         {
             if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0)
                 return;
+            CheckLease();
             // Arbitrary chosen values
             // On OSX Skia breaks OpenGL context when asked to draw, e. g. (0, 0, 623, 6666600) rect
             if (rect.Rect.Height > 8192 || rect.Rect.Width > 8192)
@@ -421,7 +472,8 @@ namespace Avalonia.Skia
         {
             if (rect.Height <= 0 || rect.Width <= 0)
                 return;
-
+            CheckLease();
+            
             var rc = rect.ToSKRect();
 
             if (brush != null)
@@ -447,6 +499,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
         {
+            CheckLease();
             using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size))
             {
                 var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
@@ -459,18 +512,21 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public IDrawingContextLayerImpl CreateLayer(Size size)
         {
+            CheckLease();
             return CreateRenderTarget(size, true);
         }
 
         /// <inheritdoc />
         public void PushClip(Rect clip)
         {
+            CheckLease();
             Canvas.Save();
             Canvas.ClipRect(clip.ToSKRect());
         }
 
         public void PushClip(RoundedRect clip)
         {
+            CheckLease();
             Canvas.Save();
             Canvas.ClipRoundRect(clip.ToSKRoundRect(), antialias:true);
         }
@@ -478,12 +534,14 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void PopClip()
         {
+            CheckLease();
             Canvas.Restore();
         }
 
         /// <inheritdoc />
         public void PushOpacity(double opacity)
         {
+            CheckLease();
             _opacityStack.Push(_currentOpacity);
             _currentOpacity *= opacity;
         }
@@ -491,6 +549,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void PopOpacity()
         {
+            CheckLease();
             _currentOpacity = _opacityStack.Pop();
         }
 
@@ -499,6 +558,7 @@ namespace Avalonia.Skia
         {
             if(_disposed)
                 return;
+            CheckLease();
             try
             {
                 if (_grContext != null)
@@ -523,6 +583,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void PushGeometryClip(IGeometryImpl clip)
         {
+            CheckLease();
             Canvas.Save();
             Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true);
         }
@@ -530,12 +591,14 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void PopGeometryClip()
         {
+            CheckLease();
             Canvas.Restore();
         }
 
         /// <inheritdoc />
         public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
         {
+            CheckLease();
             _blendingModeStack.Push(_currentBlendingMode);
             _currentBlendingMode = blendingMode;
         }
@@ -543,14 +606,20 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void PopBitmapBlendMode()
         {
+            CheckLease();
             _currentBlendingMode = _blendingModeStack.Pop();
         }
 
-        public void Custom(ICustomDrawOperation custom) => custom.Render(this);
+        public void Custom(ICustomDrawOperation custom)
+        {
+            CheckLease();
+            custom.Render(this);
+        }
 
         /// <inheritdoc />
         public void PushOpacityMask(IBrush mask, Rect bounds)
         {
+            CheckLease();
             // TODO: This should be disposed
             var paint = new SKPaint();
 
@@ -561,6 +630,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void PopOpacityMask()
         {
+            CheckLease();
             using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn })
             {
                 Canvas.SaveLayer(paint);
@@ -580,6 +650,7 @@ namespace Avalonia.Skia
             get { return _currentTransform; }
             set
             {
+                CheckLease();
                 if (_currentTransform == value)
                     return;
 
@@ -596,6 +667,14 @@ namespace Avalonia.Skia
             }
         }
 
+        [CanBeNull]
+        public object GetFeature(Type t)
+        {
+            if (t == typeof(ISkiaSharpApiLeaseFeature))
+                return new SkiaLeaseFeature(this);
+            return null;
+        }
+
         /// <summary>
         /// Configure paint wrapper for using gradient brush.
         /// </summary>

+ 7 - 5
src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs

@@ -33,7 +33,7 @@ namespace Avalonia.Skia.Helpers
         /// Unsupported - Wraps a GPU Backed SkiaSurface in an Avalonia DrawingContext.
         /// </summary>
         [Obsolete]
-        public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, GRContext grContext, Vector dpi, params IDisposable[] disposables)
+        public static IDrawingContextImpl WrapSkiaSurface(this SKSurface surface, GRContext grContext, Vector dpi, params IDisposable[] disposables)
         {
             var createInfo = new DrawingContextImpl.CreateInfo
             {
@@ -50,7 +50,7 @@ namespace Avalonia.Skia.Helpers
         /// Unsupported - Wraps a non-GPU Backed SkiaSurface in an Avalonia DrawingContext.
         /// </summary>
         [Obsolete]
-        public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, Vector dpi, params IDisposable[] disposables)
+        public static IDrawingContextImpl WrapSkiaSurface(this SKSurface surface, Vector dpi, params IDisposable[] disposables)
         {
             var createInfo = new DrawingContextImpl.CreateInfo
             {
@@ -63,7 +63,7 @@ namespace Avalonia.Skia.Helpers
         }
 
         [Obsolete]
-        public static ISkiaDrawingContextImpl CreateDrawingContext(Size size, Vector dpi, GRContext grContext = null)
+        public static IDrawingContextImpl CreateDrawingContext(Size size, Vector dpi, GRContext grContext = null)
         {
             if (grContext is null)
             {
@@ -90,9 +90,11 @@ namespace Avalonia.Skia.Helpers
         }
         
         [Obsolete]
-        public static void DrawTo(this ISkiaDrawingContextImpl source, ISkiaDrawingContextImpl destination, SKPaint paint = null)
+        public static void DrawTo(this IDrawingContextImpl source, IDrawingContextImpl destination, SKPaint paint = null)
         {
-            destination.SkCanvas.DrawSurface(source.SkSurface, new SKPoint(0, 0), paint);
+            var src = (DrawingContextImpl)source;
+            var dst = (DrawingContextImpl)destination;
+            dst.Canvas.DrawSurface(src.Surface, new SKPoint(0, 0), paint);
         }
     }
 }

+ 0 - 15
src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs

@@ -1,15 +0,0 @@
-using Avalonia.Metadata;
-using Avalonia.Platform;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
-    [Unstable]
-    public interface ISkiaDrawingContextImpl : IDrawingContextImpl
-    {
-        SKCanvas SkCanvas { get; }
-        GRContext GrContext { get; }
-        SKSurface SkSurface { get; }
-        double CurrentOpacity { get; }
-    }
-}

+ 20 - 0
src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs

@@ -0,0 +1,20 @@
+using System;
+using Avalonia.Metadata;
+using SkiaSharp;
+
+namespace Avalonia.Skia;
+
+[Unstable]
+public interface ISkiaSharpApiLeaseFeature
+{
+    public ISkiaSharpApiLease Lease();
+}
+
+[Unstable]
+public interface ISkiaSharpApiLease : IDisposable
+{
+    SKCanvas SkCanvas { get; }
+    GRContext GrContext { get; }
+    SKSurface SkSurface { get; }
+    double CurrentOpacity { get; }
+}

+ 1 - 0
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -614,5 +614,6 @@ namespace Avalonia.Direct2D1.Media
         }
         
         public void Custom(ICustomDrawOperation custom) => custom.Render(this);
+        public object GetFeature(Type t) => null;
     }
 }

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

@@ -285,11 +285,12 @@ namespace Avalonia.Win32
 
             set
             {
-                if (IsWindowVisible(_hwnd))
+                if (IsWindowVisible(_hwnd) && _lastWindowState != value)
                 {
                     ShowWindow(value, value != WindowState.Minimized); // If the window is minimized, it shouldn't be activated
                 }
 
+                _lastWindowState = value;
                 _showWindowState = value;
             }
         }

+ 4 - 1
tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs

@@ -1,4 +1,5 @@
-using Avalonia.Media;
+using System;
+using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Utilities;
@@ -99,5 +100,7 @@ namespace Avalonia.Benchmarks
         public void Custom(ICustomDrawOperation custom)
         {
         }
+
+        public object GetFeature(Type t) => null;
     }
 }

+ 44 - 5
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -9,7 +9,6 @@ using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Styling;
-using Avalonia.Threading;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Xunit;
@@ -19,7 +18,7 @@ namespace Avalonia.Controls.UnitTests
     public class ListBoxTests
     {
         private MouseTestHelper _mouse = new MouseTestHelper();
-        
+
         [Fact]
         public void Should_Use_ItemTemplate_To_Create_Item_Content()
         {
@@ -433,6 +432,47 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void ListBox_Should_Be_Valid_After_Remove_Of_Item_In_NonVisibleArea()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var items = new AvaloniaList<string>(Enumerable.Range(1, 30).Select(v => v.ToString()));
+
+                var wnd = new Window() { Width = 100, Height = 100, IsVisible = true };
+
+                var target = new ListBox()
+                {
+                    AutoScrollToSelectedItem = true,
+                    Height = 100,
+                    Width = 50,
+                    VirtualizationMode = ItemVirtualizationMode.Simple,
+                    ItemTemplate = new FuncDataTemplate<object>((c, _) => new Border() { Height = 10 }),
+                    Items = items,
+                };
+                wnd.Content = target;
+
+                var lm = wnd.LayoutManager;
+
+                lm.ExecuteInitialLayoutPass();
+
+                //select last / scroll to last item
+                target.SelectedItem = items.Last();
+
+                lm.ExecuteLayoutPass();
+
+                //remove the first item (in non realized area of the listbox)
+                items.Remove("1");
+                lm.ExecuteLayoutPass();
+
+                Assert.Equal("30", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 1).DataContext);
+                Assert.Equal("29", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 2).DataContext);
+                Assert.Equal("28", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 3).DataContext);
+                Assert.Equal("27", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 4).DataContext);
+                Assert.Equal("26", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 5).DataContext);
+            }
+        }
+
         [Fact]
         public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control()
         {
@@ -656,7 +696,6 @@ namespace Avalonia.Controls.UnitTests
             public string Value { get; }
         }
 
-
         [Fact]
         public void SelectedItem_Validation()
         {
@@ -670,11 +709,11 @@ namespace Avalonia.Controls.UnitTests
             };
 
             Prepare(target);
-            
+
             var exception = new System.InvalidCastException("failed validation");
             var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
             target.Bind(ComboBox.SelectedItemProperty, textObservable);
-                
+
             Assert.True(DataValidationErrors.GetHasErrors(target));
             Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
         }

+ 41 - 1
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@@ -1,7 +1,9 @@
 using System;
+using System.Linq;
 using System.Runtime.InteropServices;
 using System.Threading;
 using Avalonia.Controls;
+using OpenQA.Selenium;
 using OpenQA.Selenium.Appium;
 using OpenQA.Selenium.Interactions;
 using Xunit;
@@ -55,6 +57,43 @@ namespace Avalonia.IntegrationTests.Appium
                 }
             }
         }
+        
+        [PlatformFact(TestPlatforms.Windows)]
+        public void OnWindows_Docked_Windows_Retain_Size_Position_When_Restored()
+        {
+            using (OpenWindow(new Size(400, 400), ShowWindowMode.NonOwned, WindowStartupLocation.Manual))
+            {
+                var windowState = _session.FindElementByAccessibilityId("WindowState");
+
+                Assert.Equal("Normal", windowState.GetComboBoxValue());
+                
+                
+                var window = _session.FindElements(By.XPath("//Window")).First();
+                
+                new Actions(_session)
+                    .KeyDown(Keys.Meta)
+                    .SendKeys(Keys.Left)
+                    .KeyUp(Keys.Meta)
+                    .Perform();
+                
+                var original = GetWindowInfo();
+                
+                windowState.Click();
+                _session.FindElementByName("Minimized").SendClick();
+                
+                new Actions(_session)
+                    .KeyDown(Keys.Alt)
+                    .SendKeys(Keys.Tab)
+                    .KeyUp(Keys.Alt)
+                    .Perform();
+                
+                var current = GetWindowInfo();
+                
+                Assert.Equal(original.Position, current.Position);
+                Assert.Equal(original.FrameSize, current.FrameSize);
+
+            }
+        }
 
 
         [Theory]
@@ -92,7 +131,8 @@ namespace Avalonia.IntegrationTests.Appium
                 Assert.True(clientSize.Width >= current.ScreenRect.Width);
                 Assert.True(clientSize.Height >= current.ScreenRect.Height);
 
-                windowState.Click();
+                windowState.SendClick();
+                
                 _session.FindElementByName("Normal").SendClick();
 
                 current = GetWindowInfo();

+ 29 - 0
tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

@@ -211,6 +211,35 @@ namespace Avalonia.IntegrationTests.Appium
             }
         }
 
+        [PlatformFact(TestPlatforms.MacOS)]
+        public void Hidden_Child_Window_Is_Not_Reshown_When_Parent_Clicked()
+        {
+            var mainWindow = _session.FindElementByAccessibilityId("MainWindow");
+
+            // We don't use dispose to close the window here, because it seems that hiding and re-showing a window
+            // causes Appium to think it's a different window.
+            OpenWindow(null, ShowWindowMode.Owned, WindowStartupLocation.Manual);
+            
+            var secondaryWindow = FindWindow(_session, "SecondaryWindow");
+            var hideButton = secondaryWindow.FindElementByAccessibilityId("HideButton");
+
+            hideButton.Click();
+                
+            var windows = _session.FindElementsByXPath("XCUIElementTypeWindow");
+            Assert.Single(windows);
+                
+            mainWindow.Click();
+                
+            windows = _session.FindElementsByXPath("XCUIElementTypeWindow");
+            Assert.Single(windows);
+                
+            _session.FindElementByAccessibilityId("RestoreAll").Click();
+
+            // Close the window manually.
+            secondaryWindow = FindWindow(_session, "SecondaryWindow");
+            secondaryWindow.GetChromeButtons().close.Click();
+        }
+
         private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location)
         {
             var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize");

+ 43 - 0
tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using Avalonia.Controls;
@@ -217,6 +218,37 @@ namespace Avalonia.Markup.UnitTests.Data
             }
         }
 
+        [Fact]
+        public void Should_Not_Pass_UnsetValue_To_MultiBinding_During_ApplyTemplate()
+        {
+            var converter = new MultiConverter();
+            var source = new Button
+            {
+                Content = "foo",
+                Template = new FuncControlTemplate<Button>((parent, _) =>
+                    new ContentPresenter
+                    {
+                        [~ContentPresenter.ContentProperty] = new MultiBinding
+                        {
+                            Converter = converter,
+                            Bindings =
+                            {
+                                new TemplateBinding(ContentControl.ContentProperty),
+                            }
+                        }
+                    }),
+            };
+
+            source.ApplyTemplate();
+
+            var target = (ContentPresenter)source.GetVisualChildren().Single();
+
+            // #8672 was caused by TemplateBinding passing "unset" to the MultiBinding during
+            // ApplyTemplate as the TemplatedParent property doesn't get setup until after the
+            // binding is initiated.
+            Assert.Equal(new[] { "foo" }, converter.Values);
+        }
+
         private class PrefixConverter : IValueConverter
         {
             public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
@@ -247,5 +279,16 @@ namespace Avalonia.Markup.UnitTests.Data
                 return null;
             }
         }
+
+        private class MultiConverter : IMultiValueConverter
+        {
+            public List<object> Values { get; } = new();
+
+            public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
+            {
+                Values.AddRange(values);
+                return values.FirstOrDefault();
+            }
+        }
     }
 }

+ 207 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

@@ -3,6 +3,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
 using Avalonia.Media;
+using Avalonia.Media.Immutable;
 using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Xunit;
@@ -66,6 +67,212 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             }
         }
 
+        [Fact]
+        public void Item_Is_Added_To_ResourceDictionary_As_Deferred()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <SolidColorBrush x:Key='Red' Color='Red' />
+</ResourceDictionary>";
+                var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
+
+                Assert.True(resources.ContainsDeferredKey("Red"));
+            }
+        }
+        
+        [Fact]
+        public void Item_Added_To_ResourceDictionary_Is_UnDeferred_On_Read()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <SolidColorBrush x:Key='Red' Color='Red' />
+</ResourceDictionary>";
+                var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
+
+                Assert.True(resources.ContainsDeferredKey("Red"));
+
+                Assert.IsType<SolidColorBrush>(resources["Red"]);
+                
+                Assert.False(resources.ContainsDeferredKey("Red"));
+            }
+        }
+
+        [Fact]
+        public void Item_Is_Added_To_Window_Resources_As_Deferred()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Resources>
+        <SolidColorBrush x:Key='Red' Color='Red' />
+    </Window.Resources>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var resources = (ResourceDictionary)window.Resources;
+
+                Assert.True(resources.ContainsDeferredKey("Red"));
+            }
+        }
+
+        [Fact]
+        public void Item_Is_Added_To_Window_MergedDictionaries_As_Deferred()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <ResourceDictionary>
+                    <SolidColorBrush x:Key='Red' Color='Red' />
+                </ResourceDictionary>
+            </ResourceDictionary.MergedDictionaries>
+        </ResourceDictionary>
+    </Window.Resources>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var resources = (ResourceDictionary)window.Resources.MergedDictionaries[0];
+
+                Assert.True(resources.ContainsDeferredKey("Red"));
+            }
+        }
+
+        [Fact]
+        public void Item_Is_Added_To_Style_Resources_As_Deferred()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Style xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Style.Resources>
+        <SolidColorBrush x:Key='Red' Color='Red' />
+    </Style.Resources>
+</Style>";
+                var style = (Style)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var resources = (ResourceDictionary)style.Resources;
+
+                Assert.True(resources.ContainsDeferredKey("Red"));
+            }
+        }
+
+        [Fact]
+        public void Item_Is_Added_To_Styles_Resources_As_Deferred()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Styles xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Styles.Resources>
+        <SolidColorBrush x:Key='Red' Color='Red' />
+    </Styles.Resources>
+</Styles>";
+                var style = (Styles)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var resources = (ResourceDictionary)style.Resources;
+
+                Assert.True(resources.ContainsDeferredKey("Red"));
+            }
+        }
+
+        [Fact]
+        public void Item_Can_Be_StaticReferenced_As_Deferred()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Resources>
+        <SolidColorBrush x:Key='Red' Color='Red' />
+    </Window.Resources>
+    <Button>
+        <Button.Resources>
+            <StaticResource x:Key='Red2' ResourceKey='Red' />
+        </Button.Resources>
+    </Button>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var windowResources = (ResourceDictionary)window.Resources;
+                var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources;
+                
+                Assert.True(windowResources.ContainsDeferredKey("Red"));
+                Assert.True(buttonResources.ContainsDeferredKey("Red2"));
+            }
+        }
+        
+        [Fact]
+        public void Item_StaticReferenced_Is_UnDeferred_On_Read()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Resources>
+        <SolidColorBrush x:Key='Red' Color='Red' />
+    </Window.Resources>
+    <Button>
+        <Button.Resources>
+            <StaticResource x:Key='Red2' ResourceKey='Red' />
+        </Button.Resources>
+    </Button>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var windowResources = (ResourceDictionary)window.Resources;
+                var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources;
+                
+                Assert.IsType<SolidColorBrush>(buttonResources["Red2"]);
+                
+                Assert.False(windowResources.ContainsDeferredKey("Red"));
+                Assert.False(buttonResources.ContainsDeferredKey("Red2"));
+            }
+        }
+        
+        [Fact]
+        public void Value_Type_With_Parse_Converter_Should_Not_Be_Deferred()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Color x:Key='Red'>Red</Color>
+</ResourceDictionary>";
+                var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
+
+                Assert.False(resources.ContainsDeferredKey("Red"));
+                Assert.IsType<Color>(resources["Red"]);
+            }
+        }
+        
+        [Fact]
+        public void Value_Type_With_Ctor_Converter_Should_Not_Be_Deferred()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Thickness x:Key='Margin'>1 1 1 1</Thickness>
+</ResourceDictionary>";
+                var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
+
+                Assert.False(resources.ContainsDeferredKey("Margin"));
+                Assert.IsType<Thickness>(resources["Margin"]);
+            }
+        }
+
         private IDisposable StyledWindow(params (string, string)[] assets)
         {
             var services = TestServices.StyledWindow.With(

+ 0 - 8
tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs

@@ -12,14 +12,6 @@ namespace Avalonia.ReactiveUI.UnitTests
 {
     public class TransitioningContentControlTest
     {
-        [Fact]
-        public void Transitioning_Control_Should_Derive_Template_From_Content_Control()
-        {
-            var target = new TransitioningContentControl();
-            var stylable = (IStyledElement)target;
-            Assert.Equal(typeof(ContentControl),stylable.StyleKey);
-        }
-
         [Fact]
         public void Transitioning_Control_Template_Should_Be_Instantiated() 
         {