Home

Awesome

<p align="center"> <br> <img src="Assets/logo.svg" alt="EBind" height="150"><br> <br> <a href="https://github.com/SIDOVSKY/EBind/actions/workflows/ci.yml"> <img src="https://github.com/SIDOVSKY/EBind/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"> </a> <a href="https://www.nuget.org/packages/EBind.NET/"> <img src="https://img.shields.io/nuget/v/EBind.NET?logo=nuget" alt="nuget: EBind"> </a> <a href="https://www.nuget.org/packages/EBind.LinkerIncludeGenerator/"> <img src="https://img.shields.io/nuget/v/EBind.LinkerIncludeGenerator?label=nuget%20%7C%20EBind.LinkerIncludeGenerator&logo=nuget" alt="nuget: EBind.LinkerIncludeGenerator"> </a> <a href="https://codecov.io/gh/SIDOVSKY/EBind"> <img src="https://img.shields.io/codecov/c/gh/sidovsky/ebind?label=coverage%20%28strict%29&logo=codecov" alt="Codecov"> </a> </p> <!-- snippet: Bind -->

<a id='snippet-bind'></a>

var binding = new EBinding
{
    () => view.Text == vm.Text,
    () => view.Text == vm.Description.Title.Text,
    () => view.Text == (vm.Text ?? vm.FallbackText),
    () => view.Visible == !vm.TextVisible,
    () => view.Visible == (vm.TextVisible == vm.ImageVisible),
    () => view.Visible == (vm.TextVisible && vm.ImageVisible),
    () => view.Visible == (vm.TextVisible || vm.ImageVisible),
    () => view.FullName == $"{vm.FirstName} {vm.LastName}",
    () => view.FullName == vm.FirstName + " " + vm.LastName,
    () => view.Timestamp == Converter.DateTimeToEpoch(vm.DateTime),

    BindFlag.TwoWay,
    () => view.Text == vm.Text,
    () => view.SliderValueFloat == vm.AgeInt,

    BindFlag.OneTime | BindFlag.NoInitialTrigger,
    () => view.ShowImage(vm.ImageUri),
    () => Dispatcher.RunOnUiThread(() => view.ShowImage(vm.ImageUri)),

    BindFlag.OneTime,
    (view, nameof(view.Click), vm.OnViewClicked),
    (view, nameof(view.TextEditedEventHandler), () => vm.OnViewTextEdited(view.Text)),
    (view, "CustomEventForGesture", vm.OnViewClicked),

    (view, nameof(view.Click), vm.ViewClickCommand),
    (view, nameof(view.Click), () => vm.ViewClickCommand.TryExecute(view.Text)),

    new UserExtensions.CustomEBinding(),
};

binding.Dispose();

<sup><a href='/EBind.Tests/Snippets/Sample.cs#L64-L98' title='Snippet source file'>snippet source</a> | <a href='#snippet-bind' title='Start of snippet'>anchor</a></sup>

<!-- endSnippet -->

Three types of bindings here

Key points

Configuration and Extensibility

Pre-Configured Triggers

<details><summary>Xamarin.Android</summary>
View/ControlEventProperty
Android.Views.ViewClick
LongClick
FocusChange
Android.Widget.AdapterViewItemSelectedSelectedItemPosition
ItemClick
ItemLongClick
Android.Widget.CalendarViewDateChangeDate
Android.Widget.CompoundButtonCheckedChangeChecked
Android.Widget.DatePickerDateChangedDateTime
Android.Widget.NumberPickerValueChangedValue
Android.Widget.RatingBarRatingBarChangeRating
Android.Widget.SearchViewQueryTextChangeQuery
Android.Widget.SeekBarProgressChangedProgress
Android.Widget.TextViewTextChangedText
Android.Widget.TimePickerTimeChangedHour (API 23+)<br>Minute (API 23+)<br>CurrentHour<br>CurrentMinute
</details> <details><summary>Xamarin.iOS</summary>
View/ControlEventProperty
UIBarButtonItemClicked
UIControlTouchUpInside
ValueChanged
UIDatePickerValueChangedDate
UIPageControlValueChangedCurrentPage
UISearchBarTextChangedText
UISegmentedControlValueChangedSelectedSegment
UISliderValueChangedValue
UIStepperValueChangedValue
UISwitchValueChangedOn
UITabBarControllerViewControllerSelectedSelectedIndex
UITextFieldEditingChangedText
EditingDidBegin
EditingDidEnd
UITextViewChangedText
</details> <details><summary>Xamarin.Forms</summary>

