소스 검색

Merge branch 'master' into Flyouts

Jumar Macato 4 년 전
부모
커밋
ad6a976e48
45개의 변경된 파일792개의 추가작업 그리고 136개의 파일을 삭제
  1. 1 1
      build/ReactiveUI.props
  2. 11 6
      nukebuild/Build.cs
  3. 2 5
      nukebuild/BuildParameters.cs
  4. 5 1
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  5. 1 1
      samples/ControlCatalog/ControlCatalog.csproj
  6. 24 4
      src/Avalonia.Animation/Animatable.cs
  7. 6 3
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  8. 8 1
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  9. 1 1
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  10. 35 14
      src/Avalonia.Base/ValueStore.cs
  11. 12 0
      src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs
  12. 2 2
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  13. 1 0
      src/Avalonia.Controls/ApiCompatBaseline.txt
  14. 1 0
      src/Avalonia.Controls/Button.cs
  15. 45 28
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  16. 4 18
      src/Avalonia.Controls/NativeControlHost.cs
  17. 1 1
      src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
  18. 18 4
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  19. 3 1
      src/Avalonia.Controls/Slider.cs
  20. 11 1
      src/Avalonia.Diagnostics/DevToolsExtensions.cs
  21. 18 6
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  22. 26 0
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  23. 1 1
      src/Avalonia.Native/avn.idl
  24. 11 7
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  25. 31 0
      src/Avalonia.Styling/ClassBindingManager.cs
  26. 21 0
      src/Avalonia.Styling/Controls/Classes.cs
  27. 11 0
      src/Avalonia.Styling/StyledElementExtensions.cs
  28. 1 1
      src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
  29. 4 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  30. 3 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs
  31. 97 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs
  32. 40 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs
  33. 10 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  34. 1 1
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  35. 1 2
      src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
  36. 1 1
      src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
  37. 6 2
      src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
  38. 6 2
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  39. 31 0
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
  40. 122 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  41. 46 18
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  42. 29 0
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  43. 26 0
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  44. 32 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  45. 25 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

+ 1 - 1
build/ReactiveUI.props

@@ -1,5 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="ReactiveUI" Version="12.1.1" />
+    <PackageReference Include="ReactiveUI" Version="13.2.10" />
   </ItemGroup>
 </Project>

+ 11 - 6
nukebuild/Build.cs

@@ -301,14 +301,19 @@ partial class Build : NukeBuild
         .Executes(() =>
         {
             var data = Parameters;
+            var pathToProjectSource = RootDirectory / "samples" / "ControlCatalog.NetCore";
+            var pathToPublish = pathToProjectSource / "bin" / data.Configuration / "publish";
+
+            DotNetPublish(c => c
+                .SetProject(pathToProjectSource / "ControlCatalog.NetCore.csproj")
+                .EnableNoBuild()
+                .SetConfiguration(data.Configuration)
+                .AddProperty("PackageVersion", data.Version)
+                .AddProperty("PublishDir", pathToPublish));
+
             Zip(data.ZipCoreArtifacts, data.BinRoot);
             Zip(data.ZipNuGetArtifacts, data.NugetRoot);
-            Zip(data.ZipTargetControlCatalogDesktopDir,
-                GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dll").Concat(
-                    GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.config")).Concat(
-                    GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.so")).Concat(
-                    GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dylib")).Concat(
-                    GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.exe")));
+            Zip(data.ZipTargetControlCatalogNetCoreDir, pathToPublish);
         });
 
     Target CreateIntermediateNugetPackages => _ => _

+ 2 - 5
nukebuild/BuildParameters.cs

@@ -58,8 +58,7 @@ public partial class Build
         public string FileZipSuffix { get; }
         public AbsolutePath ZipCoreArtifacts { get; }
         public AbsolutePath ZipNuGetArtifacts { get; }
-        public AbsolutePath ZipSourceControlCatalogDesktopDir { get; }
-        public AbsolutePath ZipTargetControlCatalogDesktopDir { get; }
+        public AbsolutePath ZipTargetControlCatalogNetCoreDir { get; }
 
 
         public BuildParameters(Build b)
@@ -129,9 +128,7 @@ public partial class Build
             FileZipSuffix = Version + ".zip";
             ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix);
             ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix);
-            ZipSourceControlCatalogDesktopDir =
-                RootDirectory / ("samples/ControlCatalog.Desktop/bin/" + DirSuffix + "/net461");
-            ZipTargetControlCatalogDesktopDir = ZipRoot / ("ControlCatalog.Desktop-" + FileZipSuffix);
+            ZipTargetControlCatalogNetCoreDir = ZipRoot / ("ControlCatalog.NetCore-" + FileZipSuffix);
         }
 
         string GetVersion()

+ 5 - 1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <OutputType>Exe</OutputType>
+    <OutputType>WinExe</OutputType>
     <TargetFramework>netcoreapp3.1</TargetFramework>
     <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
   </PropertyGroup>
@@ -15,6 +15,10 @@
     <PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
   </ItemGroup>
 
+  <PropertyGroup>
+    <!-- For Microsoft.CodeAnalysis -->
+    <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
+  </PropertyGroup>
 
   <Import Project="..\..\build\SampleApp.props" />
   <Import Project="..\..\build\ReferenceCoreLibraries.props" />

+ 1 - 1
samples/ControlCatalog/ControlCatalog.csproj

@@ -27,6 +27,6 @@
     <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
     <ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
   </ItemGroup>
-  
+
   <Import Project="..\..\build\BuildTargets.targets" />
 </Project>

+ 24 - 4
src/Avalonia.Animation/Animatable.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
+using System.Linq;
 using Avalonia.Data;
 
 #nullable enable
@@ -93,16 +94,35 @@ namespace Avalonia.Animation
                 var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
                 var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
 
