From 28948d7ef7e0c1f1933418e99a586768c766105d Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Fri, 5 Jan 2018 02:11:51 -0700 Subject: [PATCH] VisualStateManager phase 1 (#1405) * Port from old VSM branch * Add PS and notes * Checkpoint: entry text UWP mostly working, need to check on background colors * Remove irrelevant samples from the EntryDisabledStatesGallery Make Background color work on UWP Entry with VSM * Add platform specific for disabling legacy colors on Android * Add OnPlatform example to visual state manager gallery * Add example OnIdiom in Visual State Manager * Add platform specific for disabling legacy color mode on iOS Entry * Add gallery for Button disabled states Handling legacy colors for Buttons on Android * Split out disabled states galleries; disabled legacy handling for Picker * TimePicker disabled states * DatePicker color management on Android * Color management for pre-AppCompat button * Button legacy color handling on iOS * Consolidate Platform Specifics; legacy colors working for iOS Picker and DatePicker * Fix broken search bar color management SearchBar color management working with VSM Add test page for SearchBar disabled color management Consolidate legacy color management check code into extension method on Android * Legacy color management for Editor on Android * Fix legacy color stuff for SearchBar Cancel button on iOS * C# 7 cleanup * Add colors for Cancel Button * Make sure VisualStateGroup collections set by styles are distinct objects * Validation example * Make common state names consts * Make the Windows VSM and Forms VSM work together * Update galleries for Windows * Make new methods internal * Split gallery classes and add more explanation to validation example * Remove debugging statements * Add a quick code-only example * Make legacy color management work for fast button renderer * Remove old TODO * Update docs * Move RunTimeNamePropertyAttribute to Xamarin.Forms.Xaml namespace * Verify XF namespace when looking for VisualState * Use nameof * Make common states constants public * Cast VisualElement directly so it crashes if the property is set on the wrong type * Collection -> IList for VisualStateManager * Setting fromStyle to true * Remove extraneous `private set` * Seal VSM classes * Use constraints instead of == * Add teardown method; use constraints rather than == * Remove null checking with GetVisualStateGroups * Don't explicitly initialize collections on elements * Actually, turns out that fromStyle:false *was* correct * Direct casts * Use GetIsDefault check in GoToState * Validate parents in FindTypeForVisualState * Validate group and state names on Add * Fixed check for setter collection * Fix issues with "duplicate" names when VisualStateGroups declared directly on VisualElements * Add gallery example for VSGs directly on VisualElements * Update docs * Fix bug where initial TextColor isn't set for FastRenderer Button * Move to explicit VisualStateGroupList in Setter * Fix return types for unit tests * Using string.CompareOrdinal in GetState * Update docs * Add check for null/empty VisualState Name properties --- .../BindablePropertyConverter.cs | 10 + .../SetterValueProvider.cs | 31 +- .../SetNamescopesAndRegisterNamesVisitor.cs | 10 +- .../SetPropertiesVisitor.cs | 22 ++ Xamarin.Forms.Build.Tasks/XamlGenerator.cs | 2 +- .../DefaultColorToggleTest.cs | 4 +- Xamarin.Forms.Controls/CoreGallery.cs | 2 + .../ButtonDisabledStatesGallery.xaml | 86 +++++ .../ButtonDisabledStatesGallery.xaml.cs | 48 +++ .../CodeOnlyExample.cs | 67 ++++ .../DatePickerDisabledStatesGallery.xaml | 84 ++++ .../DatePickerDisabledStatesGallery.xaml.cs | 53 +++ .../DisabledStatesGallery.cs | 47 +++ .../EditorDisabledStatesGallery.xaml | 83 ++++ .../EditorDisabledStatesGallery.xaml.cs | 54 +++ .../EntryDisabledStatesGallery.xaml | 83 ++++ .../EntryDisabledStatesGallery.xaml.cs | 53 +++ .../OnIdiomExample.xaml | 60 +++ .../OnIdiomExample.xaml.cs | 40 ++ .../OnPlatformExample.xaml | 58 +++ .../OnPlatformExample.xaml.cs | 34 ++ .../PickerDisabledStatesGallery.xaml | 104 +++++ .../PickerDisabledStatesGallery.xaml.cs | 53 +++ .../SearchBarDisabledStatesGallery.xaml | 91 +++++ .../SearchBarDisabledStatesGallery.xaml.cs | 54 +++ .../TimePickerDisabledStatesGallery.xaml | 84 ++++ .../TimePickerDisabledStatesGallery.xaml.cs | 53 +++ .../ValidationExample.xaml | 102 +++++ .../ValidationExample.xaml.cs | 65 ++++ .../VisualStateManagerGallery.cs | 30 ++ .../VisualStatesDirectlyOnElements.xaml | 62 +++ .../VisualStatesDirectlyOnElements.xaml.cs | 55 +++ .../Xamarin.Forms.Controls.csproj | 21 + .../VisualStateManagerTests.cs | 180 +++++++++ .../Xamarin.Forms.Core.UnitTests.csproj | 1 + .../BindablePropertyConverter.cs | 44 +++ .../AndroidSpecific/Elevation.cs | 30 -- .../AndroidSpecific/VisualElement.cs | 66 ++++ .../WindowsSpecific/VisualElement.cs | 37 ++ .../iOSSpecific/Entry.cs | 11 +- .../iOSSpecific/VisualElement.cs | 30 ++ .../RuntimeNamePropertyAttribute.cs | 15 + Xamarin.Forms.Core/Setter.cs | 8 +- Xamarin.Forms.Core/VisualElement.cs | 34 +- Xamarin.Forms.Core/VisualStateManager.cs | 361 ++++++++++++++++++ .../AppCompat/ButtonRenderer.cs | 7 +- .../AppCompat/PickerRenderer.cs | 6 +- .../FastRenderers/ButtonRenderer.cs | 11 +- .../Renderers/ButtonRenderer.cs | 6 +- .../Renderers/DatePickerRenderer.cs | 5 +- .../Renderers/EditorRenderer.cs | 28 +- .../Renderers/EntryRenderer.cs | 61 +-- .../Renderers/PickerRenderer.cs | 6 +- .../Renderers/SearchBarRenderer.cs | 72 +--- .../Renderers/TimePickerRenderer.cs | 6 +- .../TextColorSwitcher.cs | 33 +- .../VisualElementExtensions.cs | 9 + .../AutoSuggestStyle.xaml | 32 ++ Xamarin.Forms.Platform.UAP/ButtonRenderer.cs | 22 +- .../ConvertExtensions.cs | 3 +- .../DatePickerRenderer.cs | 21 + Xamarin.Forms.Platform.UAP/EditorRenderer.cs | 6 + Xamarin.Forms.Platform.UAP/EntryRenderer.cs | 14 +- Xamarin.Forms.Platform.UAP/FormsComboBox.cs | 5 +- Xamarin.Forms.Platform.UAP/FormsTextBox.cs | 35 +- .../FormsTextBoxStyle.xaml | 33 ++ .../InterceptVisualStateManager.cs | 115 ++++++ Xamarin.Forms.Platform.UAP/PickerRenderer.cs | 14 + .../SearchBarRenderer.cs | 28 +- Xamarin.Forms.Platform.UAP/StepperControl.cs | 20 +- .../TimePickerRenderer.cs | 22 ++ .../VisualElementExtensions.cs | 9 + .../Xamarin.Forms.Platform.UAP.csproj | 1 + .../Extensions/VisualElementExtensions.cs | 15 + .../Renderers/ButtonRenderer.cs | 16 +- .../Renderers/DatePickerRenderer.cs | 9 +- .../Renderers/EntryRenderer.cs | 38 +- .../Renderers/PickerRenderer.cs | 8 +- .../Renderers/SearchBarRenderer.cs | 47 ++- .../Renderers/TimePickerRenderer.cs | 8 +- .../Xamarin.Forms.Platform.iOS.csproj | 1 + .../VisualStateManagerTests.xaml | 119 ++++++ .../VisualStateManagerTests.xaml.cs | 175 +++++++++ .../Xamarin.Forms.Xaml.UnitTests.csproj | 13 + Xamarin.Forms.Xaml/ApplyPropertiesVisitor.cs | 62 ++- Xamarin.Forms.Xaml/NamescopingVisitor.cs | 7 +- .../VisualElement.xml | 214 +++++++++++ .../VisualElement.xml} | 76 ++-- .../VisualElement.xml | 98 +++++ .../Xamarin.Forms/VisualState.xml | 79 ++++ .../Xamarin.Forms/VisualStateGroup.xml | 100 +++++ .../Xamarin.Forms/VisualStateGroupList.xml | 283 ++++++++++++++ .../Xamarin.Forms/VisualStateManager.xml | 116 ++++++ docs/Xamarin.Forms.Core/index.xml | 248 +++++++++--- 94 files changed, 4550 insertions(+), 334 deletions(-) create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ButtonDisabledStatesGallery.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ButtonDisabledStatesGallery.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/CodeOnlyExample.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/DatePickerDisabledStatesGallery.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/DatePickerDisabledStatesGallery.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/DisabledStatesGallery.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/EditorDisabledStatesGallery.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/EditorDisabledStatesGallery.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/EntryDisabledStatesGallery.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/EntryDisabledStatesGallery.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnIdiomExample.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnIdiomExample.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnPlatformExample.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnPlatformExample.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/PickerDisabledStatesGallery.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/PickerDisabledStatesGallery.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/SearchBarDisabledStatesGallery.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/SearchBarDisabledStatesGallery.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/TimePickerDisabledStatesGallery.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/TimePickerDisabledStatesGallery.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ValidationExample.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ValidationExample.xaml.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStateManagerGallery.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStatesDirectlyOnElements.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStatesDirectlyOnElements.xaml.cs create mode 100644 Xamarin.Forms.Core.UnitTests/VisualStateManagerTests.cs delete mode 100644 Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/Elevation.cs create mode 100644 Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/VisualElement.cs create mode 100644 Xamarin.Forms.Core/PlatformConfiguration/WindowsSpecific/VisualElement.cs create mode 100644 Xamarin.Forms.Core/RuntimeNamePropertyAttribute.cs create mode 100644 Xamarin.Forms.Core/VisualStateManager.cs create mode 100644 Xamarin.Forms.Platform.UAP/InterceptVisualStateManager.cs create mode 100644 Xamarin.Forms.Platform.iOS/Extensions/VisualElementExtensions.cs create mode 100644 Xamarin.Forms.Xaml.UnitTests/VisualStateManagerTests.xaml create mode 100644 Xamarin.Forms.Xaml.UnitTests/VisualStateManagerTests.xaml.cs create mode 100644 docs/Xamarin.Forms.Core/Xamarin.Forms.PlatformConfiguration.AndroidSpecific/VisualElement.xml rename docs/Xamarin.Forms.Core/{Xamarin.Forms.PlatformConfiguration.AndroidSpecific/Elevation.xml => Xamarin.Forms.PlatformConfiguration.WindowsSpecific/VisualElement.xml} (54%) create mode 100644 docs/Xamarin.Forms.Core/Xamarin.Forms/VisualState.xml create mode 100644 docs/Xamarin.Forms.Core/Xamarin.Forms/VisualStateGroup.xml create mode 100644 docs/Xamarin.Forms.Core/Xamarin.Forms/VisualStateGroupList.xml create mode 100644 docs/Xamarin.Forms.Core/Xamarin.Forms/VisualStateManager.xml diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/BindablePropertyConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/BindablePropertyConverter.cs index ae96ce39699..fc82551c3cf 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/BindablePropertyConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/BindablePropertyConverter.cs @@ -32,6 +32,7 @@ public FieldReference GetBindablePropertyFieldReference(string value, ModuleDefi var parts = value.Split('.'); if (parts.Length == 1) { var parent = node.Parent?.Parent as IElementNode ?? (node.Parent?.Parent as IListNode)?.Parent as IElementNode; + if ((node.Parent as ElementNode)?.XmlType.NamespaceUri == XamlParser.XFUri && ((node.Parent as ElementNode)?.XmlType.Name == "Setter" || (node.Parent as ElementNode)?.XmlType.Name == "PropertyCondition")) { if (parent.XmlType.NamespaceUri == XamlParser.XFUri && @@ -41,6 +42,15 @@ public FieldReference GetBindablePropertyFieldReference(string value, ModuleDefi typeName = (ttnode as ValueNode).Value as string; else if (ttnode is IElementNode) typeName = ((ttnode as IElementNode).CollectionItems.FirstOrDefault() as ValueNode)?.Value as string ?? ((ttnode as IElementNode).Properties [new XmlName("", "TypeName")] as ValueNode)?.Value as string; + } else if (parent.XmlType.NamespaceUri == XamlParser.XFUri && parent.XmlType.Name == "VisualState") { + var current = parent.Parent.Parent.Parent as IElementNode; + if (current.XmlType.NamespaceUri == XamlParser.XFUri && current.XmlType.Name == "Setter") { + // Parent will be a Style, and the type will be that Style's TargetType + typeName = + ((current?.Parent as IElementNode)?.Properties[new XmlName("", "TargetType")] as ValueNode)?.Value as string; + } else { + typeName = current.XmlType.Name; + } } } else if ((node.Parent as ElementNode)?.XmlType.NamespaceUri == XamlParser.XFUri && (node.Parent as ElementNode)?.XmlType.Name == "Trigger") typeName = ((node.Parent as ElementNode).Properties [new XmlName("", "TargetType")] as ValueNode).Value as string; diff --git a/Xamarin.Forms.Build.Tasks/CompiledValueProviders/SetterValueProvider.cs b/Xamarin.Forms.Build.Tasks/CompiledValueProviders/SetterValueProvider.cs index 86b7968502b..5ba26909510 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledValueProviders/SetterValueProvider.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledValueProviders/SetterValueProvider.cs @@ -19,6 +19,12 @@ public IEnumerable ProvideValue(VariableDefinitionReference vardefr ((IElementNode)node).CollectionItems.Count == 1) valueNode = ((IElementNode)node).CollectionItems[0]; + var bpNode = ((ValueNode)((IElementNode)node).Properties[new XmlName("", "Property")]); + var bpRef = (new BindablePropertyConverter()).GetBindablePropertyFieldReference((string)bpNode.Value, module, bpNode); + + if (SetterValueIsCollection(bpRef, module, node, context)) + yield break; + if (valueNode == null) throw new XamlParseException("Missing Value for Setter", (IXmlLineInfo)node); @@ -27,8 +33,6 @@ public IEnumerable ProvideValue(VariableDefinitionReference vardefr yield break; var value = ((string)((ValueNode)valueNode).Value); - var bpNode = ((ValueNode)((IElementNode)node).Properties[new XmlName("", "Property")]); - var bpRef = (new BindablePropertyConverter()).GetBindablePropertyFieldReference((string)bpNode.Value, module, bpNode); TypeReference _; var setValueRef = module.ImportReference(module.ImportReference(typeof(Setter)).GetProperty(p => p.Name == "Value", out _).SetMethod); @@ -43,5 +47,28 @@ public IEnumerable ProvideValue(VariableDefinitionReference vardefr //set the value yield return Instruction.Create(OpCodes.Callvirt, setValueRef); } + + static bool SetterValueIsCollection(FieldReference bindablePropertyReference, ModuleDefinition module, BaseNode node, ILContext context) + { + var items = (node as IElementNode)?.CollectionItems; + + if (items == null || items.Count <= 0) + return false; + + // Is this a generic type ? + var generic = bindablePropertyReference.GetBindablePropertyType(node, module) as GenericInstanceType; + + // With a single generic argument? + if (generic?.GenericArguments.Count != 1) + return false; + + // Is the generic argument assignable from this value? + var genericType = generic.GenericArguments[0]; + + if (!(items[0] is IElementNode firstItem)) + return false; + + return context.Variables[firstItem].VariableType.InheritsFromOrImplements(genericType); + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Build.Tasks/SetNamescopesAndRegisterNamesVisitor.cs b/Xamarin.Forms.Build.Tasks/SetNamescopesAndRegisterNamesVisitor.cs index 53bef7de187..92321cf0432 100644 --- a/Xamarin.Forms.Build.Tasks/SetNamescopesAndRegisterNamesVisitor.cs +++ b/Xamarin.Forms.Build.Tasks/SetNamescopesAndRegisterNamesVisitor.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Xml; @@ -39,7 +40,7 @@ public void Visit(ElementNode node, INode parentNode) { VariableDefinition namescopeVarDef; IList namesInNamescope; - if (parentNode == null || IsDataTemplate(node, parentNode) || IsStyle(node, parentNode)) { + if (parentNode == null || IsDataTemplate(node, parentNode) || IsStyle(node, parentNode) || IsVisualStateGroupList(node)) { namescopeVarDef = CreateNamescope(); namesInNamescope = new List(); } else { @@ -50,7 +51,7 @@ public void Visit(ElementNode node, INode parentNode) SetNameScope(node, namescopeVarDef); Context.Scopes[node] = new System.Tuple>(namescopeVarDef, namesInNamescope); } - + public void Visit(RootNode node, INode parentNode) { var namescopeVarDef = CreateNamescope(); @@ -81,6 +82,11 @@ static bool IsStyle(INode node, INode parentNode) return pnode != null && pnode.XmlType.Name == "Style"; } + static bool IsVisualStateGroupList(ElementNode node) + { + return node != null && node.XmlType.Name == "VisualStateGroup" && node.Parent is IListNode; + } + static bool IsXNameProperty(ValueNode node, INode parentNode) { var parentElement = parentNode as IElementNode; diff --git a/Xamarin.Forms.Build.Tasks/SetPropertiesVisitor.cs b/Xamarin.Forms.Build.Tasks/SetPropertiesVisitor.cs index 0af05666d6b..108e7b8256a 100644 --- a/Xamarin.Forms.Build.Tasks/SetPropertiesVisitor.cs +++ b/Xamarin.Forms.Build.Tasks/SetPropertiesVisitor.cs @@ -64,6 +64,8 @@ public void Visit(ValueNode node, INode parentNode) return; } + if (TrySetRuntimeName(propertyName, Context.Variables[(IElementNode)parentNode], node)) + return; if (skips.Contains(propertyName)) return; if (parentNode is IElementNode && ((IElementNode)parentNode).SkipProperties.Contains (propertyName)) @@ -1354,6 +1356,26 @@ public static TypeReference GetParameterType(ParameterDefinition param) loadTemplate.Body.Optimize(); } + + bool TrySetRuntimeName(XmlName propertyName, VariableDefinition variableDefinition, ValueNode node) + { + if (propertyName != XmlName.xName) + return false; + + var attributes = variableDefinition.VariableType.Resolve() + .CustomAttributes.Where(attribute => attribute.AttributeType.FullName == "Xamarin.Forms.Xaml.RuntimeNamePropertyAttribute").ToList(); + + if (!attributes.Any()) + return false; + + var runTimeName = attributes[0].ConstructorArguments[0].Value as string; + + if (string.IsNullOrEmpty(runTimeName)) + return false; + + Context.IL.Append(SetPropertyValue(variableDefinition, new XmlName("", runTimeName), node, Context, node)); + return true; + } } class VariableDefinitionReference diff --git a/Xamarin.Forms.Build.Tasks/XamlGenerator.cs b/Xamarin.Forms.Build.Tasks/XamlGenerator.cs index 8c5a4a162a8..93334d3b001 100644 --- a/Xamarin.Forms.Build.Tasks/XamlGenerator.cs +++ b/Xamarin.Forms.Build.Tasks/XamlGenerator.cs @@ -250,7 +250,7 @@ static IEnumerable GetCodeMemberFields(XmlNode root, XmlNamespa XmlNodeList names = root.SelectNodes( "//*[@" + xPrefix + ":Name" + - "][not(ancestor:: __f__:DataTemplate) and not(ancestor:: __f__:ControlTemplate) and not(ancestor:: __f__:Style)]", nsmgr); + "][not(ancestor:: __f__:DataTemplate) and not(ancestor:: __f__:ControlTemplate) and not(ancestor:: __f__:Style) and not(ancestor:: __f__:VisualStateManager.VisualStateGroups)]", nsmgr); foreach (XmlNode node in names) { // Don't take the root canvas if (node == root) diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/DefaultColorToggleTest.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/DefaultColorToggleTest.cs index 7cfbca897d3..0ebcaa3639d 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/DefaultColorToggleTest.cs +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/DefaultColorToggleTest.cs @@ -6,19 +6,19 @@ namespace Xamarin.Forms.Controls { [Preserve (AllMembers=true)] - [Issue (IssueTracker.None, 0, "Default colors toggle test", PlatformAffected.All)] + [Issue (IssueTracker.None, 9906753, "Default colors toggle test", PlatformAffected.All)] public class DefaultColorToggleTest : TabbedPage { public DefaultColorToggleTest() { Title = "Test Color Toggle Page"; + Children.Add(EntryPage()); Children.Add(PickerPage()); Children.Add(DatePickerPage()); Children.Add(TimePickerPage()); Children.Add(ButtonPage()); Children.Add(LabelPage()); - Children.Add(EntryPage()); Children.Add(PasswordPage()); Children.Add(SearchBarPage()); } diff --git a/Xamarin.Forms.Controls/CoreGallery.cs b/Xamarin.Forms.Controls/CoreGallery.cs index 4baf8d04779..06e024e25e9 100644 --- a/Xamarin.Forms.Controls/CoreGallery.cs +++ b/Xamarin.Forms.Controls/CoreGallery.cs @@ -7,6 +7,7 @@ using Xamarin.Forms.Internals; using Xamarin.Forms.PlatformConfiguration; using Xamarin.Forms.PlatformConfiguration.iOSSpecific; +using Xamarin.Forms.Controls.GalleryPages.VisualStateManagerGalleries; namespace Xamarin.Forms.Controls { @@ -244,6 +245,7 @@ public override string ToString() } List _pages = new List { + new GalleryPageFactory(() => new VisualStateManagerGallery(), "VisualStateManager Gallery"), new GalleryPageFactory(() => new FlowDirectionGalleryLandingPage(), "FlowDirection"), new GalleryPageFactory(() => new AutomationPropertiesGallery(), "Accessibility"), new GalleryPageFactory(() => new PlatformSpecificsGallery(), "Platform Specifics"), diff --git a/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ButtonDisabledStatesGallery.xaml b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ButtonDisabledStatesGallery.xaml new file mode 100644 index 00000000000..5107a35b5cb --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ButtonDisabledStatesGallery.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnIdiomExample.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnIdiomExample.xaml.cs new file mode 100644 index 00000000000..a6ad6478535 --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnIdiomExample.xaml.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Xamarin.Forms; +using Xamarin.Forms.Xaml; + +namespace Xamarin.Forms.Controls.GalleryPages.VisualStateManagerGalleries +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + public partial class OnIdiomExample : ContentPage + { + const string DefaultState = "Normal"; + string _currentState = DefaultState; + + public OnIdiomExample () + { + InitializeComponent (); + } + + void Button_OnClicked(object sender, EventArgs e) + { + if (_currentState == DefaultState) + { + _currentState = "CustomState"; + VisualStateManager.GoToState(DemoLabel, _currentState); + ToggleButton.Text = "Change Label to Normal state"; + + } + else + { + _currentState = DefaultState; + VisualStateManager.GoToState(DemoLabel, _currentState); + ToggleButton.Text = "Change Label to Custom state"; + } + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnPlatformExample.xaml b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnPlatformExample.xaml new file mode 100644 index 00000000000..e8471d76381 --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnPlatformExample.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnPlatformExample.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnPlatformExample.xaml.cs new file mode 100644 index 00000000000..6783d9ea796 --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/OnPlatformExample.xaml.cs @@ -0,0 +1,34 @@ +using System; +using Xamarin.Forms.Xaml; + +namespace Xamarin.Forms.Controls.GalleryPages.VisualStateManagerGalleries +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + public partial class OnPlatformExample : ContentPage + { + const string DefaultState = "Normal"; + string _currentState = DefaultState; + + public OnPlatformExample() + { + InitializeComponent(); + } + + void Button_OnClicked(object sender, EventArgs e) + { + if (_currentState == DefaultState) + { + _currentState = "CustomState"; + VisualStateManager.GoToState(DemoLabel, _currentState); + ToggleButton.Text = "Change Label to Normal state"; + + } + else + { + _currentState = DefaultState; + VisualStateManager.GoToState(DemoLabel, _currentState); + ToggleButton.Text = "Change Label to Custom state"; + } + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/PickerDisabledStatesGallery.xaml b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/PickerDisabledStatesGallery.xaml new file mode 100644 index 00000000000..fe488223902 --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/PickerDisabledStatesGallery.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ValidationExample.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ValidationExample.xaml.cs new file mode 100644 index 00000000000..aeec364a314 --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/ValidationExample.xaml.cs @@ -0,0 +1,65 @@ +using System; +using Xamarin.Forms.Xaml; + +namespace Xamarin.Forms.Controls.GalleryPages.VisualStateManagerGalleries +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + public partial class ValidationExample : ContentPage + { + public ValidationExample () + { + InitializeComponent (); + + Message.TextChanged += EditorTextChanged; + } + + void EditorTextChanged(object sender, TextChangedEventArgs e) + { + var count = e.NewTextValue.Length; + + CharacterCount.Text = $"{count} characters"; + CheckMessageValid(); + } + + bool IsValid() + { + var messageValid = CheckMessageValid(); + var subjectValid = CheckSubjectValid(); + + return messageValid && subjectValid; + } + + bool CheckMessageValid() + { + var isValid = Message.Text == null || Message.Text.Length <= 40; + var state = isValid ? "Normal" : "Invalid"; + + VisualStateManager.GoToState(Message, state); + VisualStateManager.GoToState(MessageError, state); + VisualStateManager.GoToState(CharacterCount, state); + VisualStateManager.GoToState(MessageLabel, state); + + return isValid; + } + + bool CheckSubjectValid() + { + var isValid = Subject.Text?.Length > 0; + var state = isValid ? "Normal" : "Invalid"; + + VisualStateManager.GoToState(Subject, state); + VisualStateManager.GoToState(SubjectError, state); + VisualStateManager.GoToState(SubjectLabel, state); + + return isValid; + } + + void Submit_OnClicked(object sender, EventArgs e) + { + if (IsValid()) + { + DisplayAlert("Submitted", "Thank you for submitting", "OK"); + } + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStateManagerGallery.cs b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStateManagerGallery.cs new file mode 100644 index 00000000000..aa5d6fca93e --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStateManagerGallery.cs @@ -0,0 +1,30 @@ +using System; + +namespace Xamarin.Forms.Controls.GalleryPages.VisualStateManagerGalleries +{ + public class VisualStateManagerGallery : ContentPage + { + static Button GalleryNav(string galleryName, Func gallery, INavigation nav) + { + var button = new Button { Text = $"{galleryName}" }; + button.Clicked += (sender, args) => { nav.PushAsync(gallery()); }; + return button; + } + + public VisualStateManagerGallery() + { + Content = new StackLayout + { + Children = + { + GalleryNav("Disabled States Gallery", () => new DisabledStatesGallery(), Navigation), + GalleryNav("OnPlatform Example", () => new OnPlatformExample(), Navigation), + GalleryNav("OnIdiom Example", () => new OnIdiomExample(), Navigation), + GalleryNav("Validation Example", () => new ValidationExample(), Navigation), + GalleryNav("Code (No XAML) Example", () => new CodeOnlyExample(), Navigation), + GalleryNav("VisualStates directly on Elements", () => new VisualStatesDirectlyOnElements(), Navigation) + } + }; + } + } +} diff --git a/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStatesDirectlyOnElements.xaml b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStatesDirectlyOnElements.xaml new file mode 100644 index 00000000000..dcfee218753 --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStatesDirectlyOnElements.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStatesDirectlyOnElements.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStatesDirectlyOnElements.xaml.cs new file mode 100644 index 00000000000..7093df0f60c --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/VisualStateManagerGalleries/VisualStatesDirectlyOnElements.xaml.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Xamarin.Forms; +using Xamarin.Forms.Xaml; + +namespace Xamarin.Forms.Controls.GalleryPages.VisualStateManagerGalleries +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + public partial class VisualStatesDirectlyOnElements : ContentPage + { + string _currentColorState = "Normal"; + string _currentAlignmentState = "LeftAligned"; + + public VisualStatesDirectlyOnElements () + { + InitializeComponent (); + } + + void ToggleValid_OnClicked(object sender, EventArgs e) + { + if (_currentColorState == "Normal") + { + _currentColorState = "Invalid"; + } + else + { + _currentColorState = "Normal"; + } + + CurrentState.Text = $"{_currentColorState}, {_currentAlignmentState}"; + VisualStateManager.GoToState(ALabel, _currentColorState); + VisualStateManager.GoToState(AButton, _currentColorState); + } + + void ToggleAlignment_OnClicked(object sender, EventArgs e) + { + if (_currentAlignmentState == "LeftAligned") + { + _currentAlignmentState = "Centered"; + } + else + { + _currentAlignmentState = "LeftAligned"; + } + + CurrentState.Text = $"{_currentColorState}, {_currentAlignmentState}"; + VisualStateManager.GoToState(ALabel, _currentAlignmentState); + VisualStateManager.GoToState(AButton, _currentAlignmentState); + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj b/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj index e523bf7f6e0..b35c1eb2d39 100644 --- a/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj +++ b/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj @@ -31,6 +31,27 @@ + + OnPlatformExample.xaml + + + MSBuild:UpdateDesignTimeXaml + + + MSBuild:UpdateDesignTimeXaml + + + MSBuild:UpdateDesignTimeXaml + + + MSBuild:UpdateDesignTimeXaml + + + MSBuild:UpdateDesignTimeXaml + + + MSBuild:UpdateDesignTimeXaml + MSBuild:UpdateDesignTimeXaml diff --git a/Xamarin.Forms.Core.UnitTests/VisualStateManagerTests.cs b/Xamarin.Forms.Core.UnitTests/VisualStateManagerTests.cs new file mode 100644 index 00000000000..5ca1ea6c4f4 --- /dev/null +++ b/Xamarin.Forms.Core.UnitTests/VisualStateManagerTests.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace Xamarin.Forms.Core.UnitTests +{ + [TestFixture] + public class VisualStateManagerTests + { + const string NormalStateName = "Normal"; + const string InvalidStateName = "Invalid"; + const string CommonStatesName = "CommonStates"; + + static VisualStateGroupList CreateTestStateGroups() + { + var stateGroups = new VisualStateGroupList(); + var visualStateGroup = new VisualStateGroup { Name = CommonStatesName }; + var normalState = new VisualState { Name = NormalStateName }; + var invalidState = new VisualState { Name = InvalidStateName }; + + visualStateGroup.States.Add(normalState); + visualStateGroup.States.Add(invalidState); + + stateGroups.Add(visualStateGroup); + + return stateGroups; + } + + static VisualStateGroupList CreateStateGroupsWithoutNormalState() + { + var stateGroups = new VisualStateGroupList(); + var visualStateGroup = new VisualStateGroup { Name = CommonStatesName }; + var invalidState = new VisualState { Name = InvalidStateName }; + + visualStateGroup.States.Add(invalidState); + + stateGroups.Add(visualStateGroup); + + return stateGroups; + } + + [Test] + public void InitialStateIsNormalIfAvailable() + { + var label1 = new Label(); + + VisualStateManager.SetVisualStateGroups(label1, CreateTestStateGroups()); + + var groups1 = VisualStateManager.GetVisualStateGroups(label1); + + Assert.That(groups1[0].CurrentState.Name, Is.EqualTo(NormalStateName)); + } + + [Test] + public void InitialStateIsNullIfNormalNotAvailable() + { + var label1 = new Label(); + + VisualStateManager.SetVisualStateGroups(label1, CreateStateGroupsWithoutNormalState()); + + var groups1 = VisualStateManager.GetVisualStateGroups(label1); + + Assert.Null(groups1[0].CurrentState); + } + + [Test] + public void VisualElementsStateGroupsAreDistinct() + { + var label1 = new Label(); + var label2 = new Label(); + + VisualStateManager.SetVisualStateGroups(label1, CreateTestStateGroups()); + VisualStateManager.SetVisualStateGroups(label2, CreateTestStateGroups()); + + var groups1 = VisualStateManager.GetVisualStateGroups(label1); + var groups2 = VisualStateManager.GetVisualStateGroups(label2); + + Assert.AreNotSame(groups1, groups2); + + Assert.That(groups1[0].CurrentState.Name, Is.EqualTo(NormalStateName)); + Assert.That(groups2[0].CurrentState.Name, Is.EqualTo(NormalStateName)); + + VisualStateManager.GoToState(label1, InvalidStateName); + + Assert.That(groups1[0].CurrentState.Name, Is.EqualTo(InvalidStateName)); + Assert.That(groups2[0].CurrentState.Name, Is.EqualTo(NormalStateName)); + } + + [Test] + public void VisualStateGroupsFromSettersAreDistinct() + { + var x = new Setter(); + x.Property = VisualStateManager.VisualStateGroupsProperty; + x.Value = CreateTestStateGroups(); + + var label1 = new Label(); + var label2 = new Label(); + + x.Apply(label1); + x.Apply(label2); + + var groups1 = VisualStateManager.GetVisualStateGroups(label1); + var groups2 = VisualStateManager.GetVisualStateGroups(label2); + + Assert.NotNull(groups1); + Assert.NotNull(groups2); + + Assert.AreNotSame(groups1, groups2); + + Assert.That(groups1[0].CurrentState.Name, Is.EqualTo(NormalStateName)); + Assert.That(groups2[0].CurrentState.Name, Is.EqualTo(NormalStateName)); + + VisualStateManager.GoToState(label1, InvalidStateName); + + Assert.That(groups1[0].CurrentState.Name, Is.EqualTo(InvalidStateName)); + Assert.That(groups2[0].CurrentState.Name, Is.EqualTo(NormalStateName)); + } + + [Test] + public void ElementsDoNotHaveVisualStateGroupsCollectionByDefault() + { + var label1 = new Label(); + Assert.False(label1.HasVisualStateGroups()); + } + + [Test] + public void StateNamesMustBeUniqueWithinGroup() + { + IList vsgs = CreateTestStateGroups(); + + var duplicate = new VisualState { Name = NormalStateName }; + + Assert.Throws(() => vsgs[0].States.Add(duplicate)); + } + + [Test] + public void StateNamesMustBeUniqueWithinGroupList() + { + IList vsgs = CreateTestStateGroups(); + + // Create and add a second VisualStateGroup + var secondGroup = new VisualStateGroup { Name = "Foo" }; + vsgs.Add(secondGroup); + + // Create a VisualState with the same name as one in another group in this list + var duplicate = new VisualState { Name = NormalStateName }; + + Assert.Throws(() => secondGroup.States.Add(duplicate)); + } + + [Test] + public void GroupNamesMustBeUniqueWithinGroupList() + { + IList vsgs = CreateTestStateGroups(); + var secondGroup = new VisualStateGroup { Name = CommonStatesName }; + + Assert.Throws(() => vsgs.Add(secondGroup)); + } + + [Test] + public void StateNamesInGroupMayNotBeNull() + { + IList vsgs = CreateTestStateGroups(); + + var nullStateName = new VisualState(); + + Assert.Throws(() => vsgs[0].States.Add(nullStateName)); + } + + [Test] + public void StateNamesInGroupMayNotBeEmpty() + { + IList vsgs = CreateTestStateGroups(); + + var emptyStateName = new VisualState{Name = ""}; + + Assert.Throws(() => vsgs[0].States.Add(emptyStateName)); + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj index c17f440c8f8..3c361f45658 100644 --- a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj +++ b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj @@ -153,6 +153,7 @@ + diff --git a/Xamarin.Forms.Core/BindablePropertyConverter.cs b/Xamarin.Forms.Core/BindablePropertyConverter.cs index 9f9664790ff..76bcf326a86 100644 --- a/Xamarin.Forms.Core/BindablePropertyConverter.cs +++ b/Xamarin.Forms.Core/BindablePropertyConverter.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; @@ -45,10 +47,13 @@ object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceP { var style = parent as Style; var triggerBase = parent as TriggerBase; + var visualState = parent as VisualState; if (style != null) type = style.TargetType; else if (triggerBase != null) type = triggerBase.TargetType; + else if (visualState != null) + type = FindTypeForVisualState(parentValuesProvider, lineinfo); } else if (parentValuesProvider.TargetObject is Trigger) type = (parentValuesProvider.TargetObject as Trigger).TargetType; @@ -102,5 +107,44 @@ BindableProperty ConvertFrom(Type type, string propertyName, IXmlLineInfo linein throw new XamlParseException($"The PropertyName of {type.Name}.{name} is not {propertyName}", lineinfo); return bp; } + + Type FindTypeForVisualState(IProvideParentValues parentValueProvider, IXmlLineInfo lineInfo) + { + var parents = parentValueProvider.ParentObjects.ToList(); + + // Skip 0; we would not be making this check if TargetObject were not a Setter + // Skip 1; we would not be making this check if the immediate parent were not a VisualState + + // VisualStates must be in a VisualStateGroup + if(!(parents[2] is VisualStateGroup)) { + throw new XamlParseException($"Expected {nameof(VisualStateGroup)} but found {parents[2]}.", lineInfo); + } + + var vsTarget = parents[3]; + + // Are these Visual States directly on a VisualElement? + if (vsTarget is VisualElement) + { + return vsTarget.GetType(); + } + + if (!(parents[3] is VisualStateGroupList)) + { + throw new XamlParseException($"Expected {nameof(VisualStateGroupList)} but found {parents[3]}.", lineInfo); + } + + if (!(parents[4] is Setter)) + { + throw new XamlParseException($"Expected {nameof(Setter)} but found {parents[4]}.", lineInfo); + } + + // These must be part of a Style; verify that + if (!(parents[5] is Style style)) + { + throw new XamlParseException($"Expected {nameof(Style)} but found {parents[5]}.", lineInfo); + } + + return style.TargetType; + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/Elevation.cs b/Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/Elevation.cs deleted file mode 100644 index af322319734..00000000000 --- a/Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/Elevation.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Xamarin.Forms.PlatformConfiguration.AndroidSpecific -{ - public static class Elevation - { - public static readonly BindableProperty ElevationProperty = - BindableProperty.Create("Elevation", typeof(float?), - typeof(Elevation)); - - public static float? GetElevation(VisualElement element) - { - return (float?)element.GetValue(ElevationProperty); - } - - public static void SetElevation(VisualElement element, float? value) - { - element.SetValue(ElevationProperty, value); - } - - public static float? GetElevation(this IPlatformElementConfiguration config) - { - return GetElevation(config.Element); - } - - public static IPlatformElementConfiguration SetElevation(this IPlatformElementConfiguration config, float? value) - { - SetElevation(config.Element, value); - return config; - } - } -} \ No newline at end of file diff --git a/Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/VisualElement.cs b/Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/VisualElement.cs new file mode 100644 index 00000000000..a8517949ac2 --- /dev/null +++ b/Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/VisualElement.cs @@ -0,0 +1,66 @@ +namespace Xamarin.Forms.PlatformConfiguration.AndroidSpecific +{ + using FormsElement = Forms.VisualElement; + + public static class VisualElement + { + #region Elevation + + public static readonly BindableProperty ElevationProperty = + BindableProperty.Create("Elevation", typeof(float?), + typeof(FormsElement)); + + public static float? GetElevation(FormsElement element) + { + return (float?)element.GetValue(ElevationProperty); + } + + public static void SetElevation(FormsElement element, float? value) + { + element.SetValue(ElevationProperty, value); + } + + public static float? GetElevation(this IPlatformElementConfiguration config) + { + return GetElevation(config.Element); + } + + public static IPlatformElementConfiguration SetElevation(this IPlatformElementConfiguration config, float? value) + { + SetElevation(config.Element, value); + return config; + } + + #endregion + + #region IsLegacyColorModeEnabled + + public static readonly BindableProperty IsLegacyColorModeEnabledProperty = + BindableProperty.CreateAttached("IsLegacyColorModeEnabled", typeof(bool), + typeof(FormsElement), true); + + public static bool GetIsLegacyColorModeEnabled(BindableObject element) + { + return (bool)element.GetValue(IsLegacyColorModeEnabledProperty); + } + + public static void SetIsLegacyColorModeEnabled(BindableObject element, bool value) + { + element.SetValue(IsLegacyColorModeEnabledProperty, value); + } + + public static bool GetIsLegacyColorModeEnabled(this IPlatformElementConfiguration config) + { + return (bool)config.Element.GetValue(IsLegacyColorModeEnabledProperty); + } + + public static IPlatformElementConfiguration SetIsLegacyColorModeEnabled( + this IPlatformElementConfiguration config, bool value) + { + config.Element.SetValue(IsLegacyColorModeEnabledProperty, value); + return config; + } + + #endregion + } +} diff --git a/Xamarin.Forms.Core/PlatformConfiguration/WindowsSpecific/VisualElement.cs b/Xamarin.Forms.Core/PlatformConfiguration/WindowsSpecific/VisualElement.cs new file mode 100644 index 00000000000..dcfc7b447db --- /dev/null +++ b/Xamarin.Forms.Core/PlatformConfiguration/WindowsSpecific/VisualElement.cs @@ -0,0 +1,37 @@ +namespace Xamarin.Forms.PlatformConfiguration.WindowsSpecific +{ + using FormsElement = Forms.VisualElement; + + public static class VisualElement + { + #region IsLegacyColorModeEnabled + + public static readonly BindableProperty IsLegacyColorModeEnabledProperty = + BindableProperty.CreateAttached("IsLegacyColorModeEnabled", typeof(bool), + typeof(FormsElement), true); + + public static bool GetIsLegacyColorModeEnabled(BindableObject element) + { + return (bool)element.GetValue(IsLegacyColorModeEnabledProperty); + } + + public static void SetIsLegacyColorModeEnabled(BindableObject element, bool value) + { + element.SetValue(IsLegacyColorModeEnabledProperty, value); + } + + public static bool GetIsLegacyColorModeEnabled(this IPlatformElementConfiguration config) + { + return (bool)config.Element.GetValue(IsLegacyColorModeEnabledProperty); + } + + public static IPlatformElementConfiguration SetIsLegacyColorModeEnabled( + this IPlatformElementConfiguration config, bool value) + { + config.Element.SetValue(IsLegacyColorModeEnabledProperty, value); + return config; + } + + #endregion + } +} diff --git a/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/Entry.cs b/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/Entry.cs index ddd1d635d35..61457d6b3a3 100644 --- a/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/Entry.cs +++ b/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/Entry.cs @@ -8,7 +8,7 @@ public static class Entry { public static readonly BindableProperty AdjustsFontSizeToFitWidthProperty = BindableProperty.Create("AdjustsFontSizeToFitWidth", typeof(bool), - typeof(Entry), false); + typeof(Entry), false); public static bool GetAdjustsFontSizeToFitWidth(BindableObject element) { @@ -25,19 +25,22 @@ public static bool AdjustsFontSizeToFitWidth(this IPlatformElementConfiguration< return GetAdjustsFontSizeToFitWidth(config.Element); } - public static IPlatformElementConfiguration SetAdjustsFontSizeToFitWidth(this IPlatformElementConfiguration config, bool value) + public static IPlatformElementConfiguration SetAdjustsFontSizeToFitWidth( + this IPlatformElementConfiguration config, bool value) { SetAdjustsFontSizeToFitWidth(config.Element, value); return config; } - public static IPlatformElementConfiguration EnableAdjustsFontSizeToFitWidth(this IPlatformElementConfiguration config) + public static IPlatformElementConfiguration EnableAdjustsFontSizeToFitWidth( + this IPlatformElementConfiguration config) { SetAdjustsFontSizeToFitWidth(config.Element, true); return config; } - public static IPlatformElementConfiguration DisableAdjustsFontSizeToFitWidth(this IPlatformElementConfiguration config) + public static IPlatformElementConfiguration DisableAdjustsFontSizeToFitWidth( + this IPlatformElementConfiguration config) { SetAdjustsFontSizeToFitWidth(config.Element, false); return config; diff --git a/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/VisualElement.cs b/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/VisualElement.cs index 1b3760e9060..862d500be7d 100644 --- a/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/VisualElement.cs +++ b/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/VisualElement.cs @@ -29,5 +29,35 @@ public static BlurEffectStyle GetBlurEffect(this IPlatformElementConfiguration config) + { + return (bool)config.Element.GetValue(IsLegacyColorModeEnabledProperty); + } + + public static IPlatformElementConfiguration SetIsLegacyColorModeEnabled( + this IPlatformElementConfiguration config, bool value) + { + config.Element.SetValue(IsLegacyColorModeEnabledProperty, value); + return config; + } + + #endregion } } diff --git a/Xamarin.Forms.Core/RuntimeNamePropertyAttribute.cs b/Xamarin.Forms.Core/RuntimeNamePropertyAttribute.cs new file mode 100644 index 00000000000..523190246d4 --- /dev/null +++ b/Xamarin.Forms.Core/RuntimeNamePropertyAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Xamarin.Forms.Xaml +{ + [AttributeUsage(AttributeTargets.Class)] + internal sealed class RuntimeNamePropertyAttribute : Attribute + { + public RuntimeNamePropertyAttribute(string name) + { + Name = name; + } + + public string Name { get; } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Core/Setter.cs b/Xamarin.Forms.Core/Setter.cs index 07bef633215..c6cdc1bf832 100644 --- a/Xamarin.Forms.Core/Setter.cs +++ b/Xamarin.Forms.Core/Setter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using System.Xml; @@ -57,7 +58,12 @@ internal void Apply(BindableObject target, bool fromStyle = false) else if (dynamicResource != null) target.SetDynamicResource(Property, dynamicResource.Key, fromStyle); else - target.SetValue(Property, Value, fromStyle); + { + if (Value is IList visualStateGroupCollection) + target.SetValue(Property, visualStateGroupCollection.Clone(), fromStyle); + else + target.SetValue(Property, Value, fromStyle); + } } internal void UnApply(BindableObject target, bool fromStyle = false) diff --git a/Xamarin.Forms.Core/VisualElement.cs b/Xamarin.Forms.Core/VisualElement.cs index b88bbd5b2b2..ea508c526d3 100644 --- a/Xamarin.Forms.Core/VisualElement.cs +++ b/Xamarin.Forms.Core/VisualElement.cs @@ -13,7 +13,8 @@ public partial class VisualElement : Element, IAnimatable, IVisualElementControl public static readonly BindableProperty InputTransparentProperty = BindableProperty.Create("InputTransparent", typeof(bool), typeof(VisualElement), default(bool)); - public static readonly BindableProperty IsEnabledProperty = BindableProperty.Create("IsEnabled", typeof(bool), typeof(VisualElement), true); + public static readonly BindableProperty IsEnabledProperty = BindableProperty.Create("IsEnabled", typeof(bool), + typeof(VisualElement), true, propertyChanged: OnIsEnabledPropertyChanged); static readonly BindablePropertyKey XPropertyKey = BindableProperty.CreateReadOnly("X", typeof(double), typeof(VisualElement), default(double)); @@ -88,8 +89,8 @@ public partial class VisualElement : Element, IAnimatable, IVisualElementControl public static readonly BindableProperty MinimumHeightRequestProperty = BindableProperty.Create("MinimumHeightRequest", typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); [EditorBrowsable(EditorBrowsableState.Never)] - public static readonly BindablePropertyKey IsFocusedPropertyKey = BindableProperty.CreateReadOnly("IsFocused", typeof(bool), typeof(VisualElement), default(bool), - propertyChanged: OnIsFocusedPropertyChanged); + public static readonly BindablePropertyKey IsFocusedPropertyKey = BindableProperty.CreateReadOnly("IsFocused", + typeof(bool), typeof(VisualElement), default(bool), propertyChanged: OnIsFocusedPropertyChanged); public static readonly BindableProperty IsFocusedProperty = IsFocusedPropertyKey.BindableProperty; @@ -801,9 +802,30 @@ static void FlowDirectionChanged(BindableObject bindable, object oldValue, objec self.NotifyFlowDirectionChanged(); } + static void OnIsEnabledPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + var element = (VisualElement)bindable; + + if (element == null) + { + return; + } + + var isEnabled = (bool)newValue; + + VisualStateManager.GoToState(element, isEnabled + ? VisualStateManager.CommonStates.Normal + : VisualStateManager.CommonStates.Disabled); + } + static void OnIsFocusedPropertyChanged(BindableObject bindable, object oldvalue, object newvalue) { - var element = bindable as VisualElement; + var element = (VisualElement)bindable; + + if (element == null) + { + return; + } var isFocused = (bool)newvalue; if (isFocused) @@ -814,6 +836,10 @@ static void OnIsFocusedPropertyChanged(BindableObject bindable, object oldvalue, { element.OnUnfocus(); } + + VisualStateManager.GoToState(element, isFocused + ? VisualStateManager.CommonStates.Normal + : VisualStateManager.CommonStates.Focused); } static void OnRequestChanged(BindableObject bindable, object oldvalue, object newvalue) diff --git a/Xamarin.Forms.Core/VisualStateManager.cs b/Xamarin.Forms.Core/VisualStateManager.cs new file mode 100644 index 00000000000..10f2f8e7b0f --- /dev/null +++ b/Xamarin.Forms.Core/VisualStateManager.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Xamarin.Forms.Xaml; + +namespace Xamarin.Forms +{ + public static class VisualStateManager + { + internal class CommonStates + { + public const string Normal = "Normal"; + public const string Disabled = "Disabled"; + public const string Focused = "Focused"; + } + + public static readonly BindableProperty VisualStateGroupsProperty = + BindableProperty.CreateAttached("VisualStateGroups", typeof(VisualStateGroupList), typeof(VisualElement), + defaultValue: null, propertyChanged: VisualStateGroupsPropertyChanged, + defaultValueCreator: bindable => new VisualStateGroupList()); + + static void VisualStateGroupsPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + GoToState((VisualElement)bindable, CommonStates.Normal); + } + + public static IList GetVisualStateGroups(VisualElement visualElement) + { + return (IList)visualElement.GetValue(VisualStateGroupsProperty); + } + + public static void SetVisualStateGroups(VisualElement visualElement, VisualStateGroupList value) + { + visualElement.SetValue(VisualStateGroupsProperty, value); + } + + public static bool GoToState(VisualElement visualElement, string name) + { + if (visualElement.GetIsDefault(VisualStateGroupsProperty)) + { + return false; + } + + var groups = (IList)visualElement.GetValue(VisualStateGroupsProperty); + + foreach (VisualStateGroup group in groups) + { + if (group.CurrentState?.Name == name) + { + // We're already in the target state; nothing else to do + return true; + } + + // See if this group contains the new state + var target = group.GetState(name); + if (target == null) + { + continue; + } + + // If we've got a new state to transition to, unapply the setters from the current state + if (group.CurrentState != null) + { + foreach (Setter setter in group.CurrentState.Setters) + { + setter.UnApply(visualElement); + } + } + + // Update the current state + group.CurrentState = target; + + // Apply the setters from the new state + foreach (Setter setter in target.Setters) + { + setter.Apply(visualElement); + } + + return true; + } + + return false; + } + + public static bool HasVisualStateGroups(this VisualElement element) + { + return !element.GetIsDefault(VisualStateGroupsProperty); + } + } + + public class VisualStateGroupList : IList + { + readonly IList _internalList; + + void Validate(IList groups) + { + // If we have 1 group, no need to worry about duplicate group names + if (groups.Count > 1) + { + if (groups.GroupBy(vsg => vsg.Name).Any(g => g.Count() > 1)) + { + throw new InvalidOperationException("VisualStateGroup Names must be unique"); + } + } + + // State names must be unique within this group list, so pull in all + // the states in all the groups, group them by name, and see if we have + // and duplicates + if (groups.SelectMany(group => group.States) + .GroupBy(state => state.Name) + .Any(g => g.Count() > 1)) + { + throw new InvalidOperationException("VisualState Names must be unique"); + } + } + + public VisualStateGroupList() + { + _internalList = new WatchAddList(Validate); + } + + void ValidateOnStatesChanged(object sender, EventArgs eventArgs) + { + Validate(_internalList); + } + + public IEnumerator GetEnumerator() + { + return _internalList.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_internalList).GetEnumerator(); + } + + public void Add(VisualStateGroup item) + { + _internalList.Add(item); + item.StatesChanged += ValidateOnStatesChanged; + } + + public void Clear() + { + foreach (var group in _internalList) + { + group.StatesChanged -= ValidateOnStatesChanged; + } + + _internalList.Clear(); + } + + public bool Contains(VisualStateGroup item) + { + return _internalList.Contains(item); + } + + public void CopyTo(VisualStateGroup[] array, int arrayIndex) + { + _internalList.CopyTo(array, arrayIndex); + } + + public bool Remove(VisualStateGroup item) + { + item.StatesChanged -= ValidateOnStatesChanged; + return _internalList.Remove(item); + } + + public int Count => _internalList.Count; + + public bool IsReadOnly => false; + + public int IndexOf(VisualStateGroup item) + { + return _internalList.IndexOf(item); + } + + public void Insert(int index, VisualStateGroup item) + { + item.StatesChanged += ValidateOnStatesChanged; + _internalList.Insert(index, item); + } + + public void RemoveAt(int index) + { + _internalList[index].StatesChanged -= ValidateOnStatesChanged; + _internalList.RemoveAt(index); + } + + public VisualStateGroup this[int index] + { + get => _internalList[index]; + set => _internalList[index] = value; + } + } + + [RuntimeNameProperty(nameof(Name))] + [ContentProperty(nameof(States))] + public sealed class VisualStateGroup + { + public VisualStateGroup() + { + States = new WatchAddList(OnStatesChanged); + } + + public Type TargetType { get; set; } + public string Name { get; set; } + public IList States { get; } + public VisualState CurrentState { get; internal set; } + + internal VisualState GetState(string name) + { + foreach (VisualState state in States) + { + if (string.CompareOrdinal(state.Name, name) == 0) + { + return state; + } + } + + return null; + } + + internal VisualStateGroup Clone() + { + var clone = new VisualStateGroup {TargetType = TargetType, Name = Name, CurrentState = CurrentState}; + foreach (VisualState state in States) + { + clone.States.Add(state.Clone()); + } + + return clone; + } + + internal event EventHandler StatesChanged; + + void OnStatesChanged(IList list) + { + if (list.Any(state => string.IsNullOrEmpty(state.Name))) + { + throw new InvalidOperationException("State names may not be null or empty"); + } + + StatesChanged?.Invoke(this, EventArgs.Empty); + } + } + + [RuntimeNameProperty(nameof(Name))] + public sealed class VisualState + { + public VisualState() + { + Setters = new ObservableCollection(); + } + + public string Name { get; set; } + public IList Setters { get;} + public Type TargetType { get; set; } + + internal VisualState Clone() + { + var clone = new VisualState { Name = Name, TargetType = TargetType }; + foreach (var setter in Setters) + { + clone.Setters.Add(setter); + } + + return clone; + } + } + + internal static class VisualStateGroupListExtensions + { + internal static IList Clone(this IList groups) + { + var actual = new VisualStateGroupList(); + foreach (var group in groups) + { + actual.Add(group.Clone()); + } + + return actual; + } + } + + internal class WatchAddList : IList + { + readonly Action> _onAdd; + readonly List _internalList; + + public WatchAddList(Action> onAdd) + { + _onAdd = onAdd; + _internalList = new List(); + } + + public IEnumerator GetEnumerator() + { + return _internalList.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_internalList).GetEnumerator(); + } + + public void Add(T item) + { + _internalList.Add(item); + _onAdd(_internalList); + } + + public void Clear() + { + _internalList.Clear(); + } + + public bool Contains(T item) + { + return _internalList.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _internalList.CopyTo(array, arrayIndex); + } + + public bool Remove(T item) + { + return _internalList.Remove(item); + } + + public int Count => _internalList.Count; + + public bool IsReadOnly => false; + + public int IndexOf(T item) + { + return _internalList.IndexOf(item); + } + + public void Insert(int index, T item) + { + _internalList.Insert(index, item); + _onAdd(_internalList); + } + + public void RemoveAt(int index) + { + _internalList.RemoveAt(index); + } + + public T this[int index] + { + get => _internalList[index]; + set => _internalList[index] = value; + } + } +} diff --git a/Xamarin.Forms.Platform.Android/AppCompat/ButtonRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/ButtonRenderer.cs index 071dbeab46a..8dca4749a95 100644 --- a/Xamarin.Forms.Platform.Android/AppCompat/ButtonRenderer.cs +++ b/Xamarin.Forms.Platform.Android/AppCompat/ButtonRenderer.cs @@ -8,6 +8,7 @@ using Android.Support.V7.Widget; using Android.Util; using Xamarin.Forms.Internals; +using Xamarin.Forms.PlatformConfiguration.AndroidSpecific; using GlobalResource = Android.Resource; using Object = Java.Lang.Object; using AView = Android.Views.View; @@ -111,9 +112,11 @@ protected override void OnElementChanged(ElementChangedEventArgs