Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

VisualStateManager phase 1 #1405

Merged
merged 60 commits into from
Jan 5, 2018
Merged

VisualStateManager phase 1 #1405

merged 60 commits into from
Jan 5, 2018

Conversation

hartez
Copy link
Contributor

@hartez hartez commented Dec 14, 2017

Description of Change

Adds the capability to manage properties on Forms VisualElements via declarative states. States can be declared in code or in XAML.

The Xamarin.Forms.Controls\GalleryPages\VisualStateManagerGalleries folder contains example code for using this feature.

Also adds the Platform Specific IsLegacyColorModeEnabled to provide the option of disabling the partial state/color handling introduced in earlier versions of Forms.

Bugs Fixed

API Changes

Added:

VisualStateManager:

public static Collection<VisualStateGroup> GetVisualStateGroups(VisualElement visualElement)
public static void SetVisualStateGroups(VisualElement visualElement, Collection<VisualStateGroup> value)
public static bool GoToState(VisualElement visualElement, string name)

AndroidSpecific, iOSSpecific, WindowsSpecific:

public static bool GetIsLegacyColorModeEnabled(BindableObject element)
public static void SetIsLegacyColorModeEnabled(BindableObject element, bool value)
public static bool GetIsLegacyColorModeEnabled(this IPlatformElementConfiguration<Android, VisualElement> config)
public static IPlatformElementConfiguration<Android, VisualElement> SetIsLegacyColorModeEnabled( this IPlatformElementConfiguration<Android, VisualElement> config, bool value)

PR Checklist

  • Has tests (if omitted, state reason in description)
  • Rebased on top of master at time of PR
  • Changes adhere to coding standard
  • Consolidate commits as makes sense

@dnfclas
Copy link

dnfclas commented Dec 14, 2017

CLA assistant check
All CLA requirements met.

@hartez
Copy link
Contributor Author

hartez commented Dec 14, 2017

build

Copy link
Member

@StephaneDelcroix StephaneDelcroix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. api changes part of the description misses VSG
  2. I'd have to do a 2nd pass on this Xaml/XamlC changes
  3. if you do not define your VSM in Style, XamlG will probably try to generate multiple fields with the name "Normal". The x:Name handling and RuntimeNamePropertyAttribute should be handled in the visitor handling x:Name and namescopes (creading a new node, skipping the x:Name setting)

[OOPS, clicked submit to early. continuing in another review]

@@ -41,6 +42,16 @@ 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.Name == "VisualState") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check that the Namespace is XFUri to avoid mismatches

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the check.

@@ -43,5 +47,28 @@ public IEnumerable<Instruction> ProvideValue(VariableDefinitionReference vardefr
//set the value
yield return Instruction.Create(OpCodes.Callvirt, setValueRef);
}

bool IsSetterCollection(FieldReference bindablePropertyReference, ModuleDefinition module, BaseNode node, ILContext context)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method doesn't check if this is a Setter at all, I find the name misleading, and I don't understand what this does

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of the method is to check whether the setter is for a collection; renamed the method to SetterValueIsCollection to (hopefully) be more clear. It short-circuits ProvideValue if the Setter is for a collection, since the collection will be intialized later when the first item is added.

Currently I have collections in Setters (e.g. IList<VisualStateGroup>) being initialized when the first item is added to them (using EnsureSetterCollectionInitialized). I suspect that this should instead be happening in IValueProvider.ProvideValue/ICompiledValueProvider.ProvideValue, but if there's a simple way to do that, I haven't been able to see it yet.

Any suggestions?

}

static bool IsSetterCollection(VariableDefinition parent, string localName, INode node, ILContext context)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👮

return true;
}

static bool CanAddToAttachedProperty(FieldReference bpRef, bool attached, INode node, IXmlLineInfo iXmlLineInfo, ILContext context)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I /think/ this is covered already, but I have to check