+                // When transitions are replaced, we add the new transitions before removing the old
+                // transitions, so that when the old transition being disposed causes the value to
+                // change, there is a corresponding entry in `_transitionStates`. This means that we
+                // need to account for any transitions present in both the old and new transitions
+                // collections.
                 if (newTransitions is object)
                 {
+                    var toAdd = (IList)newTransitions;
+
+                    if (newTransitions.Count > 0 && oldTransitions?.Count > 0)
+                    {
+                        toAdd = newTransitions.Except(oldTransitions).ToList();
+                    }
+
                     newTransitions.CollectionChanged += TransitionsCollectionChanged;
-                    AddTransitions(newTransitions);
+                    AddTransitions(toAdd);
                 }
 
                 if (oldTransitions is object)
                 {
+                    var toRemove = (IList)oldTransitions;
+
+                    if (oldTransitions.Count > 0 && newTransitions?.Count > 0)
+                    {
+                        toRemove = oldTransitions.Except(newTransitions).ToList();
+                    }
+
                     oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
-                    RemoveTransitions(oldTransitions);
+                    RemoveTransitions(toRemove);
                 }
             }
             else if (_transitionsEnabled &&
@@ -115,9 +135,9 @@ namespace Avalonia.Animation
                 {
                     var transition = Transitions[i];
 
-                    if (transition.Property == change.Property)
+                    if (transition.Property == change.Property &&
+                        _transitionState.TryGetValue(transition, out var state))
                     {
-                        var state = _transitionState[transition];
                         var oldValue = state.BaseValue;
                         var newValue = GetAnimationBaseValue(transition.Property);
 

+ 6 - 3
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -65,7 +65,6 @@ namespace Avalonia.PropertyStore
         {
             _subscription?.Dispose();
             _subscription = null;
-            _isSubscribed = false;
             OnCompleted();
         }
 
@@ -74,6 +73,7 @@ namespace Avalonia.PropertyStore
             var oldValue = _value;
             _value = default;
             Priority = BindingPriority.Unset;
+            _isSubscribed = false;
             _sink.Completed(Property, this, oldValue);
         }
 
@@ -104,8 +104,11 @@ namespace Avalonia.PropertyStore
         public void Start(bool ignoreBatchUpdate)
         {
             // We can't use _subscription to check whether we're subscribed because it won't be set
-            // until Subscribe has finished, which will be too late to prevent reentrancy.
-            if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate))
+            // until Subscribe has finished, which will be too late to prevent reentrancy. In addition
+            // don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
+            if (!_isSubscribed &&
+                Priority != BindingPriority.Unset &&
+                (!_batchUpdate || ignoreBatchUpdate))
             {
                 _isSubscribed = true;
                 _subscription = Source.Subscribe(this);

+ 8 - 1
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -6,12 +6,19 @@ using Avalonia.Data;
 
 namespace Avalonia.PropertyStore
 {
+    /// <summary>
+    /// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
+    /// </summary>
+    internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
+    {
+    }
+
     /// <summary>
     /// Stores a value with a priority in a <see cref="ValueStore"/> or
     /// <see cref="PriorityValue{T}"/>.
     /// </summary>
     /// <typeparam name="T">The property type.</typeparam>
-    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
+    internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
     {
         private IValueSink _sink;
         private Optional<T> _value;

+ 1 - 1
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@@ -94,7 +94,7 @@ namespace Avalonia.Utilities
             return (0, false);
         }
 
-        public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value)
+        public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
         {
             (int index, bool found) = TryFindEntry(property.Id);
             if (!found)

+ 35 - 14
src/Avalonia.Base/ValueStore.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using Avalonia.Data;
 using Avalonia.PropertyStore;
 using Avalonia.Utilities;
@@ -56,7 +57,7 @@ namespace Avalonia
 
         public bool IsAnimating(AvaloniaProperty property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 return slot.Priority < BindingPriority.LocalValue;
             }
@@ -66,7 +67,7 @@ namespace Avalonia
 
         public bool IsSet(AvaloniaProperty property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 return slot.GetValue().HasValue;
             }
@@ -79,7 +80,7 @@ namespace Avalonia
             BindingPriority maxPriority,
             out T value)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 var v = ((IValue<T>)slot).GetValue(maxPriority);
 
@@ -103,7 +104,7 @@ namespace Avalonia
 
             IDisposable? result = null;
 
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 result = SetExisting(slot, property, value, priority);
             }
@@ -138,7 +139,7 @@ namespace Avalonia
             IObservable<BindingValue<T>> source,
             BindingPriority priority)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 return BindExisting(slot, property, source, priority);
             }
