diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 6f21971d3d..26908b1081 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -6,4 +6,9 @@ false + + + + + diff --git a/src/Avalonia.Base/Platform/AssetLoader.cs b/src/Avalonia.Base/Platform/AssetLoader.cs index 659cfb75df..7df446e854 100644 --- a/src/Avalonia.Base/Platform/AssetLoader.cs +++ b/src/Avalonia.Base/Platform/AssetLoader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -62,7 +63,7 @@ namespace Avalonia.Platform /// True if the asset could be found; otherwise false. public bool Exists(Uri uri, Uri? baseUri = null) { - return GetAsset(uri, baseUri) != null; + return TryGetAsset(uri, baseUri, out _); } /// @@ -94,21 +95,27 @@ namespace Avalonia.Platform /// public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) { - var asset = GetAsset(uri, baseUri); - - if (asset == null) + if (TryGetAsset(uri, baseUri, out var assetDescriptor)) { - throw new FileNotFoundException($"The resource {uri} could not be found."); + return (assetDescriptor.GetStream(), assetDescriptor.Assembly); } - return (asset.GetStream(), asset.Assembly); + throw new FileNotFoundException($"The resource {uri} could not be found."); } public Assembly? GetAssembly(Uri uri, Uri? baseUri) { if (!uri.IsAbsoluteUri && baseUri != null) + { uri = new Uri(baseUri, uri); - return GetAssembly(uri)?.Assembly; + } + + if (TryGetAssembly(uri, out var assemblyDescriptor)) + { + return assemblyDescriptor.Assembly; + } + + return null; } /// @@ -121,99 +128,145 @@ namespace Avalonia.Platform { if (uri.IsAbsoluteResm()) { - var assembly = GetAssembly(uri); + if (!TryGetAssembly(uri, out var assembly)) + { + assembly = _defaultResmAssembly; + } return assembly?.Resources? - .Where(x => x.Key.IndexOf(uri.GetUnescapeAbsolutePath(), StringComparison.Ordinal) >= 0) - .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? - Enumerable.Empty(); + .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) + .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? + Enumerable.Empty(); } uri = uri.EnsureAbsolute(baseUri); + if (uri.IsAvares()) { - var (asm, path) = GetResAsmAndPath(uri); - if (asm == null) + if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) { - throw new ArgumentException( - "No default assembly, entry assembly or explicit assembly specified; " + - "don't know where to look up for the resource, try specifying assembly explicitly."); + return Enumerable.Empty(); } - if (asm.AvaloniaResources == null) + if (assembly?.AvaloniaResources == null) + { return Enumerable.Empty(); + } - if (path[path.Length - 1] != '/') + if (path.Length > 0 && path[path.Length - 1] != '/') + { path += '/'; + } - return asm.AvaloniaResources + return assembly.AvaloniaResources .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) - .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); + .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); } return Enumerable.Empty(); } - - private IAssetDescriptor? GetAsset(Uri uri, Uri? baseUri) - { + + private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) + { + assetDescriptor = null; + if (uri.IsAbsoluteResm()) { - var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; - - if (asm == null) + if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) { - throw new ArgumentException( - "No default assembly, entry assembly or explicit assembly specified; " + - "don't know where to look up for the resource, try specifying assembly explicitly."); + assembly = _defaultResmAssembly; } - var resourceKey = uri.AbsolutePath; - IAssetDescriptor? rv = null; - asm.Resources?.TryGetValue(resourceKey, out rv); - return rv; + if (assembly?.Resources != null) + { + var resourceKey = uri.AbsolutePath; + + if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) + { + return true; + } + } } uri = uri.EnsureAbsolute(baseUri); if (uri.IsAvares()) { - var (asm, path) = GetResAsmAndPath(uri); - if (asm.AvaloniaResources == null) - return null; - asm.AvaloniaResources.TryGetValue(path, out var desc); - return desc; + if (TryGetResAsmAndPath(uri, out var assembly, out var path)) + { + if (assembly.AvaloniaResources == null) + { + return false; + } + + if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) + { + return true; + } + } } - throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); + return false; } - private static (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) + private static bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) { - var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); - return (asm, uri.GetUnescapeAbsolutePath()); + path = uri.GetUnescapeAbsolutePath(); + + if (TryLoadAssembly(uri.Authority, out assembly)) + { + return true; + } + + return false; } - - private static IAssemblyDescriptor? GetAssembly(Uri? uri) + + private static bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) { + assembly = null; + if (uri != null) { if (!uri.IsAbsoluteUri) - return null; - if (uri.IsAvares()) - return GetResAsmAndPath(uri).asm; + { + return false; + } + + if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) + { + return true; + } if (uri.IsResm()) { var assemblyName = uri.GetAssemblyNameFromQuery(); - if (assemblyName.Length > 0) - return s_assemblyDescriptorResolver.GetAssembly(assemblyName); + + if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) + { + return true; + } } } - return null; + return false; + } + + private static bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; + + try + { + assembly = s_assemblyDescriptorResolver.GetAssembly(assemblyName); + + return true; + } + catch (Exception) { } + + return false; } #endif - + public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 0887f11ec9..53cd3ff307 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -924,7 +924,7 @@ namespace Avalonia.PropertyStore { _effectiveValues.GetKeyValue(i, out var key, out var e); - if (e.Priority == BindingPriority.Unset) + if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue) { RemoveEffectiveValue(key, i); e.DisposeAndRaiseUnset(this, key); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 4d8759f545..4b09bc9280 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -21,7 +21,7 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IImmutableBrush foreground, IRef glyphRun) - : base(new Rect(glyphRun.Item.Size), transform, foreground) + : base(new Rect(glyphRun.Item.BaselineOrigin, glyphRun.Item.Size), transform, foreground) { GlyphRun = glyphRun.Clone(); } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index fa18ee468c..eff6603727 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -1,13 +1,12 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using Avalonia.Controls.Presenters; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Reactive; -using Avalonia.Styling; namespace Avalonia.Controls { @@ -59,6 +58,11 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } + /// + /// Gets whether the hosts the items created by an . + /// + public bool IsItemsHost { get; internal set; } + event EventHandler? IChildIndexProvider.ChildIndexChanged { add @@ -129,24 +133,29 @@ namespace Avalonia.Controls /// The event args. protected virtual void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) { - List controls; - switch (e.Action) { case NotifyCollectionChangedAction.Add: - controls = e.NewItems!.OfType().ToList(); - LogicalChildren.InsertRange(e.NewStartingIndex, controls); + if (!IsItemsHost) + { + LogicalChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType().ToList()); + } VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType()); break; case NotifyCollectionChangedAction.Move: - LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); - VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex); + if (!IsItemsHost) + { + LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); + } + VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); break; case NotifyCollectionChangedAction.Remove: - controls = e.OldItems!.OfType().ToList(); - LogicalChildren.RemoveAll(controls); + if (!IsItemsHost) + { + LogicalChildren.RemoveAll(e.OldItems!.OfType().ToList()); + } VisualChildren.RemoveAll(e.OldItems!.OfType()); break; @@ -155,7 +164,10 @@ namespace Avalonia.Controls { var index = i + e.OldStartingIndex; var child = (Control)e.NewItems![i]!; - LogicalChildren[index] = child; + if (!IsItemsHost) + { + LogicalChildren[index] = child; + } VisualChildren[index] = child; } break; @@ -200,6 +212,7 @@ namespace Avalonia.Controls return child is Control control ? Children.IndexOf(control) : -1; } + /// public bool TryGetTotalCount(out int count) { count = Children.Count; diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index e8eaac7d17..0a762c438d 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -167,6 +167,7 @@ namespace Avalonia.Controls.Presenters Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); + Panel.IsItemsHost = true; _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo; LogicalChildren.Add(Panel); VisualChildren.Add(Panel); diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs index f5b2d462ab..1ab4c0d2dc 100644 --- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs +++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs @@ -4,7 +4,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input { - static class KeyInterop + public static class KeyInterop { private static readonly Dictionary s_virtualKeyFromKey = new Dictionary { diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index 0a27602604..0c0b487dba 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -396,7 +396,7 @@ public partial class AvaloniaPropertyAnalyzer hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration } - var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; + var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; // assume that the property is owned by its containing type at the point of assignment, until we find its registration result.SetAssignment(s, new(s.ContainingType, Location.None)); @@ -570,7 +570,7 @@ public partial class AvaloniaPropertyAnalyzer if (_allGetSetMethods.Contains(originalMethod)) { - if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && GetReferencedProperty(invocation.Arguments[0]) is { } refProp && refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) && !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) && @@ -694,9 +694,14 @@ public partial class AvaloniaPropertyAnalyzer void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) { - if (method == null) + if (method is null) { - context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + if (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Public || + (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Protected + && avaloniaPropertyStorage.ContainingSymbol.DeclaredAccessibility == Accessibility.Public)) + { + context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + } } else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) { diff --git a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs index 28fb19e119..894b6578e3 100644 --- a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs @@ -9,7 +9,7 @@ namespace Avalonia.Base.UnitTests; public class AssetLoaderTests : IDisposable { - public class MockAssembly : Assembly {} + public class MockAssembly : Assembly { } private const string AssemblyNameWithWhitespace = "Awesome Library"; @@ -50,6 +50,17 @@ public class AssetLoaderTests : IDisposable Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName); } + [Fact] + public void Invalid_AssemblyName_Should_Yield_Empty_Enumerable() + { + var uri = new Uri($"avares://InvalidAssembly"); + var loader = new AssetLoader(); + + var assemblyActual = loader.GetAssets(uri, null); + + Assert.Empty(assemblyActual); + } + private static IAssemblyDescriptor CreateAssemblyDescriptor(string assemblyName) { var assembly = Mock.Of(); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 8ad36a583e..c850fbdb08 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -1,6 +1,9 @@ using System; +using Avalonia.Controls; using Avalonia.Data; using Avalonia.Diagnostics; +using Avalonia.Styling; +using Avalonia.UnitTests; using Xunit; using Observable = Avalonia.Reactive.Observable; @@ -275,6 +278,79 @@ namespace Avalonia.Base.UnitTests Assert.Equal("style", target.Foo); } + [Fact] + public void SetCurrent_Value_Persists_When_Toggling_Style_1() + { + var target = new Class1(); + var root = new TestRoot(target) + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(Class1.BarProperty, "bar") }, + } + } + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + + target.Classes.Add("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bar", target.Bar); + + target.Classes.Remove("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + } + + [Fact] + public void SetCurrent_Value_Persists_When_Toggling_Style_2() + { + var target = new Class1(); + var root = new TestRoot(target) + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.BarProperty, "bar"), + new Setter(Class1.InheritedProperty, "inherited"), + }, + } + } + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + Assert.Equal("inheriteddefault", target.Inherited); + + target.Classes.Add("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bar", target.Bar); + Assert.Equal("inherited", target.Inherited); + + target.Classes.Remove("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + Assert.Equal("inheriteddefault", target.Inherited); + } + private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) { return target.GetDiagnostic(property).Priority; @@ -285,16 +361,19 @@ namespace Avalonia.Base.UnitTests return target.GetDiagnostic(property).IsOverriddenCurrentValue; } - private class Class1 : AvaloniaObject + private class Class1 : Control { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register(nameof(Foo), "foodefault"); + public static readonly StyledProperty BarProperty = + AvaloniaProperty.Register(nameof(Bar), "bardefault"); public static readonly StyledProperty InheritedProperty = AvaloniaProperty.Register(nameof(Inherited), "inheriteddefault", inherits: true); public static readonly StyledProperty CoercedProperty = AvaloniaProperty.Register(nameof(Coerced), coerce: Coerce); public string Foo => GetValue(FooProperty); + public string Bar => GetValue(BarProperty); public string Inherited => GetValue(InheritedProperty); public double Coerced => GetValue(CoercedProperty); public double CoerceMax { get; set; } = 100; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index f074972cef..3aaf62f0bf 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -73,6 +73,19 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(target, target.Presenter.Panel.TemplatedParent); } + [Fact] + public void Panel_Should_Have_ItemsHost_Set_To_True() + { + var target = new ItemsControl(); + + target.Template = GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + target.Presenter!.ApplyTemplate(); + + Assert.True(target.Presenter.Panel!.IsItemsHost); + } + [Fact] public void Container_Should_Have_TemplatedParent_Set_To_Null() { @@ -634,7 +647,7 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.LogicalChildren[0]; Assert.Null(NameScope.GetNameScope((TextBlock)item)); } diff --git a/tests/Avalonia.Controls.UnitTests/PanelTests.cs b/tests/Avalonia.Controls.UnitTests/PanelTests.cs index a31f0dd4c2..82f133d533 100644 --- a/tests/Avalonia.Controls.UnitTests/PanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/PanelTests.cs @@ -141,5 +141,20 @@ namespace Avalonia.Controls.UnitTests var panel = new Panel(); Assert.Throws(() => panel.Children.Add(null!)); } + + [Fact] + public void Adding_Control_To_Items_Host_Panel_Should_Not_Affect_Logical_Children() + { + var child = new Control(); + var realParent = new ContentControl { Content = child }; + var panel = new Panel { IsItemsHost = true }; + + panel.Children.Add(child); + + Assert.Empty(panel.LogicalChildren); + Assert.Same(child.Parent, realParent); + Assert.Same(child.GetLogicalParent(), realParent); + Assert.Same(child.GetVisualParent(), panel); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 4c5397db60..0f72b2101a 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -327,7 +327,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); - var logicalChildren = target.ItemsPresenterPart.Panel.GetLogicalChildren(); + var logicalChildren = target.GetLogicalChildren(); var result = logicalChildren .OfType() diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 1a9fb33b7e..2ca716fa8f 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1178,7 +1178,7 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.LogicalChildren[0]; Assert.Null(NameScope.GetNameScope((TreeViewItem)item)); }