Type FindTypeForVisualState(List<object> parentObjects)
{
var obj = parentObjects.Skip(3).Take(1).FirstOrDefault(); // Skip this Setter, VisualState, and VisualStateGroup
Copy link
Member

@StephaneDelcroix StephaneDelcroix Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should skip them one by one, and verify their types (content properties)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added type verification.

Copy link
Member

@StephaneDelcroix StephaneDelcroix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part 2 of the review:

  1. I only reviewed the Xaml and Core parts.
  2. it misses Xaml unit tests for the xaml changes

Minus the comments, it looks all good. I'd have to make time to review the Xaml and XamlC changes

namespace Xamarin.Forms
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class RuntimeNamePropertyAttribute : Attribute
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we reuse the System.Windows.Markup one now that we're on netstandard ? that'd make the life of people writing designers a bit easier.

if not, should be in X.F.Xaml ns

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the namespace.

target.SetValue(Property, Value, fromStyle);
{
if (Value is Collection<VisualStateGroup> visualStateGroupCollection)
target.SetValue(Property, visualStateGroupCollection.Clone(), fromStyle);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the cloning should happen in the BP itself

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only want the cloning to happen if the property is being set by a Setter; if it's being set directly on the element in Xaml or in code, we don't want to clone it. Is there some way to determine that a Setter is the source from within the BP?

}

public static readonly BindableProperty VisualStateGroupsProperty =
BindableProperty.CreateAttached("VisualStateGroups", typeof(Collection<VisualStateGroup>), typeof(VisualElement),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should expose ICollection<VSG> as a public api

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, what's the point of a collection vs a list ? the collection is cloned at the time it is applied, so we don't have to handle collectionEventArgs ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, using IList now.


static void VisualStateGroupsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is VisualElement visualElement)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a case this condition is false ? if it is, it means the user added a VSG to a non VE, and we'd better crash (like we do for all other BP) in direct casting.

at some point in the future, we'll have to modify BO so it validates types...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - now uses a direct cast.

if (bindable is VisualElement visualElement)
{
// Start out in the Normal state, if one is defined
GoToState(visualElement, CommonStates.Normal);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a extension method on VE could be handy, if GoToState is meant to be extensively used in user code


internal VisualStateGroup Clone()
{
var clone = new VisualStateGroup {TargetType = TargetType, Name = Name, CurrentState = CurrentState};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this the right place to set the CurrentState as "Normal" ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seems a right place as well to throw if the name is null (x:Name is mandatory on VSG)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setters aren't the only way to set the VisualStateGroups for an element. They can also be set directly in Xaml or from code. And we don't want to set CurrentState to normal directly on the property - we need it to be set with GoToState so the values for the Normal state are applied to the element.

}

public string Name { get; set; }
public Collection<Setter> Setters { get;}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you know...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. Fixed.

}

[RuntimeNameProperty("Name")]
public class VisualState
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no storyboard, no triggers, right ? if we don't have plans to support them in the future, the ContentProperty could be set to "Setters"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan is to support them in the future.

}
}

[RuntimeNameProperty("Name")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nameof(Name)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

}

[RuntimeNameProperty("Name")]
[ContentProperty("States")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nameof for both

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@StephaneDelcroix StephaneDelcroix added this to Ready in v3.1.0 via automation Dec 15, 2017
@StephaneDelcroix StephaneDelcroix moved this from Ready to In Review in v3.1.0 Dec 15, 2017
Name = name;
}