@@ -160,7 +161,7 @@ namespace Avalonia
 
         public void ClearLocalValue<T>(StyledPropertyBase<T> property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 if (slot is PriorityValue<T> p)
                 {
@@ -173,7 +174,7 @@ namespace Avalonia
                     // During batch update values can't be removed immediately because they're needed to raise
                     // a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
                     // by setting their priority to Unset.
-                    if (_batchUpdate is null)
+                    if (!IsBatchUpdating())
                     {
                         _values.Remove(property);
                     }
@@ -198,7 +199,7 @@ namespace Avalonia
 
         public void CoerceValue<T>(StyledPropertyBase<T> property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 if (slot is PriorityValue<T> p)
                 {
@@ -209,7 +210,7 @@ namespace Avalonia
 
         public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (TryGetValue(property, out var slot))
             {
                 var slotValue = slot.GetValue();
                 return new Diagnostics.AvaloniaPropertyValue(
@@ -242,6 +243,7 @@ namespace Avalonia
             IPriorityValueEntry entry,
             Optional<T> oldValue)
         {
+            // We need to include remove sentinels here so call `_values.TryGetValue` directly.
             if (_values.TryGetValue(property, out var slot) && slot == entry)
             {
                 if (_batchUpdate is null)
@@ -285,7 +287,7 @@ namespace Avalonia
                 else
                 {
                     var priorityValue = new PriorityValue<T>(_owner, property, this, l);
-                    if (_batchUpdate is object)
+                    if (IsBatchUpdating())
                         priorityValue.BeginBatchUpdate();
                     result = priorityValue.SetValue(value, priority);
                     _values.SetValue(property, priorityValue);
@@ -311,7 +313,7 @@ namespace Avalonia
             {
                 priorityValue = new PriorityValue<T>(_owner, property, this, e);
 
-                if (_batchUpdate is object)
+                if (IsBatchUpdating())
                 {
                     priorityValue.BeginBatchUpdate();
                 }
@@ -338,7 +340,7 @@ namespace Avalonia
         private void AddValue(AvaloniaProperty property, IValue value)
         {
             _values.AddValue(property, value);
-            if (_batchUpdate is object && value is IBatchUpdate batch)
+            if (IsBatchUpdating() && value is IBatchUpdate batch)
                 batch.BeginBatchUpdate();
             value.Start();
         }
@@ -364,6 +366,21 @@ namespace Avalonia
             }
         }
 
+        private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true;
+
+        private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value)
+        {
+            return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value);
+        }
+
+        private static bool IsRemoveSentinel(IValue value)
+        {
+            // Local value entries are optimized and contain only a single value field to save space,
+            // so there's no way to mark them for removal at the end of a batch update. Instead a
+            // ConstantValueEntry with a priority of Unset is used as a sentinel value.
+            return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset;
+        }
+
         private class BatchUpdate
         {
             private ValueStore _owner;
@@ -373,6 +390,8 @@ namespace Avalonia
 
             public BatchUpdate(ValueStore owner) => _owner = owner;
 
+            public bool IsBatchUpdating => _batchUpdateCount > 0;
+
             public void Begin()
             {
                 if (_batchUpdateCount++ == 0)
@@ -437,8 +456,10 @@ namespace Avalonia
 
                             // During batch update values can't be removed immediately because they're needed to raise
                             // the _sink.ValueChanged notification. They instead mark themselves for removal by setting
-                            // their priority to Unset.
-                            if (slot.Priority == BindingPriority.Unset)
+                            // their priority to Unset. We need to re-read the slot here because raising ValueChanged
+                            // could have caused it to be updated.
+                            if (values.TryGetValue(entry.property, out var updatedSlot) &&
+                                updatedSlot.Priority == BindingPriority.Unset)
                             {
                                 values.Remove(entry.property);
                             }

+ 12 - 0
src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs

@@ -0,0 +1,12 @@
+using System;
+using XamlX.Transform;
+
+namespace Avalonia.Build.Tasks
+{
+    public class DeterministicIdGenerator : IXamlIdentifierGenerator
+    {
+        private int _nextId = 1;
+        
+        public string GenerateIdentifierPart() => (_nextId++).ToString();
+    }
+}

+ 2 - 2
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@@ -22,7 +22,6 @@ using XamlX.IL;
 
 namespace Avalonia.Build.Tasks
 {
-    
     public static partial class XamlCompilerTaskExecutor
     {
         static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
@@ -99,7 +98,8 @@ namespace Avalonia.Build.Tasks
                 XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage),
                 AvaloniaXamlIlLanguage.CustomValueConverter,
                 new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)),
-                new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)));
+                new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)),
+                new DeterministicIdGenerator());
 
 
             var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", 

+ 1 - 0
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -3,6 +3,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Control
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
 MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
+EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
 MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.

+ 1 - 0
src/Avalonia.Controls/Button.cs

@@ -234,6 +234,7 @@ namespace Avalonia.Controls
             if (Command != null)
             {
                 Command.CanExecuteChanged += CanExecuteChanged;
+                CanExecuteChanged(this, EventArgs.Empty);
             }
         }
 

+ 45 - 28
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@@ -14,17 +14,21 @@ namespace Avalonia.Controls.Chrome
     public class CaptionButtons : TemplatedControl
     {
         private CompositeDisposable? _disposables;
-        private Window? _hostWindow;
 
-        public void Attach(Window hostWindow)
+        /// <summary>
+        /// Currently attached window.
+        /// </summary>
+        protected Window? HostWindow { get; private set; }
+
+        public virtual void Attach(Window hostWindow)
         {
             if (_disposables == null)
             {
-                _hostWindow = hostWindow;
+                HostWindow = hostWindow;
 
                 _disposables = new CompositeDisposable
                 {
-                    _hostWindow.GetObservable(Window.WindowStateProperty)
+                    HostWindow.GetObservable(Window.WindowStateProperty)
                     .Subscribe(x =>
                     {
                         PseudoClasses.Set(":minimized", x == WindowState.Minimized);
@@ -36,14 +40,45 @@ namespace Avalonia.Controls.Chrome
             }
         }
 
-        public void Detach()
+        public virtual void Detach()
         {
             if (_disposables != null)
             {
                 _disposables.Dispose();
                 _disposables = null;
 
-                _hostWindow = null;
+                HostWindow = null;
+            }
+        }
+
+        protected virtual void OnClose()
+        {
+            HostWindow?.Close();
+        }
+
+        protected virtual void OnRestore()
+        {
+            if (HostWindow != null)
+            {
+                HostWindow.WindowState = HostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+            }
+        }
+
+        protected virtual void OnMinimize()
+        {
+            if (HostWindow != null)
+            {
+                HostWindow.WindowState = WindowState.Minimized;
+            }
+        }
+
+        protected virtual void OnToggleFullScreen()
+        {
+            if (HostWindow != null)
+            {
+                HostWindow.WindowState = HostWindow.WindowState == WindowState.FullScreen
+                    ? WindowState.Normal
+                    : WindowState.FullScreen;
             }
         }
 
@@ -56,31 +91,13 @@ namespace Avalonia.Controls.Chrome
             var minimiseButton = e.NameScope.Get<Panel>("PART_MinimiseButton");
             var fullScreenButton = e.NameScope.Get<Panel>("PART_FullScreenButton");
 
-            closeButton.PointerReleased += (sender, e) => _hostWindow?.Close();
+            closeButton.PointerReleased += (sender, e) => OnClose();
 
-            restoreButton.PointerReleased += (sender, e) =>
-            {
-                if (_hostWindow != null)
-                {
-                    _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
-                }
-            };
+            restoreButton.PointerReleased += (sender, e) => OnRestore();
 
-            minimiseButton.PointerReleased += (sender, e) =>
-            {
-                if (_hostWindow != null)
-                {
-                    _hostWindow.WindowState = WindowState.Minimized;
-                }
-            };
+            minimiseButton.PointerReleased += (sender, e) => OnMinimize();
 
-            fullScreenButton.PointerReleased += (sender, e) =>
-            {
-                if (_hostWindow != null)
-                {
-                    _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen;
-                }
-            };
+            fullScreenButton.PointerReleased += (sender, e) => OnToggleFullScreen();
         }
     }
 }

+ 4 - 18
src/Avalonia.Controls/NativeControlHost.cs

@@ -16,30 +16,16 @@ namespace Avalonia.Controls
         private bool _queuedForDestruction;
         private bool _queuedForMoveResize;
         private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
-        private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
-        static NativeControlHost()
-        {
-            IsVisibleProperty.Changed.AddClassHandler<NativeControlHost>(OnVisibleChanged);
-        }
-
-        public NativeControlHost()
-        {
-            _propertyChangedHandler = PropertyChangedHandler;
-        }
-
-        private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2)
-            => host.UpdateHost();
 
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
             _currentRoot = e.Root as TopLevel;
             var visual = (IVisual)this;
