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 01/66] 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 02/66] 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 03/66] 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 04/66] 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 5c407f966c86bc7766b2c93fa6635161a0a69755 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Sep 2019 11:27:12 +0200 Subject: [PATCH 05/66] 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 06/66] 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