-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Allow LINQ Expressions for Binding Expressions #1667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow LINQ Expressions for Binding Expressions #1667
Conversation
…esn't generate IndexExpressions (they weren't in S.L.Expressions v1 so they aren't auto-genned).
…Expression paths and raw ExpressionNodes (now public) the primarily supported syntax.
grokys
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @jkoritzinsky I really love this! It's something I intended to do from the beginning but never got round to.
Just a few things:
- Indexers don't seem to be working. We should really be testing expressions in the unit tests to catch stuff like this
- Should we also allow
Bindingto be created with expressions?Bindingprovides a few extra features like binding to theDataContextor to other controls that could be really useful along with strong typing. This could be a separate PR though.
| /// <param name="node">The expression.</param> | ||
| /// <param name="description"> | ||
| /// A description of the expression. If null, <paramref name="expression"/> will be used. | ||
| /// A description of the expression. If null, <paramref name="node"/> will be used. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement doesn't seem to be true.
| _root = new WeakReference(root); | ||
| } | ||
|
|
||
| public static ExpressionObserver Create<T, U>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need an XML doc here.
| { | ||
| var source = new { Foo = new MethodBound() }; | ||
| var target = new ExpressionObserver(source, "Foo.A"); | ||
| var target = ExpressionObserver.Create(source, o => (Action)o.Foo.A); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm getting an exception thrown here:
System.InvalidOperationException: No coercion operator is defined between types 'Avalonia.LeakTests.ExpressionObserverTests+MethodBound' and 'System.Action'.
Not sure why this isn't showing up on CI?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leak Tests aren't running on CI currently. That's fixed as part of my Cake PR.
| { | ||
| var data = new { Foo = "foo" }; | ||
| var target = new ExpressionObserver(data, "Foo"); | ||
| var target = ExpressionObserverBuilder.Build(data, "Foo"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should all these tests be using ExpressionObserver.Create rather than ExpressionObserverBuilder? We should really be testing the ExpressionObserver itself here, and putting the ExpressionObserverBuilder stuff in the markup tests I think.
|
|
||
| namespace Avalonia.Markup.Parsers | ||
| { | ||
| public class ExpressionObserverBuilder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like this should probably be internal - is it public just for the ExpressionObserver tests? If we make them use ExpressionObserver.Create then could this be made internal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently it's public so XAML layer classes (MemberSelector and TreeDataTemplate) can use it just like they were using ExpressionObserver beforehand.
| { | ||
| var data = new { Foo = new [] { "foo", "bar" } }; | ||
| var target = new ExpressionObserver(data, "Foo[1]"); | ||
| var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I change this to be:
var target = ExpressionObserver.Create(data, x => x.Foo[1]);
I get:
Xunit.Sdk.EqualException: Assert.Equal() Failure
Expected: bar
Actual: {Error: System.MissingMemberException: Could not find CLR property 'Foo' on 'System.String[]'}
We really need to be testing ExpressionObserver with actual expressions here!
|
Also, does this PR depend on #1668? A lot of the changes seem to be similar. |
|
#1668 is dependent on some of the changes in this PR. After this gets merged in the changes over there will clean up a lot. |
…For tests that require using invalid members or are more tedious to test with expression trees, test them in Avalonia.Markup.UnitTests with ExpressionObserverBuilder.
…-expressionobserver
|
This is going to conflict with #1694, which do you think it makes more sense to get merged first? |
|
I'm on vacation for a week, so probably #1694. Alternatively, all that's left to do here is the XML doc comments, so if you want to put those in we could merge this on approval. |
|
@grokys I've made all the requested changes. Can you take another review pass when possible? |
grokys
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mainly a few nits here, but I do wonder about my point 2 above: by making ExpressionObserver only handle expressions and pushing string expressions into Binding, we're making ExpressionNode part of the public API when it feels like an implementation detail.
What would be the cons of making ExpressionObserver handle strings as well?
| { | ||
| class ExpressionTreeParser | ||
| { | ||
| private readonly bool enableDataValidation; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This applies to quite a few places, but elsewhere we use _field with an underscore for fields to follow the .net core coding guidelines.
| { | ||
| NotifyingBase test = new Class1 { Foo = "Test" }; | ||
|
|
||
| var target = ExpressionObserver.Create(test, o => ((Class1)o).Foo); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh nice! I was just about to check if this works!
|
|
||
| namespace Avalonia.Data.Core.Parsers | ||
| { | ||
| class ExpressionTreeParser |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be a separate class? Could this and ExpressionVisitorNodeBuilder be combined into a single class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could combine it, but I like having the visitor implementation in its own class. Keeps the messy logic of the visitor separated from the simpler logic in ExpressionTreeParser.
| /// <param name="description"> | ||
| /// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used. | ||
| /// </param> | ||
| public static ExpressionObserver Create<T, U>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually, methods come after ctors. I understand why you've done it like this (every ctor has an accompanying static creation method) but I'd rather stick to our standard ordering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved them to after the constructors.
|
I have a few reasons why I like having the string support outside of
|
…-expressionobserver
|
Ok, yes those sound like good arguments for doing it this way 👍 |
ExpressionObservers with LINQ Expressions instead of strings.ExpressionObservers can only have their binding paths specified as strings.AvaloniaPropertyAccessorPlugintries to resolve an attached property name without the XAML context, so it just makes a guess of which property is the correct property.ExpressionObservers can be created from LINQ Expressions,ExpressionNodes, or strings.AvaloniaPropertyAccessorNodeclass instead with an already-resolvedAvaloniaPropertythat is resolved via a callback (i.e. XAML type lookup) when built from a string.StreamBindinggenerates a stream binding node at that point in the expression.Checklist:
If the pull request fixes issue(s) list them like this:
Fixes #1652.