-            while (visual != _currentRoot)
+            while (visual != null)
             {
-
                 if (visual is Visual v)
                 {
-                    v.PropertyChanged += _propertyChangedHandler;
+                    v.PropertyChanged += PropertyChangedHandler;
                     _propertyChangedSubscriptions.Add(v);
                 }
 
@@ -51,7 +37,7 @@ namespace Avalonia.Controls
 
         private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e)
         {
-            if (e.IsEffectiveValueChange && e.Property == BoundsProperty)
+            if (e.IsEffectiveValueChange && (e.Property == BoundsProperty || e.Property == IsVisibleProperty))
                 EnqueueForMoveResize();
         }
 
@@ -61,7 +47,7 @@ namespace Avalonia.Controls
             if (_propertyChangedSubscriptions != null)
             {
                 foreach (var v in _propertyChangedSubscriptions)
-                    v.PropertyChanged -= _propertyChangedHandler;
+                    v.PropertyChanged -= PropertyChangedHandler;
                 _propertyChangedSubscriptions.Clear();
             }
             UpdateHost();

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

@@ -16,7 +16,7 @@ namespace Avalonia.Platform
         /// <summary>
         /// The default for the platform.
         /// </summary>
-        Default = SystemChrome,
+        Default = PreferSystemChrome,
 
         /// <summary>
         /// Use SystemChrome

+ 18 - 4
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives
                 nameof(SelectedItem),
                 o => o.SelectedItem,
                 (o, v) => o.SelectedItem = v,
-                defaultBindingMode: BindingMode.TwoWay);
+                defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="SelectedItems"/> property.
@@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives
             EndUpdating();
         }
 
+        /// <summary>
+        /// Called to update the validation state for properties for which data validation is
+        /// enabled.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <param name="value">The new binding value for the property.</param>
+        protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
+        {
+            if (property == SelectedItemProperty)
+            {
+                DataValidationErrors.SetError(this, value.Error);
+            }
+        }
+        
         protected override void OnInitialized()
         {
             base.OnInitialized();
@@ -707,7 +721,7 @@ namespace Avalonia.Controls.Primitives
                 _oldSelectedItem = SelectedItem;
             }
             else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
-                _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
+                     _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
             {
                 RaisePropertyChanged(
                     SelectedItemsProperty,
@@ -977,7 +991,7 @@ namespace Avalonia.Controls.Primitives
             public Optional<ISelectionModel> Selection { get; set; }
             public Optional<IList?> SelectedItems { get; set; }
 
-            public Optional<int> SelectedIndex 
+            public Optional<int> SelectedIndex
             {
                 get => _selectedIndex;
                 set
@@ -996,6 +1010,6 @@ namespace Avalonia.Controls.Primitives
                     _selectedIndex = default;
                 }
             }
-       }
+        }
     }
 }

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

@@ -341,7 +341,9 @@ namespace Avalonia.Controls
 
             var pointNum = orient ? x.Position.X : x.Position.Y;
             var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d);
-            var invert = orient ? 0 : 1;
+            var invert = orient ? 
+                IsDirectionReversed ? 1 : 0 :
+                IsDirectionReversed ? 0 : 1;
             var calcVal = Math.Abs(invert - logicalPos);
             var range = Maximum - Minimum;
             var finalValue = calcVal * range + Minimum;

+ 11 - 1
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@@ -15,7 +15,7 @@ namespace Avalonia
         /// <param name="root">The window to attach DevTools to.</param>
         public static void AttachDevTools(this TopLevel root)
         {
-            DevTools.Attach(root, new KeyGesture(Key.F12));
+            DevTools.Attach(root, new DevToolsOptions());
         }
 
         /// <summary>
@@ -27,5 +27,15 @@ namespace Avalonia
         {
             DevTools.Attach(root, gesture);
         }
+
+        /// <summary>
+        /// Attaches DevTools to a window, to be opened with the specified options.
+        /// </summary>
+        /// <param name="root">The window to attach DevTools to.</param>
+        /// <param name="options">Additional settings of DevTools.</param>
+        public static void AttachDevTools(this TopLevel root, DevToolsOptions options)
+        {
+            DevTools.Attach(root, options);
+        }
     }
 }

+ 18 - 6
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@@ -6,6 +6,8 @@ using Avalonia.Diagnostics.Views;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 
+#nullable enable 
+
 namespace Avalonia.Diagnostics
 {
     public static class DevTools
@@ -13,12 +15,20 @@ namespace Avalonia.Diagnostics
         private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
 
         public static IDisposable Attach(TopLevel root, KeyGesture gesture)
+        {
+            return Attach(root, new DevToolsOptions()
+            {
+                Gesture = gesture,
+            });
+        }
+
+        public static IDisposable Attach(TopLevel root, DevToolsOptions options)
         {
             void PreviewKeyDown(object sender, KeyEventArgs e)
             {
-                if (gesture.Matches(e))
+                if (options.Gesture.Matches(e))
                 {
-                    Open(root);
+                    Open(root, options);
                 }
             }
 
@@ -28,7 +38,9 @@ namespace Avalonia.Diagnostics
                 RoutingStrategies.Tunnel);
         }
 
