Skip to content

Keflon/FunctionZero.Maui.zBind

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FunctionZero.Maui.zBind

FunctionZero.Maui.zBind contains an alternative to Microsoft.Maui.Controls.Binding and allows DataBinding to an Expression

NuGet package here
Xamarin version here
Xamarin NuGet here

Contents

  1. Quickstart
  2. z:Bind
  3. The Great Escape, and other cautionary tales
  4. z:Function
  5. z:TapTrigger
  6. z:EdgeTrigger
  7. z:Latch
  8. Advanced Usage - Functions, aliases and operator-overloads

Quickstart

Setup

  1. Use the package manager to add the NugetPackage FunctionZero.Maui.zBind
  2. Add a namespace alias to your xaml, like this:
    xmlns:z="clr-namespace:FunctionZero.Maui.zBind.z;assembly=FunctionZero.Maui.zBind"

Getting Started ...

z:Bind - animate an Image based on 'Count' in your ViewModel ...

<Image Source="dotnet_bot.png"
       TranslationX="{z:Bind Sin(Count / 25.0) * 100}"
       TranslationY="{z:Bind Cos(Count / 15.0) * 100}"
       Rotation="{z:Bind Sin(Count / 5.0) * 20}"
/>

z:TapTrigger - modify a ViewModel property when tapped ...

<Button Text="Reset 'Count' in the ViewModel">
    <Button.Behaviors>
        <z:TapTrigger TapAction="{z:Function 'Count = 0'}"/>
    </Button.Behaviors>
</Button>

z:EdgeTrigger - when a Condition changes, execute a Rising or Falling Expression ...

<ContentPage.Behaviors>
    <z:EdgeTrigger 
        Condition="{z:Bind '(Value LT 0.5)', Source={x:Reference TheSlider}}" 
        Rising="{z:Function 'RotationY = 90', Source={x:Reference TheImage}}"
        Falling="{z:Function 'RotationY = 180', Source={x:Reference TheImage}}" />
</ContentPage.Behaviors>

z:Bind

Used to DataBind to an Expression. If the Expression is just a property-name then it is equivalent to Binding
A z:Bind automatically updates if any referenced properties notify they have changed

Usage

TargetProperty = { z:Bind <some expression> [, Source=<some source>] }

Source

Just like a standard Binding, the data-source is the current BindingContext, unless Source is specified.
If this data-source supports INotifyPropertyChanged, changes will be tracked.

z:Bind Expressions

Sample Expression Source Notes
{z:Bind Count} BindingContext Bind to Count, same as Binding
{z:Bind Count * 2} BindingContext Bind to an expression that yields Count * 2
{z:Bind '(Delta.X &lt; 0.2) &amp;&amp; (Delta.X &gt; -0.2)' } BindingContext True if (Delta.X < 0.2) && (Delta.X > -0.2)
{z:Bind '(Delta.X LT 0.2) AND (Delta.X GT -0.2)' } BindingContext As above, using aliases instead of escape-sequences
{z:Bind (Count * 2) LT 10} BindingContext True if (Count * 2) < 10
{z:Bind Sin(Count / 25.0)} BindingContext Calls a function (see below)
{z:Bind 'Value LT 0.2', Source={x:Reference MySlider}} An Element called MySlider True if MySlider.Value < 0.2

z:Bind Examples

Hide a StackLayout if Things.Count != 0 ...

<StackLayout IsVisible="{z:Bind '(Things.Count != 0)'}" ... > ...

Animate an Image based on a Count property ...

<Image TranslationX="{z:Bind Sin(Count / 25.0) * 100}" 
       TranslationY="{ ... }" />

Scale a Label based on the Value of a Slider Control ...

<Label Scale="{z:Bind Value * 3 + 0.1, Source={x:Reference TheSlider}}" > ...

The Great Escape, and other cautionary tales

As with xaml, string literals can be enclosed within 'single quotes' or "double quotes" with appropriate use of xml escape-sequences.

Commas

If your expression string has commas in it, you must hide them from the xaml parser, otherwise z:Bind etc. will be given an incomplete string and things won't work as expected.

You can do this by enclosing the string inside quotes, like this:

Something="{z:Bind 'SomeFunction(param1, param2)'}"

or this

Something="{z:Bind \"SomeFunction(param1, param2)\"}"

and so on

Strings

If your expression string has string literals in it, you must 'escape' them, otherwise z:Bind etc. will be given an incorrect string and things won't work as expected.
For example:

"{z:Bind Status == \'Administrator\'}"

or this

"{z:Bind Status == '\'Administrator\''}"

and so on

Long form

If your expression is getting bogged down in escape-sequences and commas and quotes, or if that's just the way you roll, you can use the long-form of expressing a z:Bind expression:

<Label Text="{z:Bind '\'Score: \'+ Count + \' points\''}"

becomes

<Label>
    <Label.Text>
        <z:Bind>
            'Score: '+ Count + ' points'
        </z:Bind>
    </Label.Text>
</Label>

Casting

Functions and assignments will try to cast to the correct type, so Sin(someFloat) will work despite Sin requiring a double
If you want to explicitly cast you can do so like this: Sin((Double)someFloat)
See ExpressionParserZero.Operands.OperandType for details.

Short-circuit

Just like c#, the underlying expression parser supports short-circuit, so expressions like (thing != null) AND (thing.part == 5) will work even if thing is null

Errors

Error reporting is quite good, so check the debug output if things aren't working as expected.

Aliases supported to simplify xaml