All views implement INotifyPropertyChanged so the main trigger is invoked for every bindable property change.

</details>

Member Triggers

In Configuration you may specify how to subscribe and unsubscribe for signals of property and field updates that are not tracked out of the box.
There are overloads for the property/field update handler as System.EventHandler, System.EventHandler<TEventArgs> or any other class:

<!-- snippet: Configure-Member-Trigger -->

<a id='snippet-configure-member-trigger'></a>

EBinding.DefaultConfiguration.ConfigureTrigger<View, string>(
    v => v.Text,
    (v, h) => v.TextEditedEventHandler += h,
    (v, h) => v.TextEditedEventHandler -= h);

EBinding.DefaultConfiguration.ConfigureTrigger<View, View.TextEditedEventArgs, string>(
    v => v.Text,
    (v, h) => v.TextEditedGenericEventHandler += h,
    (v, h) => v.TextEditedGenericEventHandler -= h);

EBinding.DefaultConfiguration.ConfigureTrigger<View, Action<string>, string>(
    v => v.Text,
    trigger => _ => trigger(),
    (v, h) => v.TextEditedCustomEventHandler += h,
    (v, h) => v.TextEditedCustomEventHandler -= h);

<sup><a href='/EBind.Tests/Snippets/Sample.cs#L17-L33' title='Snippet source file'>snippet source</a> | <a href='#snippet-configure-member-trigger' title='Start of snippet'>anchor</a></sup>

<!-- endSnippet -->

Event Triggers

You may configure your own triggers for event bindings under custom identifiers even if they represent a Pub-Sub pattern differently from C# events (IObservable, Add/RemoveListener methods).

<!-- snippet: Configure-Custom-Event-Trigger -->

<a id='snippet-configure-custom-event-trigger'></a>

EBinding.DefaultConfiguration.ConfigureTrigger<View, View.GestureRecognizer>(
    "CustomEventForGesture",
    trigger => new View.GestureRecognizer(trigger),
    (v, h) => v.AddGestureRecognizer(h),
    (v, h) => v.RemoveGestureRecognizer(h));

<sup><a href='/EBind.Tests/Snippets/Sample.cs#L53-L59' title='Snippet source file'>snippet source</a> | <a href='#snippet-configure-custom-event-trigger' title='Start of snippet'>anchor</a></sup>

<!-- endSnippet -->

The same identifier (event name) can be used with multiple classes.
For example on Xamarin.iOS a pre-defined Tap event can be used with both UIControl and UIView:

using EBind;
using static EBind.Platform.Configuration.ExtraEventNames;

var binding = new EBinding
{
    (uiButton, Tap, OnButtonClick),
    (uiImageView, Tap, OnImageClick),
};

Configuration of C# events is not required – they can be found by the name: nameof(obj.Event).
But it's recommended to specify subscription and unsubscription delegates to improve cold-start performance and avoid linker errors.

<!-- snippet: Configure-Event-Trigger -->

<a id='snippet-configure-event-trigger'></a>

EBinding.DefaultConfiguration.ConfigureTrigger<View>(
    nameof(View.TextEditedEventHandler),
    (v, h) => v.TextEditedEventHandler += h,
    (v, h) => v.TextEditedEventHandler -= h);