-        public static IDisposable Open(TopLevel root)
+        public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions());
+
+        public static IDisposable Open(TopLevel root, DevToolsOptions options)
         {
             if (s_open.TryGetValue(root, out var window))
             {
@@ -38,15 +50,15 @@ namespace Avalonia.Diagnostics
             {
                 window = new MainWindow
                 {
-                    Width = 1024,
-                    Height = 512,
                     Root = root,
+                    Width = options.Size.Width,
+                    Height = options.Size.Height,
                 };
 
                 window.Closed += DevToolsClosed;
                 s_open.Add(root, window);
 
-                if (root is Window inspectedWindow)
+                if (options.ShowAsChildWindow && root is Window inspectedWindow)
                 {
                     window.Show(inspectedWindow);
                 }

+ 26 - 0
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

@@ -0,0 +1,26 @@
+using Avalonia.Input;
+
+namespace Avalonia.Diagnostics
+{
+    /// <summary>
+    /// Describes options used to customize DevTools.
+    /// </summary>
+    public class DevToolsOptions
+    {
+        /// <summary>
+        /// Gets or sets the key gesture used to open DevTools.
+        /// </summary>
+        public KeyGesture Gesture { get; set; } = new KeyGesture(Key.F12);
+
+        /// <summary>
+        /// Gets or sets a value indicating whether DevTools should be displayed as a child window
+        /// of the window being inspected. The default value is true.
+        /// </summary>
+        public bool ShowAsChildWindow { get; set; } = true;
+
+        /// <summary>
+        /// Gets or sets the initial size of the DevTools window. The default value is 1024x512.
+        /// </summary>
+        public Size Size { get; set; } = new Size(1024, 512);
+    }
+}

+ 1 - 1
src/Avalonia.Native/avn.idl

@@ -397,7 +397,7 @@ enum AvnExtendClientAreaChromeHints
     AvnSystemChrome = 0x01,
     AvnPreferSystemChrome = 0x02,
     AvnOSXThickTitleBar = 0x08,
-    AvnDefaultChrome = AvnSystemChrome,
+    AvnDefaultChrome = AvnPreferSystemChrome,
 }
 
 [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]

+ 11 - 7
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@@ -9,18 +9,22 @@ namespace Avalonia.ReactiveUI
     {
         /// <summary>
         /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia 
-        /// scheduler and Avalonia activation for view fetcher. Always remember to
-        /// call this method if you are using ReactiveUI in your application.
+        /// scheduler, an activation for view fetcher, a template binding hook. Remember
+        /// to call this method if you are using ReactiveUI in your application.
         /// </summary>
         public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder)
-            where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
-        {
-            return builder.AfterPlatformServicesSetup(_ =>
+            where TAppBuilder : AppBuilderBase<TAppBuilder>, new() =>
+            builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() =>
             {
+                if (Locator.CurrentMutable is null)
+                {
+                    return;
+                }
+
+                PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia);
                 RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
                 Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
                 Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
-            });
-        }
+            }));
     }
 }

+ 31 - 0
src/Avalonia.Styling/ClassBindingManager.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Data;
+
+namespace Avalonia
+{
+    internal static class ClassBindingManager
+    {
+        private static readonly Dictionary<string, AvaloniaProperty> s_RegisteredProperties =
+            new Dictionary<string, AvaloniaProperty>();
+        
+        public static IDisposable Bind(IStyledElement target, string className, IBinding source, object anchor)
+        {
+            if (!s_RegisteredProperties.TryGetValue(className, out var prop))
+                s_RegisteredProperties[className] = prop = RegisterClassProxyProperty(className);
+            return target.Bind(prop, source, anchor);
+        }
+
+        private static AvaloniaProperty RegisterClassProxyProperty(string className)
+        {
+            var prop = AvaloniaProperty.Register<StyledElement, bool>("__AvaloniaReserved::Classes::" + className);
+            prop.Changed.Subscribe(args =>
+            {
+                var classes = ((IStyledElement)args.Sender).Classes;
+                classes.Set(className, args.NewValue.GetValueOrDefault());
+            });
+            
+            return prop;
+        }
+    }
+}

+ 21 - 0
src/Avalonia.Styling/Controls/Classes.cs

@@ -265,5 +265,26 @@ namespace Avalonia.Controls
                     $"The pseudoclass '{name}' may only be {operation} by the control itself.");
             }
         }
+
+        /// <summary>
+        /// Adds a or removes a  style class to/from the collection.
+        /// </summary>
+        /// <param name="name">The class names.</param>
+        /// <param name="value">If true adds the class, if false, removes it.</param>
+        /// <remarks>
+        /// Only standard classes may be added or removed via this method. To add pseudoclasses (classes
+        /// beginning with a ':' character) use the protected <see cref="StyledElement.PseudoClasses"/>
+        /// property.
+        /// </remarks>
+        public void Set(string name, bool value)
+        {
+            if (value)
+            {
+                if (!Contains(name))
+                    Add(name);
+            }
+            else
+                Remove(name);
+        }
     }
 }

+ 11 - 0
src/Avalonia.Styling/StyledElementExtensions.cs

@@ -0,0 +1,11 @@
+using System;
+using Avalonia.Data;
+
+namespace Avalonia
+{
+    public static class StyledElementExtensions
+    {
+        public static IDisposable BindClass(this IStyledElement target, string className, IBinding source, object anchor) =>
+            ClassBindingManager.Bind(target, className, source, anchor);
+    }
+}

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml

@@ -35,7 +35,7 @@
       <ItemsControl Items="{Binding}">
         <ItemsControl.ItemTemplate>
           <DataTemplate>
-            <TextBlock Text="{Binding Message}" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
+            <TextBlock Text="{Binding }" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
           </DataTemplate>
         </ItemsControl.ItemTemplate>
       </ItemsControl>

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