Alias Operator
NOT !
MOD %
LT <
GT >
GTE >=
LTE <=
BAND &
XOR ^
BOR |
AND &&
OR ||

Supported value types

All csharp value-types and their nullable variants

Supported reference types

string, object

z:Function

z:Function is similar to, and has the same syntax as z:Bind
It is different because it does not self-evaluate and instead relies on its consumer to ask it to evaluate
z:Function can be consumed by TapTrigger, EdgeTrigger and Latch

Usage

TargetProperty = "{ z:Function <some expression>   [, Source=<some source>] }"

Where <some expression> is any valid expression, making use of properties on the BindingSource
Typically you would put either an assignment into a z:Function

TapAction="{z:Function 'Things.TapCount = Things.TapCount + 1'}"

or a function call

TapAction="{z:Function 'OpenUrl(Customer.LoginUrl, Customer.UserName)'}"

or both ...

TapAction="{z:Function 'Things.TapCount = Things.TapCount + 1, OpenUrl(Customer.LoginUrl)'}"

z:TapTrigger

z:TapTrigger is a Behaviour that evaluates a z:Function when its host is tapped

Usage

<z:TapTrigger TapAction="{z:Function '<some expression>'}"/>

For example:

<Button Text="Reset 'Count' using a TapTrigger"
    <Button.Behaviors>
        <z:TapTrigger TapAction="{z:Function 'Count = 0'}"/>
    </Button.Behaviors>
</Button>

z:EdgeTrigger

z:EdgeTrigger is a Behaviour that evaluates a 'Rising' or 'Falling' z:Function when a Condition changes

Usage

<ContentPage.Behaviors>
    <z:EdgeTrigger 
        Condition="<some z:Bind>" 
        Rising="<some z:Function>"
        Falling="<some z:Function>" />
</ContentPage.Behaviors>

For example, to show or hide UI based on a player data found in the ViewModel

<ContentPage.Behaviors>
    <z:EdgeTrigger 
        Condition="{z:Bind '(Player.Score GTE 50) AND (Player.IsVerified == true)'}" 
        Rising="{z:Function 'IsVisible=true', Source={x:Reference TheAchevementUi}}"
        Falling="{z:Function 'IsVisible=false', Source={x:Reference TheAchevementUi}}" />
</ContentPage.Behaviors>

z:Latch

Coming spoon *

Usage

Coming spoon *

Advanced Usage - Functions, aliases and operator-overloads

z:Bind uses FunctionZero.ExpressionParserZero to do the heavy lifting, so take a look at the documentation if you want to take a deeper dive. Here is a taster ...

Functions

Sin, Cos and Tan are registered by default, as are the aliases listed above.

<Label TranslationX="{z:Bind Sin(Count / 25.0) * 100.0}" ...

Suppose you wanted a new function to to do a linear interpolation between two values, like this:
(Spoiler: Lerp is also pre-registered)

float Lerp(float a, float b, float t)
{
  return a + t * (b - a);
}

For use like this:

<Label Rotation={z:Bind Lerp(0, 360, rotationPercent / 100.0)} ...

First you will need a reference to the default ExpressionParser

var ep = ExpressionParserFactory.GetExpressionParser();

Then register a function that takes 3 parameters:

ep.RegisterFunction("Lerp", DoLerp, 3);

Finally write the DoLerp method referenced above.

private static void DoLerp(Stack<IOperand> stack, IBackingStore backingStore, long paramCount)
{
    // Pop the correct number of parameters from the operands stack, ** in reverse order **
    // If an operand is a variable, it is resolved from the backing store provided
    IOperand third = OperatorActions.PopAndResolve(operands, backingStore);
    IOperand second = OperatorActions.PopAndResolve(operands, backingStore);
    IOperand first = OperatorActions.PopAndResolve(operands, backingStore);

    float a = Convert.ToSingle(first.GetValue());
    float b = Convert.ToSingle(second.GetValue());
    float t = Convert.ToSingle(third.GetValue());

    // The result is of type float
    float result = a + t * (b - a);

    // Push the result back onto the operand stack
    stack.Push(new Operand(-1, OperandType.Float, result));
}

Aliases

Get a reference to the default ExpressionParser:

var ep = ExpressionParserFactory.GetExpressionParser();

Then register a new operator and use the existing matrix for &&

(See the ExpressionParserZero source and documentation for more details)

ep.RegisterOperator("AND", 4, LogicalAndMatrix.Create());

Overloads

Suppose you want to add a long to a string

Get a reference to the default ExpressionParser:

var ep = ExpressionParserFactory.GetExpressionParser();

Then simply register the overload like this

// Overload that will allow a long to be appended to a string
// To add a string to a long you'll need to add another overload
ep.RegisterOverload("+", OperandType.String, OperandType.Long, 
    (left, right) => new Operand(OperandType.String, (string)left.GetValue() + ((long)right.GetValue()).ToString()));

and to add a string to a long:

// Overload that will allow a string to be appended to a long
// To add a long to a string you'll need to add another overload
ep.RegisterOverload("+", OperandType.Long, OperandType.String, 
    (left, right) => new Operand(OperandType.String, (long)left.GetValue() + ((string)right.GetValue()).ToString()));

Putting the above into action, you can then start to really have some fun

<Label 
    Text="{z:Bind '\'Player 1 score \' + playerOne.Score + \'points\''}
    Rotation="{z:Bind 'Lerp(0, 360, rotationPercent / 100.0)'"}
/>

* There is no spoon.

About

DataBind to Expressions

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

No packages published

Languages