diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props
index f74ab07e31..c3b136d41d 100644
--- a/build/ReactiveUI.props
+++ b/build/ReactiveUI.props
@@ -1,5 +1,5 @@
-
+
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index d627a2bf19..eec21d4423 100644
--- a/nukebuild/Build.cs
+++ b/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 => _ => _
diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs
index c76019d9eb..a92c988fbd 100644
--- a/nukebuild/BuildParameters.cs
+++ b/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()
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index d5aedf7783..3c2d2ee359 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -1,7 +1,7 @@
- Exe
+ WinExe
netcoreapp3.1
true
@@ -15,6 +15,10 @@
+
+
+ en
+
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 1aa926a2a6..53ad213d92 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -27,6 +27,6 @@
-
+
diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs
index 067d9f462f..a415046513 100644
--- a/src/Avalonia.Animation/Animatable.cs
+++ b/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();
var newTransitions = change.NewValue.GetValueOrDefault();
+ // 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);
diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
index 38c1728cd9..3e17a81dd8 100644
--- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs
+++ b/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);
diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
index 600d725187..d39fc3bb1e 100644
--- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
@@ -6,12 +6,19 @@ using Avalonia.Data;
namespace Avalonia.PropertyStore
{
+ ///
+ /// Represents an untyped interface to .
+ ///
+ internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
+ {
+ }
+
///
/// Stores a value with a priority in a or
/// .
///
/// The property type.
- internal class ConstantValueEntry : IPriorityValueEntry, IDisposable
+ internal class ConstantValueEntry : IPriorityValueEntry, IConstantValueEntry
{
private IValueSink _sink;
private Optional _value;
diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
index 1af6f21156..c513f75962 100644
--- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
+++ b/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)
diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs
index e32b20cc96..495f13e1a9 100644
--- a/src/Avalonia.Base/ValueStore.cs
+++ b/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)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> 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(StyledPropertyBase property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue 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(StyledPropertyBase property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue 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 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(_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(_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);
}
diff --git a/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs
new file mode 100644
index 0000000000..f207b558a3
--- /dev/null
+++ b/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();
+ }
+}
diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
index 6ef8a98fae..508045dccb 100644
--- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
+++ b/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",
diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt
index 0284463f1c..3a6810eed9 100644
--- a/src/Avalonia.Controls/ApiCompatBaseline.txt
+++ b/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.
diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs
index a7ebced0ca..7abb47f963 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -234,6 +234,7 @@ namespace Avalonia.Controls
if (Command != null)
{
Command.CanExecuteChanged += CanExecuteChanged;
+ CanExecuteChanged(this, EventArgs.Empty);
}
}
diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs
index cd60130c5b..be15d3d444 100644
--- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs
+++ b/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)
+ ///
+ /// Currently attached window.
+ ///
+ 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("PART_MinimiseButton");
var fullScreenButton = e.NameScope.Get("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();
}
}
}
diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs
index 64414b1f47..c2fe1bb691 100644
--- a/src/Avalonia.Controls/NativeControlHost.cs
+++ b/src/Avalonia.Controls/NativeControlHost.cs
@@ -16,30 +16,16 @@ namespace Avalonia.Controls
private bool _queuedForDestruction;
private bool _queuedForMoveResize;
private readonly List _propertyChangedSubscriptions = new List();
- private readonly EventHandler _propertyChangedHandler;
- static NativeControlHost()
- {
- IsVisibleProperty.Changed.AddClassHandler(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();
diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
index de3f58886b..bb3c0288eb 100644
--- a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
+++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
@@ -16,7 +16,7 @@ namespace Avalonia.Platform
///
/// The default for the platform.
///
- Default = SystemChrome,
+ Default = PreferSystemChrome,
///
/// Use SystemChrome
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 2cd69793dc..19824a71f0 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/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);
///
/// Defines the property.
@@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives
EndUpdating();
}
+ ///
+ /// Called to update the validation state for properties for which data validation is
+ /// enabled.
+ ///
+ /// The property.
+ /// The new binding value for the property.
+ protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue 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 Selection { get; set; }
public Optional SelectedItems { get; set; }
- public Optional SelectedIndex
+ public Optional SelectedIndex
{
get => _selectedIndex;
set
@@ -996,6 +1010,6 @@ namespace Avalonia.Controls.Primitives
_selectedIndex = default;
}
}
- }
+ }
}
}
diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs
index e02efc2bd2..6419981fb1 100644
--- a/src/Avalonia.Controls/Slider.cs
+++ b/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;
diff --git a/src/Avalonia.Diagnostics/DevToolsExtensions.cs b/src/Avalonia.Diagnostics/DevToolsExtensions.cs
index 4bc2ca313f..aa585dca40 100644
--- a/src/Avalonia.Diagnostics/DevToolsExtensions.cs
+++ b/src/Avalonia.Diagnostics/DevToolsExtensions.cs
@@ -15,7 +15,7 @@ namespace Avalonia
/// The window to attach DevTools to.
public static void AttachDevTools(this TopLevel root)
{
- DevTools.Attach(root, new KeyGesture(Key.F12));
+ DevTools.Attach(root, new DevToolsOptions());
}
///
@@ -27,5 +27,15 @@ namespace Avalonia
{
DevTools.Attach(root, gesture);
}
+
+ ///
+ /// Attaches DevTools to a window, to be opened with the specified options.
+ ///
+ /// The window to attach DevTools to.
+ /// Additional settings of DevTools.
+ public static void AttachDevTools(this TopLevel root, DevToolsOptions options)
+ {
+ DevTools.Attach(root, options);
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
index 4899be2955..7942d22962 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
+++ b/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 s_open = new Dictionary();
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);
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
new file mode 100644
index 0000000000..ee46192207
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
@@ -0,0 +1,26 @@
+using Avalonia.Input;
+
+namespace Avalonia.Diagnostics
+{
+ ///
+ /// Describes options used to customize DevTools.
+ ///
+ public class DevToolsOptions
+ {
+ ///
+ /// Gets or sets the key gesture used to open DevTools.
+ ///
+ public KeyGesture Gesture { get; set; } = new KeyGesture(Key.F12);
+
+ ///
+ /// 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.
+ ///
+ public bool ShowAsChildWindow { get; set; } = true;
+
+ ///
+ /// Gets or sets the initial size of the DevTools window. The default value is 1024x512.
+ ///
+ public Size Size { get; set; } = new Size(1024, 512);
+ }
+}
diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl
index 9cada1120a..adcbeb2d3a 100644
--- a/src/Avalonia.Native/avn.idl
+++ b/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)]
diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
index 6771d3e179..359da3d7c2 100644
--- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
+++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
@@ -9,18 +9,22 @@ namespace Avalonia.ReactiveUI
{
///
/// 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.
///
public static TAppBuilder UseReactiveUI(this TAppBuilder builder)
- where TAppBuilder : AppBuilderBase, new()
- {
- return builder.AfterPlatformServicesSetup(_ =>
+ where TAppBuilder : AppBuilderBase, 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));
- });
- }
+ }));
}
}
diff --git a/src/Avalonia.Styling/ClassBindingManager.cs b/src/Avalonia.Styling/ClassBindingManager.cs
new file mode 100644
index 0000000000..e8b1cc301d
--- /dev/null
+++ b/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 s_RegisteredProperties =
+ new Dictionary();
+
+ 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("__AvaloniaReserved::Classes::" + className);
+ prop.Changed.Subscribe(args =>
+ {
+ var classes = ((IStyledElement)args.Sender).Classes;
+ classes.Set(className, args.NewValue.GetValueOrDefault());
+ });
+
+ return prop;
+ }
+ }
+}
diff --git a/src/Avalonia.Styling/Controls/Classes.cs b/src/Avalonia.Styling/Controls/Classes.cs
index 51dca57928..4e2783d4ec 100644
--- a/src/Avalonia.Styling/Controls/Classes.cs
+++ b/src/Avalonia.Styling/Controls/Classes.cs
@@ -265,5 +265,26 @@ namespace Avalonia.Controls
$"The pseudoclass '{name}' may only be {operation} by the control itself.");
}
}
+
+ ///
+ /// Adds a or removes a style class to/from the collection.
+ ///
+ /// The class names.
+ /// If true adds the class, if false, removes it.
+ ///
+ /// Only standard classes may be added or removed via this method. To add pseudoclasses (classes
+ /// beginning with a ':' character) use the protected
+ /// property.
+ ///
+ public void Set(string name, bool value)
+ {
+ if (value)
+ {
+ if (!Contains(name))
+ Add(name);
+ }
+ else
+ Remove(name);
+ }
}
}
diff --git a/src/Avalonia.Styling/StyledElementExtensions.cs b/src/Avalonia.Styling/StyledElementExtensions.cs
new file mode 100644
index 0000000000..0c5a5f7438
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
index ed31f7b573..f83af266c2 100644
--- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
@@ -35,7 +35,7 @@
-
+
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index abff763bb1..a191dc59fb 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -41,10 +41,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
// Targeted
InsertBefore(
+ new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
new AvaloniaXamlIlTransformInstanceAttachedProperties(),
new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());
InsertAfter(
- new AvaloniaXamlIlAvaloniaPropertyResolver());
+ new AvaloniaXamlIlAvaloniaPropertyResolver(),
+ new AvaloniaXamlIlReorderClassesPropertiesTransformer()
+ );
InsertBefore(
new AvaloniaXamlIlBindingPathParser(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs
index 0c0dcb1634..f6f47dce0d 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs
+++ b/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;
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs
new file mode 100644
index 0000000000..23232dbcf3
--- /dev/null
+++ b/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
+ {
+ 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 Parameters { get; }
+ }
+
+ class ClassBindingSetter : IXamlEmitablePropertySetter
+ {
+ 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 Parameters { get; }
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs
new file mode 100644
index 0000000000..ae3515a6d6
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index 34aae2c5ed..c4995b2de3 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/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);
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
index 07c5451135..650534b347 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
+++ b/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;
}
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
index c8843a3176..c096ed7ed7 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
+++ b/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();
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
index 65323ae665..45fae9cb28 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
+++ b/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();
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
index 96f25668fb..483a1a5d06 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
+++ b/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));
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
index d785ac4ac0..7b065c7f47 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
+++ b/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;
}
}
diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
index 7633a761a3..b01fb70f58 100644
--- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
+++ b/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 CreateTarget()
{
return CreateTransition(Visual.OpacityProperty);
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
index 9f0e52c8d9..53ad87421e 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
+++ b/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("foo");
+ var raised = new List();
+
+ 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("foo");
+ var raised = new List();
+
+ 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();
+
+ 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("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();
+ var observable1 = new TestObservable("foo");
+ var observable2 = new TestObservable("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 FooProperty =
diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
index 051f6c3fd3..b93e48618d 100644
--- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs
+++ b/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
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -113,9 +117,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -172,9 +176,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -206,9 +210,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -235,9 +239,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -269,9 +273,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection
{
- "Foo",
- "Bar",
- "FooBar"
+ "Foo",
+ "Bar",
+ "FooBar"
};
var target = new Carousel
@@ -311,5 +315,29 @@ namespace Avalonia.Controls.UnitTests
contentPresenter.UpdateChild();
return Assert.IsType(contentPresenter.Child);
}
+
+ [Fact]
+ public void SelectedItem_Validation()
+ {
+ using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+ {
+ var target = new Carousel
+ {
+ Template = new FuncControlTemplate(CreateTemplate), IsVirtualized = false
+ };
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ var exception = new System.InvalidCastException("failed validation");
+ var textObservable =
+ new BehaviorSubject(new BindingNotification(exception,
+ BindingErrorType.DataValidationError));
+ target.Bind(ComboBox.SelectedItemProperty, textObservable);
+
+ Assert.True(DataValidationErrors.GetHasErrors(target));
+ Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
index 4ea838358c..8f9c7fdb0b 100644
--- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
+++ b/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(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ target.Bind(ComboBox.SelectedItemProperty, textObservable);
+
+ Assert.True(DataValidationErrors.GetHasErrors(target));
+ Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
+
+ }
+
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
index 145fce4fed..963bba7c83 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
+++ b/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((_, __) => new Canvas()),
+ SelectionMode = SelectionMode.AlwaysSelected,
+ VirtualizationMode = ItemVirtualizationMode.None
+ };
+
+ Prepare(target);
+
+ var exception = new System.InvalidCastException("failed validation");
+ var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ target.Bind(ComboBox.SelectedItemProperty, textObservable);
+
+ Assert.True(DataValidationErrors.GetHasErrors(target));
+ Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
+ }
}
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
index 4f2b580bce..8af638c5d7 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
+++ b/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 = @"
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var button = window.FindControl