@@ -41,10 +41,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             
             // Targeted
             InsertBefore<PropertyReferenceResolver>(
+                new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
                 new AvaloniaXamlIlTransformInstanceAttachedProperties(),
                 new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());
             InsertAfter<PropertyReferenceResolver>(
-                new AvaloniaXamlIlAvaloniaPropertyResolver());
+                new AvaloniaXamlIlAvaloniaPropertyResolver(),
+                new AvaloniaXamlIlReorderClassesPropertiesTransformer()
+            );
 
             InsertBefore<ContentConvertTransformer>(                
                 new AvaloniaXamlIlBindingPathParser(),

+ 3 - 2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs

@@ -14,8 +14,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             XamlXmlnsMappings xmlnsMappings,
             XamlValueConverter customValueConverter,
             XamlIlClrPropertyInfoEmitter clrPropertyEmitter,
-            XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter)
-            : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter)
+            XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter,
+            IXamlIdentifierGenerator identifierGenerator = null)
+            : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter, identifierGenerator)
         {
             ClrPropertyEmitter = clrPropertyEmitter;
             AccessorFactoryEmitter = accessorFactoryEmitter;

+ 97 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs

@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using XamlX.Ast;
+using XamlX.Emit;
+using XamlX.IL;
+using XamlX.Transform;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
+{
+    class AvaloniaXamlIlResolveClassesPropertiesTransformer : IXamlAstTransformer
+    {
+        public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+        {
+            if (node is XamlAstNamePropertyReference prop
+                && prop.TargetType is XamlAstClrTypeReference targetRef
+                && prop.DeclaringType is XamlAstClrTypeReference declaringRef)
+            {
+                var types = context.GetAvaloniaTypes();
+                if (types.StyledElement.IsAssignableFrom(targetRef.Type)
+                    && types.Classes.Equals(declaringRef.Type))
+                {
+                    return new XamlAstClrProperty(node, "class:" + prop.Name, types.Classes,
+                        null)
+                    {
+                        Setters = { new ClassValueSetter(types, prop.Name), new ClassBindingSetter(types, prop.Name) }
+                    };
+                }
+            }
+            return node;
+        }
+
+       
+        class ClassValueSetter :  IXamlEmitablePropertySetter<IXamlILEmitter>
+        {
+            private readonly AvaloniaXamlIlWellKnownTypes _types;
+            private readonly string _className;
+
+            public ClassValueSetter(AvaloniaXamlIlWellKnownTypes types, string className)
+            {
+                _types = types;
+                _className = className;
+                Parameters = new[] { types.XamlIlTypes.Boolean };
+            }
+            
+            public void Emit(IXamlILEmitter emitter)
+            {
+                using (var value = emitter.LocalsPool.GetLocal(_types.XamlIlTypes.Boolean))
+                {
+                    emitter
+                        .Stloc(value.Local)
+                        .EmitCall(_types.StyledElementClassesProperty.Getter)
+                        .Ldstr(_className)
+                        .Ldloc(value.Local)
+                        .EmitCall(_types.Classes.GetMethod(new FindMethodMethodSignature("Set",
+                        _types.XamlIlTypes.Void, _types.XamlIlTypes.String, _types.XamlIlTypes.Boolean)));
+                }
+            }
+
+            public IXamlType TargetType => _types.StyledElement;
+
+            public PropertySetterBinderParameters BinderParameters { get; } =
+                new PropertySetterBinderParameters { AllowXNull = false };
+            public IReadOnlyList<IXamlType> Parameters { get; }
+        }
+
+        class ClassBindingSetter : IXamlEmitablePropertySetter<IXamlILEmitter>
+        {
+            private readonly AvaloniaXamlIlWellKnownTypes _types;
+            private readonly string _className;
+
+            public ClassBindingSetter(AvaloniaXamlIlWellKnownTypes types, string className)
+            {
+                _types = types;
+                _className = className;
+                Parameters = new[] {types.IBinding};
+            }
+            
+            public void Emit(IXamlILEmitter emitter)
+            {
+                using (var bloc = emitter.LocalsPool.GetLocal(_types.IBinding))
+                    emitter
+                        .Stloc(bloc.Local)
+                        .Ldstr(_className)
+                        .Ldloc(bloc.Local)
+                        // TODO: provide anchor?
+                        .Ldnull();
+                emitter.EmitCall(_types.ClassesBindMethod, true);
+            }
+
+            public IXamlType TargetType => _types.StyledElement;
+
+            public PropertySetterBinderParameters BinderParameters { get; } =
+                new PropertySetterBinderParameters { AllowXNull = false };
+            public IReadOnlyList<IXamlType> Parameters { get; }
+        }
+    }
+}

+ 40 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs

@@ -0,0 +1,40 @@
+using XamlX.Ast;
+using XamlX.Transform;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
+{
+    class AvaloniaXamlIlReorderClassesPropertiesTransformer : IXamlAstTransformer
+    {
+        public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+        {
+            if (node is XamlAstObjectNode obj)
+            {
+                IXamlAstNode classesNode = null;
+                IXamlAstNode firstSingleClassNode = null;
+                var types = context.GetAvaloniaTypes();
+                foreach (var child in obj.Children)
+                {
+                    if (child is XamlAstXamlPropertyValueNode propValue
+                        && propValue.Property is XamlAstClrProperty prop)
+                    {
+                        if (prop.DeclaringType.Equals(types.Classes))
+                        {
+                            if (firstSingleClassNode == null)
+                                firstSingleClassNode = child;
+                        }
+                        else if (prop.Name == "Classes" && prop.DeclaringType.Equals(types.StyledElement))
+                            classesNode = child;
+                    }
+                }
+
+                if (classesNode != null && firstSingleClassNode != null)
+                {
+                    obj.Children.Remove(classesNode);
+                    obj.Children.Insert(obj.Children.IndexOf(firstSingleClassNode), classesNode);
+                }
+            }
+
+            return node;
+        }
+    }
+}

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

@@ -25,6 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
         public IXamlType AssignBindingAttribute { get; }
         public IXamlType UnsetValueType { get; }
         public IXamlType StyledElement { get; }
+        public IXamlType IStyledElement { get; }
         public IXamlType NameScope { get; }
         public IXamlMethod NameScopeSetNameScope { get; }
         public IXamlType INameScope { get; }
@@ -78,6 +79,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
         public IXamlType ColumnDefinition { get; }
         public IXamlType ColumnDefinitions { get; }
         public IXamlType Classes { get; }
+        public IXamlMethod ClassesBindMethod { get; }
+        public IXamlProperty StyledElementClassesProperty { get; set; }
 
         public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
         {
@@ -97,6 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
                 IBinding, cfg.WellKnownTypes.Object);
             UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType");
             StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement");
+            IStyledElement = cfg.TypeSystem.GetType("Avalonia.IStyledElement");
             INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope");
             INameScopeRegister = INameScope.GetMethod(
                 new FindMethodMethodSignature("Register", XamlIlTypes.Void,
@@ -168,6 +172,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
             RowDefinition = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinition");
             RowDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinitions");
             Classes = cfg.TypeSystem.GetType("Avalonia.Controls.Classes");
+            StyledElementClassesProperty =
+                StyledElement.Properties.First(x => x.Name == "Classes" && x.PropertyType.Equals(Classes));
+            ClassesBindMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions")
+                .FindMethod( "BindClass", IDisposable, false, IStyledElement,
+                cfg.WellKnownTypes.String,
+                IBinding, cfg.WellKnownTypes.Object);
         }
     }
 

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates
 
         public IControl Build(object data, IControl existing)
         {
-            return existing ?? TemplateContent.Load(Content).Control;
+            return existing ?? TemplateContent.Load(Content)?.Control;
         }
     }
 }