EBinding.DefaultConfiguration.ConfigureTrigger<View, View.TextEditedEventArgs>(
    nameof(View.TextEditedGenericEventHandler),
    (v, h) => v.TextEditedGenericEventHandler += h,
    (v, h) => v.TextEditedGenericEventHandler -= h);

EBinding.DefaultConfiguration.ConfigureTrigger<View, Action<string>>(
    nameof(View.TextEditedCustomEventHandler),
    trigger => _ => trigger(),
    (v, h) => v.TextEditedCustomEventHandler += h,
    (v, h) => v.TextEditedCustomEventHandler -= h);

<sup><a href='/EBind.Tests/Snippets/Sample.cs#L35-L51' title='Snippet source file'>snippet source</a> | <a href='#snippet-configure-event-trigger' title='Start of snippet'>anchor</a></sup>

<!-- endSnippet -->

Event triggers configured for a class are available to all its children unless they have their own variation defined.

Triggers may be overwritten by onward setups including the pre-defined ones.

Binding Dispatcher

Sometimes ViewModel properties are set from the background thread that leads to data-binding view update triggering in the non-ui thread.

Establishing a proper thread switching in place is not possible for some code. Also, the risk of missing a thread is not acceptable or not worth caring about for some projects, and delegation of this problem is a good deal.
In this case, data-binding as a point of integration may be a good place to switch threads between the ui-threaded View and the thread-insensitive ViewModel layers.

Dispatchers which force the UI thread are set up in Configuration:

EBinding.DefaultConfiguration.AssignmentDispatchDelegate = Dispatcher.RunOnUiThread;
EBinding.DefaultConfiguration.ActionDispatchDelegate = Dispatcher.RunOnUiThread;

For Xamarin platform Xamarin.Essentials.MainThread.BeginInvokeOnMainThread will be the best option most of the time.

Custom Bindings

EBinding collection initializer accepts any form of IEBinding.
By implementing this simple interface you can create your own binding types and adapters for data-binding components of other systems (e.g. Rx.Net), use them in the same collection initializer, and keep all bindings in one place.

