Skip to content

Conversation

@jkoritzinsky
Copy link
Contributor

@jkoritzinsky jkoritzinsky commented Jun 10, 2018

  • What does the pull request do?
    • Add new API to allow users to create ExpressionObservers with LINQ Expressions instead of strings.
    • Move string-based binding paths up to Avalonia.Markup.
    • Fix attached property resolution to always resolve the correct property.
  • What is the current behavior?
    • ExpressionObservers can only have their binding paths specified as strings.
    • The AvaloniaPropertyAccessorPlugin tries to resolve an attached property name without the XAML context, so it just makes a guess of which property is the correct property.
  • What is the updated/expected behavior with this PR?
    • ExpressionObservers can be created from LINQ Expressions, ExpressionNodes, or strings.
    • Attached property bindings now use the AvaloniaPropertyAccessorNode class instead with an already-resolved AvaloniaProperty that is resolved via a callback (i.e. XAML type lookup) when built from a string.
  • How was the solution implemented (if it's not obvious)?
    • Stream Bindings in LINQ Expressions are pattern based. A method that is either an instance method or extension method without parameters named StreamBinding generates a stream binding node at that point in the expression.

Checklist:

If the pull request fixes issue(s) list them like this:

Fixes #1652.

@jkoritzinsky jkoritzinsky requested a review from a team June 10, 2018 03:05
@jkoritzinsky jkoritzinsky mentioned this pull request Jun 10, 2018
3 tasks
Copy link
Member

@grokys grokys left a 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:

  1. Indexers don't seem to be working. We should really be testing expressions in the unit tests to catch stuff like this
  2. Should we also allow Binding to be created with expressions? Binding provides a few extra features like binding to the DataContext or 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.
Copy link
Member

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>(
Copy link
Member

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

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?

Copy link
Contributor Author

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");
Copy link
Member

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
Copy link
Member

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?

Copy link
Contributor Author

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

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!

@grokys
Copy link
Member

grokys commented Jun 13, 2018

Also, does this PR depend on #1668? A lot of the changes seem to be similar.

@jkoritzinsky
Copy link
Contributor Author

#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.
@grokys
Copy link
Member

grokys commented Jun 26, 2018

This is going to conflict with #1694, which do you think it makes more sense to get merged first?

@jkoritzinsky
Copy link
Contributor Author

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.

@jkoritzinsky
Copy link
Contributor Author

@grokys I've made all the requested changes. Can you take another review pass when possible?

@jkoritzinsky jkoritzinsky assigned grokys and unassigned jkoritzinsky Jul 3, 2018
Copy link
Member

@grokys grokys left a 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;
Copy link
Member

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

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
Copy link
Member

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?

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 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>(
Copy link
Member

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.

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've moved them to after the constructors.

@jkoritzinsky
Copy link
Contributor Author

I have a few reasons why I like having the string support outside of ExpressionObserver.

  1. By having the string support in the Markup layer, the Reader class moves up as well (enabling Remove Sprache dependency #1668)
  2. If we have string support in the base layer as a constructor (as done currently), then we make string bindings the easiest path of use. I'd like to push advanced users that want to directly use ExpressionObserver to use type-safe binding expressions if possible.
  3. By making ExpressionNode and subclasses part of the API, we allow advanced users who want to customize the binding system (which is already supported on ExpressionObserver in master) to create custom binding nodes if they so wish.
  4. Everything in Avalonia.Data.Core is already an implementation detail namespace. I don't think exposing anything in that namespace publicly is necessarily bad.

@grokys
Copy link
Member

grokys commented Jul 10, 2018

Ok, yes those sound like good arguments for doing it this way 👍

@jkoritzinsky jkoritzinsky merged commit 3dd5e3a into AvaloniaUI:master Jul 10, 2018
@jkoritzinsky jkoritzinsky deleted the linq-expression-expressionobserver branch July 10, 2018 15:49
@grokys grokys added this to the 0.7.0 milestone Apr 3, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can not bind to attached property from xaml

2 participants