+ 1 - 2
src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs

@@ -10,8 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates
         [TemplateContent]
         public object Content { get; set; }
 
-        public IPanel Build()
-                => (IPanel)TemplateContent.Load(Content).Control;
+        public IPanel Build() => (IPanel)TemplateContent.Load(Content)?.Control;
 
         object ITemplate.Build() => Build();
     }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates
         [TemplateContent]
         public object Content { get; set; }
 
-        public IControl Build() => TemplateContent.Load(Content).Control;
+        public IControl Build() => TemplateContent.Load(Content)?.Control;
 
         object ITemplate.Build() => Build();
     }

+ 6 - 2
src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs

@@ -1,6 +1,4 @@
 using System;
-using Avalonia.Controls;
-using System.Collections.Generic;
 using Avalonia.Controls.Templates;
 
 namespace Avalonia.Markup.Xaml.Templates
@@ -14,6 +12,12 @@ namespace Avalonia.Markup.Xaml.Templates
             {
                 return (ControlTemplateResult)direct(null);
             }
+
+            if (templateContent is null)
+            {
+                return null;
+            }
+
             throw new ArgumentException(nameof(templateContent));
         }
     }

+ 6 - 2
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@@ -51,8 +51,12 @@ namespace Avalonia.Markup.Xaml.Templates
 
         public IControl Build(object data)
         {
-            var visualTreeForItem = TemplateContent.Load(Content).Control;
-            visualTreeForItem.DataContext = data;
+            var visualTreeForItem = TemplateContent.Load(Content)?.Control;
+            if (visualTreeForItem != null)
+            {
+                visualTreeForItem.DataContext = data;
+            }
+
             return visualTreeForItem;
         }
     }

+ 31 - 0
tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

@@ -330,6 +330,37 @@ namespace Avalonia.Animation.UnitTests
             }
         }
 
+        [Fact]
+        public void Transitions_Can_Be_Changed_To_Collection_That_Contains_The_Same_Transitions()
+        {
+            var target = CreateTarget();
+            var control = CreateControl(target.Object);
+
+            control.Transitions = new Transitions { target.Object };
+        }
+
+        [Fact]
+        public void Transitions_Can_Re_Set_During_Batch_Update()
+        {
+            var target = CreateTarget();
+            var control = CreateControl(target.Object);
+
+            // Assigning and then clearing Transitions ensures we have a transition state
+            // collection created.
+            control.Transitions = null;
+
+            control.BeginBatchUpdate();
+
+            // Setting opacity then Transitions means that we receive the Transitions change
+            // after the Opacity change when EndBatchUpdate is called.
+            control.Opacity = 0.5;
+            control.Transitions = new Transitions { target.Object };
+
+            // Which means that the transition state hasn't been initialized with the new
+            // Transitions when the Opacity change notification gets raised here.
+            control.EndBatchUpdate();
+        }
+
         private static Mock<ITransition> CreateTarget()
         {
             return CreateTransition(Visual.OpacityProperty);

+ 122 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@@ -53,6 +53,21 @@ namespace Avalonia.Base.UnitTests
             Assert.Empty(raised);
         }
 
+        [Fact]
+        public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<string>();
+
+            var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+            target.BeginBatchUpdate();
+            sub.Dispose();
+
+            Assert.Empty(raised);
+        }
+
         [Fact]
         public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
         {
@@ -240,6 +255,27 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal(BindingPriority.Unset, raised[0].Priority);
         }
 
+        [Fact]
+        public void Binding_Disposal_Should_Be_Raised_After_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            sub.Dispose();
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Null(target.Foo);
+            Assert.Equal("foo", raised[0].OldValue);
+            Assert.Null(raised[0].NewValue);
+            Assert.Equal(BindingPriority.Unset, raised[0].Priority);
+        }
+
         [Fact]
         public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1()
         {
@@ -449,6 +485,92 @@ namespace Avalonia.Base.UnitTests
             Assert.Null(raised[1].NewValue);
         }
 