Custom binding creation can be encapsulated inside an extension method Add(this EBinding, ...) so that the binding inputs are accepted in the collection initializer (supported since C# 6).

After a custom binding is created, it must be added to the collection via EBinding.Add(IEBinding) so that it can be disposed along with other bindings.

<!-- snippet: Custom-EBinding -->

<a id='snippet-custom-ebinding'></a>

public class CustomEBinding : IEBinding
{
    public void Dispose()
    {
    }
}

public static void Add(this EBinding bindingHolder, CustomEBinding binding,
    [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
    try
    {
        if (binding == null)
            throw new ArgumentNullException(nameof(binding));

        if (bindingHolder.CurrentFlag == BindFlag.OneTime)
            throw new NotSupportedException();

        bindingHolder.Add(binding);
    }
    catch (Exception ex)
    {
        throw new Exception(
            $"Error in entry {bindingHolder.Count} at line #{sourceLineNumber}", ex);
    }
}

<sup><a href='/EBind.Tests/Snippets/Sample.cs#L167-L194' title='Snippet source file'>snippet source</a> | <a href='#snippet-custom-ebinding' title='Start of snippet'>anchor</a></sup>

<!-- endSnippet -->

It's recommended to decorate exceptions during binding creation with additional information about the binding position (EBinding.Count) and its line number (CallerLineNumberAttribute) as debuggers do not highlight that.

<details><summary>Exception screenshot</summary>

Location info in exception

</details>

Benchmarks

<details><summary>Environment</summary>
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.572 (2004/?/20H1)
AMD Ryzen 5 1600, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.202
  [Host]     : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT
  DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT
</details> <details open> <summary>Comparison: Trigger <sup><a id='benchmark-comparison-trigger' href='#benchmark-comparison-trigger' title='Anchor'>🔗</a></sup></summary>
MethodMeanErrorStdDevRatioGen 0Allocated
EBind68.71 ns0.422 ns0.395 ns1.000.0305128 B
Mugen206.29 ns2.305 ns2.156 ns3.000.017272 B
XamarinFormsCompiled351.32 ns1.516 ns1.418 ns5.110.0267112 B
MvvmLight1,070.28 ns3.529 ns3.128 ns15.580.1259528 B
MvvmCross1,368.35 ns8.320 ns7.376 ns19.920.1678704 B
ReactiveUI3,054.42 ns30.750 ns28.763 ns44.450.1831777 B
PraeclarumBind150,506.37 ns372.197 ns329.943 ns2,190.780.97664415 B

<sup>sources</sup>

</details> <details open> <summary>Comparison: Creation, One-Way <sup><a id='benchmark-comparison-creation-one-way' href='#benchmark-comparison-creation-one-way' title='Anchor'>🔗</a></sup></summary>
MethodMeanErrorStdDevRatioGen 0Allocated
XamarinFormsCompiled2.955 us0.0117 us0.0110 us0.740.1831768 B
EBind3.994 us0.0240 us0.0213 us1.000.52642232 B
MvvmLight7.118 us0.0506 us0.0474 us1.780.59512504 B
Mugen8.075 us0.1554 us0.1790 us2.010.43492014 B
MvvmCross9.263 us0.0583 us0.0546 us2.320.91553873 B
ReactiveUI49.217 us0.8883 us0.8309 us12.343.540014953 B
PraeclarumBind300.919 us0.6047 us0.5657 us75.322.441410634 B

<sup>sources</sup>

</details> <details> <summary>Comparison: Creation, Two-Way <sup><a id='benchmark-comparison-creation-two-way' href='#benchmark-comparison-creation-two-way' title='Anchor'>🔗</a></sup></summary>
MethodMeanErrorStdDevRatioGen 0Allocated
XamarinFormsCompiled3.058 us0.0167 us0.0156 us0.620.1831768 B
EBind4.961 us0.0309 us0.0289 us1.000.75533184 B
Mugen7.669 us0.0444 us0.0370 us1.550.48832064 B
MvvmLight8.812 us0.0465 us0.0435 us1.780.77823288 B
MvvmCross13.041 us0.0972 us0.0909 us2.631.05294449 B
ReactiveUI80.447 us0.4630 us0.4331 us16.226.469727237 B
PraeclarumBind572.390 us3.0060 us2.8118 us115.393.906319551 B

<sup>sources</sup>

</details> <details> <summary>Comparison: Cold Start <sup><a id='benchmark-comparison-cold-start' href='#benchmark-comparison-cold-start' title='Anchor'>🔗</a></sup></summary>

IterationCount=1 LaunchCount=100 RunStrategy=ColdStart

TypeMethodMeanErrorStdDevRatioAllocated
CreationEBind13,948.6 us411.78 us1,214.14 us1.003504 B
CreationMvvmLight14,719.1 us365.87 us1,078.77 us1.063216 B
CreationXamarinFormsCompiled18,116.8 us93.64 us276.11 us1.30848 B
CreationMvvmCross19,323.5 us46.63 us137.50 us1.396144 B
CreationPraeclarumBind21,130.5 us70.75 us208.60 us1.5212312 B
CreationMugen74,633.6 us117.36 us346.03 us5.377472 B
CreationReactiveUI151,959.5 us181.95 us536.48 us10.9416304 B
TriggerEBind705.5 us3.10 us9.15 us0.05224 B
TriggerMvvmCross1,144.8 us9.13 us26.91 us0.08704 B
TriggerXamarinFormsCompiled1,307.2 us6.60 us19.46 us0.09152 B
TriggerMvvmLight2,010.1 us13.96 us41.15 us0.14608 B
TriggerReactiveUI3,354.7 us9.44 us27.84 us0.24792 B
TriggerPraeclarumBind4,118.7 us15.92 us46.93 us0.305000 B
TriggerMugen4,326.1 us18.50 us54.54 us0.31152 B
</details> <details> <summary>EBind Creation <sup><a id='benchmark-ebind-creation' href='#benchmark-ebind-creation' title='Anchor'>🔗</a></sup></summary>
MethodMeanErrorStdDevGen 0Allocated
(a, "nameof(a.Event)", Method)337.5 ns2.31 ns1.93 ns0.0858360 B
a.Method()3,256.9 ns20.82 ns18.46 ns0.43491824 B
a.Prop == b.Prop // INPC4,089.8 ns54.06 ns50.57 ns0.52642232 B
a.Method(b.Prop.Method())4,406.1 ns20.41 ns17.05 ns0.54932312 B
a.Prop == !b.Prop4,611.5 ns46.32 ns43.33 ns0.56462392 B
a.Prop == b.Prop // EventHandler4,658.2 ns46.37 ns43.37 ns0.51122152 B
a.Float == b.Int4,785.8 ns45.54 ns38.03 ns0.57982448 B
a.Enum == b.Enum5,265.1 ns22.89 ns21.42 ns0.54932328 B
a.Prop == Static.Method(b.Prop)6,325.2 ns67.12 ns62.78 ns0.67902864 B
a.Prop == (b.Prop && c.Prop)6,423.7 ns21.64 ns18.07 ns0.80873408 B
a.Prop == (b.Prop == c.Prop)6,535.5 ns49.44 ns46.25 ns0.81633432 B
a.Prop == (b.Prop || c.Prop)6,653.2 ns54.95 ns51.40 ns0.81633432 B
a.Prop == b.Method(c.Prop)6,823.8 ns60.00 ns56.13 ns0.75533184 B
a.Prop == (b.Prop ?? c.Prop)7,271.6 ns53.72 ns50.25 ns0.83923536 B
a.Prop == b.Method(c.Prop, d.Prop)7,357.3 ns50.22 ns46.98 ns0.80113360 B
a.Prop == b.Method(c.Prop, d.Prop, e.Prop)7,552.6 ns40.44 ns37.83 ns0.83923536 B
a.Prop == b.Prop + c.Prop7,743.0 ns77.14 ns64.41 ns0.88503736 B
a.Prop == $"{b.Prop}_{c.Prop}"9,898.2 ns20.60 ns16.08 ns0.99184176 B
a.Prop == (b.Prop + c.Prop).Method()12,735.2 ns71.11 ns63.04 ns0.91553880 B
Static.Method(() => Method(a.Prop))16,007.6 ns131.27 ns116.37 ns1.25125344 B

<sup>sources</sup>

</details> <details> <summary>EBind Trigger <sup><a id='benchmark-ebind-trigger' href='#benchmark-ebind-trigger' title='Anchor'>🔗</a></sup></summary>
MethodMeanErrorStdDevGen 0Allocated
a.Prop == !b.Prop69.32 ns0.879 ns0.822 ns0.0305128 B
a.Prop == b.Prop // INPC72.64 ns0.810 ns0.758 ns0.0305128 B
a.Method()79.61 ns0.653 ns0.611 ns0.0248104 B
a.Enum == b.Enum80.15 ns1.008 ns0.943 ns0.0362152 B
a.Prop == b.Prop // EventHandler82.12 ns0.523 ns0.489 ns0.007632 B
a.Prop == (b.Prop || c.Prop)85.83 ns0.672 ns0.628 ns0.0362152 B
a.Prop == (b.Prop && c.Prop)88.38 ns1.761 ns1.884 ns0.0362152 B
a.Prop == (b.Prop == c.Prop)104.50 ns1.162 ns1.087 ns0.0362152 B
a.Prop == (b.Prop ?? c.Prop)115.92 ns0.883 ns0.826 ns0.013456 B
a.Prop == b.Prop + c.Prop122.91 ns1.117 ns1.045 ns0.020084 B
a.Float == b.Int128.55 ns0.769 ns0.719 ns0.0362152 B
a.Method(b.Prop.Method())128.60 ns1.223 ns1.144 ns0.0324136 B
a.Prop == Static.Method(b.Prop)168.87 ns2.003 ns1.873 ns0.0267112 B
a.Prop == b.Method(c.Prop)261.03 ns1.913 ns1.597 ns0.0286120 B
a.Prop == b.Method(c.Prop, d.Prop)265.79 ns3.130 ns2.928 ns0.0305128 B
a.Prop == b.Method(c.Prop, d.Prop, e.Prop)297.89 ns2.282 ns2.135 ns0.0324136 B
a.Prop == $"{b.Prop}_{c.Prop}"351.95 ns2.445 ns2.042 ns0.0362152 B
a.Prop == (b.Prop + c.Prop).Method()417.61 ns3.072 ns2.724 ns0.0515216 B

<sup>sources</sup>

</details>

Linking

The library is linker-safe internally, but some exposed APIs rely on Linq Expression trees and therefore the reflection which have always been hard to process for the mono linker.

Although linker can analyze expression trees and some reflection patterns pretty well, the following code units may not be mentioned in the code, appear unused and end up trimmed away:

The most common solution for hinting the linker to keep a member is to imitate its usage with a dummy call and mark it with a [Preserve] attribute. Your project may already have a LinkerPleaseInclude.cs file for that purpose.

EBind.LinkerIncludeGenerator will generate such files for the mentioned members used in EBinding and there wont be any EBind-related linker issues in your project.
Adding its NuGet package is enough for the installation: <sub>NuGet</sub>

AOT Compilation <sub>PLATFORM</sub>

This library uses C# 9 function pointers to create fast delegates for property accessors. It's the safest solution for AOT compilation so far.
However, Xamarin.iOS AOT compiler used for device builds requires a direct indication of value-types as a generic type parameter for them.
All standard structs are pre-seeded.

If you came across an exception like that:

System.ExecutionEngineException:
  Attempting to JIT compile method 'object EBind.PropertyAccessors.PropertyAccessor`2<..., ...>:Get (object)' while running in aot-only mode.

please add a hint for the compiler:

EBind.Platform.AotCompilerHints.Include<MyStruct>(); // custom struct as a member
// or
EBind.Platform.AotCompilerHints.Include<MyStruct, PropertyType>(); // custom struct as a target

Contributions

If you've found an error, please file an issue.

Patches are encouraged and may be submitted by forking this project and submitting a pull request.
If your change is substantial, please raise an issue or start a discussion first.

Development

Requirements

Device Testing

Device tests may run manually from the test app UI or automatically with XHarness CLI.

Android:

xharness android test \
  --app="./EBind.Tests.Android/bin/Release/EBind.Tests.Android-Signed.apk" \
  --package-name="EBind.Tests.Android" \
  --instrumentation="ebind.tests.droid.xharness_instrumentation" \
  --device-arch="x86" \
  --output-directory="./EBind.Tests.Android/TestResults/xharness_android" \
  --verbosity

iOS:

xharness apple test \
  --app="./EBind.Tests.iOS/bin/iPhoneSimulator/Release/EBind.Tests.iOS.app" \
  --output-directory="./EBind.Tests.iOS/TestResults/xharness_apple" \
  --targets=ios-simulator-64 \
  --verbosity \
  -- -app-arg:xharness # to be passed in Application.Main(string[])

It's also a part of the github actions workflow. Check it out!

The Story

Once upon a time, there was a small but powerful library called Bind from the Praeclarum family.

No other library could be compared with it in terms of concision and beauty. Every developer was dreaming about using it in his project. But it had several problems and missed some optimizations. The bravest of us could dare to use it in production.

Beauty was stronger than fear and this is how another fork has begun.
Bugs were fixed, some optimizations were made but it was never enough. That library deserved to SHINE!

So...

It ended up being completely rewritten with only some tests remained.

Credits

License

This project is licensed under the Apache License, Version 2.0 - see the LICENSE and NOTICE files for details.