From ce92286112282e5cead7fd5f9fc40590c4bb46cc Mon Sep 17 00:00:00 2001 From: JaggerJo Date: Mon, 22 Jul 2019 00:21:43 +0200 Subject: [PATCH 001/165] add basic operations as static methods and use them in the declared operators. - Dot - Cross - Normalize - Divide - Multiply - Add - Subtract - Negate --- src/Avalonia.Visuals/Vector.cs | 115 +++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 2f1690184d..f185682dc0 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -65,9 +65,7 @@ namespace Avalonia /// Second vector /// The dot product public static double operator *(Vector a, Vector b) - { - return a.X * b.X + a.Y * b.Y; - } + => Dot(a, b); /// /// Scales a vector. @@ -76,9 +74,7 @@ namespace Avalonia /// The scaling factor. /// The scaled vector. public static Vector operator *(Vector vector, double scale) - { - return new Vector(vector._x * scale, vector._y * scale); - } + => Multiply(vector, scale); /// /// Scales a vector. @@ -87,9 +83,7 @@ namespace Avalonia /// The divisor. /// The scaled vector. public static Vector operator /(Vector vector, double scale) - { - return new Vector(vector._x / scale, vector._y / scale); - } + => Divide(vector, scale); /// /// Length of the vector @@ -102,9 +96,7 @@ namespace Avalonia /// The vector. /// The negated vector. public static Vector operator -(Vector a) - { - return new Vector(-a._x, -a._y); - } + => Negate(a); /// /// Adds two vectors. @@ -113,9 +105,7 @@ namespace Avalonia /// The second vector. /// A vector that is the result of the addition. public static Vector operator +(Vector a, Vector b) - { - return new Vector(a._x + b._x, a._y + b._y); - } + => Add(a, b); /// /// Subtracts two vectors. @@ -124,9 +114,7 @@ namespace Avalonia /// The second vector. /// A vector that is the result of the subtraction. public static Vector operator -(Vector a, Vector b) - { - return new Vector(a._x - b._x, a._y - b._y); - } + => Subtract(a, b); /// /// Check if two vectors are equal (bitwise). @@ -155,7 +143,8 @@ namespace Avalonia public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(null, obj)) + return false; return obj is Vector vector && Equals(vector); } @@ -206,5 +195,93 @@ namespace Avalonia { return new Vector(_x, y); } + + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + public static double Dot(Vector a, Vector b) + => a._x * b._x + a._y * b._y; + + /// + /// Returns the cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The cross product. + public static double Cross(Vector a, Vector b) + => a._x * b._y - a._y * b._x; + + /// + /// Normalizes the given vector. + /// + /// The vector + /// The normalized vector. + public static Vector Normalize(Vector vector) + => Divide(vector, vector.Length); + + /// + /// Divides the first vector by the second. + /// + /// The first vector. + /// The second vector. + /// The scaled vector. + public static Vector Divide(Vector a, Vector b) + => new Vector(a._x / b._x, a._y / b._y); + + /// + /// Divides the vector by the given scalar. + /// + /// The vector + /// The scalar value + /// The scaled vector. + public static Vector Divide(Vector vector, double scalar) + => new Vector(vector._x / scalar, vector._y / scalar); + + /// + /// Multiplies the first vector by the second. + /// + /// The first vector. + /// The second vector. + /// The scaled vector. + public static Vector Multiply(Vector a, Vector b) + => new Vector(a._x * b._x, a._y * b._y); + + /// + /// Multiplies the vector by the given scalar. + /// + /// The vector + /// The scalar value + /// The scaled vector. + public static Vector Multiply(Vector vector, double scalar) + => new Vector(vector._x * scalar, vector._y * scalar); + + /// + /// Adds the second to the first vector + /// + /// The first vector. + /// The second vector. + /// The summed vector. + public static Vector Add(Vector a, Vector b) + => new Vector(a._x + b._x, a._y + b._y); + + /// + /// Subtracts the second from the first vector + /// + /// The first vector. + /// The second vector. + /// The difference vector. + public static Vector Subtract(Vector a, Vector b) + => new Vector(a._x - b._x, a._y - b._y); + + /// + /// Negates the vector + /// + /// The vector to negate. + /// The scaled vector. + public static Vector Negate(Vector vector) + => new Vector(-vector._x, -vector._y); } } From c0a595f48e8601296ffbf667e0192dc1961d988e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Jul 2019 18:32:37 +0200 Subject: [PATCH 002/165] Fix scrolling to selected item in TreeView. - Define a `PART_Header` in the `TreeViewItem` template which represents the header to be scrolled into view when a `TreeViewItem` is selected. Make this header only take up the minimum of required space. - Catch the `RequestBringIntoViewEventArgs` in `TreeViewItem` and update the target rect to that of `PART_Header` --- src/Avalonia.Controls/TreeViewItem.cs | 23 +++++++++++++++++++ src/Avalonia.Themes.Default/TreeViewItem.xaml | 4 +++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 4d229390cb..e185bc227b 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -42,6 +42,7 @@ namespace Avalonia.Controls new FuncTemplate(() => new StackPanel()); private TreeView _treeView; + private IControl _header; private bool _isExpanded; private int _level; @@ -53,6 +54,7 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + RequestBringIntoViewEvent.AddClassHandler(x => x.OnRequestBringIntoView); } /// @@ -120,6 +122,21 @@ namespace Avalonia.Controls ItemContainerGenerator.Clear(); } + protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) + { + if (e.TargetObject == this && _header != null) + { + var m = _header.TransformToVisual(this); + + if (m.HasValue) + { + var bounds = new Rect(_header.Bounds.Size); + var rect = bounds.TransformToAABB(m.Value); + e.TargetRect = rect; + } + } + } + /// protected override void OnKeyDown(KeyEventArgs e) { @@ -146,6 +163,12 @@ namespace Avalonia.Controls // Don't call base.OnKeyDown - let events bubble up to containing TreeView. } + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + _header = e.NameScope.Find("PART_Header"); + } + private static int CalculateDistanceFromLogicalParent(ILogical logical, int @default = -1) where T : class { var result = 0; diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml index 5dd082cf7a..0d826806d0 100644 --- a/src/Avalonia.Themes.Default/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml @@ -16,7 +16,9 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" TemplatedControl.IsTemplateFocusTarget="True"> - Date: Thu, 15 Aug 2019 20:17:58 +0200 Subject: [PATCH 003/165] add squared length --- src/Avalonia.Visuals/Vector.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index f185682dc0..78ed688295 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -90,6 +90,11 @@ namespace Avalonia /// public double Length => Math.Sqrt(X * X + Y * Y); + /// + /// Squared Length of the vector + /// + public double SquaredLength => Math.Sqrt(Length); + /// /// Negates a vector. /// From 22ac34d89b912cef0b5de1b5660201e27916e011 Mon Sep 17 00:00:00 2001 From: JaggerJo Date: Thu, 15 Aug 2019 20:28:42 +0200 Subject: [PATCH 004/165] add - Zero - One - UnitX - UnitY --- src/Avalonia.Visuals/Vector.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index f185682dc0..aede9f3905 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -283,5 +283,29 @@ namespace Avalonia /// The scaled vector. public static Vector Negate(Vector vector) => new Vector(-vector._x, -vector._y); + + /// + /// Returnes the vector (0.0, 0.0) + /// + public static Vector Zero + => new Vector(0, 0); + + /// + /// Returnes the vector (1.0, 1.0) + /// + public static Vector One + => new Vector(1, 1); + + /// + /// Returnes the vector (1.0, 0.0) + /// + public static Vector UnitX + => new Vector(1, 0); + + /// + /// Returnes the vector (0.0, 1.0) + /// + public static Vector UnitY + => new Vector(0, 1); } } From b475acab1e677c3d570e83fe791fdffac54f0e0b Mon Sep 17 00:00:00 2001 From: JaggerJo Date: Thu, 15 Aug 2019 21:17:38 +0200 Subject: [PATCH 005/165] - add tests - add instance methods for normalize & negate - make length squared simpler --- src/Avalonia.Visuals/Vector.cs | 18 ++- .../Avalonia.Visuals.UnitTests/VectorTests.cs | 112 ++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.Visuals.UnitTests/VectorTests.cs diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 2c02c00d00..782a917c62 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -88,12 +88,12 @@ namespace Avalonia /// /// Length of the vector /// - public double Length => Math.Sqrt(X * X + Y * Y); + public double Length => Math.Sqrt(_x * _x + _y * _y); /// /// Squared Length of the vector /// - public double SquaredLength => Math.Sqrt(Length); + public double SquaredLength => _x * _x + _y * _y; /// /// Negates a vector. @@ -201,6 +201,20 @@ namespace Avalonia return new Vector(_x, y); } + /// + /// Returns a normalized version of this vector. + /// + /// The normalized vector. + public Vector Normalize() + => Normalize(this); + + /// + /// Returns a negated version of this vector. + /// + /// The negated vector. + public Vector Negate() + => Negate(this); + /// /// Returns the dot product of two vectors. /// diff --git a/tests/Avalonia.Visuals.UnitTests/VectorTests.cs b/tests/Avalonia.Visuals.UnitTests/VectorTests.cs new file mode 100644 index 0000000000..1bcc165aef --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/VectorTests.cs @@ -0,0 +1,112 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Xunit; +using Avalonia; +using System; + +namespace Avalonia.Visuals.UnitTests +{ + public class VectorTests + { + [Fact] + public void Length_Should_Return_Correct_Length_Of_Vector() + { + var vector = new Vector(2, 4); + var length = Math.Sqrt(2 * 2 + 4 * 4); + + Assert.Equal(length, vector.Length); + } + + [Fact] + public void Length_Squared_Should_Return_Correct_Length_Of_Vector() + { + var vectorA = new Vector(2, 4); + var squaredLengthA = 2 * 2 + 4 * 4; + + Assert.Equal(squaredLengthA, vectorA.SquaredLength); + } + + [Fact] + public void Normalize_Should_Return_Normalized_Vector() + { + // the length of a normalized vector must be 1 + + var vectorA = new Vector(13, 84); + var vectorB = new Vector(-34, 345); + var vectorC = new Vector(-34, -84); + + Assert.Equal(1.0, vectorA.Normalize().Length); + Assert.Equal(1.0, vectorB.Normalize().Length); + Assert.Equal(1.0, vectorC.Normalize().Length); + } + + [Fact] + public void Negate_Should_Return_Negated_Vector() + { + var vector = new Vector(2, 4); + var negated = new Vector(-2, -4); + + Assert.Equal(negated, vector.Negate()); + } + + [Fact] + public void Dot_Should_Return_Correct_Value() + { + var a = new Vector(-6, 8.0); + var b = new Vector(5, 12.0); + + Assert.Equal(66.0, Vector.Dot(a, b)); + } + + [Fact] + public void Cross_Should_Return_Correct_Value() + { + var a = new Vector(-6, 8.0); + var b = new Vector(5, 12.0); + + Assert.Equal(-112.0, Vector.Cross(a, b)); + } + + [Fact] + public void Divied_By_Vector_Should_Return_Correct_Value() + { + var a = new Vector(10, 2); + var b = new Vector(5, 2); + + var expected = new Vector(2, 1); + + Assert.Equal(expected, Vector.Divide(a, b)); + } + + [Fact] + public void Divied_Should_Return_Correct_Value() + { + var vector = new Vector(10, 2); + var expected = new Vector(5, 1); + + Assert.Equal(expected, Vector.Divide(vector, 2)); + } + + [Fact] + public void Multiply_By_Vector_Should_Return_Correct_Value() + { + var a = new Vector(10, 2); + var b = new Vector(2, 2); + + var expected = new Vector(20, 4); + + Assert.Equal(expected, Vector.Multiply(a, b)); + } + + [Fact] + public void Multiply_Should_Return_Correct_Value() + { + var vector = new Vector(10, 2); + + var expected = new Vector(20, 4); + + Assert.Equal(expected, Vector.Multiply(vector, 2)); + } + } +} From ba873bf33936234ad533c56da161a2890954c958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josua=20J=C3=A4ger?= Date: Sat, 17 Aug 2019 17:48:01 +0200 Subject: [PATCH 006/165] use field directly instead of property Co-Authored-By: Benedikt Schroeder --- src/Avalonia.Visuals/Vector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 782a917c62..11bda8b00e 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -88,7 +88,7 @@ namespace Avalonia /// /// Length of the vector /// - public double Length => Math.Sqrt(_x * _x + _y * _y); + public double Length => Math.Sqrt(SquaredLength); /// /// Squared Length of the vector From f3dfddc112739ac8c1602fbf9516004b803d54c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 21 Aug 2019 01:15:55 +0100 Subject: [PATCH 007/165] Fixed Binding.DoNothing for MultiBinding. --- .../Avalonia.Markup/Data/MultiBinding.cs | 12 +-- .../Converters/MultiValueConverterTests.cs | 76 +++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 19f92149ec..29945e25c3 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -76,7 +76,12 @@ namespace Avalonia.Data } var children = Bindings.Select(x => x.Initiate(target, null)); - var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter)); + + var input = children.Select(x => x.Observable) + .CombineLatest() + .Select(x => ConvertValue(x, targetType, converter)) + .Where(x => x != BindingOperations.DoNothing); + var mode = Mode == BindingMode.Default ? targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; @@ -97,11 +102,6 @@ namespace Avalonia.Data var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); - if (converted == BindingOperations.DoNothing) - { - return converted; - } - if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs new file mode 100644 index 0000000000..a77723afe1 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Converters +{ + public class MultiValueConverterTests : XamlTestBase + { + [Fact] + public void MultiValueConverter_Special_Values_Work() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.ApplyTemplate(); + + window.DataContext = Tuple.Create(2, 2); + Assert.Equal("foo", textBlock.Text); + + window.DataContext = Tuple.Create(-3, 3); + Assert.Equal("foo", textBlock.Text); + + window.DataContext = Tuple.Create(0, 2); + Assert.Equal("bar", textBlock.Text); + } + } + } + + public class TestMultiValueConverter : IMultiValueConverter + { + public static readonly TestMultiValueConverter Instance = new TestMultiValueConverter(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values[0] is int i && values[1] is int j) + { + var p = i * j; + + if (p > 0) + { + return "foo"; + } + + if (p == 0) + { + return AvaloniaProperty.UnsetValue; + } + + return BindingOperations.DoNothing; + } + + return "(default)"; + } + } +} From 3e17bb571fb9e9c9ee2e13de95affdee464fd01a Mon Sep 17 00:00:00 2001 From: Matthias Hoste <42743095+lifecoder-phoenix@users.noreply.github.com> Date: Sat, 24 Aug 2019 22:06:00 +0200 Subject: [PATCH 008/165] Fix for #2544 --- src/Shared/PlatformSupport/AssetLoader.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Shared/PlatformSupport/AssetLoader.cs b/src/Shared/PlatformSupport/AssetLoader.cs index 9d921acde6..06f0b114e8 100644 --- a/src/Shared/PlatformSupport/AssetLoader.cs +++ b/src/Shared/PlatformSupport/AssetLoader.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Web; using System.Linq; using System.Reflection; using Avalonia.Platform; @@ -242,6 +243,7 @@ namespace Avalonia.Shared.PlatformSupport throw new InvalidOperationException( $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); #else + name = HttpUtility.UrlDecode(name); AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); #endif } From 7a65748b040d32a4167cd59f85af1be8453279d4 Mon Sep 17 00:00:00 2001 From: LifeCoder Date: Sat, 24 Aug 2019 22:36:59 +0200 Subject: [PATCH 009/165] Update to my fix --- src/Shared/PlatformSupport/AssetLoader.cs | 1 - src/Shared/PlatformSupport/HttpUtility.cs | 693 ++++++++++++++++++ .../PlatformSupport/PlatformSupport.projitems | 1 + 3 files changed, 694 insertions(+), 1 deletion(-) create mode 100644 src/Shared/PlatformSupport/HttpUtility.cs diff --git a/src/Shared/PlatformSupport/AssetLoader.cs b/src/Shared/PlatformSupport/AssetLoader.cs index 06f0b114e8..1daec490b9 100644 --- a/src/Shared/PlatformSupport/AssetLoader.cs +++ b/src/Shared/PlatformSupport/AssetLoader.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Web; using System.Linq; using System.Reflection; using Avalonia.Platform; diff --git a/src/Shared/PlatformSupport/HttpUtility.cs b/src/Shared/PlatformSupport/HttpUtility.cs new file mode 100644 index 0000000000..8a56481a19 --- /dev/null +++ b/src/Shared/PlatformSupport/HttpUtility.cs @@ -0,0 +1,693 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Security.Permissions; +using System.Text; +using System.Web.Util; + +namespace Avalonia.Shared.PlatformSupport +{ + public sealed class HttpUtility + { + sealed class HttpQSCollection : NameValueCollection + { + public override string ToString() + { + int count = Count; + if (count == 0) + return ""; + StringBuilder sb = new StringBuilder(); + string[] keys = AllKeys; + for (int i = 0; i < count; i++) + { + sb.AppendFormat("{0}={1}&", keys[i], UrlEncode(this[keys[i]])); + } + if (sb.Length > 0) + sb.Length--; + return sb.ToString(); + } + } + + #region Constructors + + public HttpUtility() + { + } + + #endregion // Constructors + + #region Methods + + public static void HtmlAttributeEncode(string s, TextWriter output) + { + if (output == null) + { + throw new ArgumentNullException("output"); + } + HttpEncoder.Current.HtmlAttributeEncode(s, output); + } + + public static string HtmlAttributeEncode(string s) + { + if (s == null) + return null; + + using (var sw = new StringWriter()) + { + HttpEncoder.Current.HtmlAttributeEncode(s, sw); + return sw.ToString(); + } + } + + public static string UrlDecode(string str) + { + return UrlDecode(str, Encoding.UTF8); + } + + static char[] GetChars(MemoryStream b, Encoding e) + { + return e.GetChars(b.GetBuffer(), 0, (int)b.Length); + } + + static void WriteCharBytes(IList buf, char ch, Encoding e) + { + if (ch > 255) + { + foreach (byte b in e.GetBytes(new char[] { ch })) + buf.Add(b); + } + else + buf.Add((byte)ch); + } + + public static string UrlDecode(string str, Encoding e) + { + if (null == str) + return null; + + if (str.IndexOf('%') == -1 && str.IndexOf('+') == -1) + return str; + + if (e == null) + e = Encoding.UTF8; + + long len = str.Length; + var bytes = new List(); + int xchar; + char ch; + + for (int i = 0; i < len; i++) + { + ch = str[i]; + if (ch == '%' && i + 2 < len && str[i + 1] != '%') + { + if (str[i + 1] == 'u' && i + 5 < len) + { + // unicode hex sequence + xchar = GetChar(str, i + 2, 4); + if (xchar != -1) + { + WriteCharBytes(bytes, (char)xchar, e); + i += 5; + } + else + WriteCharBytes(bytes, '%', e); + } + else if ((xchar = GetChar(str, i + 1, 2)) != -1) + { + WriteCharBytes(bytes, (char)xchar, e); + i += 2; + } + else + { + WriteCharBytes(bytes, '%', e); + } + continue; + } + + if (ch == '+') + WriteCharBytes(bytes, ' ', e); + else + WriteCharBytes(bytes, ch, e); + } + + byte[] buf = bytes.ToArray(); + bytes = null; + return e.GetString(buf); + + } + + public static string UrlDecode(byte[] bytes, Encoding e) + { + if (bytes == null) + return null; + + return UrlDecode(bytes, 0, bytes.Length, e); + } + + static int GetInt(byte b) + { + char c = (char)b; + if (c >= '0' && c <= '9') + return c - '0'; + + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + + return -1; + } + + static int GetChar(byte[] bytes, int offset, int length) + { + int value = 0; + int end = length + offset; + for (int i = offset; i < end; i++) + { + int current = GetInt(bytes[i]); + if (current == -1) + return -1; + value = (value << 4) + current; + } + + return value; + } + + static int GetChar(string str, int offset, int length) + { + int val = 0; + int end = length + offset; + for (int i = offset; i < end; i++) + { + char c = str[i]; + if (c > 127) + return -1; + + int current = GetInt((byte)c); + if (current == -1) + return -1; + val = (val << 4) + current; + } + + return val; + } + + public static string UrlDecode(byte[] bytes, int offset, int count, Encoding e) + { + if (bytes == null) + return null; + if (count == 0) + return String.Empty; + + if (bytes == null) + throw new ArgumentNullException("bytes"); + + if (offset < 0 || offset > bytes.Length) + throw new ArgumentOutOfRangeException("offset"); + + if (count < 0 || offset + count > bytes.Length) + throw new ArgumentOutOfRangeException("count"); + + StringBuilder output = new StringBuilder(); + MemoryStream acc = new MemoryStream(); + + int end = count + offset; + int xchar; + for (int i = offset; i < end; i++) + { + if (bytes[i] == '%' && i + 2 < count && bytes[i + 1] != '%') + { + if (bytes[i + 1] == (byte)'u' && i + 5 < end) + { + if (acc.Length > 0) + { + output.Append(GetChars(acc, e)); + acc.SetLength(0); + } + xchar = GetChar(bytes, i + 2, 4); + if (xchar != -1) + { + output.Append((char)xchar); + i += 5; + continue; + } + } + else if ((xchar = GetChar(bytes, i + 1, 2)) != -1) + { + acc.WriteByte((byte)xchar); + i += 2; + continue; + } + } + + if (acc.Length > 0) + { + output.Append(GetChars(acc, e)); + acc.SetLength(0); + } + + if (bytes[i] == '+') + { + output.Append(' '); + } + else + { + output.Append((char)bytes[i]); + } + } + + if (acc.Length > 0) + { + output.Append(GetChars(acc, e)); + } + + acc = null; + return output.ToString(); + } + + public static byte[] UrlDecodeToBytes(byte[] bytes) + { + if (bytes == null) + return null; + + return UrlDecodeToBytes(bytes, 0, bytes.Length); + } + + public static byte[] UrlDecodeToBytes(string str) + { + return UrlDecodeToBytes(str, Encoding.UTF8); + } + + public static byte[] UrlDecodeToBytes(string str, Encoding e) + { + if (str == null) + return null; + + if (e == null) + throw new ArgumentNullException("e"); + + return UrlDecodeToBytes(e.GetBytes(str)); + } + + public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count) + { + if (bytes == null) + return null; + if (count == 0) + return new byte[0]; + + int len = bytes.Length; + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException("offset"); + + if (count < 0 || offset > len - count) + throw new ArgumentOutOfRangeException("count"); + + MemoryStream result = new MemoryStream(); + int end = offset + count; + for (int i = offset; i < end; i++) + { + char c = (char)bytes[i]; + if (c == '+') + { + c = ' '; + } + else if (c == '%' && i < end - 2) + { + int xchar = GetChar(bytes, i + 1, 2); + if (xchar != -1) + { + c = (char)xchar; + i += 2; + } + } + result.WriteByte((byte)c); + } + + return result.ToArray(); + } + + public static string UrlEncode(string str) + { + return UrlEncode(str, Encoding.UTF8); + } + + public static string UrlEncode(string str, Encoding e) + { + if (str == null) + return null; + + if (str == String.Empty) + return String.Empty; + + bool needEncode = false; + int len = str.Length; + for (int i = 0; i < len; i++) + { + char c = str[i]; + if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) + { + if (HttpEncoder.NotEncoded(c)) + continue; + + needEncode = true; + break; + } + } + + if (!needEncode) + return str; + + // avoided GetByteCount call + byte[] bytes = new byte[e.GetMaxByteCount(str.Length)]; + int realLen = e.GetBytes(str, 0, str.Length, bytes, 0); + return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, 0, realLen)); + } + + public static string UrlEncode(byte[] bytes) + { + if (bytes == null) + return null; + + if (bytes.Length == 0) + return String.Empty; + + return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, 0, bytes.Length)); + } + + public static string UrlEncode(byte[] bytes, int offset, int count) + { + if (bytes == null) + return null; + + if (bytes.Length == 0) + return String.Empty; + + return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, offset, count)); + } + + public static byte[] UrlEncodeToBytes(string str) + { + return UrlEncodeToBytes(str, Encoding.UTF8); + } + + public static byte[] UrlEncodeToBytes(string str, Encoding e) + { + if (str == null) + return null; + + if (str.Length == 0) + return new byte[0]; + + byte[] bytes = e.GetBytes(str); + return UrlEncodeToBytes(bytes, 0, bytes.Length); + } + + public static byte[] UrlEncodeToBytes(byte[] bytes) + { + if (bytes == null) + return null; + + if (bytes.Length == 0) + return new byte[0]; + + return UrlEncodeToBytes(bytes, 0, bytes.Length); + } + + public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count) + { + if (bytes == null) + return null; + return HttpEncoder.Current.UrlEncode(bytes, offset, count); + } + + public static string UrlEncodeUnicode(string str) + { + if (str == null) + return null; + + return Encoding.ASCII.GetString(UrlEncodeUnicodeToBytes(str)); + } + + public static byte[] UrlEncodeUnicodeToBytes(string str) + { + if (str == null) + return null; + + if (str.Length == 0) + return new byte[0]; + + MemoryStream result = new MemoryStream(str.Length); + foreach (char c in str) + { + HttpEncoder.UrlEncodeChar(c, result, true); + } + return result.ToArray(); + } + + /// + /// Decodes an HTML-encoded string and returns the decoded string. + /// + /// The HTML string to decode. + /// The decoded text. + public static string HtmlDecode(string s) + { + if (s == null) + return null; + + using (var sw = new StringWriter()) + { + HttpEncoder.Current.HtmlDecode(s, sw); + return sw.ToString(); + } + } + + /// + /// Decodes an HTML-encoded string and sends the resulting output to a TextWriter output stream. + /// + /// The HTML string to decode + /// The TextWriter output stream containing the decoded string. + public static void HtmlDecode(string s, TextWriter output) + { + if (output == null) + { + throw new ArgumentNullException("output"); + } + + if (!String.IsNullOrEmpty(s)) + { + HttpEncoder.Current.HtmlDecode(s, output); + } + } + + public static string HtmlEncode(string s) + { + if (s == null) + return null; + + using (var sw = new StringWriter()) + { + HttpEncoder.Current.HtmlEncode(s, sw); + return sw.ToString(); + } + } + + /// + /// HTML-encodes a string and sends the resulting output to a TextWriter output stream. + /// + /// The string to encode. + /// The TextWriter output stream containing the encoded string. + public static void HtmlEncode(string s, TextWriter output) + { + if (output == null) + { + throw new ArgumentNullException("output"); + } + + if (!String.IsNullOrEmpty(s)) + { + HttpEncoder.Current.HtmlEncode(s, output); + } + } + public static string HtmlEncode(object value) + { + if (value == null) + return null; + +#if !(MOBILE || NO_SYSTEM_WEB_DEPENDENCY) + IHtmlString htmlString = value as IHtmlString; + if (htmlString != null) + return htmlString.ToHtmlString(); +#endif + + return HtmlEncode(value.ToString()); + } + + public static string JavaScriptStringEncode(string value) + { + return JavaScriptStringEncode(value, false); + } + + public static string JavaScriptStringEncode(string value, bool addDoubleQuotes) + { + if (String.IsNullOrEmpty(value)) + return addDoubleQuotes ? "\"\"" : String.Empty; + + int len = value.Length; + bool needEncode = false; + char c; + for (int i = 0; i < len; i++) + { + c = value[i]; + + if (c >= 0 && c <= 31 || c == 34 || c == 39 || c == 60 || c == 62 || c == 92) + { + needEncode = true; + break; + } + } + + if (!needEncode) + return addDoubleQuotes ? "\"" + value + "\"" : value; + + var sb = new StringBuilder(); + if (addDoubleQuotes) + sb.Append('"'); + + for (int i = 0; i < len; i++) + { + c = value[i]; + if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 39 || c == 60 || c == 62) + sb.AppendFormat("\\u{0:x4}", (int)c); + else switch ((int)c) + { + case 8: + sb.Append("\\b"); + break; + + case 9: + sb.Append("\\t"); + break; + + case 10: + sb.Append("\\n"); + break; + + case 12: + sb.Append("\\f"); + break; + + case 13: + sb.Append("\\r"); + break; + + case 34: + sb.Append("\\\""); + break; + + case 92: + sb.Append("\\\\"); + break; + + default: + sb.Append(c); + break; + } + } + + if (addDoubleQuotes) + sb.Append('"'); + + return sb.ToString(); + } + public static string UrlPathEncode(string str) + { + return HttpEncoder.Current.UrlPathEncode(str); + } + + public static NameValueCollection ParseQueryString(string query) + { + return ParseQueryString(query, Encoding.UTF8); + } + + public static NameValueCollection ParseQueryString(string query, Encoding encoding) + { + if (query == null) + throw new ArgumentNullException("query"); + if (encoding == null) + throw new ArgumentNullException("encoding"); + if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) + return new HttpQSCollection(); + if (query[0] == '?') + query = query.Substring(1); + + NameValueCollection result = new HttpQSCollection(); + ParseQueryString(query, encoding, result); + return result; + } + + internal static void ParseQueryString(string query, Encoding encoding, NameValueCollection result) + { + if (query.Length == 0) + return; + + string decoded = HtmlDecode(query); + int decodedLength = decoded.Length; + int namePos = 0; + bool first = true; + while (namePos <= decodedLength) + { + int valuePos = -1, valueEnd = -1; + for (int q = namePos; q < decodedLength; q++) + { + if (valuePos == -1 && decoded[q] == '=') + { + valuePos = q + 1; + } + else if (decoded[q] == '&') + { + valueEnd = q; + break; + } + } + + if (first) + { + first = false; + if (decoded[namePos] == '?') + namePos++; + } + + string name, value; + if (valuePos == -1) + { + name = null; + valuePos = namePos; + } + else + { + name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding); + } + if (valueEnd < 0) + { + namePos = -1; + valueEnd = decoded.Length; + } + else + { + namePos = valueEnd + 1; + } + value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding); + + result.Add(name, value); + if (namePos == -1) + break; + } + } + #endregion // Methods + } +} diff --git a/src/Shared/PlatformSupport/PlatformSupport.projitems b/src/Shared/PlatformSupport/PlatformSupport.projitems index 34515a0912..cec13ebffe 100644 --- a/src/Shared/PlatformSupport/PlatformSupport.projitems +++ b/src/Shared/PlatformSupport/PlatformSupport.projitems @@ -11,6 +11,7 @@ + From d37b1f3359d19cf1a7212647362f47926a8344ab Mon Sep 17 00:00:00 2001 From: LifeCoder Date: Sat, 24 Aug 2019 22:46:17 +0200 Subject: [PATCH 010/165] Only add functions that we need --- src/Shared/PlatformSupport/HttpUtility.cs | 399 ---------------------- 1 file changed, 399 deletions(-) diff --git a/src/Shared/PlatformSupport/HttpUtility.cs b/src/Shared/PlatformSupport/HttpUtility.cs index 8a56481a19..59a23e733f 100644 --- a/src/Shared/PlatformSupport/HttpUtility.cs +++ b/src/Shared/PlatformSupport/HttpUtility.cs @@ -5,31 +5,11 @@ using System.Globalization; using System.IO; using System.Security.Permissions; using System.Text; -using System.Web.Util; namespace Avalonia.Shared.PlatformSupport { public sealed class HttpUtility { - sealed class HttpQSCollection : NameValueCollection - { - public override string ToString() - { - int count = Count; - if (count == 0) - return ""; - StringBuilder sb = new StringBuilder(); - string[] keys = AllKeys; - for (int i = 0; i < count; i++) - { - sb.AppendFormat("{0}={1}&", keys[i], UrlEncode(this[keys[i]])); - } - if (sb.Length > 0) - sb.Length--; - return sb.ToString(); - } - } - #region Constructors public HttpUtility() @@ -40,27 +20,6 @@ namespace Avalonia.Shared.PlatformSupport #region Methods - public static void HtmlAttributeEncode(string s, TextWriter output) - { - if (output == null) - { - throw new ArgumentNullException("output"); - } - HttpEncoder.Current.HtmlAttributeEncode(s, output); - } - - public static string HtmlAttributeEncode(string s) - { - if (s == null) - return null; - - using (var sw = new StringWriter()) - { - HttpEncoder.Current.HtmlAttributeEncode(s, sw); - return sw.ToString(); - } - } - public static string UrlDecode(string str) { return UrlDecode(str, Encoding.UTF8); @@ -330,364 +289,6 @@ namespace Avalonia.Shared.PlatformSupport return result.ToArray(); } - - public static string UrlEncode(string str) - { - return UrlEncode(str, Encoding.UTF8); - } - - public static string UrlEncode(string str, Encoding e) - { - if (str == null) - return null; - - if (str == String.Empty) - return String.Empty; - - bool needEncode = false; - int len = str.Length; - for (int i = 0; i < len; i++) - { - char c = str[i]; - if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) - { - if (HttpEncoder.NotEncoded(c)) - continue; - - needEncode = true; - break; - } - } - - if (!needEncode) - return str; - - // avoided GetByteCount call - byte[] bytes = new byte[e.GetMaxByteCount(str.Length)]; - int realLen = e.GetBytes(str, 0, str.Length, bytes, 0); - return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, 0, realLen)); - } - - public static string UrlEncode(byte[] bytes) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return String.Empty; - - return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, 0, bytes.Length)); - } - - public static string UrlEncode(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return String.Empty; - - return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, offset, count)); - } - - public static byte[] UrlEncodeToBytes(string str) - { - return UrlEncodeToBytes(str, Encoding.UTF8); - } - - public static byte[] UrlEncodeToBytes(string str, Encoding e) - { - if (str == null) - return null; - - if (str.Length == 0) - return new byte[0]; - - byte[] bytes = e.GetBytes(str); - return UrlEncodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return new byte[0]; - - return UrlEncodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; - return HttpEncoder.Current.UrlEncode(bytes, offset, count); - } - - public static string UrlEncodeUnicode(string str) - { - if (str == null) - return null; - - return Encoding.ASCII.GetString(UrlEncodeUnicodeToBytes(str)); - } - - public static byte[] UrlEncodeUnicodeToBytes(string str) - { - if (str == null) - return null; - - if (str.Length == 0) - return new byte[0]; - - MemoryStream result = new MemoryStream(str.Length); - foreach (char c in str) - { - HttpEncoder.UrlEncodeChar(c, result, true); - } - return result.ToArray(); - } - - /// - /// Decodes an HTML-encoded string and returns the decoded string. - /// - /// The HTML string to decode. - /// The decoded text. - public static string HtmlDecode(string s) - { - if (s == null) - return null; - - using (var sw = new StringWriter()) - { - HttpEncoder.Current.HtmlDecode(s, sw); - return sw.ToString(); - } - } - - /// - /// Decodes an HTML-encoded string and sends the resulting output to a TextWriter output stream. - /// - /// The HTML string to decode - /// The TextWriter output stream containing the decoded string. - public static void HtmlDecode(string s, TextWriter output) - { - if (output == null) - { - throw new ArgumentNullException("output"); - } - - if (!String.IsNullOrEmpty(s)) - { - HttpEncoder.Current.HtmlDecode(s, output); - } - } - - public static string HtmlEncode(string s) - { - if (s == null) - return null; - - using (var sw = new StringWriter()) - { - HttpEncoder.Current.HtmlEncode(s, sw); - return sw.ToString(); - } - } - - /// - /// HTML-encodes a string and sends the resulting output to a TextWriter output stream. - /// - /// The string to encode. - /// The TextWriter output stream containing the encoded string. - public static void HtmlEncode(string s, TextWriter output) - { - if (output == null) - { - throw new ArgumentNullException("output"); - } - - if (!String.IsNullOrEmpty(s)) - { - HttpEncoder.Current.HtmlEncode(s, output); - } - } - public static string HtmlEncode(object value) - { - if (value == null) - return null; - -#if !(MOBILE || NO_SYSTEM_WEB_DEPENDENCY) - IHtmlString htmlString = value as IHtmlString; - if (htmlString != null) - return htmlString.ToHtmlString(); -#endif - - return HtmlEncode(value.ToString()); - } - - public static string JavaScriptStringEncode(string value) - { - return JavaScriptStringEncode(value, false); - } - - public static string JavaScriptStringEncode(string value, bool addDoubleQuotes) - { - if (String.IsNullOrEmpty(value)) - return addDoubleQuotes ? "\"\"" : String.Empty; - - int len = value.Length; - bool needEncode = false; - char c; - for (int i = 0; i < len; i++) - { - c = value[i]; - - if (c >= 0 && c <= 31 || c == 34 || c == 39 || c == 60 || c == 62 || c == 92) - { - needEncode = true; - break; - } - } - - if (!needEncode) - return addDoubleQuotes ? "\"" + value + "\"" : value; - - var sb = new StringBuilder(); - if (addDoubleQuotes) - sb.Append('"'); - - for (int i = 0; i < len; i++) - { - c = value[i]; - if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 39 || c == 60 || c == 62) - sb.AppendFormat("\\u{0:x4}", (int)c); - else switch ((int)c) - { - case 8: - sb.Append("\\b"); - break; - - case 9: - sb.Append("\\t"); - break; - - case 10: - sb.Append("\\n"); - break; - - case 12: - sb.Append("\\f"); - break; - - case 13: - sb.Append("\\r"); - break; - - case 34: - sb.Append("\\\""); - break; - - case 92: - sb.Append("\\\\"); - break; - - default: - sb.Append(c); - break; - } - } - - if (addDoubleQuotes) - sb.Append('"'); - - return sb.ToString(); - } - public static string UrlPathEncode(string str) - { - return HttpEncoder.Current.UrlPathEncode(str); - } - - public static NameValueCollection ParseQueryString(string query) - { - return ParseQueryString(query, Encoding.UTF8); - } - - public static NameValueCollection ParseQueryString(string query, Encoding encoding) - { - if (query == null) - throw new ArgumentNullException("query"); - if (encoding == null) - throw new ArgumentNullException("encoding"); - if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) - return new HttpQSCollection(); - if (query[0] == '?') - query = query.Substring(1); - - NameValueCollection result = new HttpQSCollection(); - ParseQueryString(query, encoding, result); - return result; - } - - internal static void ParseQueryString(string query, Encoding encoding, NameValueCollection result) - { - if (query.Length == 0) - return; - - string decoded = HtmlDecode(query); - int decodedLength = decoded.Length; - int namePos = 0; - bool first = true; - while (namePos <= decodedLength) - { - int valuePos = -1, valueEnd = -1; - for (int q = namePos; q < decodedLength; q++) - { - if (valuePos == -1 && decoded[q] == '=') - { - valuePos = q + 1; - } - else if (decoded[q] == '&') - { - valueEnd = q; - break; - } - } - - if (first) - { - first = false; - if (decoded[namePos] == '?') - namePos++; - } - - string name, value; - if (valuePos == -1) - { - name = null; - valuePos = namePos; - } - else - { - name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding); - } - if (valueEnd < 0) - { - namePos = -1; - valueEnd = decoded.Length; - } - else - { - namePos = valueEnd + 1; - } - value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding); - - result.Add(name, value); - if (namePos == -1) - break; - } - } #endregion // Methods } } From 83abab7d509308eb7e8d1dc63e0f82aa0c6a1213 Mon Sep 17 00:00:00 2001 From: LifeCoder Date: Sun, 25 Aug 2019 14:22:58 +0200 Subject: [PATCH 011/165] Fix it and remove the extra code --- src/Shared/PlatformSupport/AssetLoader.cs | 2 +- src/Shared/PlatformSupport/HttpUtility.cs | 294 ------------------ .../PlatformSupport/PlatformSupport.projitems | 1 - 3 files changed, 1 insertion(+), 296 deletions(-) delete mode 100644 src/Shared/PlatformSupport/HttpUtility.cs diff --git a/src/Shared/PlatformSupport/AssetLoader.cs b/src/Shared/PlatformSupport/AssetLoader.cs index 1daec490b9..dd72934560 100644 --- a/src/Shared/PlatformSupport/AssetLoader.cs +++ b/src/Shared/PlatformSupport/AssetLoader.cs @@ -242,7 +242,7 @@ namespace Avalonia.Shared.PlatformSupport throw new InvalidOperationException( $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); #else - name = HttpUtility.UrlDecode(name); + name = Uri.UnescapeDataString(name); AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); #endif } diff --git a/src/Shared/PlatformSupport/HttpUtility.cs b/src/Shared/PlatformSupport/HttpUtility.cs deleted file mode 100644 index 59a23e733f..0000000000 --- a/src/Shared/PlatformSupport/HttpUtility.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.IO; -using System.Security.Permissions; -using System.Text; - -namespace Avalonia.Shared.PlatformSupport -{ - public sealed class HttpUtility - { - #region Constructors - - public HttpUtility() - { - } - - #endregion // Constructors - - #region Methods - - public static string UrlDecode(string str) - { - return UrlDecode(str, Encoding.UTF8); - } - - static char[] GetChars(MemoryStream b, Encoding e) - { - return e.GetChars(b.GetBuffer(), 0, (int)b.Length); - } - - static void WriteCharBytes(IList buf, char ch, Encoding e) - { - if (ch > 255) - { - foreach (byte b in e.GetBytes(new char[] { ch })) - buf.Add(b); - } - else - buf.Add((byte)ch); - } - - public static string UrlDecode(string str, Encoding e) - { - if (null == str) - return null; - - if (str.IndexOf('%') == -1 && str.IndexOf('+') == -1) - return str; - - if (e == null) - e = Encoding.UTF8; - - long len = str.Length; - var bytes = new List(); - int xchar; - char ch; - - for (int i = 0; i < len; i++) - { - ch = str[i]; - if (ch == '%' && i + 2 < len && str[i + 1] != '%') - { - if (str[i + 1] == 'u' && i + 5 < len) - { - // unicode hex sequence - xchar = GetChar(str, i + 2, 4); - if (xchar != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 5; - } - else - WriteCharBytes(bytes, '%', e); - } - else if ((xchar = GetChar(str, i + 1, 2)) != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 2; - } - else - { - WriteCharBytes(bytes, '%', e); - } - continue; - } - - if (ch == '+') - WriteCharBytes(bytes, ' ', e); - else - WriteCharBytes(bytes, ch, e); - } - - byte[] buf = bytes.ToArray(); - bytes = null; - return e.GetString(buf); - - } - - public static string UrlDecode(byte[] bytes, Encoding e) - { - if (bytes == null) - return null; - - return UrlDecode(bytes, 0, bytes.Length, e); - } - - static int GetInt(byte b) - { - char c = (char)b; - if (c >= '0' && c <= '9') - return c - '0'; - - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - - return -1; - } - - static int GetChar(byte[] bytes, int offset, int length) - { - int value = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - int current = GetInt(bytes[i]); - if (current == -1) - return -1; - value = (value << 4) + current; - } - - return value; - } - - static int GetChar(string str, int offset, int length) - { - int val = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - char c = str[i]; - if (c > 127) - return -1; - - int current = GetInt((byte)c); - if (current == -1) - return -1; - val = (val << 4) + current; - } - - return val; - } - - public static string UrlDecode(byte[] bytes, int offset, int count, Encoding e) - { - if (bytes == null) - return null; - if (count == 0) - return String.Empty; - - if (bytes == null) - throw new ArgumentNullException("bytes"); - - if (offset < 0 || offset > bytes.Length) - throw new ArgumentOutOfRangeException("offset"); - - if (count < 0 || offset + count > bytes.Length) - throw new ArgumentOutOfRangeException("count"); - - StringBuilder output = new StringBuilder(); - MemoryStream acc = new MemoryStream(); - - int end = count + offset; - int xchar; - for (int i = offset; i < end; i++) - { - if (bytes[i] == '%' && i + 2 < count && bytes[i + 1] != '%') - { - if (bytes[i + 1] == (byte)'u' && i + 5 < end) - { - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - acc.SetLength(0); - } - xchar = GetChar(bytes, i + 2, 4); - if (xchar != -1) - { - output.Append((char)xchar); - i += 5; - continue; - } - } - else if ((xchar = GetChar(bytes, i + 1, 2)) != -1) - { - acc.WriteByte((byte)xchar); - i += 2; - continue; - } - } - - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - acc.SetLength(0); - } - - if (bytes[i] == '+') - { - output.Append(' '); - } - else - { - output.Append((char)bytes[i]); - } - } - - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - } - - acc = null; - return output.ToString(); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes) - { - if (bytes == null) - return null; - - return UrlDecodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlDecodeToBytes(string str) - { - return UrlDecodeToBytes(str, Encoding.UTF8); - } - - public static byte[] UrlDecodeToBytes(string str, Encoding e) - { - if (str == null) - return null; - - if (e == null) - throw new ArgumentNullException("e"); - - return UrlDecodeToBytes(e.GetBytes(str)); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; - if (count == 0) - return new byte[0]; - - int len = bytes.Length; - if (offset < 0 || offset >= len) - throw new ArgumentOutOfRangeException("offset"); - - if (count < 0 || offset > len - count) - throw new ArgumentOutOfRangeException("count"); - - MemoryStream result = new MemoryStream(); - int end = offset + count; - for (int i = offset; i < end; i++) - { - char c = (char)bytes[i]; - if (c == '+') - { - c = ' '; - } - else if (c == '%' && i < end - 2) - { - int xchar = GetChar(bytes, i + 1, 2); - if (xchar != -1) - { - c = (char)xchar; - i += 2; - } - } - result.WriteByte((byte)c); - } - - return result.ToArray(); - } - #endregion // Methods - } -} diff --git a/src/Shared/PlatformSupport/PlatformSupport.projitems b/src/Shared/PlatformSupport/PlatformSupport.projitems index cec13ebffe..34515a0912 100644 --- a/src/Shared/PlatformSupport/PlatformSupport.projitems +++ b/src/Shared/PlatformSupport/PlatformSupport.projitems @@ -11,7 +11,6 @@ - From c64cc02ec68d5d5bdecc64e9001d2c09691cc43b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 28 Aug 2019 14:06:00 +0800 Subject: [PATCH 012/165] Move transitions initializer to setter. --- src/Avalonia.Animation/Animatable.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 3a3d00b94a..89bcd2efa5 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -44,6 +44,10 @@ namespace Avalonia.Animation public Transitions Transitions { get + { + return _transitions; + } + set { if (_transitions == null) _transitions = new Transitions(); @@ -51,10 +55,6 @@ namespace Avalonia.Animation if (_previousTransitions == null) _previousTransitions = new Dictionary(); - return _transitions; - } - set - { SetAndRaise(TransitionsProperty, ref _transitions, value); } } From 87245c90b46684a01c0c9283a40af4180002eae4 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 28 Aug 2019 14:20:43 +0800 Subject: [PATCH 013/165] Fixes: part 2 of n. --- src/Avalonia.Animation/Animatable.cs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 89bcd2efa5..09991a5990 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -49,10 +49,10 @@ namespace Avalonia.Animation } set { - if (_transitions == null) - _transitions = new Transitions(); + if (value is null) + return; - if (_previousTransitions == null) + if (_previousTransitions is null) _previousTransitions = new Dictionary(); SetAndRaise(TransitionsProperty, ref _transitions, value); @@ -66,19 +66,18 @@ namespace Avalonia.Animation /// The event args. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - if (e.Priority != BindingPriority.Animation && Transitions != null && _previousTransitions != null) - { - var match = Transitions.FirstOrDefault(x => x.Property == e.Property); + if (Transitions is null || e.Priority == BindingPriority.Animation) return; + + var match = Transitions.FirstOrDefault(x => x.Property == e.Property); - if (match != null) - { - if (_previousTransitions.TryGetValue(e.Property, out var dispose)) - dispose.Dispose(); + if (match != null) + { + if (_previousTransitions.TryGetValue(e.Property, out var dispose)) + dispose.Dispose(); - var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); + var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); - _previousTransitions[e.Property] = instance; - } + _previousTransitions[e.Property] = instance; } } } From b376313acff353872ce610d39997c9c553180ed4 Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 28 Aug 2019 16:28:17 +0100 Subject: [PATCH 014/165] speed up GetValue and GetDefaultValue --- src/Avalonia.Base/AvaloniaObject.cs | 49 +++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 0e2f0feada..2fee277f21 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -208,20 +208,9 @@ namespace Avalonia { return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); } - else if (_values != null) - { - var result = Values.GetValue(property); - - if (result == AvaloniaProperty.UnsetValue) - { - result = GetDefaultValue(property); - } - - return result; - } else { - return GetDefaultValue(property); + return GetValueOrDefault(property); } } @@ -598,10 +587,44 @@ namespace Avalonia private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValue(property); + return aobj.GetValueOrDefault(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } + /// + /// Gets the value or default value for a property. + /// + /// The property. + /// The default value. + private object GetValueOrDefault(AvaloniaProperty property) + { + var aobj = this; + if (aobj.Values != null) + { + var result = aobj.Values.GetValue(property); + if (result != AvaloniaProperty.UnsetValue) + { + return result; + } + } + if (property.Inherits) + { + while(aobj.InheritanceParent is AvaloniaObject parent) + { + aobj = parent; + if (aobj.Values != null) + { + var result = aobj.Values.GetValue(property); + if (result != AvaloniaProperty.UnsetValue) + { + return result; + } + } + } + } + return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + } + /// /// Sets the value of a direct property. /// From ebccccb8e4c8a6138274eeb1e51769f93919c857 Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 28 Aug 2019 17:37:01 +0100 Subject: [PATCH 015/165] add UnChecked suffix --- src/Avalonia.Base/AvaloniaObject.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 2fee277f21..5fa6dab29e 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -210,7 +210,7 @@ namespace Avalonia } else { - return GetValueOrDefault(property); + return GetValueOrDefaultUnChecked(property); } } @@ -596,7 +596,7 @@ namespace Avalonia /// /// The property. /// The default value. - private object GetValueOrDefault(AvaloniaProperty property) + private object GetValueOrDefaultUnChecked(AvaloniaProperty property) { var aobj = this; if (aobj.Values != null) From e8ba46160d3aad056a121b23b82f5290d8d360e6 Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 28 Aug 2019 17:39:59 +0100 Subject: [PATCH 016/165] UnChecked suffix fixed --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 5fa6dab29e..f8efb36562 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -587,7 +587,7 @@ namespace Avalonia private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValueOrDefault(property); + return aobj.GetValueOrDefaultUnChecked(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } From d176b1d7dc10e22137a21b7e2cf1836ec4cd2e27 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 29 Aug 2019 12:34:03 +0800 Subject: [PATCH 017/165] Fix unit tests. --- src/Avalonia.Animation/Animatable.cs | 4 ++-- tests/Avalonia.Animation.UnitTests/TransitionsTests.cs | 4 ++-- tests/Avalonia.LeakTests/TransitionTests.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 09991a5990..5ff3b17fb5 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -35,7 +35,7 @@ namespace Avalonia.Animation (o, v) => o.Transitions = v); private Transitions _transitions; - + private bool _isTransitionsSet = false; private Dictionary _previousTransitions; /// @@ -66,7 +66,7 @@ namespace Avalonia.Animation /// The event args. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - if (Transitions is null || e.Priority == BindingPriority.Animation) return; + if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return; var match = Transitions.FirstOrDefault(x => x.Property == e.Property); diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index f1b4b0d071..a8efc2c8ae 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { @@ -51,7 +51,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { diff --git a/tests/Avalonia.LeakTests/TransitionTests.cs b/tests/Avalonia.LeakTests/TransitionTests.cs index c7add1fe11..699dec7229 100644 --- a/tests/Avalonia.LeakTests/TransitionTests.cs +++ b/tests/Avalonia.LeakTests/TransitionTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.LeakTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { From 7ac47b7437749a00352f72503f56bd805aecf875 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 29 Aug 2019 23:44:04 +0200 Subject: [PATCH 018/165] Fix drag and drop not working on .net core 2.2+ --- samples/ControlCatalog.NetCore/Program.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 5aef0b5520..d683092edf 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,22 +4,16 @@ using System.Globalization; using System.Linq; using System.Threading; using Avalonia; -using Avalonia.Controls; -using Avalonia.LinuxFramebuffer.Output; -using Avalonia.Skia; using Avalonia.ReactiveUI; using Avalonia.Dialogs; -using System.Collections.Generic; -using System.Threading.Tasks; namespace ControlCatalog.NetCore { static class Program { - + [STAThread] static int Main(string[] args) { - Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); if (args.Contains("--wait-for-attach")) { Console.WriteLine("Attach debugger and use 'Set next statement'"); From addc1ddce2e4a2200953277517f54b92f941b3d0 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 30 Aug 2019 01:13:19 +0200 Subject: [PATCH 019/165] Avoid initializing properties if there is no observer. Optimize access. --- src/Avalonia.Base/AvaloniaProperty.cs | 5 ++ src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 74 ++++++++++++++----- .../AvaloniaObjectInitializationBenchmark.cs | 15 ++++ 3 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 1de5cb06c6..56ad241187 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -492,6 +492,11 @@ namespace Avalonia return Name; } + /// + /// True if has any observers. + /// + internal bool HasNotifyInitializedObservers => _initialized.HasObservers; + /// /// Notifies the observable. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 037e0dd72e..88b0201fcb 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -24,8 +24,8 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _attachedCache = new Dictionary>(); - private readonly Dictionary>> _initializedCache = - new Dictionary>>(); + private readonly Dictionary> _initializedCache = + new Dictionary>(); /// /// Gets the instance @@ -286,35 +286,73 @@ namespace Avalonia property.NotifyInitialized(e); } - if (!_initializedCache.TryGetValue(type, out var items)) + if (!_initializedCache.TryGetValue(type, out var initializationData)) { - var build = new Dictionary(); + var visited = new HashSet(); - foreach (var property in GetRegistered(type)) + initializationData = new List(); + + foreach (AvaloniaProperty property in GetRegistered(type)) { - var value = !property.IsDirect ? - ((IStyledPropertyAccessor)property).GetDefaultValue(type) : - null; - build.Add(property, value); + if (property.IsDirect) + { + initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property)); + } + else + { + initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); + } + + visited.Add(property); } - foreach (var property in GetRegisteredAttached(type)) + foreach (AvaloniaProperty property in GetRegisteredAttached(type)) { - if (!build.ContainsKey(property)) + if (!visited.Contains(property)) { - var value = ((IStyledPropertyAccessor)property).GetDefaultValue(type); - build.Add(property, value); + initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); + + visited.Add(property); } } - items = build.ToList(); - _initializedCache.Add(type, items); + _initializedCache.Add(type, initializationData); + } + + foreach (PropertyInitializationData data in initializationData) + { + if (!data.Property.HasNotifyInitializedObservers) + { + continue; + } + + object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value; + + Notify(data.Property, value); + } + } + + private readonly struct PropertyInitializationData + { + public AvaloniaProperty Property { get; } + public object Value { get; } + public bool IsDirect { get; } + public IDirectPropertyAccessor DirectAccessor { get; } + + public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor) + { + Property = property; + Value = null; + IsDirect = true; + DirectAccessor = directAccessor; } - foreach (var i in items) + public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type) { - var value = i.Key.IsDirect ? o.GetValue(i.Key) : i.Value; - Notify(i.Key, value); + Property = property; + Value = styledAccessor.GetDefaultValue(type); + IsDirect = false; + DirectAccessor = null; } } } diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs new file mode 100644 index 0000000000..06716f7102 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs @@ -0,0 +1,15 @@ +using Avalonia.Controls; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class AvaloniaObjectInitializationBenchmark + { + [Benchmark(OperationsPerInvoke = 1000)] + public Button InitializeButton() + { + return new Button(); + } + } +} From 77d9ae1cacfee35930c0543643c34a167ffe3b87 Mon Sep 17 00:00:00 2001 From: ahopper Date: Fri, 30 Aug 2019 10:18:30 +0100 Subject: [PATCH 020/165] change UnChecked suffix to Unchecked --- src/Avalonia.Base/AvaloniaObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index f8efb36562..48a222d5b4 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -210,7 +210,7 @@ namespace Avalonia } else { - return GetValueOrDefaultUnChecked(property); + return GetValueOrDefaultUnchecked(property); } } @@ -587,7 +587,7 @@ namespace Avalonia private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValueOrDefaultUnChecked(property); + return aobj.GetValueOrDefaultUnchecked(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } @@ -596,7 +596,7 @@ namespace Avalonia /// /// The property. /// The default value. - private object GetValueOrDefaultUnChecked(AvaloniaProperty property) + private object GetValueOrDefaultUnchecked(AvaloniaProperty property) { var aobj = this; if (aobj.Values != null) From 4d8973226d10ed188181367d5b522f0e2fd99bc7 Mon Sep 17 00:00:00 2001 From: ahopper Date: Fri, 30 Aug 2019 10:35:24 +0100 Subject: [PATCH 021/165] unbreak lazy initialization of Values --- src/Avalonia.Base/AvaloniaObject.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 48a222d5b4..035e0056e0 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -599,9 +599,9 @@ namespace Avalonia private object GetValueOrDefaultUnchecked(AvaloniaProperty property) { var aobj = this; - if (aobj.Values != null) + if (aobj._values != null) { - var result = aobj.Values.GetValue(property); + var result = aobj._values.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; @@ -612,9 +612,9 @@ namespace Avalonia while(aobj.InheritanceParent is AvaloniaObject parent) { aobj = parent; - if (aobj.Values != null) + if (aobj._values != null) { - var result = aobj.Values.GetValue(property); + var result = aobj._values.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; From a1ee8d98eb4231ac29449939a028bd813225bd21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Fri, 30 Aug 2019 15:10:53 +0200 Subject: [PATCH 022/165] "Fix" TreeView SelectedItems causing side effects and resetting selection. --- src/Avalonia.Controls/TreeView.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 4514109e12..eb68ba647e 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -105,11 +105,13 @@ namespace Avalonia.Controls get => _selectedItem; set { + var selectedItems = SelectedItems; + SetAndRaise(SelectedItemProperty, ref _selectedItem, value); if (value != null) { - if (SelectedItems.Count != 1 || SelectedItems[0] != value) + if (selectedItems.Count != 1 || selectedItems[0] != value) { _syncingSelectedItems = true; SelectSingleItem(value); From 02ebded6a227481bde5347d5fc49ae1e2b6cde0d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 31 Aug 2019 18:25:23 +0200 Subject: [PATCH 023/165] Don't bring hidden controls into view. --- src/Avalonia.Controls/ControlExtensions.cs | 15 +++++++++------ .../Presenters/ScrollContentPresenter.cs | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ControlExtensions.cs b/src/Avalonia.Controls/ControlExtensions.cs index 2fccb15acc..3bd222d132 100644 --- a/src/Avalonia.Controls/ControlExtensions.cs +++ b/src/Avalonia.Controls/ControlExtensions.cs @@ -34,14 +34,17 @@ namespace Avalonia.Controls { Contract.Requires(control != null); - var ev = new RequestBringIntoViewEventArgs + if (control.IsEffectivelyVisible) { - RoutedEvent = Control.RequestBringIntoViewEvent, - TargetObject = control, - TargetRect = rect, - }; + var ev = new RequestBringIntoViewEventArgs + { + RoutedEvent = Control.RequestBringIntoViewEvent, + TargetObject = control, + TargetRect = rect, + }; - control.RaiseEvent(ev); + control.RaiseEvent(ev); + } } /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index e7d8018a42..ec6a228421 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls.Presenters /// True if the scroll offset was changed; otherwise false. public bool BringDescendantIntoView(IVisual target, Rect targetRect) { - if (Child == null) + if (Child?.IsEffectivelyVisible != true) { return false; } From 394b8f77b70a7a0d4b92d0e961a0fc53f7b7ff33 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 31 Aug 2019 22:37:22 +0200 Subject: [PATCH 024/165] Get rid of allocations in Rect.TransformToAABB. --- src/Avalonia.Visuals/Rect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 49d4724b9a..8f08f7f51f 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -371,12 +371,12 @@ namespace Avalonia /// The bounding box public Rect TransformToAABB(Matrix matrix) { - var points = new[] + ReadOnlySpan points = stackalloc Point[4] { TopLeft.Transform(matrix), TopRight.Transform(matrix), BottomRight.Transform(matrix), - BottomLeft.Transform(matrix), + BottomLeft.Transform(matrix) }; var left = double.MaxValue; From a9da85e4aed730e286c3b07a6288f525e8556204 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 1 Sep 2019 12:49:30 +0200 Subject: [PATCH 025/165] Avoid per-frame allocations in renderer lock. --- .../Rendering/ManagedDeferredRendererLock.cs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index 2d4a39e026..1295961a1b 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive.Disposables; using System.Threading; namespace Avalonia.Rendering @@ -7,7 +6,13 @@ namespace Avalonia.Rendering public class ManagedDeferredRendererLock : IDeferredRendererLock { private readonly object _lock = new object(); - + private readonly LockDisposable _lockDisposable; + + public ManagedDeferredRendererLock() + { + _lockDisposable = new LockDisposable(_lock); + } + /// /// Tries to lock the target surface or window /// @@ -15,7 +20,7 @@ namespace Avalonia.Rendering public IDisposable TryLock() { if (Monitor.TryEnter(_lock)) - return Disposable.Create(() => Monitor.Exit(_lock)); + return _lockDisposable; return null; } @@ -25,7 +30,22 @@ namespace Avalonia.Rendering public IDisposable Lock() { Monitor.Enter(_lock); - return Disposable.Create(() => Monitor.Exit(_lock)); + return _lockDisposable; + } + + private class LockDisposable : IDisposable + { + private readonly object _lock; + + public LockDisposable(object @lock) + { + _lock = @lock; + } + + public void Dispose() + { + Monitor.Exit(_lock); + } } } } From 26b1320971d3e7b8f75e0a7f5b4a2f84f71d9fb0 Mon Sep 17 00:00:00 2001 From: ahopper Date: Sun, 1 Sep 2019 19:39:18 +0100 Subject: [PATCH 026/165] reduce repeated field access --- src/Avalonia.Base/AvaloniaObject.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 035e0056e0..c619d80e23 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -599,9 +599,10 @@ namespace Avalonia private object GetValueOrDefaultUnchecked(AvaloniaProperty property) { var aobj = this; - if (aobj._values != null) + var valuestore = aobj._values; + if (valuestore != null) { - var result = aobj._values.GetValue(property); + var result = valuestore.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; @@ -612,9 +613,10 @@ namespace Avalonia while(aobj.InheritanceParent is AvaloniaObject parent) { aobj = parent; - if (aobj._values != null) + valuestore = aobj._values; + if (valuestore != null) { - var result = aobj._values.GetValue(property); + var result = valuestore.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; From 112484d7e40c1b85ab41d77ae21d866d2dfdac7c Mon Sep 17 00:00:00 2001 From: ahopper Date: Mon, 2 Sep 2019 11:45:15 +0100 Subject: [PATCH 027/165] reduce allocation, cache Inherited props,remove duplicate events --- src/Avalonia.Base/AvaloniaObject.cs | 37 ++++++++++------ src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 44 +++++++++++++++++++ 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index c619d80e23..34b209473b 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -82,6 +82,7 @@ namespace Avalonia set { + VerifyAccess(); if (_inheritanceParent != value) { if (_inheritanceParent != null) @@ -89,25 +90,33 @@ namespace Avalonia _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; } - var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this) - .Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())); - var inherited = (from property in properties - where property.Inherits - select new - { - Property = property, - Value = GetValue(property), - }).ToList(); - + var oldInheritanceParent = _inheritanceParent; _inheritanceParent = value; + var valuestore = _values; - foreach (var i in inherited) + foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) { - object newValue = GetValue(i.Property); + if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue) + { + // if local value set there can be no change + continue; + } + // get the value as it would have been with the previous InheritanceParent + object oldValue; + if (oldInheritanceParent is AvaloniaObject aobj) + { + oldValue = aobj.GetValueOrDefaultUnchecked(property); + } + else + { + oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + } + + object newValue = GetDefaultValue(property); - if (!Equals(i.Value, newValue)) + if (!Equals(oldValue, newValue)) { - RaisePropertyChanged(i.Property, i.Value, newValue, BindingPriority.LocalValue); + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); } } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 88b0201fcb..d718f5917c 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -26,6 +26,8 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _initializedCache = new Dictionary>(); + private readonly Dictionary> _inheritedCache = + new Dictionary>(); /// /// Gets the instance @@ -103,6 +105,46 @@ namespace Avalonia return result; } + /// + /// Gets all inherited s registered on a type. + /// + /// The type. + /// A collection of definitions. + public IEnumerable GetRegisteredInherited(Type type) + { + Contract.Requires(type != null); + + if (_inheritedCache.TryGetValue(type, out var result)) + { + return result; + } + + result = new List(); + var visited = new HashSet(); + + foreach (var property in GetRegistered(type)) + { + if (property.Inherits) + { + result.Add(property); + visited.Add(property); + } + } + foreach (var property in GetRegisteredAttached(type)) + { + if (property.Inherits) + { + if (!visited.Contains(property)) + { + result.Add(property); + } + } + } + + _inheritedCache.Add(type, result); + return result; + } + /// /// Gets all s registered on a object. /// @@ -230,6 +272,7 @@ namespace Avalonia _registeredCache.Clear(); _initializedCache.Clear(); + _inheritedCache.Clear(); } /// @@ -266,6 +309,7 @@ namespace Avalonia _attachedCache.Clear(); _initializedCache.Clear(); + _inheritedCache.Clear(); } internal void NotifyInitialized(AvaloniaObject o) From 8ebe1c1288544bbf31426ba7b253b5c79c4acc7b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 3 Sep 2019 14:49:14 +0200 Subject: [PATCH 028/165] Added failing test for #2901. After the selected item is removed from a `ListBox` with `AlwaysSelected == true`, the container for the newly selected item does not get `:selected` applied. --- .../SelectingItemsControlTests_AutoSelect.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 72f2b8022f..a7010c521b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -102,6 +102,25 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Null(target.SelectedItem); } + [Fact] + public void Removing_Selected_First_Item_Should_Select_Next_Item() + { + var items = new AvaloniaList(new[] { "foo", "bar" }); + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + items.RemoveAt(0); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[0].Classes); + } + private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => From 5de0bf5fecbc52cb2c8b56323df9b2a7113e568d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 3 Sep 2019 14:54:08 +0200 Subject: [PATCH 029/165] Fix AlwaysSelected selected state. Fixes #2901. Two things needed to be done here: - When an item is removed, causing indexes to be reassigned, raise `Recycled` so that `SelectingItemsControl` knows to update the selection state - Update selection state in `SelectingItemsControl` when the selected item changes, but the selected index does not (due to an item being remove) --- src/Avalonia.Controls/Generators/ItemContainerGenerator.cs | 6 ++++++ src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 4fd6f4135c..8d1d69db1c 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -128,6 +128,12 @@ namespace Avalonia.Controls.Generators } Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); + + if (toMove.Count > 0) + { + var containers = toMove.Select(x => x.Value).ToList(); + Recycled?.Invoke(this, new ItemContainerEventArgs(containers[0].Index, containers)); + } } return result; diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c8c15bc079..44ae89fdbc 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -979,13 +979,14 @@ namespace Avalonia.Controls.Primitives } var item = ElementAt(Items, index); + var itemChanged = !Equals(item, oldItem); var added = -1; HashSet removed = null; _selectedIndex = index; _selectedItem = item; - if (oldIndex != index || _selection.HasMultiple) + if (oldIndex != index || itemChanged || _selection.HasMultiple) { if (clear) { @@ -1022,7 +1023,7 @@ namespace Avalonia.Controls.Primitives index); } - if (!Equals(item, oldItem)) + if (itemChanged) { RaisePropertyChanged( SelectedItemProperty, From bd354143caba5dca2ad0878989803edac2caa5c2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 3 Sep 2019 18:04:48 +0200 Subject: [PATCH 030/165] Notify MouseDevice when TopLevel closed. So it can remove pointerover state. --- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.Input/IMouseDevice.cs | 2 ++ src/Avalonia.Input/MouseDevice.cs | 5 +++++ .../TopLevelTests.cs | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 87100ceeb0..e0acab1133 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -269,8 +269,8 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; - Closed?.Invoke(this, EventArgs.Empty); Renderer?.Dispose(); Renderer = null; diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index 7e6bf657ae..a641544f7a 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -16,6 +16,8 @@ namespace Avalonia.Input [Obsolete("Use PointerEventArgs.GetPosition")] PixelPoint Position { get; } + void TopLevelClosed(IInputRoot root); + void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index d5152f58d5..0d5471f790 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -86,6 +86,11 @@ namespace Avalonia.Input ProcessRawEvent(margs); } + public void TopLevelClosed(IInputRoot root) + { + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + } + public void SceneInvalidated(IInputRoot root, Rect rect) { var clientPoint = root.PointToClient(Position); diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index c744543f99..901d780f16 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -224,6 +224,24 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Close_Should_Notify_MouseDevice() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + var mouseDevice = new Mock(); + impl.SetupAllProperties(); + impl.Setup(x => x.MouseDevice).Returns(mouseDevice.Object); + + var target = new TestTopLevel(impl.Object); + + impl.Object.Closed(); + + mouseDevice.Verify(x => x.TopLevelClosed(target)); + } + } + private FuncControlTemplate CreateTemplate() { return new FuncControlTemplate((x, scope) => From 5c407f966c86bc7766b2c93fa6635161a0a69755 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Sep 2019 11:27:12 +0200 Subject: [PATCH 031/165] Added failing test for #2821. `ContentPresenter` doesn't set the logical parent of its child control after it has been removed from the logical tree and re-added. --- .../ContentControlTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index 93355a22f2..ecddb322fa 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs @@ -331,6 +331,44 @@ namespace Avalonia.Controls.UnitTests Assert.Null(textBlock.GetLogicalParent()); } + [Fact] + public void Should_Set_Child_LogicalParent_After_Removing_And_Adding_Back_To_Logical_Tree() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = new ContentControl(); + var root = new TestRoot + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(ContentControl.TemplateProperty, GetTemplate()), + } + } + }, + Child = target + }; + + target.Content = "Foo"; + target.ApplyTemplate(); + + Assert.Equal(target, target.Presenter.Child.LogicalParent); + + root.Child = null; + + Assert.Null(target.Template); + + target.Content = null; + root.Child = target; + target.Content = "Bar"; + + Assert.Equal(target, target.Presenter.Child.LogicalParent); + } + } + private FuncControlTemplate GetTemplate() { return new FuncControlTemplate((parent, scope) => From d7357ec8769dcd5436af4159c0b2aecb54ce0c30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Sep 2019 13:10:37 +0200 Subject: [PATCH 032/165] Remove ContentControlMixin. And implement the functionality in the content controls themselves. `ContentControlMixin` was too complex and even with its complexity had bugs (such as in #2821). By moving the functionality to the content controls there is some repeated code but it's much more straightforward. --- src/Avalonia.Controls/ContentControl.cs | 32 +++- .../Mixins/ContentControlMixin.cs | 166 ------------------ .../Presenters/ContentPresenter.cs | 47 ++--- .../Presenters/IContentPresenter.cs | 13 -- .../Presenters/IContentPresenterHost.cs | 13 +- .../Primitives/HeaderedContentControl.cs | 26 ++- .../Primitives/HeaderedItemsControl.cs | 32 +++- .../HeaderedSelectingItemsControl.cs | 32 +++- src/Avalonia.Controls/TabControl.cs | 36 +++- .../AutoCompleteBoxTests.cs | 1 + .../ContentControlTests.cs | 2 + .../ContextMenuTests.cs | 12 +- .../Mixins/ContentControlMixinTests.cs | 107 ----------- .../ContentPresenterTests_InTemplate.cs | 13 ++ .../Primitives/PopupRootTests.cs | 2 + .../TabControlTests.cs | 12 +- .../TopLevelTests.cs | 3 +- .../Xaml/BindingTests.cs | 1 + .../Xaml/BindingTests_RelativeSource.cs | 3 + 19 files changed, 186 insertions(+), 367 deletions(-) delete mode 100644 src/Avalonia.Controls/Mixins/ContentControlMixin.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 16f17ae1bd..02d7890404 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -1,11 +1,13 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Metadata; namespace Avalonia.Controls @@ -39,12 +41,9 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = AvaloniaProperty.Register(nameof(VerticalContentAlignment)); - /// - /// Initializes static members of the class. - /// static ContentControl() { - ContentControlMixin.Attach(ContentProperty, x => x.LogicalChildren); + ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); } /// @@ -95,20 +94,39 @@ namespace Avalonia.Controls } /// - void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - RegisterContentPresenter(presenter); + return RegisterContentPresenter(presenter); } /// /// Called when an is registered with the control. /// /// The presenter. - protected virtual void RegisterContentPresenter(IContentPresenter presenter) + protected virtual bool RegisterContentPresenter(IContentPresenter presenter) { if (presenter.Name == "PART_ContentPresenter") { Presenter = presenter; + return true; + } + + return false; + } + + private void ContentChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); } } } diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs deleted file mode 100644 index b826fb982e..0000000000 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Linq; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; -using Avalonia.Collections; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using Avalonia.LogicalTree; - -namespace Avalonia.Controls.Mixins -{ - /// - /// Adds content control functionality to control classes. - /// - /// - /// The adds behavior to a control which acts as a content - /// control such as and . It - /// keeps the control's logical children in sync with the content being displayed by the - /// control. - /// - public class ContentControlMixin - { - private static Lazy> subscriptions = - new Lazy>(() => - new ConditionalWeakTable()); - - /// - /// Initializes a new instance of the class. - /// - /// The control type. - /// The content property. - /// - /// Given an control of should return the control's - /// logical children collection. - /// - /// - /// The name of the content presenter in the control's template. - /// - public static void Attach( - AvaloniaProperty content, - Func> logicalChildrenSelector, - string presenterName = "PART_ContentPresenter") - where TControl : TemplatedControl - { - Contract.Requires(content != null); - Contract.Requires(logicalChildrenSelector != null); - - void ChildChanging(object s, AvaloniaPropertyChangedEventArgs e) - { - if (s is IControl sender && sender?.TemplatedParent is TControl parent) - { - UpdateLogicalChild( - sender, - logicalChildrenSelector(parent), - e.OldValue, - null); - } - } - - void TemplateApplied(object s, RoutedEventArgs ev) - { - if (s is TControl sender) - { - var e = (TemplateAppliedEventArgs)ev; - var presenter = e.NameScope.Find(presenterName) as IContentPresenter; - - if (presenter != null) - { - presenter.ApplyTemplate(); - - var logicalChildren = logicalChildrenSelector(sender); - var subscription = new CompositeDisposable(); - - presenter.ChildChanging += ChildChanging; - subscription.Add(Disposable.Create(() => presenter.ChildChanging -= ChildChanging)); - - subscription.Add(presenter - .GetPropertyChangedObservable(ContentPresenter.ChildProperty) - .Subscribe(c => UpdateLogicalChild( - sender, - logicalChildren, - null, - c.NewValue))); - - UpdateLogicalChild( - sender, - logicalChildren, - null, - presenter.GetValue(ContentPresenter.ChildProperty)); - - if (subscriptions.Value.TryGetValue(sender, out IDisposable previousSubscription)) - { - subscription = new CompositeDisposable(previousSubscription, subscription); - subscriptions.Value.Remove(sender); - } - - subscriptions.Value.Add(sender, subscription); - } - } - } - - TemplatedControl.TemplateAppliedEvent.AddClassHandler( - typeof(TControl), - TemplateApplied, - RoutingStrategies.Direct); - - content.Changed.Subscribe(e => - { - if (e.Sender is TControl sender) - { - var logicalChildren = logicalChildrenSelector(sender); - UpdateLogicalChild(sender, logicalChildren, e.OldValue, e.NewValue); - } - }); - - Control.TemplatedParentProperty.Changed.Subscribe(e => - { - if (e.Sender is TControl sender) - { - var logicalChild = logicalChildrenSelector(sender).FirstOrDefault() as IControl; - logicalChild?.SetValue(Control.TemplatedParentProperty, sender.TemplatedParent); - } - }); - - TemplatedControl.TemplateProperty.Changed.Subscribe(e => - { - if (e.Sender is TControl sender) - { - if (subscriptions.Value.TryGetValue(sender, out IDisposable subscription)) - { - subscription.Dispose(); - subscriptions.Value.Remove(sender); - } - } - }); - } - - private static void UpdateLogicalChild( - IControl control, - IAvaloniaList logicalChildren, - object oldValue, - object newValue) - { - if (oldValue != newValue) - { - if (oldValue is IControl child) - { - logicalChildren.Remove(child); - ((ISetInheritanceParent)child).SetParent(child.Parent); - } - - child = newValue as IControl; - - if (child != null && !logicalChildren.Contains(child)) - { - child.SetValue(Control.TemplatedParentProperty, control.TemplatedParent); - logicalChildren.Add(child); - } - } - } - } -} diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 1072b21b1b..a5374e7c5a 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.Data; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; @@ -83,7 +84,6 @@ namespace Avalonia.Controls.Presenters private IControl _child; private bool _createdChild; - EventHandler _childChanging; private IDataTemplate _dataTemplate; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); @@ -190,12 +190,10 @@ namespace Avalonia.Controls.Presenters set { SetValue(PaddingProperty, value); } } - /// - event EventHandler IContentPresenter.ChildChanging - { - add => _childChanging += value; - remove => _childChanging -= value; - } + /// + /// Gets the host content control. + /// + internal IContentPresenterHost Host { get; private set; } /// public sealed override void ApplyTemplate() @@ -222,34 +220,16 @@ namespace Avalonia.Controls.Presenters var content = Content; var oldChild = Child; var newChild = CreateChild(); + var logicalChildren = Host?.LogicalChildren ?? LogicalChildren; // Remove the old child if we're not recycling it. if (newChild != oldChild) { + if (oldChild != null) { VisualChildren.Remove(oldChild); - } - - if (oldChild?.Parent == this) - { - // If we're the child's parent then the presenter isn't in a ContentControl's - // template. - LogicalChildren.Remove(oldChild); - } - else if (TemplatedParent != null) - { - // If we're in a ContentControl's template then invoke ChildChanging to let - // ContentControlMixin handle removing the logical child. - _childChanging?.Invoke(this, new AvaloniaPropertyChangedEventArgs( - this, - ChildProperty, - oldChild, - newChild, - BindingPriority.LocalValue)); - } - else if (oldChild != null) - { + logicalChildren.Remove(oldChild); ((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent); } } @@ -272,15 +252,11 @@ namespace Avalonia.Controls.Presenters else if (newChild != oldChild) { ((ISetInheritanceParent)newChild).SetParent(this); - Child = newChild; - // If we're in a ContentControl's template then the child's parent will have been - // set by ContentControlMixin in response to Child changing. If not, then we're - // standalone and should make the control our own logical child. - if (newChild.Parent == null && TemplatedParent == null) + if (!logicalChildren.Contains(newChild)) { - LogicalChildren.Add(newChild); + logicalChildren.Add(newChild); } VisualChildren.Add(newChild); @@ -459,7 +435,8 @@ namespace Avalonia.Controls.Presenters private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e) { - (e.NewValue as IContentPresenterHost)?.RegisterContentPresenter(this); + var host = e.NewValue as IContentPresenterHost; + Host = host?.RegisterContentPresenter(this) == true ? host : null; } } } diff --git a/src/Avalonia.Controls/Presenters/IContentPresenter.cs b/src/Avalonia.Controls/Presenters/IContentPresenter.cs index 78bffec93b..31ab3a21a6 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IContentPresenter.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; namespace Avalonia.Controls.Presenters @@ -22,16 +20,5 @@ namespace Avalonia.Controls.Presenters /// Gets or sets the content to be displayed by the presenter. /// object Content { get; set; } - - /// - /// Raised when property is about to change. - /// - /// - /// This event should be raised after the child has been removed from the visual tree, - /// but before the property has changed. It is intended for consumption - /// by in order to update the host control's logical - /// children. - /// - event EventHandler ChildChanging; } } diff --git a/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs b/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs index 3aa7e625ed..4acfba2c71 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs +++ b/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Collections; +using Avalonia.LogicalTree; using Avalonia.Styling; namespace Avalonia.Controls.Presenters @@ -18,10 +20,19 @@ namespace Avalonia.Controls.Presenters /// public interface IContentPresenterHost : ITemplatedControl { + /// + /// Gets a collection describing the logical children of the host control. + /// + IAvaloniaList LogicalChildren { get; } + /// /// Registers an with a host control. /// /// The content presenter. - void RegisterContentPresenter(IContentPresenter presenter); + /// + /// True if the content presenter should add its child to the logical children of the + /// host; otherwise false. + /// + bool RegisterContentPresenter(IContentPresenter presenter); } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index 98476c9c94..3cf50a7b80 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives { @@ -29,10 +30,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedContentControl() { - ContentControlMixin.Attach( - HeaderProperty, - x => x.LogicalChildren, - "PART_HeaderPresenter"); + ContentProperty.Changed.AddClassHandler(x => x.HeaderChanged); } /// @@ -63,13 +61,29 @@ namespace Avalonia.Controls.Primitives } /// - protected override void RegisterContentPresenter(IContentPresenter presenter) + protected override bool RegisterContentPresenter(IContentPresenter presenter) { - base.RegisterContentPresenter(presenter); + var result = base.RegisterContentPresenter(presenter); if (presenter.Name == "PART_HeaderPresenter") { HeaderPresenter = presenter; + result = true; + } + + return result; + } + + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index bda426c23b..e0eb0b005f 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -1,8 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives { @@ -22,10 +24,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedItemsControl() { - ContentControlMixin.Attach( - HeaderProperty, - x => x.LogicalChildren, - "PART_HeaderPresenter"); + HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); } /// @@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives } /// - void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - RegisterContentPresenter(presenter); + return RegisterContentPresenter(presenter); } /// /// Called when an is registered with the control. /// /// The presenter. - protected virtual void RegisterContentPresenter(IContentPresenter presenter) + protected virtual bool RegisterContentPresenter(IContentPresenter presenter) { if (presenter.Name == "PART_HeaderPresenter") { HeaderPresenter = presenter; + return true; + } + + return false; + } + + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index d59be66b2b..533b643ea6 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -1,8 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives { @@ -22,10 +24,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedSelectingItemsControl() { - ContentControlMixin.Attach( - HeaderProperty, - x => x.LogicalChildren, - "PART_HeaderPresenter"); + HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); } /// @@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives } /// - void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - RegisterContentPresenter(presenter); + return RegisterContentPresenter(presenter); } /// /// Called when an is registered with the control. /// /// The presenter. - protected virtual void RegisterContentPresenter(IContentPresenter presenter) + protected virtual bool RegisterContentPresenter(IContentPresenter presenter) { if (presenter.Name == "PART_HeaderPresenter") { HeaderPresenter = presenter; + return true; + } + + return false; + } + + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); } } } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index fc2c118132..50bcb034ac 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; @@ -9,6 +10,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -16,7 +18,7 @@ namespace Avalonia.Controls /// /// A tab control that displays a tab strip along with the content of the selected tab. /// - public class TabControl : SelectingItemsControl + public class TabControl : SelectingItemsControl, IContentPresenterHost { /// /// Defines the property. @@ -68,10 +70,6 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); - ContentControlMixin.Attach( - SelectedContentProperty, - x => x.LogicalChildren, - "PART_SelectedContentHost"); } /// @@ -136,7 +134,31 @@ namespace Avalonia.Controls internal ItemsPresenter ItemsPresenterPart { get; private set; } - internal ContentPresenter ContentPart { get; private set; } + internal IContentPresenter ContentPart { get; private set; } + + /// + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + { + return RegisterContentPresenter(presenter); + } + + /// + /// Called when an is registered with the control. + /// + /// The presenter. + protected virtual bool RegisterContentPresenter(IContentPresenter presenter) + { + if (presenter.Name == "PART_SelectedContentHost") + { + ContentPart = presenter; + return true; + } + + return false; + } protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -148,8 +170,6 @@ namespace Avalonia.Controls base.OnTemplateApplied(e); ItemsPresenterPart = e.NameScope.Get("PART_ItemsPresenter"); - - ContentPart = e.NameScope.Get("PART_SelectedContentHost"); } /// diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index ef7dc33f76..03d061e04f 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -984,6 +984,7 @@ namespace Avalonia.Controls.UnitTests TextBox textBox = GetTextBox(control); var window = new Window {Content = control}; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); Dispatcher.UIThread.RunJobs(); test.Invoke(control, textBox); } diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index ecddb322fa..f7332415ac 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs @@ -50,6 +50,7 @@ namespace Avalonia.Controls.UnitTests root.Child = target; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); @@ -354,6 +355,7 @@ namespace Avalonia.Controls.UnitTests target.Content = "Foo"; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); Assert.Equal(target, target.Presenter.Child.LogicalParent); diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 522afc9546..ac80fc6c7a 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -27,7 +27,9 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }.ApplyTemplate(); + var window = new Window { Content = target }; + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); int openedCount = 0; @@ -53,7 +55,9 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }.ApplyTemplate(); + var window = new Window { Content = target }; + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); sut.Open(target); @@ -86,6 +90,7 @@ namespace Avalonia.Controls.UnitTests var window = new Window {Content = target}; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); _mouse.Click(target, MouseButton.Right); @@ -115,7 +120,8 @@ namespace Avalonia.Controls.UnitTests var window = new Window {Content = target}; window.ApplyTemplate(); - + window.Presenter.ApplyTemplate(); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs deleted file mode 100644 index 638443e17f..0000000000 --- a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; -using System.Linq; -using Avalonia.Collections; -using Avalonia.Controls.Mixins; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.LogicalTree; -using Moq; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Mixins -{ - public class ContentControlMixinTests - { - [Fact] - public void Multiple_Mixin_Usages_Should_Not_Throw() - { - var target = new TestControl() - { - Template = new FuncControlTemplate((_, scope) => new Panel - { - Children = - { - new ContentPresenter {Name = "Content_1_Presenter"}.RegisterInNameScope(scope), - new ContentPresenter {Name = "Content_2_Presenter"}.RegisterInNameScope(scope) - } - }) - }; - - - var ex = Record.Exception(() => target.ApplyTemplate()); - - Assert.Null(ex); - } - - [Fact] - public void Replacing_Template_Releases_Events() - { - var p1 = new ContentPresenter { Name = "Content_1_Presenter" }; - var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; - var target = new TestControl - { - Template = new FuncControlTemplate((_, scope) => new Panel - { - Children = - { - p1.RegisterInNameScope(scope), - p2.RegisterInNameScope(scope) - } - }) - }; - target.ApplyTemplate(); - - Control tc; - - p1.Content = tc = new Control(); - p1.UpdateChild(); - Assert.Contains(tc, target.GetLogicalChildren()); - - p2.Content = tc = new Control(); - p2.UpdateChild(); - Assert.Contains(tc, target.GetLogicalChildren()); - - target.Template = null; - - p1.Content = tc = new Control(); - p1.UpdateChild(); - Assert.DoesNotContain(tc, target.GetLogicalChildren()); - - p2.Content = tc = new Control(); - p2.UpdateChild(); - Assert.DoesNotContain(tc, target.GetLogicalChildren()); - - } - - private class TestControl : TemplatedControl - { - public static readonly StyledProperty Content1Property = - AvaloniaProperty.Register(nameof(Content1)); - - public static readonly StyledProperty Content2Property = - AvaloniaProperty.Register(nameof(Content2)); - - static TestControl() - { - ContentControlMixin.Attach(Content1Property, x => x.LogicalChildren, "Content_1_Presenter"); - ContentControlMixin.Attach(Content2Property, x => x.LogicalChildren, "Content_2_Presenter"); - } - - public object Content1 - { - get { return GetValue(Content1Property); } - set { SetValue(Content1Property, value); } - } - - public object Content2 - { - get { return GetValue(Content2Property); } - set { SetValue(Content2Property, value); } - } - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index 6ab9c345d4..6d6dfc9230 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -343,6 +343,19 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); } + [Fact] + public void Should_Clear_Host_When_Host_Template_Cleared() + { + var (target, host) = CreateTarget(); + + Assert.Same(host, target.Host); + + host.Template = null; + host.ApplyTemplate(); + + Assert.Null(target.Host); + } + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() { var templatedParent = new ContentControl diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 0ebe6833d3..f75f6fcf91 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -51,6 +51,7 @@ namespace Avalonia.Controls.UnitTests.Primitives window.Content = target; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); @@ -167,6 +168,7 @@ namespace Avalonia.Controls.UnitTests.Primitives window.Content = target; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); target.PopupContent = null; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ee8d9cc62e..ddf7e7a0fa 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -183,27 +183,27 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); var dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); dataContext = ((Button)target.ContentPart.Child).DataContext; Assert.Equal(items[1], dataContext); target.SelectedIndex = 2; - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Base", dataContext); target.SelectedIndex = 3; - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Qux", dataContext); target.SelectedIndex = 4; - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); dataContext = target.ContentPart.DataContext; Assert.Equal("Base", dataContext); } @@ -279,7 +279,7 @@ namespace Avalonia.Controls.UnitTests }; ApplyTemplate(target); - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); var content = Assert.IsType(target.ContentPart.Child); Assert.Equal("bar", content.Tag); diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 901d780f16..645f87163a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -202,8 +202,9 @@ namespace Avalonia.Controls.UnitTests target.Template = CreateTemplate(); target.Content = child; + target.ApplyTemplate(); - Assert.Throws(() => target.ApplyTemplate()); + Assert.Throws(() => target.Presenter.ApplyTemplate()); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index b1abc9ea54..77bb215ad5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -142,6 +142,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml window.DataContext = new { Foo = "foo" }; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); Assert.Equal("foo", border.DataContext); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index 86b874f75c..f8678ee22e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -73,6 +73,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var button = window.FindControl