public string Name { get; private set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turn on r#. it'll tell you you don't have to say private set;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to turn if off when I'm working in your Xaml code, otherwise it formats everything correctly :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate that public BP

Copy link
Contributor Author

@hartez hartez Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was like that when I got here. ¯\(ツ)

static void OnIsFocusedPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var element = bindable as VisualElement;
if (!(bindable is VisualElement element))
Copy link
Member

@StephaneDelcroix StephaneDelcroix Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check for BO.GetIsDefault(). element will only be a VE, or null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying that GetIsDefault will be faster than the pattern matching for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using direct cast now.

public static class CollectionExtensions
{
public static object EnsureCollectionInitialized(this BindableObject bindable, BindableProperty property)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not up to the Xaml to create collection if not explicitly declared. if you want to add to a collection, that collection has to exists (can be lazily created in the property getter).
we're doing this at other places

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed for BindableObject - the collections are now initialized lazily when the value is retrieved.

var currentState1 = groups1[0].CurrentState;
var currentState2 = groups2[0].CurrentState;

Assert.That(currentState1.Name == "Normal");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either use

Assert.AreEqual(currentState1.Name, "Normal")

or

Assert.That(currentState1.Name, IS.EqualTo("Normal")) // actually my preferred form

or even

Assert.True(currentState1.Name == "Normal")

but try to avoid this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

{
Device.PlatformServices = new MockPlatformServices ();
Application.Current = new MockApplication ();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a TearDown, both of those are static, and could influence the test running next

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the TearDown.


[RuntimeNameProperty(nameof(Name))]
[ContentProperty(nameof(States))]
public class VisualStateGroup
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as the Clone is internal, this class should be sealed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

}

[RuntimeNameProperty(nameof(Name))]
public class VisualState
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seal as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


var isEnabled = (bool)newValue;

VisualStateManager.GoToState(element, isEnabled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't call the VisualStateManager directly. There should be a method such as "UpdateVisualState" that is called whenever the VisualElement thinks the visual state should be updated. This should be avirtual method so that developers can easily sub-class and override the visual state manager behaviour.

@StephaneDelcroix
Copy link
Member

StephaneDelcroix commented Dec 16, 2017 via email

@hartez
Copy link
Contributor Author

hartez commented Dec 17, 2017

if you do not define your VSM in Style, XamlG will probably try to generate multiple fields with the name "Normal".

Tested this, and you are absolutely correct.

The x:Name handling and RuntimeNamePropertyAttribute should be handled in the visitor handling x:Name and namescopes (creading a new node, skipping the x:Name setting)

So in the NameScopeVisitor, should I be creating a new namescope when I've got a VisualStateGroups element which is not part of a Style?

@hartez
Copy link
Contributor Author

hartez commented Dec 21, 2017

if you do not define your VSM in Style, XamlG will probably try to generate multiple fields with the name "Normal".
This should be fixed now.


internal VisualState GetState(string name)
{
foreach (VisualState state in States)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this method is called often, does it make sense to switch to for loop? Or is this premature optimisation?

Copy link
Contributor Author

@hartez hartez Dec 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think there'd be a significant difference, since the underlying implementation is using List. But just to be certain I wrote up some quick-and-dirty benchmarking tests to see if for would make any difference vs foreach here.

For the first batch, I created VisualStateGroupLists with 1 group, and the group had 2 states (which I think will be a pretty common scenario). I then had the test run through 100,000 iterations of moving randomly between the states (using the GoToState method). Between the two implementations (foreach and for), the test runs took the same amount of time on average.

For the second batch, I created an uglier scenario with VisualStateGroupLists with 10 groups, each group having 10 states. Running that over 100,000 iterations, the foreach actually ended up being slightly faster.

Looking at the tests in the profiler, the bulk of the time spent in the method (in either implementation) is the string comparison. The looping method doesn't end up making much difference.

So for the time being I think we'll stick with foreach for clarity. If we start running into performance problems with this later, we can revisit it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference isn't in performance but in allocation. foreach generates an iterator which adds some GC pressure. It probably doesn't matter much though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enumerator for List is a struct, so it's pretty low-impact.

@@ -117,7 +123,7 @@ void SetNameScope(ElementNode node, VariableDefinition ns)
void RegisterName(string str, VariableDefinition namescopeVarDef, IList<string> namesInNamescope, VariableDefinition element, INode node)
{
if (namesInNamescope.Contains(str))
throw new XamlParseException($"An element with the name \"{str}\" already exists in this NameScope", node as IXmlLineInfo);
throw new XamlParseException($"An element with the name \"{str}\" already exists in this NameScope 1", node as IXmlLineInfo);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@rmarinho
Copy link
Member

rmarinho commented Jan 2, 2018

failed test not related

Copy link
Member

@rmarinho rmarinho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good stuff, i wonder if we should make useLegacy color a Flag instead of PS ..

@@ -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)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can drop the 990 now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I like the number 9906753.

@rmarinho rmarinho merged commit 28948d7 into master Jan 5, 2018
v3.1.0 automation moved this from In Review to Done Jan 5, 2018
@hartez hartez deleted the vsm2 branch January 24, 2018 15:57
@samhouts samhouts added the API-change Heads-up to reviewers that this PR may contain an API change label Apr 3, 2018
@samhouts samhouts added this to the 3.0.0 milestone May 5, 2018
@samhouts samhouts removed this from Done in v3.1.0 May 7, 2018
@samhouts samhouts modified the milestones: 3.0.0, 2.5.0 Aug 23, 2019
@samhouts samhouts modified the milestones: 3.0.0, 2.5.0 Oct 29, 2019
mattleibow added a commit that referenced this pull request Jan 28, 2021
* Added the new Contacts.GetAllAsync
* Removed the ContactType enum as it was not quite ready (see more #1545)
* Added more members to the Contact type

Co-authored-by: sung-su.kim <sung-su.kim@samsung.com>
Co-authored-by: Matthew Leibowitz <mattleibow@live.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants