diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c15a5babb4c5..b051da2f3d39 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "microsoft.dotnet.xharness.cli": { - "version": "10.0.0-prerelease.24467.4", + "version": "10.0.0-prerelease.24476.1", "commands": [ "xharness" ] diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 510e3cbe95cb..745842be7c70 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -50,6 +50,7 @@ body: - 9.0.0-preview.3.10457 - 9.0.0-preview.2.10293 - 9.0.0-preview.1.9973 + - 8.0.91 SR9.1 - 8.0.90 SR9 - 8.0.82 SR8.2 - 8.0.80 SR8 @@ -126,6 +127,7 @@ body: - 8.0.80 SR8 - 8.0.82 SR8.2 - 8.0.90 SR9 + - 8.0.91 SR9.1 - 9.0.0-preview.1.9973 - 9.0.0-preview.2.10293 - 9.0.0-preview.3.10457 diff --git a/.github/workflows/similarIssues.yml b/.github/workflows/similarIssues.yml deleted file mode 100644 index ecba7b66952e..000000000000 --- a/.github/workflows/similarIssues.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: GitGudSimilarIssues comments - -on: - issues: - types: [opened] - issue_comment: - types: [created] - -jobs: - getSimilarIssues: - runs-on: ubuntu-latest - if: >- - (github.event_name == 'issues' && github.event.action == 'opened') || - (github.event_name == 'issue_comment' && github.event.action == 'created' && startsWith(github.event.comment.body, '/similarissues')) - outputs: - message: ${{ steps.getBody.outputs.message }} - steps: - - id: getBody - uses: craigloewen-msft/GitGudSimilarIssues@main - with: - issueTitle: ${{ github.event.issue.title }} - issueBody: ${{ github.event.issue.body }} - repo: ${{ github.repository }} - similaritytolerance: "0.70" - add-comment: - needs: getSimilarIssues - runs-on: ubuntu-latest - permissions: - issues: write - if: needs.getSimilarIssues.outputs.message != '' - steps: - - name: Add comment - run: gh issue comment "$NUMBER" --repo "$REPO" --body "$BODY" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NUMBER: ${{ github.event.issue.number }} - REPO: ${{ github.repository }} - BODY: ${{ needs.getSimilarIssues.outputs.message }} diff --git a/Microsoft.Maui-dev.sln b/Microsoft.Maui-dev.sln index e8dc43aced84..570c40a5f558 100644 --- a/Microsoft.Maui-dev.sln +++ b/Microsoft.Maui-dev.sln @@ -135,6 +135,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SharedSource", "SharedSource", "{4F2926C8-43AB-4328-A735-D9EAD699F81D}" ProjectSection(SolutionItems) = preProject src\BlazorWebView\src\SharedSource\AutoCloseOnReadCompleteStream.cs = src\BlazorWebView\src\SharedSource\AutoCloseOnReadCompleteStream.cs + src\BlazorWebView\src\SharedSource\HostAddressHelper.cs = src\BlazorWebView\src\SharedSource\HostAddressHelper.cs src\BlazorWebView\src\SharedSource\QueryStringHelper.cs = src\BlazorWebView\src\SharedSource\QueryStringHelper.cs src\BlazorWebView\src\SharedSource\UrlLoadingEventArgs.cs = src\BlazorWebView\src\SharedSource\UrlLoadingEventArgs.cs src\BlazorWebView\src\SharedSource\UrlLoadingStrategy.cs = src\BlazorWebView\src\SharedSource\UrlLoadingStrategy.cs diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index f8150876bcd9..86dbe8673ce2 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -135,17 +135,17 @@ https://github.com/dotnet/runtime 46cfb747b4c22471242dee0d106f5c79cf9fd4c5 - + https://github.com/dotnet/xharness - 3cfb1a3d86da666fb80ba0adb970525e88339d57 + 7d5c32dbda0c6c8b9dc20cde4e1261b191896138 - + https://github.com/dotnet/xharness - 3cfb1a3d86da666fb80ba0adb970525e88339d57 + 7d5c32dbda0c6c8b9dc20cde4e1261b191896138 - + https://github.com/dotnet/xharness - 3cfb1a3d86da666fb80ba0adb970525e88339d57 + 7d5c32dbda0c6c8b9dc20cde4e1261b191896138 diff --git a/eng/Versions.props b/eng/Versions.props index 8fc3873ec6f9..4fb0c857efe7 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -124,9 +124,9 @@ <_HarfBuzzSharpVersion>7.3.0.2 <_SkiaSharpNativeAssetsVersion>0.0.0-commit.7af1d0840a381c0ce7ef2877454a88dbb2949686.1086 7.0.114 - 10.0.0-prerelease.24467.4 - 10.0.0-prerelease.24467.4 - 10.0.0-prerelease.24467.4 + 10.0.0-prerelease.24476.1 + 10.0.0-prerelease.24476.1 + 10.0.0-prerelease.24476.1 0.9.2 1.0.0.16 1.3.0 diff --git a/eng/pipelines/common/variables.yml b/eng/pipelines/common/variables.yml index 5d7677141e71..581bf9355bdf 100644 --- a/eng/pipelines/common/variables.yml +++ b/eng/pipelines/common/variables.yml @@ -22,7 +22,7 @@ variables: - name: isTargetMainBranch value: $[eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/main')] - name: isLocPRBranch - value: $[startsWith(variables['System.PullRequest.SourceBranch'], 'loc-hb')] + value: $[startsWith(variables['System.PullRequest.SourceBranch'], 'lego')] - name: isPullRequest value: $[eq(variables['Build.Reason'], 'PullRequest')] - name: isLocHandoffBranch diff --git a/global.json b/global.json index 0ea1b4e4523d..ddb0d5a2b186 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "tools": { - "dotnet": "9.0.100-preview.7.24407.12" + "dotnet": "9.0.100-rc.1.24452.12" }, "msbuild-sdks": { "MSBuild.Sdk.Extras": "3.0.44", diff --git a/src/BlazorWebView/src/Maui/BlazorWebView.cs b/src/BlazorWebView/src/Maui/BlazorWebView.cs index 330ead19c2d7..d7dbab342c08 100644 --- a/src/BlazorWebView/src/Maui/BlazorWebView.cs +++ b/src/BlazorWebView/src/Maui/BlazorWebView.cs @@ -11,44 +11,7 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui /// public partial class BlazorWebView : View, IBlazorWebView { - internal static string AppHostAddress { get; } = GetAppHostAddress(); - - private const string AppHostAddressAlways0000Switch = "BlazorWebView.AppHostAddressAlways0000"; - - private static bool IsAppHostAddressAlways0000Enabled => - AppContext.TryGetSwitch(AppHostAddressAlways0000Switch, out var enabled) && enabled; - - private static string GetAppHostAddress() - { - if (IsAppHostAddressAlways0000Enabled) - { - return "0.0.0.0"; - } - else - { -#if IOS || MACCATALYST - // On iOS/MacCatalyst 18 and higher the 0.0.0.0 address does not work, so we use localhost instead. - // This preserves behavior on older versions of those systems, while defaulting to new behavior on - // the new system. - - // Note that pre-release versions of iOS/MacCatalyst have the expected Major/Minor values, - // but the Build, MajorRevision, MinorRevision, and Revision values are all -1, so we need - // to pass in int.MinValue for those values. - - if (System.OperatingSystem.IsIOSVersionAtLeast(major: 18, minor: int.MinValue, build: int.MinValue) || - System.OperatingSystem.IsMacCatalystVersionAtLeast(major: 18, minor: int.MinValue, build: int.MinValue)) - { - return "localhost"; - } - else - { - return "0.0.0.0"; - } -#else - return "0.0.0.0"; -#endif - } - } + internal static string AppHostAddress { get; } = HostAddressHelper.GetAppHostAddress(); private readonly JSComponentConfigurationStore _jSComponents = new(); diff --git a/src/BlazorWebView/src/SharedSource/HostAddressHelper.cs b/src/BlazorWebView/src/SharedSource/HostAddressHelper.cs new file mode 100644 index 000000000000..1ae50ab20a2e --- /dev/null +++ b/src/BlazorWebView/src/SharedSource/HostAddressHelper.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Components.WebView; + +internal static class HostAddressHelper +{ + private const string AppHostAddressAlways0000Switch = "BlazorWebView.AppHostAddressAlways0000"; + + private static bool IsAppHostAddressAlways0000Enabled => + AppContext.TryGetSwitch(AppHostAddressAlways0000Switch, out var enabled) && enabled; + + public static string GetAppHostAddress() + => IsAppHostAddressAlways0000Enabled + ? "0.0.0.0" + : "0.0.0.1"; +} diff --git a/src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs b/src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs index 6f7c1dd01674..4ff3a97195e1 100644 --- a/src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs +++ b/src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs @@ -49,10 +49,10 @@ internal class WebView2WebViewManager : WebViewManager // Using an IP address means that WebView2 doesn't wait for any DNS resolution, // making it substantially faster. Note that this isn't real HTTP traffic, since // we intercept all the requests within this origin. - internal static readonly string AppHostAddress = "0.0.0.0"; + internal static readonly string AppHostAddress = HostAddressHelper.GetAppHostAddress(); /// - /// Gets the application's base URI. Defaults to https://0.0.0.0/ + /// Gets the application's base URI. Defaults to https://0.0.0.1/. /// protected static readonly string AppOrigin = $"https://{AppHostAddress}/"; diff --git a/src/Compatibility/ControlGallery/src/WinUI/Compatibility.ControlGallery.WinUI.csproj b/src/Compatibility/ControlGallery/src/WinUI/Compatibility.ControlGallery.WinUI.csproj index e3c68f9c0f4f..e889e2cb6272 100644 --- a/src/Compatibility/ControlGallery/src/WinUI/Compatibility.ControlGallery.WinUI.csproj +++ b/src/Compatibility/ControlGallery/src/WinUI/Compatibility.ControlGallery.WinUI.csproj @@ -11,7 +11,7 @@ 1701;1702;CS8305;8305;CA1416;0612;CS0672;CS0618 true false - + false true MSIX win10-x64 diff --git a/src/Controls/src/Build.Tasks/BuildException.cs b/src/Controls/src/Build.Tasks/BuildException.cs index a4a25c59edcc..e54f266069bd 100644 --- a/src/Controls/src/Build.Tasks/BuildException.cs +++ b/src/Controls/src/Build.Tasks/BuildException.cs @@ -93,7 +93,8 @@ class BuildExceptionCode public static BuildExceptionCode ResourceDictDuplicateKey = new BuildExceptionCode("XC", 0125, nameof(ResourceDictDuplicateKey), ""); public static BuildExceptionCode ResourceDictMissingKey = new BuildExceptionCode("XC", 0126, nameof(ResourceDictMissingKey), ""); public static BuildExceptionCode XKeyNotLiteral = new BuildExceptionCode("XC", 0127, nameof(XKeyNotLiteral), ""); - + public static BuildExceptionCode StaticResourceSyntax = new BuildExceptionCode("XC", 0128, nameof(StaticResourceSyntax), ""); + //CSC equivalents public static BuildExceptionCode ObsoleteProperty = new BuildExceptionCode("XC", 0618, nameof(ObsoleteProperty), ""); //warning diff --git a/src/Controls/src/Build.Tasks/CompiledMarkupExtensions/StaticResourceExtension.cs b/src/Controls/src/Build.Tasks/CompiledMarkupExtensions/StaticResourceExtension.cs new file mode 100644 index 000000000000..a78664dd4997 --- /dev/null +++ b/src/Controls/src/Build.Tasks/CompiledMarkupExtensions/StaticResourceExtension.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; +using Microsoft.Maui.Controls.Xaml; +using Mono.Cecil; +using Mono.Cecil.Cil; +using static Mono.Cecil.Cil.Instruction; +using static Mono.Cecil.Cil.OpCodes; + +namespace Microsoft.Maui.Controls.Build.Tasks +{ + //yes, this is a ICompiledMarkupExtension, but declared as ICompiledValueProvider so it's evaluated later (in SetPropertyValue, not CreateObject) + class StaticResourceExtension : ICompiledValueProvider + { + public IEnumerable ProvideValue(VariableDefinitionReference vardefref, ModuleDefinition module, BaseNode node, ILContext context) + { + var name = new XmlName("", "Key"); + var eNode = node as ElementNode; + + if (!eNode.Properties.TryGetValue(name, out INode keyNode) && eNode.CollectionItems.Any()) + keyNode = eNode.CollectionItems[0]; + + if (!(keyNode is ValueNode keyValueNode)) + throw new BuildException(BuildExceptionCode.StaticResourceSyntax, eNode as IXmlLineInfo, null, null); + + var n = eNode; + while (n != null) + { + if (n.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "Resources"), out var resourcesNode)) + { + //single resource in + if (resourcesNode is IElementNode irn + && irn.Properties.TryGetValue(XmlName.xKey, out INode xKeyNode) + && context.Variables.ContainsKey(irn) + && xKeyNode is ValueNode xKeyValueNode + && xKeyValueNode.Value as string == keyValueNode.Value as string) + { + if (context.Variables[resourcesNode as IElementNode].VariableType.FullName == "System.String") { + foreach (var instruction in TryConvert(irn.CollectionItems[0] as ValueNode, eNode, vardefref, module, context)) + yield return instruction; + yield break; + } + + vardefref.VariableDefinition = context.Variables[irn]; + yield break; + } + //multiple resources in + else if (resourcesNode is ListNode lr) { + foreach (var rn in lr.CollectionItems) { + if (rn is IElementNode irn2 + && irn2.Properties.TryGetValue(XmlName.xKey, out INode xKeyNode2) + && context.Variables.ContainsKey(irn2) + && xKeyNode2 is ValueNode xKeyValueNode2 + && xKeyValueNode2.Value as string == keyValueNode.Value as string) + { + if (irn2.CollectionItems.Count == 1 && irn2.CollectionItems[0] is ValueNode vn2 && vn2.Value is string) { + foreach (var instruction in TryConvert(vn2, eNode, vardefref, module, context)) + yield return instruction; + yield break; + } + + vardefref.VariableDefinition = context.Variables[irn2]; + yield break; + } + } + } + //explicit ResourceDictionary in Resources + else if (resourcesNode is IElementNode resourceDictionary + && resourceDictionary.XmlType.Name == "ResourceDictionary") { + foreach (var rn in resourceDictionary.CollectionItems) { + if (rn is IElementNode irn3 + && irn3.Properties.TryGetValue(XmlName.xKey, out INode xKeyNode3) + && irn3.XmlType.Name != "OnPlatform" + && context.Variables.ContainsKey(irn3) + && xKeyNode3 is ValueNode xKeyValueNode3 + && xKeyValueNode3.Value as string == keyValueNode.Value as string) + { + if (irn3.CollectionItems.Count == 1 && irn3.CollectionItems[0] is ValueNode vn3 && vn3.Value is string) { + foreach (var instruction in TryConvert(vn3, eNode, vardefref, module, context)) + yield return instruction; + yield break; + } + + vardefref.VariableDefinition = context.Variables[irn3]; + yield break; + } + } + } + } + + n = n.Parent as ElementNode; + } + + + //Fallback + foreach (var instruction in FallBack(keyValueNode.Value as string, eNode, module, context).ToList()) + yield return instruction; + + var vardef = new VariableDefinition(module.TypeSystem.Object); + yield return Create(Stloc, vardef); + vardefref.VariableDefinition = vardef; + } + + public static IEnumerable TryConvert(ValueNode stringResourceNode, IElementNode node, VariableDefinitionReference vardefref, ModuleDefinition module, ILContext context) + { + XmlName propertyName = XmlName.Empty; + SetPropertiesVisitor.TryGetPropertyName(node, node.Parent, out propertyName); + var localName = propertyName.LocalName; + var parentType = module.ImportReference((node.Parent as IElementNode).XmlType.GetTypeReference(context.Cache, module, (IXmlLineInfo)node)); + + var bpRef = SetPropertiesVisitor.GetBindablePropertyReference(parentType, propertyName.NamespaceURI, ref localName, out _, context, (IXmlLineInfo)node); + //BindableProperty + if (bpRef != null) + { + var targetTypeRef = module.ImportReference(bpRef.GetBindablePropertyType(context.Cache, node as IXmlLineInfo, module)); + foreach (var instruction in stringResourceNode.PushConvertedValue(context, bpRef, requiredServices => stringResourceNode.PushServiceProvider(context, requiredServices, bpRef: bpRef), true, false)) + yield return instruction; + var vardef = new VariableDefinition(targetTypeRef); + yield return Create(Stloc, vardef); + vardefref.VariableDefinition = vardef; + yield break; + } + + var propertyRef = parentType.GetProperty(context.Cache, pd => pd.Name == localName, out var declaringTypeReference); + if (propertyRef != null) + { + foreach (var instruction in stringResourceNode.PushConvertedValue(context, propertyRef.PropertyType, new ICustomAttributeProvider[] { propertyRef, propertyRef.PropertyType.ResolveCached(context.Cache) }, requiredServices => stringResourceNode.PushServiceProvider(context, requiredServices, propertyRef: propertyRef), true, false)) + yield return instruction; + var vardef = new VariableDefinition(propertyRef.PropertyType); + yield return Create(Stloc, vardef); + vardefref.VariableDefinition = vardef; + yield break; + } + + + } + + public static IEnumerable FallBack(string key, IElementNode node, ModuleDefinition module, ILContext context) + { + var staticResourceExtensionType = module.ImportReference(context.Cache, + ("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml", "StaticResourceExtension")); + yield return Create(Newobj, module.ImportCtorReference(context.Cache, + staticResourceExtensionType, + paramCount: 0)); + + SetPropertiesVisitor.TryGetPropertyName(node, node.Parent, out var propertyName); + var localName = propertyName.LocalName; + + //Set the Key + var keyProperty = staticResourceExtensionType.GetProperty(context.Cache, pd => pd.Name == "Key", out _); + yield return Create(Dup); + yield return Create(Ldstr, key); + yield return Create(Callvirt, module.ImportReference(keyProperty.SetMethod)); + + FieldReference bpRef = null; + PropertyDefinition propertyRef = null; + TypeReference declaringTypeReference = null; + if (node.Parent is IElementNode parentNode && propertyName != XmlName.Empty) + { + var parentType = module.ImportReference(parentNode.XmlType.GetTypeReference(context.Cache, module, (IXmlLineInfo)node)); + bpRef = SetPropertiesVisitor.GetBindablePropertyReference(parentType, + propertyName.NamespaceURI, + ref localName, + out _, + context, + (IXmlLineInfo)node); + propertyRef = parentType.GetProperty(context.Cache, pd => pd.Name == localName, out declaringTypeReference); + + } + + var requiredServices = staticResourceExtensionType.GetRequiredServices(context.Cache, module); + foreach (var instruction in node.PushServiceProvider(context, requiredServices, bpRef, propertyRef, declaringTypeReference)) + yield return instruction; + + yield return Create(Callvirt, module.ImportMethodReference(context.Cache, + ("Microsoft.Maui.Controls.Xaml", "Microsoft.Maui.Controls.Xaml", "StaticResourceExtension"), + methodName: "ProvideValue", + parameterTypes: new[] { ("System.ComponentModel", "System", "IServiceProvider") })); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Build.Tasks/ErrorMessages.resx b/src/Controls/src/Build.Tasks/ErrorMessages.resx index 14f3c32018c0..39907273d5d0 100644 --- a/src/Controls/src/Build.Tasks/ErrorMessages.resx +++ b/src/Controls/src/Build.Tasks/ErrorMessages.resx @@ -268,5 +268,7 @@ x:Key expects a string literal. - + + A key is required in {StaticResource}. + \ No newline at end of file diff --git a/src/Controls/src/Build.Tasks/NodeILExtensions.cs b/src/Controls/src/Build.Tasks/NodeILExtensions.cs index d0f87e92a89c..2e68a52c2e17 100644 --- a/src/Controls/src/Build.Tasks/NodeILExtensions.cs +++ b/src/Controls/src/Build.Tasks/NodeILExtensions.cs @@ -197,9 +197,7 @@ public static IEnumerable PushConvertedValue(this ValueNode node, I if (isExtendedConverter) { - var requireServiceAttribute = typeConverter.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "RequireServiceAttribute")); - var requiredServiceType = (requireServiceAttribute?.ConstructorArguments[0].Value as CustomAttributeArgument[])?.Select(ca => ca.Value as TypeReference).ToArray(); - + var requiredServiceType = typeConverter.GetRequiredServices(context.Cache, module); foreach (var instruction in pushServiceProvider(requiredServiceType)) yield return instruction; } @@ -329,6 +327,12 @@ public static IEnumerable PushConvertedValue(this ValueNode node, I yield return Create(Box, module.ImportReference(originalTypeRef)); } + public static TypeReference[] GetRequiredServices(this TypeReference type, XamlCache cache, ModuleDefinition module) + { + var requireServiceAttribute = type.GetCustomAttribute(cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "RequireServiceAttribute")); + return (requireServiceAttribute?.ConstructorArguments[0].Value as CustomAttributeArgument[])?.Select(ca => ca.Value as TypeReference).ToArray(); + } + static Instruction PushParsedEnum(XamlCache cache, TypeReference enumRef, string value, IXmlLineInfo lineInfo) { var enumDef = enumRef.ResolveCached(cache); diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index f66d66a5f847..b823f24dc347 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -285,10 +285,9 @@ public static IEnumerable ProvideValue(VariableDefinitionReference out markupExtension, out genericArguments)) { var acceptEmptyServiceProvider = vardefref.VariableDefinition.VariableType.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "AcceptEmptyServiceProviderAttribute")) != null; - var requireServiceAttribute = vardefref.VariableDefinition.VariableType.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "RequireServiceAttribute")); - var requiredServiceType = (requireServiceAttribute?.ConstructorArguments[0].Value as CustomAttributeArgument[])?.Select(ca => ca.Value as TypeReference).ToArray(); + var requiredServiceType = vardefref.VariableDefinition.VariableType.GetRequiredServices(context.Cache, module); - if (!acceptEmptyServiceProvider && requireServiceAttribute == null) + if (!acceptEmptyServiceProvider && requiredServiceType is null) context.LoggingHelper.LogWarningOrError(BuildExceptionCode.UnattributedMarkupType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, vardefref.VariableDefinition.VariableType); (string, string, string)? bindingExtensionType = vardefref.VariableDefinition.VariableType.FullName switch @@ -338,13 +337,32 @@ public static IEnumerable ProvideValue(VariableDefinitionReference else if (context.Variables[node].VariableType.ImplementsInterface(context.Cache, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IMarkupExtension")))) { var acceptEmptyServiceProvider = context.Variables[node].VariableType.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "AcceptEmptyServiceProviderAttribute")) != null; - var requireServiceAttribute = vardefref.VariableDefinition.VariableType.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "RequireServiceAttribute")); - var requiredServiceType = (requireServiceAttribute?.ConstructorArguments[0].Value as CustomAttributeArgument[])?.Select(ca => ca.Value as TypeReference).ToArray(); + var requiredServiceType = vardefref.VariableDefinition.VariableType.GetRequiredServices(context.Cache, module); - if (!acceptEmptyServiceProvider && requireServiceAttribute == null) + if (!acceptEmptyServiceProvider && requiredServiceType is null) context.LoggingHelper.LogWarningOrError(BuildExceptionCode.UnattributedMarkupType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, vardefref.VariableDefinition.VariableType); var markupExtensionType = ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IMarkupExtension"); + //some markup extensions weren't compiled earlier (on purpose), so we need to compile them now + var compiledValueProviderName = context.Variables[node].VariableType.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "ProvideCompiledAttribute"))?.ConstructorArguments?[0].Value as string; + Type compiledValueProviderType; + ICompiledValueProvider valueProvider; + + if ( compiledValueProviderName != null + && (compiledValueProviderType = Type.GetType(compiledValueProviderName)) != null + && (valueProvider = Activator.CreateInstance(compiledValueProviderType) as ICompiledValueProvider) != null) + { + var cProvideValue = typeof(ICompiledValueProvider).GetMethods().FirstOrDefault(md => md.Name == "ProvideValue"); + var instructions = (IEnumerable)cProvideValue.Invoke(valueProvider, [ + vardefref, + context.Body.Method.Module, + node as BaseNode, + context]); + foreach (var i in instructions) + yield return i; + yield break; + } + vardefref.VariableDefinition = new VariableDefinition(module.TypeSystem.Object); foreach (var instruction in context.Variables[node].LoadAs(context.Cache, module.GetTypeDefinition(context.Cache, markupExtensionType), module)) yield return instruction; @@ -362,10 +380,9 @@ public static IEnumerable ProvideValue(VariableDefinitionReference else if (context.Variables[node].VariableType.ImplementsInterface(context.Cache, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "IValueProvider")))) { var acceptEmptyServiceProvider = context.Variables[node].VariableType.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "AcceptEmptyServiceProviderAttribute")) != null; - var requireServiceAttribute = vardefref.VariableDefinition.VariableType.GetCustomAttribute(context.Cache, module, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Xaml", "RequireServiceAttribute")); - var requiredServiceType = (requireServiceAttribute?.ConstructorArguments[0].Value as CustomAttributeArgument[])?.Select(ca => ca.Value as TypeReference).ToArray(); + var requiredServiceType = vardefref.VariableDefinition.VariableType.GetRequiredServices(context.Cache, module); - if (!acceptEmptyServiceProvider && requireServiceAttribute == null) + if (!acceptEmptyServiceProvider && requiredServiceType is null) context.LoggingHelper.LogWarningOrError(BuildExceptionCode.UnattributedMarkupType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, vardefref.VariableDefinition.VariableType); var valueProviderType = context.Variables[node].VariableType; @@ -1110,7 +1127,7 @@ public static IEnumerable GetPropertyValue(VariableDefinition paren if (CanGetValue(parent, bpRef, attached, lineInfo, context, out _)) return GetValue(parent, bpRef, lineInfo, context, out propertyType); - //If it's a property, set it + //If it's a property, get it if (CanGet(parent, localName, context, out _)) return Get(parent, localName, lineInfo, context, out propertyType); @@ -1118,12 +1135,14 @@ public static IEnumerable GetPropertyValue(VariableDefinition paren } static FieldReference GetBindablePropertyReference(VariableDefinition parent, string namespaceURI, ref string localName, out bool attached, ILContext context, IXmlLineInfo iXmlLineInfo) + => GetBindablePropertyReference(parent.VariableType, namespaceURI, ref localName, out attached, context, iXmlLineInfo); + + public static FieldReference GetBindablePropertyReference(TypeReference bpOwnerType, string namespaceURI, ref string localName, out bool attached, ILContext context, IXmlLineInfo iXmlLineInfo) { var module = context.Body.Method.Module; TypeReference declaringTypeReference; //If it's an attached BP, update elementType and propertyName - var bpOwnerType = parent.VariableType; attached = GetNameAndTypeRef(ref bpOwnerType, namespaceURI, ref localName, context, iXmlLineInfo); var name = $"{localName}Property"; FieldDefinition bpDef = bpOwnerType.GetField(context.Cache, @@ -1322,10 +1341,12 @@ static bool CanSetValue(FieldReference bpRef, bool attached, INode node, IXmlLin if (!context.Variables.TryGetValue(elementNode, out VariableDefinition varValue)) return false; + + var bpTypeRef = bpRef.GetBindablePropertyType(context.Cache, iXmlLineInfo, module); // If it's an attached BP, there's no second chance to handle IMarkupExtensions, so we try here. // Worst case scenario ? InvalidCastException at runtime - if (attached && varValue.VariableType.FullName == "System.Object") + if (varValue.VariableType.FullName == "System.Object") return true; var implicitOperator = varValue.VariableType.GetImplicitOperatorTo(context.Cache, bpTypeRef, module); if (implicitOperator != null) @@ -1453,7 +1474,7 @@ static bool CanSet(VariableDefinition parent, string localName, INode node, ILCo return false; var valueNode = node as ValueNode; - if (valueNode != null && valueNode.CanConvertValue(context, propertyType, new ICustomAttributeProvider[] { property, propertyType.ResolveCached(context.Cache) })) + if (valueNode != null && valueNode.CanConvertValue(context, propertyType, new ICustomAttributeProvider[] { property, propertyType.ResolveCached(context.Cache) })) return true; var elementNode = node as IElementNode; diff --git a/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs index 566fe6a282cb..4b72db7ba14a 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs @@ -504,7 +504,7 @@ void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) { UpdateUseLargeTitles(); } - else if (e.PropertyName == NavigationPage.BackButtonTitleProperty.PropertyName) + else if (e.PropertyName == NavigationPage.BackButtonTitleProperty.PropertyName || e.PropertyName == NavigationPage.TitleProperty.PropertyName) { var pack = (ParentingViewController)TopViewController; pack?.UpdateTitleArea(pack.Child); @@ -1510,16 +1510,17 @@ internal void UpdateTitleArea(Page page) if (!(OperatingSystem.IsIOSVersionAtLeast(11) || OperatingSystem.IsMacCatalystVersionAtLeast(11)) && !isBackButtonTextSet) backButtonText = ""; + _navigation.TryGetTarget(out NavigationRenderer n); + // First page and we have a flyout detail to contend with UpdateLeftBarButtonItem(); - UpdateBackButtonTitle(page.Title, backButtonText); + UpdateBackButtonTitle(page.Title ?? n?.NavPage.Title, backButtonText); //var hadTitleView = NavigationItem.TitleView != null; ClearTitleViewContainer(); if (needContainer) { - NavigationRenderer n; - if (!_navigation.TryGetTarget(out n)) + if (n is null) return; Container titleViewContainer = new Container(titleView, n.NavigationBar); diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs index 32c8a6b7a12b..24ceab4a95cf 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs @@ -149,6 +149,30 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, return _rootView = root; } + void OnShellContentPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == ShellContent.TitleProperty.PropertyName && sender is ShellContent shellContent) + { + UpdateTabTitle(shellContent); + } + } + + void UpdateTabTitle(ShellContent shellContent) + { + if (_tablayout == null || SectionController.GetItems().Count == 0) + return; + + int index = SectionController.GetItems().IndexOf(shellContent); + if (index >= 0) + { + var tab = _tablayout.GetTabAt(index); + if (tab != null) + { + tab.SetText(new string(shellContent.Title)); + } + } + } + void OnTabLayoutChange(object sender, AView.LayoutChangeEventArgs e) { if (_disposed) @@ -327,6 +351,10 @@ void HookEvents() SectionController.ItemsCollectionChanged += OnItemsCollectionChanged; ((IShellController)_shellContext.Shell).AddAppearanceObserver(this, ShellSection); ShellSection.PropertyChanged += OnShellItemPropertyChanged; + foreach (var item in SectionController.GetItems()) + { + item.PropertyChanged += OnShellContentPropertyChanged; + } } void UnhookEvents() @@ -334,6 +362,10 @@ void UnhookEvents() SectionController.ItemsCollectionChanged -= OnItemsCollectionChanged; ((IShellController)_shellContext?.Shell)?.RemoveAppearanceObserver(this); ShellSection.PropertyChanged -= OnShellItemPropertyChanged; + foreach (var item in SectionController.GetItems()) + { + item.PropertyChanged -= OnShellContentPropertyChanged; + } } protected virtual void OnPageSelected(int position) diff --git a/src/Controls/src/Core/Element/Element.cs b/src/Controls/src/Core/Element/Element.cs index 849967d69d14..ecf15fc254f0 100644 --- a/src/Controls/src/Core/Element/Element.cs +++ b/src/Controls/src/Core/Element/Element.cs @@ -782,7 +782,7 @@ internal void OnResourcesChanged(IEnumerable> value internal override void OnSetDynamicResource(BindableProperty property, string key, SetterSpecificity specificity) { base.OnSetDynamicResource(property, key, specificity); - if (!DynamicResources.TryGetValue(property, out var existing) || existing.Item2 < specificity) + if (!DynamicResources.TryGetValue(property, out var existing) || existing.Item2 <= specificity) DynamicResources[property] = (key, specificity); if (this.TryGetResource(key, out var value)) OnResourceChanged(property, value, specificity); diff --git a/src/Controls/src/Core/Frame/Frame.cs b/src/Controls/src/Core/Frame/Frame.cs index ca79dcedefd4..8c6a2a28ceff 100644 --- a/src/Controls/src/Core/Frame/Frame.cs +++ b/src/Controls/src/Core/Frame/Frame.cs @@ -112,7 +112,8 @@ IShadow IView.Shadow Size ICrossPlatformLayout.CrossPlatformArrange(Graphics.Rect bounds) { #if !WINDOWS - bounds = bounds.Inset(((IBorderElement)this).BorderWidth); // Windows' implementation would cause an incorrect double-counting of the inset + if (BorderColor is not null) + bounds = bounds.Inset(((IBorderElement)this).BorderWidth); // Windows' implementation would cause an incorrect double-counting of the inset #endif this.ArrangeContent(bounds); return bounds.Size; @@ -122,7 +123,8 @@ Size ICrossPlatformLayout.CrossPlatformMeasure(double widthConstraint, double he { var inset = Padding; #if !WINDOWS - inset += ((IBorderElement)this).BorderWidth; // Windows' implementation would cause an incorrect double-counting of the inset + if (BorderColor is not null) + inset += ((IBorderElement)this).BorderWidth; // Windows' implementation would cause an incorrect double-counting of the inset #endif return this.MeasureContent(inset, widthConstraint, heightConstraint); } diff --git a/src/Controls/src/Core/Handlers/Items/iOS/GroupableItemsViewController.cs b/src/Controls/src/Core/Handlers/Items/iOS/GroupableItemsViewController.cs index 3e618c05deb3..f1064d9abe34 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/GroupableItemsViewController.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/GroupableItemsViewController.cs @@ -173,17 +173,9 @@ internal CGSize GetReferenceSizeForheaderOrFooter(UICollectionView collectionVie return CGSize.Empty; } - if (!(OperatingSystem.IsIOSVersionAtLeast(11) || OperatingSystem.IsTvOSVersionAtLeast(11))) - { - // iOS 10 crashes if we try to dequeue a cell for measurement - // so we'll use an alternate method - return MeasureSupplementaryView(elementKind, section); - } - - var cell = GetViewForSupplementaryElement(collectionView, elementKind, - NSIndexPath.FromItemSection(0, section)) as ItemsViewCell; - - return cell.Measure(); + // Dequeuing a supplementary view for measurement caused multiple instances of the header/footer to appear in the view. + // We now always use MeasureSupplementaryView, an alternate approach for calculating the size without dequeuing the view. + return MeasureSupplementaryView(elementKind, section); } internal void SetScrollAnimationEndedCallback(Action callback) diff --git a/src/Controls/src/Core/Handlers/Items/iOS/ObservableGroupedSource.cs b/src/Controls/src/Core/Handlers/Items/iOS/ObservableGroupedSource.cs index feba35a2451f..050133eb7569 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/ObservableGroupedSource.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/ObservableGroupedSource.cs @@ -204,7 +204,7 @@ bool NotLoadedYet() void Add(NotifyCollectionChangedEventArgs args) { - if (ReloadRequired()) + if (NotLoadedYet()) { Reload(); return; diff --git a/src/Controls/src/Core/Handlers/Shell/ShellContentHandler.Windows.cs b/src/Controls/src/Core/Handlers/Shell/ShellContentHandler.Windows.cs index 776e69c38373..3ae9eb434f34 100644 --- a/src/Controls/src/Core/Handlers/Shell/ShellContentHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Shell/ShellContentHandler.Windows.cs @@ -7,7 +7,7 @@ namespace Microsoft.Maui.Controls.Handlers public partial class ShellContentHandler : ElementHandler { public static PropertyMapper Mapper = - new PropertyMapper(ElementMapper); + new PropertyMapper(ElementMapper) { [nameof(ShellContent.Title)] = MapTitle }; public static CommandMapper CommandMapper = new CommandMapper(ElementCommandMapper); @@ -16,6 +16,14 @@ public ShellContentHandler() : base(Mapper, CommandMapper) { } + internal static void MapTitle(ShellContentHandler handler, ShellContent item) + { + var shellSection = item.Parent as ShellSection; + var shellItem = shellSection?.Parent as ShellItem; + var shellItemHandler = shellItem?.Handler as ShellItemHandler; + shellItemHandler?.UpdateTitle(); + } + protected override FrameworkElement CreatePlatformElement() { return (VirtualView as IShellContentController).GetOrCreateContent().ToPlatform(MauiContext); diff --git a/src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs b/src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs index 9e58809acca2..f51ff1e35388 100644 --- a/src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs +++ b/src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs @@ -273,7 +273,7 @@ string GetTitle() return string.Empty; } - return _currentNavigationPage?.CurrentPage?.Title; + return _currentNavigationPage?.CurrentPage?.Title ?? _currentNavigationPage?.Title; } VisualElement GetTitleView() diff --git a/src/Controls/src/Core/SetterSpecificityList.cs b/src/Controls/src/Core/SetterSpecificityList.cs index 11f33229370a..9dd26389f500 100644 --- a/src/Controls/src/Core/SetterSpecificityList.cs +++ b/src/Controls/src/Core/SetterSpecificityList.cs @@ -15,6 +15,7 @@ internal class SetterSpecificityList where T : class SetterSpecificity[] _keys; T[] _values; + int _count; public int Count => _count; @@ -231,4 +232,4 @@ void SetCapacity(int currentCapacity, int capacity) _values = newValues; } } -} +} \ No newline at end of file diff --git a/src/Controls/src/Core/Shell/BaseShellItem.cs b/src/Controls/src/Core/Shell/BaseShellItem.cs index f7237a2cd298..2759d64b5e8c 100644 --- a/src/Controls/src/Core/Shell/BaseShellItem.cs +++ b/src/Controls/src/Core/Shell/BaseShellItem.cs @@ -50,7 +50,7 @@ public class BaseShellItem : NavigableElement, IPropertyPropagationController, I /// Bindable property for . public static readonly BindableProperty TitleProperty = - BindableProperty.Create(nameof(Title), typeof(string), typeof(BaseShellItem), null, BindingMode.OneTime, propertyChanged: OnTitlePropertyChanged); + BindableProperty.Create(nameof(Title), typeof(string), typeof(BaseShellItem), null, BindingMode.TwoWay, propertyChanged: OnTitlePropertyChanged); /// Bindable property for . public static readonly BindableProperty IsVisibleProperty = diff --git a/src/Controls/src/Core/Toolbar/Toolbar.Android.cs b/src/Controls/src/Core/Toolbar/Toolbar.Android.cs index 3e33d47fab4e..fb632db95961 100644 --- a/src/Controls/src/Core/Toolbar/Toolbar.Android.cs +++ b/src/Controls/src/Core/Toolbar/Toolbar.Android.cs @@ -138,6 +138,7 @@ public static void MapIsVisible(ToolbarHandler arg1, Toolbar arg2) => public static void MapBarTextColor(IToolbarHandler arg1, Toolbar arg2) { arg1.PlatformView.UpdateBarTextColor(arg2); + arg2.UpdateMenu(); } public static void MapBarBackground(IToolbarHandler arg1, Toolbar arg2) diff --git a/src/Controls/src/Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Controls/src/Xaml/MarkupExtensions/StaticResourceExtension.cs index 6af7ec29d1d4..99cd45f53938 100644 --- a/src/Controls/src/Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Controls/src/Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -7,6 +7,7 @@ namespace Microsoft.Maui.Controls.Xaml { [ContentProperty(nameof(Key))] [RequireService([typeof(IXmlLineInfoProvider), typeof(IProvideParentValues)])] + [ProvideCompiled("Microsoft.Maui.Controls.Build.Tasks.StaticResourceExtension")] public sealed class StaticResourceExtension : IMarkupExtension { public string Key { get; set; } diff --git a/src/Controls/src/Xaml/XamlServiceProvider.cs b/src/Controls/src/Xaml/XamlServiceProvider.cs index f69c958fe0fb..c2b86f47db39 100644 --- a/src/Controls/src/Xaml/XamlServiceProvider.cs +++ b/src/Controls/src/Xaml/XamlServiceProvider.cs @@ -372,4 +372,4 @@ static bool IsBindingBaseProperty(IElementNode node, HydrationContext context) public string BindingDataType { get; } public HydrationContext Context { get; } } -} \ No newline at end of file +} diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ChangeShellContentTitle.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ChangeShellContentTitle.png new file mode 100644 index 000000000000..ab55034b484c Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ChangeShellContentTitle.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewAddGroupWhenViewIsEmpty.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewAddGroupWhenViewIsEmpty.png new file mode 100644 index 000000000000..5d661ee0c610 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewAddGroupWhenViewIsEmpty.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewDuplicateViewsWhenAddItemToGroup.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewDuplicateViewsWhenAddItemToGroup.png new file mode 100644 index 000000000000..cb1e0f385aa5 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewDuplicateViewsWhenAddItemToGroup.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NavigationPageTitle.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NavigationPageTitle.png new file mode 100644 index 000000000000..3699d81a9306 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/NavigationPageTitle.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarItemsShouldBeVisible.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarItemsShouldBeVisible.png new file mode 100644 index 000000000000..614e03c2473e Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ToolbarItemsShouldBeVisible.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ValidateFrameOffsets.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ValidateFrameOffsets.png new file mode 100644 index 000000000000..c041d90cdb19 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ValidateFrameOffsets.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue17969.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue17969.xaml new file mode 100644 index 000000000000..aab6b5922577 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue17969.xaml @@ -0,0 +1,37 @@ + + + + + +