+        [Fact]
+        public void Can_Set_Cleared_Value_When_Ending_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = 0;
+
+            target.Foo = "foo";
+
+            target.BeginBatchUpdate();
+            target.ClearValue(TestClass.FooProperty);
+            target.PropertyChanged += (sender, e) =>
+            {
+                if (e.Property == TestClass.FooProperty && e.NewValue is null)
+                {
+                    target.Foo = "bar";
+                    ++raised;
+                }
+            };
+            target.EndBatchUpdate();
+
+            Assert.Equal("bar", target.Foo);
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Can_Bind_Cleared_Value_When_Ending_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = 0;
+            var notifications = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.Foo = "foo";
+
+            target.BeginBatchUpdate();
+            target.ClearValue(TestClass.FooProperty);
+            target.PropertyChanged += (sender, e) =>
+            {
+                if (e.Property == TestClass.FooProperty && e.NewValue is null)
+                {
+                    target.Bind(TestClass.FooProperty, new TestObservable<string>("bar"));
+                    ++raised;
+                }
+
+                notifications.Add(e);
+            };
+            target.EndBatchUpdate();
+
+            Assert.Equal("bar", target.Foo);
+            Assert.Equal(1, raised);
+            Assert.Equal(2, notifications.Count);
+            Assert.Equal(null, notifications[0].NewValue);
+            Assert.Equal("bar", notifications[1].NewValue);
+        }
+
+        [Fact]
+        public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = 0;
+            var notifications = new List<AvaloniaPropertyChangedEventArgs>();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("foo");
+
+            target.Bind(TestClass.FooProperty, observable1);
+
+            target.BeginBatchUpdate();
+            observable1.OnCompleted();
+            target.PropertyChanged += (sender, e) =>
+            {
+                if (e.Property == TestClass.FooProperty && e.NewValue is null)
+                {
+                    target.Bind(TestClass.FooProperty, observable2);
+                    ++raised;
+                }
+
+                notifications.Add(e);
+            };
+            target.EndBatchUpdate();
+
+            Assert.Equal("foo", target.Foo);
+            Assert.Equal(1, raised);
+            Assert.Equal(2, notifications.Count);
+            Assert.Equal(null, notifications[0].NewValue);
+            Assert.Equal("foo", notifications[1].NewValue);
+        }
+
         public class TestClass : AvaloniaObject
         {
             public static readonly StyledProperty<string> FooProperty =

+ 46 - 18
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@@ -1,10 +1,14 @@
 using System.Collections.ObjectModel;
 using System.Linq;
+using System.Reactive.Subjects;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.LogicalTree;
+using Avalonia.Threading;
 using Avalonia.VisualTree;
+using Avalonia.UnitTests;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -77,9 +81,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -113,9 +117,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -172,9 +176,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -206,9 +210,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -235,9 +239,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -269,9 +273,9 @@ namespace Avalonia.Controls.UnitTests
         {
             var items = new ObservableCollection<string>
             {
-               "Foo",
-               "Bar",
-               "FooBar"
+                "Foo",
+                "Bar",
+                "FooBar"
             };
 
             var target = new Carousel
@@ -311,5 +315,29 @@ namespace Avalonia.Controls.UnitTests
             contentPresenter.UpdateChild();
             return Assert.IsType<TextBlock>(contentPresenter.Child);
         }
+
+        [Fact]
+        public void SelectedItem_Validation()
+        {
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                var target = new Carousel
+                {
+                    Template = new FuncControlTemplate<Carousel>(CreateTemplate), IsVirtualized = false
+                };
+
+                target.ApplyTemplate();
+                target.Presenter.ApplyTemplate();
+
+                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 }));
+            }
+        }
     }
 }

+ 29 - 0
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@@ -1,11 +1,14 @@
 using System.Linq;
+using System.Reactive.Subjects;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
+using Avalonia.Threading;
 using Avalonia.UnitTests;
 using Xunit;
 
@@ -173,5 +176,31 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Equal(expectedSelectedIndex, target.SelectedIndex);
             }
         }
+        
+        [Fact]
+        public void SelectedItem_Validation()
+        {
+
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                var target = new ComboBox
+                {
+                    Template = GetTemplate(),
+                    VirtualizationMode =  ItemVirtualizationMode.None
+                };
+
+                target.ApplyTemplate();
+                target.Presenter.ApplyTemplate();
+                
+                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 }));
+                
+            }
+            
+        } 
     }
 }

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

@@ -1,11 +1,14 @@
 using System.Linq;
+using System.Reactive.Subjects;
 using Avalonia.Collections;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Styling;
+using Avalonia.Threading;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Xunit;
@@ -559,5 +562,28 @@ namespace Avalonia.Controls.UnitTests
 
             public string Value { get; }
         }
+
+
+        [Fact]
+        public void SelectedItem_Validation()
+        {
+            var target = new ListBox
+            {
+                Template = ListBoxTemplate(),
+                Items = new[] { "Foo" },
+                ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
+                SelectionMode = SelectionMode.AlwaysSelected,
+                VirtualizationMode = ItemVirtualizationMode.None
+            };
+
+            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 }));
+        }
     }
 }

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

@@ -377,5 +377,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             public string Greeting1 { get; set; } = "Hello";
             public string Greeting2 { get; set; } = "World";
         }
+        
+        [Fact]
+        public void Binding_Classes_Works()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                // Note, this test also checks `Classes` reordering, so it should be kept AFTER the last single class
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <Button Name='button' Classes.MyClass='{Binding Foo}' Classes.MySecondClass='True' Classes='foo bar'/>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                button.DataContext = new { Foo = true };
+                window.ApplyTemplate();
+
+                Assert.True(button.Classes.Contains("MyClass"));
+                Assert.True(button.Classes.Contains("MySecondClass"));
+                Assert.True(button.Classes.Contains("foo"));
+                Assert.True(button.Classes.Contains("bar"));
+
+                button.DataContext = new { Foo = false };
+                
+                Assert.False(button.Classes.Contains("MyClass"));
+                Assert.True(button.Classes.Contains("MySecondClass"));
+                Assert.True(button.Classes.Contains("foo"));
+                Assert.True(button.Classes.Contains("bar"));
+            }
+        }
     }
 }

+ 25 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

@@ -7,6 +7,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 {
     public class DataTemplateTests : XamlTestBase
     {
+        [Fact]
+        public void DataTemplate_Can_Be_Empty()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:sys='clr-namespace:System;assembly=netstandard'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.DataTemplates>
+        <DataTemplate DataType='{x:Type sys:String}' />
+    </Window.DataTemplates>
+    <ContentControl Name='target' Content='Foo'/>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var target = window.FindControl<ContentControl>("target");
+
+                window.ApplyTemplate();
+                target.ApplyTemplate();
+                ((ContentPresenter)target.Presenter).UpdateChild();
+
+                Assert.Null(target.Presenter.Child);
+            }
+        }
+
         [Fact]
         public void DataTemplate_Can_Contain_Name()
         {