diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index 7b49f0485d0d..849eea9f5aba 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -384,6 +384,12 @@ public static IEnumerable ProvideValue(VariableDefinitionReference if (bindingExtensionType.Value.Item3 == "BindingExtension") { + // extension.TypedBinding.ConverterCulture = extension.ConverterCulture; + yield return Create(Ldloc, vardefref.VariableDefinition); + yield return Create(Callvirt, module.ImportPropertyGetterReference(context.Cache, bindingExtensionType.Value, propertyName: "TypedBinding")); + yield return Create(Ldloc, vardefref.VariableDefinition); + yield return Create(Callvirt, module.ImportPropertyGetterReference(context.Cache, bindingExtensionType.Value, propertyName: "ConverterCulture")); + yield return Create(Callvirt, module.ImportPropertySetterReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls.Internals", "TypedBindingBase"), propertyName: "ConverterCulture")); // // extension.TypedBinding.Source = extension.Source; yield return Create(Ldloc, vardefref.VariableDefinition); yield return Create(Callvirt, module.ImportPropertyGetterReference(context.Cache, bindingExtensionType.Value, propertyName: "TypedBinding")); diff --git a/src/Controls/src/Core/Binding.cs b/src/Controls/src/Core/Binding.cs index 4faf2ce91dbe..d4577dbee227 100644 --- a/src/Controls/src/Core/Binding.cs +++ b/src/Controls/src/Core/Binding.cs @@ -16,6 +16,7 @@ public sealed class Binding : BindingBase public const string SelfPath = "."; IValueConverter _converter; object _converterParameter; + CultureInfo _converterCulture; BindingExpression _expression; string _path; @@ -81,6 +82,21 @@ public object ConverterParameter } } + /// + /// Gets or sets the culture information used by the converter. + /// + [TypeConverter(typeof(CultureInfoConverter))] + public CultureInfo ConverterCulture + { + get { return _converterCulture ?? CultureInfo.CurrentUICulture; } + set + { + ThrowIfApplied(); + + _converterCulture = value; + } + } + /// /// Gets or sets the property path to bind to on the source object. /// @@ -195,6 +211,7 @@ internal override BindingBase Clone() { Converter = Converter, ConverterParameter = ConverterParameter, + ConverterCulture = _converterCulture, StringFormat = StringFormat, Source = Source, UpdateSourceEventName = UpdateSourceEventName, @@ -211,7 +228,7 @@ internal override BindingBase Clone() internal override object GetSourceValue(object value, Type targetPropertyType) { if (Converter != null) - value = Converter.Convert(value, targetPropertyType, ConverterParameter, CultureInfo.CurrentUICulture); + value = Converter.Convert(value, targetPropertyType, ConverterParameter, ConverterCulture); return base.GetSourceValue(value, targetPropertyType); } @@ -219,7 +236,7 @@ internal override object GetSourceValue(object value, Type targetPropertyType) internal override object GetTargetValue(object value, Type sourcePropertyType) { if (Converter != null) - value = Converter.ConvertBack(value, sourcePropertyType, ConverterParameter, CultureInfo.CurrentUICulture); + value = Converter.ConvertBack(value, sourcePropertyType, ConverterParameter, ConverterCulture); return base.GetTargetValue(value, sourcePropertyType); } diff --git a/src/Controls/src/Core/MultiBinding.cs b/src/Controls/src/Core/MultiBinding.cs index 1f59b28d48f6..e48f99383358 100644 --- a/src/Controls/src/Core/MultiBinding.cs +++ b/src/Controls/src/Core/MultiBinding.cs @@ -1,6 +1,7 @@ #nullable disable using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Controls.Xaml.Diagnostics; @@ -14,6 +15,7 @@ namespace Microsoft.Maui.Controls public sealed class MultiBinding : BindingBase { IMultiValueConverter _converter; + CultureInfo _converterCulture; object _converterParameter; IList _bindings; BindableProperty _targetProperty; @@ -35,6 +37,20 @@ public IMultiValueConverter Converter } } + /// + /// Gets or sets the culture in which to evaluate the . + /// + [TypeConverter(typeof(CultureInfoConverter))] + public CultureInfo ConverterCulture + { + get { return _converterCulture ?? CultureInfo.CurrentUICulture; } + set + { + ThrowIfApplied(); + _converterCulture = value; + } + } + /// /// Gets or sets an optional parameter to pass to the . /// @@ -70,6 +86,7 @@ internal override BindingBase Clone() var clone = new MultiBinding() { Converter = Converter, + ConverterCulture = _converterCulture, ConverterParameter = ConverterParameter, Bindings = bindingsclone, FallbackValue = FallbackValue, @@ -208,7 +225,7 @@ internal override object GetSourceValue(object value, Type targetPropertyType) { var valuearray = value as object[]; if (valuearray != null && Converter != null) - value = Converter.Convert(valuearray, targetPropertyType, ConverterParameter, CultureInfo.CurrentUICulture); + value = Converter.Convert(valuearray, targetPropertyType, ConverterParameter, ConverterCulture); if (valuearray != null && Converter == null && StringFormat != null && BindingBase.TryFormat(StringFormat, valuearray, out var formatted)) return formatted; @@ -227,7 +244,7 @@ internal override object GetTargetValue(object value, Type sourcePropertyType) var types = new Type[_bpProxies.Length]; for (var i = 0; i < _bpProxies.Length; i++) types[i] = values[i]?.GetType() ?? typeof(object); - return Converter.ConvertBack(value, types, ConverterParameter, CultureInfo.CurrentUICulture); + return Converter.ConvertBack(value, types, ConverterParameter, ConverterCulture); } return base.GetTargetValue(value, sourcePropertyType); diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index 3d583efe170e..f476435dd6bf 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1,4 +1,10 @@ #nullable enable +~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Binding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList *REMOVED*~static readonly Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.DefaultForegroundColor -> Microsoft.Maui.Graphics.Color *REMOVED*~static readonly Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.DefaultTitleColor -> Microsoft.Maui.Graphics.Color diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 58d048308522..ab6ba99304e2 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1,6 +1,12 @@ #nullable enable *REMOVED*~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRootRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList +~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Binding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void Microsoft.Maui.Controls.AppThemeBinding Microsoft.Maui.Controls.AppThemeBinding.AppThemeBinding() -> void Microsoft.Maui.Controls.BoxView.~BoxView() -> void diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 58d048308522..ab6ba99304e2 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1,6 +1,12 @@ #nullable enable *REMOVED*~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRootRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList +~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Binding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void Microsoft.Maui.Controls.AppThemeBinding Microsoft.Maui.Controls.AppThemeBinding.AppThemeBinding() -> void Microsoft.Maui.Controls.BoxView.~BoxView() -> void diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 65f89237056a..19689c0ceb0b 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -1,4 +1,10 @@ #nullable enable +~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Binding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList Microsoft.Maui.Controls.AppThemeBinding Microsoft.Maui.Controls.AppThemeBinding.AppThemeBinding() -> void diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index dd6618904c10..792cd9d42180 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1,4 +1,10 @@ #nullable enable +~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Binding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList Microsoft.Maui.Controls.AppThemeBinding Microsoft.Maui.Controls.AppThemeBinding.AppThemeBinding() -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index 32221f10a47a..e9b256163de7 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,5 +1,11 @@ #nullable enable *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList +~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Binding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void Microsoft.Maui.Controls.AppThemeBinding Microsoft.Maui.Controls.AppThemeBinding.AppThemeBinding() -> void Microsoft.Maui.Controls.BoxView.~BoxView() -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index acc2cabf3fbd..87b86aadb26b 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,5 +1,21 @@ #nullable enable *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList +~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Binding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.set -> void +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void +~Microsoft.Maui.Controls.BackButtonBehavior.AccessibilityLabel.get -> string +~Microsoft.Maui.Controls.BackButtonBehavior.AccessibilityLabel.set -> void +~Microsoft.Maui.Controls.BaseShellItem.BadgeColor.get -> Microsoft.Maui.Graphics.Color +~Microsoft.Maui.Controls.BaseShellItem.BadgeColor.set -> void +~Microsoft.Maui.Controls.BaseShellItem.BadgeText.get -> string +~Microsoft.Maui.Controls.BaseShellItem.BadgeText.set -> void +~Microsoft.Maui.Controls.BaseShellItem.BadgeTextColor.get -> Microsoft.Maui.Graphics.Color +~Microsoft.Maui.Controls.BaseShellItem.BadgeTextColor.set -> void +~Microsoft.Maui.Controls.BoxView.Fill.get -> Microsoft.Maui.Controls.Brush +~Microsoft.Maui.Controls.BoxView.Fill.set -> void Microsoft.Maui.Controls.BoxView.~BoxView() -> void Microsoft.Maui.Controls.ImageSource.InvalidateStyle() -> void Microsoft.Maui.Controls.LongPressGestureRecognizer @@ -47,16 +63,6 @@ static readonly Microsoft.Maui.Controls.LongPressGestureRecognizer.NumberOfTouch static readonly Microsoft.Maui.Controls.LongPressGestureRecognizer.StateProperty -> Microsoft.Maui.Controls.BindableProperty! virtual Microsoft.Maui.Controls.LongPressedEventArgs.GetPosition(Microsoft.Maui.Controls.Element? relativeTo) -> Microsoft.Maui.Graphics.Point? virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui.Controls.Element? relativeTo) -> Microsoft.Maui.Graphics.Point? -~Microsoft.Maui.Controls.BackButtonBehavior.AccessibilityLabel.get -> string -~Microsoft.Maui.Controls.BackButtonBehavior.AccessibilityLabel.set -> void -~Microsoft.Maui.Controls.BaseShellItem.BadgeColor.get -> Microsoft.Maui.Graphics.Color -~Microsoft.Maui.Controls.BaseShellItem.BadgeColor.set -> void -~Microsoft.Maui.Controls.BaseShellItem.BadgeText.get -> string -~Microsoft.Maui.Controls.BaseShellItem.BadgeText.set -> void -~Microsoft.Maui.Controls.BaseShellItem.BadgeTextColor.get -> Microsoft.Maui.Graphics.Color -~Microsoft.Maui.Controls.BaseShellItem.BadgeTextColor.set -> void -~Microsoft.Maui.Controls.BoxView.Fill.get -> Microsoft.Maui.Controls.Brush -~Microsoft.Maui.Controls.BoxView.Fill.set -> void ~Microsoft.Maui.Controls.Editor.ReturnCommand.get -> System.Windows.Input.ICommand ~Microsoft.Maui.Controls.Editor.ReturnCommand.set -> void ~Microsoft.Maui.Controls.Editor.ReturnCommandParameter.get -> object diff --git a/src/Controls/src/Core/TypedBinding.cs b/src/Controls/src/Core/TypedBinding.cs index 4a5ad5545fb2..322d74904c3a 100644 --- a/src/Controls/src/Core/TypedBinding.cs +++ b/src/Controls/src/Core/TypedBinding.cs @@ -17,6 +17,7 @@ public abstract class TypedBindingBase : BindingBase { IValueConverter _converter; object _converterParameter; + CultureInfo _converterCulture; object _source; string _updateSourceEventName; @@ -42,6 +43,20 @@ public object ConverterParameter } } + /// Gets or sets the culture information used by the converter. + [TypeConverter(typeof(CultureInfoConverter))] + public CultureInfo ConverterCulture + { + get { return _converterCulture ?? CultureInfo.CurrentUICulture; } + set + { + ThrowIfApplied(); + _converterCulture = value; + } + } + + internal CultureInfo ConverterCultureValue => _converterCulture; + /// Gets or sets the source object for the binding. public object Source { @@ -237,6 +252,7 @@ internal override BindingBase Clone() Mode = Mode, Converter = Converter, ConverterParameter = ConverterParameter, + ConverterCulture = ConverterCultureValue, StringFormat = StringFormat, Source = Source, UpdateSourceEventName = UpdateSourceEventName, @@ -263,7 +279,7 @@ internal override void ApplyToResolvedSource(object source, BindableObject targe internal override object GetSourceValue(object value, Type targetPropertyType) { if (Converter != null) - value = Converter.Convert(value, targetPropertyType, ConverterParameter, CultureInfo.CurrentUICulture); + value = Converter.Convert(value, targetPropertyType, ConverterParameter, ConverterCulture); return base.GetSourceValue(value, targetPropertyType); } @@ -271,7 +287,7 @@ internal override object GetSourceValue(object value, Type targetPropertyType) internal override object GetTargetValue(object value, Type sourcePropertyType) { if (Converter != null) - value = Converter.ConvertBack(value, sourcePropertyType, ConverterParameter, CultureInfo.CurrentUICulture); + value = Converter.ConvertBack(value, sourcePropertyType, ConverterParameter, ConverterCulture); //return base.GetTargetValue(value, sourcePropertyType); return value; diff --git a/src/Controls/src/SourceGen/CompiledBindingMarkup.cs b/src/Controls/src/SourceGen/CompiledBindingMarkup.cs index abb896df9caf..594169e1d579 100644 --- a/src/Controls/src/SourceGen/CompiledBindingMarkup.cs +++ b/src/Controls/src/SourceGen/CompiledBindingMarkup.cs @@ -92,6 +92,7 @@ public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, ou propertyFlags |= BindingPropertyFlags.FallbackValue; if (_node.HasProperty("TargetNullValue")) propertyFlags |= BindingPropertyFlags.TargetNullValue; + var hasConverterCulture = !isTemplateBinding && _node.HasProperty("ConverterCulture"); //Generate the complete inline binding creation method using var stringWriter = new StringWriter(); @@ -191,7 +192,7 @@ public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, ou code.Indent--; // Object initializer if any properties are set - if (propertyFlags != BindingPropertyFlags.None) + if (propertyFlags != BindingPropertyFlags.None || hasConverterCulture) { code.WriteLine(); code.WriteLine("{"); @@ -203,6 +204,8 @@ public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, ou code.WriteLine("Converter = extension.Converter,"); if (propertyFlags.HasFlag(BindingPropertyFlags.ConverterParameter)) code.WriteLine("ConverterParameter = extension.ConverterParameter,"); + if (hasConverterCulture) + code.WriteLine("ConverterCulture = extension.ConverterCulture,"); if (propertyFlags.HasFlag(BindingPropertyFlags.StringFormat)) code.WriteLine("StringFormat = extension.StringFormat,"); if (propertyFlags.HasFlag(BindingPropertyFlags.Source)) diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs index 195b6554023e..6a401edb255a 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -6,6 +6,7 @@ using System.Xml; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.Maui.Controls.Xaml; using static System.String; @@ -404,10 +405,15 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde } else { - value = expression + - $"{{ UpdateSourceEventName = {extVariable.ValueAccessor}.UpdateSourceEventName, " + - $"FallbackValue = {extVariable.ValueAccessor}.FallbackValue, " + - $"TargetNullValue = {extVariable.ValueAccessor}.TargetNullValue }}"; + var objectInitializer = new StringBuilder() + .Append($"{{ UpdateSourceEventName = {extVariable.ValueAccessor}.UpdateSourceEventName, "); + if (markupNode.HasProperty("ConverterCulture")) + objectInitializer.Append($"ConverterCulture = {extVariable.ValueAccessor}.ConverterCulture, "); + objectInitializer + .Append($"FallbackValue = {extVariable.ValueAccessor}.FallbackValue, ") + .Append($"TargetNullValue = {extVariable.ValueAccessor}.TargetNullValue }}"); + + value = expression + objectInitializer; return true; } } @@ -441,6 +447,17 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde expression += ") {"; if (markupNode.Properties.TryGetValue(new XmlName(null, "UpdateSourceEventName"), out var updateSourceEventNameNode)) expression += $"UpdateSourceEventName = {getNodeValue(updateSourceEventNameNode, context.Compilation.GetTypeByMetadataName("System.String")!).ValueAccessor}, "; + if (markupNode.Properties.TryGetValue(new XmlName(null, "ConverterCulture"), out var converterCultureNode)) + { + if (converterCultureNode is ValueNode { Value: string converterCulture }) + { + expression += $"ConverterCulture = global::System.Globalization.CultureInfo.GetCultureInfo({SymbolDisplay.FormatLiteral(converterCulture, true)}), "; + } + else + { + expression += $"ConverterCulture = {getNodeValue(converterCultureNode, context.Compilation.GetTypeByMetadataName("System.Globalization.CultureInfo")!).ValueAccessor}, "; + } + } if (markupNode.Properties.TryGetValue(new XmlName(null, "FallbackValue"), out var fallbackValueNode)) expression += $"FallbackValue = {getNodeValue(fallbackValueNode, context.Compilation.GetTypeByMetadataName("System.Object")!).ValueAccessor}, "; if (markupNode.Properties.TryGetValue(new XmlName(null, "TargetNullValue"), out var targetNullValueNode)) diff --git a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs index 5d86ae0daa12..8b8454413249 100644 --- a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Microsoft.Maui.Controls.Internals; namespace Microsoft.Maui.Controls.Xaml @@ -32,6 +33,12 @@ public sealed class BindingExtension : IMarkupExtension /// public object ConverterParameter { get; set; } + /// + /// Gets or sets the culture information used by the converter. + /// + [TypeConverter(typeof(CultureInfoConverter))] + public CultureInfo ConverterCulture { get; set; } + /// /// Gets or sets a format string to use when converting the bound value to a string. /// @@ -70,6 +77,7 @@ BindingBase IMarkupExtension.ProvideValue(IServiceProvider serviceP TypedBinding.Mode = Mode; TypedBinding.Converter = Converter; TypedBinding.ConverterParameter = ConverterParameter; + TypedBinding.ConverterCulture = ConverterCulture; TypedBinding.StringFormat = StringFormat; TypedBinding.Source = Source; TypedBinding.UpdateSourceEventName = UpdateSourceEventName; @@ -92,6 +100,7 @@ BindingBase CreateBinding() } return new Binding(Path, Mode, Converter, ConverterParameter, StringFormat, Source) { + ConverterCulture = ConverterCulture, UpdateSourceEventName = UpdateSourceEventName, FallbackValue = FallbackValue, TargetNullValue = TargetNullValue, diff --git a/src/Controls/src/Xaml/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Xaml/PublicAPI/net-android/PublicAPI.Unshipped.txt index 7dc5c58110bf..6c1e50f86a3a 100644 --- a/src/Controls/src/Xaml/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Xaml/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.set -> void diff --git a/src/Controls/src/Xaml/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Xaml/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 7dc5c58110bf..6c1e50f86a3a 100644 --- a/src/Controls/src/Xaml/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Xaml/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.set -> void diff --git a/src/Controls/src/Xaml/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Xaml/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 7dc5c58110bf..6c1e50f86a3a 100644 --- a/src/Controls/src/Xaml/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Xaml/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.set -> void diff --git a/src/Controls/src/Xaml/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Xaml/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 7dc5c58110bf..6c1e50f86a3a 100644 --- a/src/Controls/src/Xaml/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Xaml/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.set -> void diff --git a/src/Controls/src/Xaml/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Xaml/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 7dc5c58110bf..6c1e50f86a3a 100644 --- a/src/Controls/src/Xaml/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Xaml/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.set -> void diff --git a/src/Controls/src/Xaml/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Xaml/PublicAPI/net/PublicAPI.Unshipped.txt index 7dc5c58110bf..6c1e50f86a3a 100644 --- a/src/Controls/src/Xaml/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Xaml/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.set -> void diff --git a/src/Controls/src/Xaml/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Xaml/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 7dc5c58110bf..6c1e50f86a3a 100644 --- a/src/Controls/src/Xaml/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Xaml/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.get -> System.Globalization.CultureInfo +~Microsoft.Maui.Controls.Xaml.BindingExtension.ConverterCulture.set -> void diff --git a/src/Controls/tests/Core.UnitTests/BindingConverterCultureTests.cs b/src/Controls/tests/Core.UnitTests/BindingConverterCultureTests.cs new file mode 100644 index 000000000000..2cdb4ae354ba --- /dev/null +++ b/src/Controls/tests/Core.UnitTests/BindingConverterCultureTests.cs @@ -0,0 +1,129 @@ +using System; +using System.Globalization; +using System.Threading; +using Microsoft.Maui.Controls.Internals; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class BindingConverterCultureTests : BaseTestFixture + { + [Fact] + public void BindingConverterCultureOverridesCurrentUICulture() + { + var originalCulture = Thread.CurrentThread.CurrentUICulture; + + try + { + Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US"); + + var vm = new MockViewModel { Text = "Text" }; + var bindable = new MockBindable { BindingContext = vm }; + bindable.SetBinding(MockBindable.TextProperty, new Binding("Text", converter: new CultureNameConverter()) + { + ConverterCulture = new CultureInfo("nl-NL") + }); + + Assert.Equal("nl-NL", bindable.Text); + + bindable.SetValueFromRenderer(MockBindable.TextProperty, "Updated"); + + Assert.Equal("nl-NL", vm.Text); + } + finally + { + Thread.CurrentThread.CurrentUICulture = originalCulture; + } + } + + [Fact] + public void TypedBindingConverterCultureOverridesCurrentUICulture() + { + var originalCulture = Thread.CurrentThread.CurrentUICulture; + + try + { + Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US"); + + var vm = new MockViewModel { Text = "Text" }; + var bindable = new MockBindable { BindingContext = vm }; + bindable.SetBinding(MockBindable.TextProperty, CreateTypedBinding(new CultureInfo("nl-NL"))); + + Assert.Equal("nl-NL", bindable.Text); + + bindable.SetValueFromRenderer(MockBindable.TextProperty, "Updated"); + + Assert.Equal("nl-NL", vm.Text); + } + finally + { + Thread.CurrentThread.CurrentUICulture = originalCulture; + } + } + + [Fact] + public void ClonesPreserveExplicitConverterCulture() + { + var culture = new CultureInfo("nl-NL"); + + var bindingClone = (Binding)new Binding(".", converter: new CultureNameConverter()) + { + ConverterCulture = culture + }.Clone(); + + var typedBindingClone = (TypedBinding)CreateTypedBinding(culture).Clone(); + + Assert.Same(culture, bindingClone.ConverterCulture); + Assert.Same(culture, typedBindingClone.ConverterCulture); + } + + [Fact] + public void ClonesKeepUnsetConverterCultureDynamic() + { + var originalCulture = Thread.CurrentThread.CurrentUICulture; + + try + { + Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US"); + + var bindingClone = (Binding)new Binding(".", converter: new CultureNameConverter()).Clone(); + var typedBindingClone = (TypedBinding)CreateTypedBinding().Clone(); + + Thread.CurrentThread.CurrentUICulture = new CultureInfo("nl-NL"); + + Assert.Equal("nl-NL", bindingClone.ConverterCulture.Name); + Assert.Equal("nl-NL", typedBindingClone.ConverterCulture.Name); + } + finally + { + Thread.CurrentThread.CurrentUICulture = originalCulture; + } + } + + static TypedBinding CreateTypedBinding(CultureInfo culture = null) + { + var binding = new TypedBinding( + getter: vm => (vm.Text, true), + setter: (vm, value) => vm.Text = value, + handlers: new[] + { + new Tuple, string>(vm => vm, nameof(MockViewModel.Text)) + }) + { + Converter = new CultureNameConverter() + }; + + if (culture is not null) + binding.ConverterCulture = culture; + + return binding; + } + + class CultureNameConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => culture.Name; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => culture.Name; + } + } +} diff --git a/src/Controls/tests/Core.UnitTests/MultiBindingTests.cs b/src/Controls/tests/Core.UnitTests/MultiBindingTests.cs index 57ce12b11e05..b6c540abd951 100644 --- a/src/Controls/tests/Core.UnitTests/MultiBindingTests.cs +++ b/src/Controls/tests/Core.UnitTests/MultiBindingTests.cs @@ -541,6 +541,109 @@ public void TestConverterWithStringFormat() Assert.Equal("Hello FOO 042 BAZ", bindable.GetValue(property)); } + [Fact] + public void ConverterCulturePassedToConvert() + { + var property = BindableProperty.Create("foo", typeof(string), typeof(MockBindable), null); + var bindable = new MockBindable + { + BindingContext = new { foo = "FOO", bar = "BAR" } + }; + var converter = new CultureReportingMultiConverter(); + var culture = CultureInfo.GetCultureInfo("nl-NL"); + + bindable.SetBinding(property, new MultiBinding + { + Bindings = + { + new Binding("foo"), + new Binding("bar"), + }, + Converter = converter, + ConverterCulture = culture, + }); + + Assert.Equal("nl-NL", bindable.GetValue(property)); + Assert.Same(culture, converter.ConvertCulture); + } + + [Fact] + public void ConverterCulturePassedToConvertBack() + { + var viewModel = new PersonViewModel + { + FirstName = "Jane", + MiddleName = "A.", + LastName = "Doe", + }; + var label = new Label + { + BindingContext = viewModel + }; + var converter = new CultureReportingMultiConverter(); + var culture = CultureInfo.GetCultureInfo("nl-NL"); + + label.SetBinding(Label.TextProperty, new MultiBinding + { + Bindings = + { + new Binding(nameof(PersonViewModel.FirstName)), + new Binding(nameof(PersonViewModel.MiddleName)), + new Binding(nameof(PersonViewModel.LastName)), + }, + Converter = converter, + ConverterCulture = culture, + Mode = BindingMode.TwoWay, + }); + + label.SetValueCore(Label.TextProperty, "John Q. Public", Internals.SetValueFlags.None, BindableObject.SetValuePrivateFlags.Default, SetterSpecificity.ManualValueSetter); + + Assert.Same(culture, converter.ConvertBackCulture); + Assert.Equal("John", viewModel.FirstName); + Assert.Equal("Q.", viewModel.MiddleName); + Assert.Equal("Public", viewModel.LastName); + } + + [Fact] + public void ClonePreservesExplicitConverterCulture() + { + var culture = CultureInfo.GetCultureInfo("nl-NL"); + var binding = new MultiBinding + { + Bindings = { new Binding("foo") }, + Converter = new CultureReportingMultiConverter(), + ConverterCulture = culture, + }; + + var clone = (MultiBinding)binding.Clone(); + + Assert.Same(culture, clone.ConverterCulture); + } + + [Fact] + public void ClonePreservesDynamicDefaultConverterCulture() + { + var binding = new MultiBinding + { + Bindings = { new Binding("foo") }, + Converter = new CultureReportingMultiConverter(), + }; + + var oldCulture = CultureInfo.CurrentUICulture; + try + { + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); + var clone = (MultiBinding)binding.Clone(); + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("nl-NL"); + + Assert.Equal(CultureInfo.GetCultureInfo("nl-NL"), clone.ConverterCulture); + } + finally + { + CultureInfo.CurrentUICulture = oldCulture; + } + } + private Label GenerateNameLabel(string person, BindingMode mode) { var label = new Label(); @@ -704,6 +807,25 @@ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, } } + public class CultureReportingMultiConverter : IMultiValueConverter + { + public CultureInfo ConvertCulture { get; private set; } + + public CultureInfo ConvertBackCulture { get; private set; } + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + ConvertCulture = culture; + return culture.Name; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + ConvertBackCulture = culture; + return (value as string)?.Split(' ').Cast().ToArray(); + } + } + public class GroupViewModel : INotifyPropertyChanged { PersonViewModel _person1 = new PersonViewModel diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui5696.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui5696.xaml new file mode 100644 index 000000000000..ce96a3da09b6 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui5696.xaml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui5696.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui5696.xaml.cs new file mode 100644 index 000000000000..9d0562af8a16 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui5696.xaml.cs @@ -0,0 +1,55 @@ +using System; +using System.Globalization; +using System.Threading; +using Xunit; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public partial class Maui5696 : ContentPage +{ + public Maui5696() + { + InitializeComponent(); + } + + [Collection("Issue")] + public class Tests + { + [Theory] + [XamlInflatorData] + internal void BindingConverterCultureOverridesCurrentUICulture(XamlInflator inflator) + { + var originalCulture = Thread.CurrentThread.CurrentUICulture; + + try + { + Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US"); + + var page = new Maui5696(inflator) + { + BindingContext = new Maui5696ViewModel { Text = "Text" } + }; + + Assert.Equal("nl-NL", page.label.Text); + } + finally + { + Thread.CurrentThread.CurrentUICulture = originalCulture; + } + } + } +} + +public class Maui5696ViewModel +{ + public string Text { get; set; } = string.Empty; +} + +public class Maui5696Converter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => culture.Name; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => culture.Name; +}