Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Xamarin

Building a Data-Binding Library for Xamarin.Android

4.88/5 (20 votes)
10 Jan 2016BSD11 min read 51.5K   664  
A powerful, yet light-weight, data-binding library for binding XML layout files in Xamarin.Android.

Introduction

Data-binding is a key ingredient of the MVVM pattern. One of my favorite things about working with XAML in WPF, UWP, or Xamarin Forms is XAML's rich data-binding capabilities. Though, data-binding is not native to many other UI stacks. And while Xamarin.Forms is steadily closing the gap on platform specific technologies, if you're looking to create highly custom UI, and unless you're willing to write custom renderers, you may be better of building out your UI in either (or both) Xamarin.Android or Xamarin.iOS. Stepping into the world of Xamarin.Android, however, you'll soon discover that data binding support doesn't come baked-in. In fact, there is no support for the data-binding features present in the Google's Data Binding Library for Android. There are third-party libraries, that offer data-binding features for Xamarin.Android, such as MVVMCross. However, my intent for this project was to create something light-weight yet feature rich that I could use both as a standalone library and as something I could incorporate into the Calcium MVVM Toolkit.

Background

When I began work on this project, I came across some good work that had already been done by Thomas Lebrun. Thomas whipped up the beginnings of a data-binding library for Xamarin.Android. I decided to build on Thomas's work.

The data-binding code for this article can be used as a standalone library, or referenced via the Calcium Nuget package. I recommend referencing the Calcium Nuget package in order to receive updates and bugfixes. You will, however, want to download the sample attached to this article to see the example app and unit tests.

Some of the features included in my implementation are:

  • Bind to methods, events, and properties.
  • Unlimited source nesting within binding paths. For example, you can define the path to a source object as "Child1.Child2.Child3.Text" At run-time this resolves to the Text property of the Child3 property, which is the Child2 property of the Child1 property of the data-context. You get the idea. If any parent with the path implements INotifyPropertyChanged (and the binding is a OneWay or TwoWay mode binding), then the binding is re-applied if a child is replaced. This allows you to switch out viewmodels and so forth.
  • The ability to remove bindings. This allows you to detach a view from its viewmodel, preventing memory leaks.
  • Collection support. This allows, for example, the binding of a ListView to a collection using a IValueConverter, and specifying a layout (data-template) within the binding.
  • Specify the view event that raises a source update. This is the ability to specify the view event that triggers the target value to be pushed to the source. This provides support for multiple view properties, without relying on a principle event for each view.
  • An extensibility point for adding new view types. This allows you to associate a view event with a property, so that you don't need to specify it in each binding.

Getting Started

The sample includes an example project that demonstrates various binding scenarios.

Figure 1: Binding Example App

The layout file for the Main activity is named Main.axml and is present in the Resources/layout directory of the example project.

Assigning a Data Context to an Activity

The example app's MainActivity class contains a ViewModel property. See Listing 1. During MainActivity's OnCreate method it calls the XmlBindingApplicator.ApplyBindings method, which parses the XML layout file, decomposes the main view into child views, and connects the properties of the viewmodel with those of the view and vice versa.

Listing 1: MainActivity class

C#
[Activity(Label = "Binding Example", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : FragmentActivity
{
    readonly XmlBindingApplicator bindingApplicator = new XmlBindingApplicator();

    public MainViewModel ViewModel { get; private set; }

    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        var layoutResourceId = Resource.Layout.Main;

        SetContentView(layoutResourceId);

        ViewModel = new MainViewModel();

        bindingApplicator.ApplyBindings(this, nameof(ViewModel), layoutResourceId);
    }

    protected override void OnDestroy()
    {
        bindingApplicator.RemoveBindings();

        base.OnDestroy();
    }
}

Binding ViewModel Properties

The binding system supports six keywords within the binding expression:

  • Target
  • Source
  • Mode
  • ValueConverter
  • ConverterParameter
  • ChangeEvent

The first five mirror those of WPF, Silverlight, and UWP. Target is the view object. Source is data context (AKA the viewmodel). Mode can be one of three values: OneWay (default), TwoWay, or OneTime. If OneWay, only those changes in the data context are pushed to the view; changes to the view property are not reflected in the data context. A Mode equal to OneTime is a light-weight option in that no PropertyChanged events (in the source or path objects) are subscribed to. ValueConverter is the short name of an IValueConverter type that is located within your project or a referenced project.

ChangeEvent is particular to this data-binding library. ChangeEvent identifies the view event that is used to trigger an update of the source property. You see an example of its use later in the article. Let's move on to some examples.

The MainViewModel class contains a property named SampleText. This property is bound to the Text property of an EditText view in the Main.axml layout file, as shown:

XML
<EditText
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/Hello"
    local:Binding="{Source=SampleText, Target=Text, Mode=TwoWay}" />

The binding is a TwoWay binding. Changes to the text via the EditText view are pushed to the viewmodel when the TextChanged event is raised.

A TextView (beneath the EditText view) has a OneWay binding to the SampleText property of the viewmodel. When the user makes a change via the EditText view, the change is reflected in the TextView. See the following:

XML
<TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/Hello"
    local:Binding="{Source=SampleText, Target=Text, Mode=OneWay}" />

Nested properties are also supported. A TextView in Main.axml is bound to the Text property of the Foo property of the viewmodel, as shown:

XML
<TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/Hello"
    local:Binding="{Source=Foo.Text, Target=Text, Mode=OneWay}" />

The next example demonstrates binding the CheckBox's Checked property to a viewmodel property:

XML
<CheckBox
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    local:Binding="{Source=CheckBox1Checked, Target=Checked, Mode=TwoWay}" />

You can also specify an event on the view that triggers an update. You do this by using the binding's ChangedEvent property, as shown:

XML
<CheckBox
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    local:Binding="{Source=CheckBox1Checked, Target=Checked, 
                    Mode=TwoWay, ChangedEvent=CheckedChange}" />

You can bind a command to a view by specifying it as the source of the binding, as shown in the following example. Also note that multiple binding expressions can be included in a single binding definition. They are separated them with a semicolon.

XML
<Button
    android:id="@+id/MyButton"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/Hello"
    local:Binding="{Target=Click, Source=IncrementCommand;
                    Target=Text, Source=ClickCount, Mode=OneWay}" />

The binding system supports calling a method on the viewmodel when a view event is raised. In the following example, a button's Click event is used to call the viewmodel's HandleClick method:

XML
<Button
    android:id="@+id/MyButton"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/Hello"
    local:Binding="{Target=Click, Source=HandleClick;
                    Target=Text, Source=ClickCount, Mode=OneWay}" />

When specifying a method to call, the method may either have 0 or 1 parameter. In the case of single parameter method, the parameter is the data context of the view. This is useful when binding list items.

Using the Binding attribute in a layout file requires that you add a resource to your Resources/values directory. In the example, this file is name BindingAttributes.xml, and consists of a single declare-stylable resource, as shown:

XML
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="BindingExpression">
    <attr name="Binding" format="string"/>
  </declare-styleable>
</resources>

At the top of any layout file, where you wish to use the binding system, add a namespace alias for local types, as shown:

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
...
​</LinearLayout>

Binding to Collections

The way you implement the displaying of lists in Android differs markedly from XAML based frameworks. With Android there's quite a bit of plumbing code that you need to put in place. Your view requires an adapter, and the adapter takes care of inflating the layout and setting the relevant properties of the view for that item.

The approach I came up with leverages a custom BindableListAdapter. See Listing 2. BindableListAdapter is a ListAdapter that relies on a custom ApplicationContextHolder class to retrieve the Context for the application. The context object is used to inflate the layout of each view during the GetView method. The XmlBindingApplicator binds the view to its data context. 

Listing 2: BindableListAdapter

C#
public class BindableListAdapter<TItem> : BaseAdapter<TItem>
{
    readonly IList<TItem> list;
    readonly int layoutId;
    readonly ObservableCollection<TItem> observableCollection;
    readonly LayoutInflater inflater;

    public BindableListAdapter(IList<TItem> list, int layoutId)
    {
        this.list = list;
        this.layoutId = layoutId;

        observableCollection = list as ObservableCollection<TItem>;
        if (observableCollection != null)
        {
            observableCollection.CollectionChanged += HandleCollectionChanged;
        }

        Context context = ApplicationContextHolder.Context;
        inflater = LayoutInflater.From(context);
    }

    void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        NotifyDataSetChanged();
    }

    public override int Count => list.Count;

    public override long GetItemId(int position)
    {
        return position;
    }

    public override TItem this[int index] => list[index];

    readonly Dictionary<View, XmlBindingApplicator> bindingsDictionary 
                = new Dictionary<View, XmlBindingApplicator>();

    public override View GetView(int position, View convertView, ViewGroup parent)
    {
        View view = convertView ?? inflater.Inflate(layoutId, parent, false);

        TItem item = this[position];

        XmlBindingApplicator applicator;
        if (!bindingsDictionary.TryGetValue(view, out applicator))
        {
            applicator = new XmlBindingApplicator();
        }
        else
        {
            applicator.RemoveBindings();
        }

        applicator.ApplyBindings(view, item, layoutId);

        return view;
    }
}

Being unburdoned from creating an adapter each time you wish to display a list brings you a little closer to the joys of XAML. Just like in XAML, all you need is to define a layout for your list items. This is akin to a DataTemplate in XAML.

In the example app, the ListItemRow.axml file defines how each item in the list is presented. See Listing 3. It's a bare-bones example. The layout consists of a RelativeLayout with two TextView elements. The first TextView is bound to the Title of the data context (a Post object). The second is bound to the description of the data context. 

Listing 3: ListItemRow.axml

XML
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:minHeight="50dp"
    android:orientation="horizontal">
    <TextView
        android:id="@+id/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:lineSpacingExtra="3dp"
        android:paddingLeft="10dp"
        android:paddingTop="5dp"
        android:textColor="#ffffff"
        android:textStyle="bold"
        android:typeface="sans"
        local:Binding="{Source=Title, Target=Text, Mode=OneTime}" />
    <TextView
        android:id="@+id/Description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/Title"
        android:paddingLeft="10dp"
        android:paddingTop="5dp"
        android:textColor="#fff"
        android:textSize="11sp"
        local:Binding="{Source=Description, Target=Text, Mode=OneTime}" />
</RelativeLayout>

The Main.axml layout contains a ListView at the bottom. There exists two binding definitions within the binding attribute for the ListView. See Listing 4.

The first binding definition binds the source Posts collection to the Adapter property of the view. Of course, the Adapter property is not a collection type, but the ListAdapterConverter is responsible for turning the collection into an adapter. The layout file for each item is specified using the ConverterParameter of the binding. In this case it's AndroidBindingExampleApp.Resource+Layout.ListItemRow

The ConverterParameter specifies the namespace qualified path to the integer constant value of the layout ID. No, that '+' symbol is not a formatting issue. The '+' symbol denotes that the Layout class is an inner class of the Resource class.

The second binding definition includes a subscription to the ItemClick event. When the user taps on an item the viewmodel's HandleItemClick method is called.

NOTE: In a modern app, a RecyclerView, rather than a ListView, might be a better choice, but I wanted to minimize dependencies in the example app.

Listing 4: ListView bound to source property.

XML
<ListView
    android:minWidth="25px"
    android:minHeight="25px"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/ListView"
    local:Binding="{Target=Adapter, Source=Posts, Mode=OneTime,                                     
                   Converter=ListAdapterConverter,              
                   ConverterParameter=AndroidBindingExampleApp.Resource+Layout.ListItemRow;          
                   Target=ItemClick, Source=HandleItemClick, Mode=OneTime}" />

There is no IValueConverter in the Xamarin.Android class libraries. It's a custom interface, that mirrors that found in WPF and Silverlight. The task of the ListAdapterConverter is take a collection and instantiate a BindableListAdapter. See Listing 5.

The Convert method of the ListAdapterConverter (see Listing 5) constructs a generic instance of the BindableListAdapter using the generic parameter of the bound collection. For example, if Convert receives a value that is of type ObservableCollection<Post>, the Convert method will create a BindableListAdapter<Post>. Recall that the ConverterParameter is the path to the layout file's ID. This is used by the BindableListAdapter during its GetView method.

There's quite a bit of reflection taking place in this method. So, for the same of performance, the FieldInfo object for the layout ID is cached in a dictionary. On looking over this method, I realize there's some other caching I could do here to improve performance. I'll perhaps enhance that in the future.

Listing 5: ListAdapterConverter class

C#
public class ListAdapterConverter : IValueConverter
{
    static readonly Dictionary<string, FieldInfo> adapterDictionary 
        = new Dictionary<string, FieldInfo>();
         
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null)
        {
            return null;
        }

        Type valueType = value.GetType();
        if (valueType.IsGenericType)
        {
            Type[] typeArguments = valueType.GetGenericArguments();
            if (typeArguments.Any())
            {
                if (typeArguments.Count() > 1)
                {
                    throw new Exception("List contains to many type arguments. Unable to create " 
                        + nameof(BindableListAdapter<object>) + " in ListAdapterConverter.");
                }

                Type itemType = typeArguments[0];
                Type listType = typeof(BindableListAdapter<>);
                Type[] typeArgs = { itemType };
                Type constructed = listType.MakeGenericType(typeArgs);

                string layoutName = parameter?.ToString();

                if (layoutName != null)
                {
                    var dotIndex = layoutName.LastIndexOf(".", StringComparison.Ordinal);
                    string propertyName = layoutName.Substring(
                               dotIndex + 1, layoutName.Length - (dotIndex + 1));
                    string typeName = layoutName.Substring(0, dotIndex);

                    FieldInfo fieldInfo;
                    if (!adapterDictionary.TryGetValue(layoutName, out fieldInfo))
                    {
                        Type type = Type.GetType(typeName, false, true);
                        if (type == null)
                        {
                            throw new Exception("Unable to locate layout type code for layout " 
                                                + layoutName + " Type could not be resolved.");
                        }

                        fieldInfo = type.GetField(propertyName, 
                                   BindingFlags.Public | BindingFlags.Static);

                        if (fieldInfo != null)
                        {
                            adapterDictionary[layoutName] = fieldInfo;
                        }
                    }
                        
                    if (fieldInfo == null)
                    {
                        throw new Exception("Unable to locate layout type code for layout " 
                            + layoutName + " FieldInfo is null.");
                    }

                    int resourceId = (int)fieldInfo.GetValue(null);

                    var result = Activator.CreateInstance(constructed, value, resourceId);
                    return result;
                }
            }
        }
        else
        {
            throw new Exception("Value is not a generic collection." + parameter);
        }

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

To recap, the bound collection of the ListView is passed to the value converter, which creates an adapter. The adapter then performs all rendering using the layout ID specified by the binding's ConverterParameter.

Leveraging View Binding Extensibility

Property bindings that do not specify a ChangedEvent attribute, rely on an association between a view event and a property of the view. For example, the TextView's TextChanged event is associated with the TextView's Text property. If the TextChanged event is raised for that view, the source property of any TwoWay bindings using the Text property of that view are updated. There are various pre-configured view bindings located in the ViewBinderRegistery.

The ViewBinderRegistery class allows you to override or add new binding behaviors. You create a new binding behavior either by registering an IViewBinder object. You may either implement the IViewBinder interface, which gives you gives you a lot of control over how the binder is done and, for example, lets you subscribe to multiple view events. Alternatively, you may choose to use the ViewEventBinder class, which is the simplest approach as all of the plumbing to bind to a single event is taken care of.

When using the ViewEventBinder, you supply actions to add and remove the event handler from the control, and a Func that is used to resolve the new value that is pushed to the viewmodel when the event is raised.

The following shows how you might create a view binder for a RatingBar view:

C#
var ratingViewBinder 
    = new ViewEventBinder<RatingBar, RatingBar.RatingBarChangeEventArgs, float>(
        (view, h) => view.RatingBarChange += h, 
        (view, h) => view.RatingBarChange -= h, 
        (view, args) => args.Rating);
                        
XmlBindingApplicator.SetViewBinder(
    typeof(RatingBar), nameof(RatingBar.Rating), ratingViewBinder);

To add a new view binder, or override an existing one, use the SetViewBinder method of the XmlBindingApplicator class.

Binding - Behind the Scenes

The XmlBindingApplicator is responsible for loading the XML layout file, and marrying elements within the file with corresponding views within the activity's view. The XmlBindingApplicator class relies on the BindingApplicator class to connect each target view with its data context (viewmodel). The majority of this occurs within the recursive Bind method of the BindingApplicator class. See Listing 6. Bind, splits and walks the source path. It subscribes to the NotifyPropertyChanged property of source objects, until it reaches the source object, where it wires up the binding. Any event subscriptions also result in a removal Action that is added to a global list (pertaining to all views bound using an XmlBindingApplicator instance) , and a local list. These come into play when an object in the source path is swapped out, in which case all actions within the local list of removal actions are invoked and the binding rebuilt. When an Activity unbinds from a View, the XmlBindingApplicator invokes all the removal actions in the global list. 

Listing 6: BindingApplicator.Bind method

C#
void Bind(
    BindingExpression bindingExpression, 
    object dataContext, 
    string[] sourcePath, 
    IValueConverter converter, 
    PropertyInfo targetProperty, 
    IList<Action> localRemoveActions, 
    IList<Action> globalRemoveActions, 
    int position)
{
    object currentContext = dataContext;
            
    var pathSplitLength = sourcePath.Length;
    int lastIndex = pathSplitLength - 1;
    PropertyBinding[] propertyBinding = new PropertyBinding[1];

    for (int i = position; i < pathSplitLength; i++)
    {
        if (currentContext == null)
        {
            break;
        }

        var inpc = currentContext as INotifyPropertyChanged;

        string sourceSegment = sourcePath[i];
        var sourceProperty = currentContext.GetType().GetProperty(sourceSegment);

        if (i == lastIndex) /* The value. */
        {
            /* Add a property binding between the source (the viewmodel) 
                * and the target (the view) so we can update the target property 
                * when the source property changes (a OneWay binding). */
            propertyBinding[0] = new PropertyBinding
            {
                SourceProperty = sourceProperty,
                TargetProperty = targetProperty,
                Converter = converter,
                ConverterParameter = bindingExpression.ConverterParameter,
                View = bindingExpression.View
            };

            {
                /* When this value changes, the value must be pushed to the target. */

                if (inpc != null && bindingExpression.Mode != BindingMode.OneTime)
                {
                    object context = currentContext;

                    PropertyChangedEventHandler handler 
                        = delegate(object sender, PropertyChangedEventArgs args)
                    {
                        if (args.PropertyName != sourceSegment)
                        {
                            return;
                        }

                        PropertyBinding binding = propertyBinding[0];

                        if (binding != null)
                        {
                            if (binding.PreventUpdateForTargetProperty)
                            {
                                return;
                            }

                            try
                            {
                                binding.PreventUpdateForSourceProperty = true;

                                SetTargetProperty(sourceProperty, context, 
                                    binding.View, binding.TargetProperty, 
                                    binding.Converter, binding.ConverterParameter);
                            }
                            finally
                            {
                                binding.PreventUpdateForSourceProperty = false;
                            }
                        }
                    };

                    inpc.PropertyChanged += handler;

                    Action removeHandler = () =>
                    {
                        inpc.PropertyChanged -= handler;
                        propertyBinding[0] = null;
                    };

                    localRemoveActions.Add(removeHandler);
                    globalRemoveActions.Add(removeHandler);
                }
            }

            /* Determine if the target is an event, 
                * in which case use that to trigger an update. */

            var bindingEvent = bindingExpression.View.GetType().GetEvent(bindingExpression.Target);

            if (bindingEvent != null)
            {
                /* The target is an event of the view. */
                if (sourceProperty != null)
                {
                    /* The source must be an ICommand so we can call its Execute method. */
                    var command = sourceProperty.GetValue(currentContext) as ICommand;
                    if (command == null)
                    {
                        throw new InvalidOperationException(
                            $"The source property {bindingExpression.Source}, "
                            + $"bound to the event {bindingEvent.Name}, " 
                            + "needs to implement the interface ICommand.");
                    }

                    /* Subscribe to the specified event to execute 
                        * the command when the event is raised. */
                    var executeMethodInfo = typeof(ICommand).GetMethod(nameof(ICommand.Execute), 
                                                                          new[] {typeof(object)});

                    Action action = () =>
                    {
                        executeMethodInfo.Invoke(command, new object[] {null});
                    };

                    Action removeAction = DelegateUtility.AddHandler(
                                     bindingExpression.View, bindingExpression.Target, action);
                    localRemoveActions.Add(removeAction);
                    globalRemoveActions.Add(removeAction);

                    /* Subscribe to the CanExecuteChanged event of the command 
                        * to disable or enable the view associated to the command. */
                    var view = bindingExpression.View;

                    var enabledProperty = view.GetType().GetProperty(viewEnabledPropertyName);
                    if (enabledProperty != null)
                    {
                        enabledProperty.SetValue(view, command.CanExecute(null));

                        Action canExecuteChangedAction 
                             = () => enabledProperty.SetValue(view, command.CanExecute(null));
                        removeAction = DelegateUtility.AddHandler(
                            command, nameof(ICommand.CanExecuteChanged), canExecuteChangedAction);

                        localRemoveActions.Add(removeAction);
                        globalRemoveActions.Add(removeAction);
                    }
                }
                else /* sourceProperty == null */
                {
                    /* If the Source property of the data context 
                        * is not a property, check if it's a method. */
                    var sourceMethod = currentContext.GetType().GetMethod(sourceSegment, 
                        BindingFlags.Public | BindingFlags.NonPublic 
                        | BindingFlags.Instance | BindingFlags.Static);

                    if (sourceMethod == null)
                    {
                        throw new InvalidOperationException(
                            $"No property or event named {bindingExpression.Source} "
                            + $"found to bind it to the event {bindingEvent.Name}.");
                    }

                    var parameterCount = sourceMethod.GetParameters().Length;
                    if (parameterCount > 1)
                    {
                        /* Only calls to methods without parameters are supported. */
                        throw new InvalidOperationException(
                            $"Method {sourceMethod.Name} should not have zero or one parameter "
                            + $"to be called when event {bindingEvent.Name} is raised.");
                    }

                    /* It's a method therefore subscribe to the specified event 
                        * to execute the method when event is raised. */
                    var context = currentContext;
                    Action removeAction = DelegateUtility.AddHandler(
                        bindingExpression.View,
                        bindingExpression.Target,
                        () => { sourceMethod.Invoke(context, 
                                     parameterCount > 0 ? new []{ context } : null); });

                    localRemoveActions.Add(removeAction);
                    globalRemoveActions.Add(removeAction);
                }
            }
            else /* bindingEvent == null */
            {
                if (sourceProperty == null)
                {
                    throw new InvalidOperationException(
                        $"Source property {bindingExpression.Source} does not exist "
                        + $"on {currentContext?.GetType().Name ?? "null"}.");
                }

                /* Set initial binding value. */
                SetTargetProperty(sourceProperty, currentContext, bindingExpression.View, 
                    targetProperty, converter, bindingExpression.ConverterParameter);

                if (bindingExpression.Mode == BindingMode.TwoWay)
                {
                    /* TwoWay bindings require that the ViewModel property be updated 
                        * when an event is raised on the bound view. */
                    string changedEvent = bindingExpression.ViewValueChangedEvent;
                    if (!string.IsNullOrWhiteSpace(changedEvent))
                    {
                        var context = currentContext;

                        Action changeAction = () =>
                        {
                            var pb = propertyBinding[0];
                            if (pb == null)
                            {
                                return;
                            }

                            ViewValueChangedHandler.HandleViewValueChanged(pb, context);
                        };

                        var view = bindingExpression.View;
                        var removeHandler = DelegateUtility.AddHandler(
                                                   view, changedEvent, changeAction);
                                
                        localRemoveActions.Add(removeHandler);
                        globalRemoveActions.Add(removeHandler);
                    }
                    else
                    {
                        var binding = propertyBinding[0];
                        IViewBinder binder;
                        if (ViewBinderRegistry.TryGetViewBinder(
                                binding.View.GetType(), binding.TargetProperty.Name, out binder))
                        {
                            var unbindAction = binder.BindView(binding, currentContext);
                            if (unbindAction != null)
                            {
                                localRemoveActions.Add(unbindAction);
                                globalRemoveActions.Add(unbindAction);
                            }
                        }
                        else
                        {
                            if (Debugger.IsAttached)
                            {
                                Debugger.Break();
                            }
                        }
                    }
                }
            }
        }
        else 
        {
            /* The source is a child of another object, 
                * therefore we must subscribe to the parents PropertyChanged event 
                * and re-bind when the child changes. */

            if (inpc != null && bindingExpression.Mode != BindingMode.OneTime)
            {
                var context = currentContext;

                var iCopy = i;

                PropertyChangedEventHandler handler 
                    = delegate (object sender, PropertyChangedEventArgs args)
                {
                    if (args.PropertyName != sourceSegment)
                    {
                        return;
                    }

                    /* Remove existing child event subscribers. */
                    var removeActionCount = localRemoveActions.Count;
                    for (int j = position; j < removeActionCount; j++)
                    {
                        var removeAction = localRemoveActions[j];
                        try
                        {
                            removeAction();
                        }
                        catch (Exception ex)
                        {
                            /* TODO: log error. */
                        }

                        localRemoveActions.Remove(removeAction);
                        globalRemoveActions.Remove(removeAction);
                    }

                    propertyBinding[0] = null;
                            
                    /* Bind child bindings. */
                    Bind(bindingExpression,
                        context,
                        sourcePath,
                        converter,
                        targetProperty,
                        localRemoveActions, globalRemoveActions, iCopy);
                };

                inpc.PropertyChanged += handler;

                Action removeHandler = () =>
                {
                    inpc.PropertyChanged -= handler;
                    propertyBinding[0] = null;
                };

                localRemoveActions.Add(removeHandler);
                globalRemoveActions.Add(removeHandler);
            }

            currentContext = sourceProperty?.GetValue(currentContext);
        }
    }
}

With TwoWay bindings, when a View event is raised, it raises a call to the ViewValueChangedHandler class's HandleViewValueChanged method. This method retrieves the raw value (before any IValueConverters are applied) using the newValueFunc, as shown:

C#
internal static void HandleViewValueChanged(
    PropertyBinding propertyBinding,
    object dataContext)
{
    try
    {
        propertyBinding.PreventUpdateForTargetProperty = true;
    
        var newValue = propertyBinding.TargetProperty.GetValue(propertyBinding.View);
    
        UpdateSourceProperty(propertyBinding.SourceProperty, dataContext, newValue, 
            propertyBinding.Converter, propertyBinding.ConverterParameter);
    }
    catch (Exception ex)
    {
        /* TODO: log exception */
        if (Debugger.IsAttached)
        {
            Debugger.Break();
        }
    }
    finally
    {
        propertyBinding.PreventUpdateForTargetProperty = false;
    }
}

The UpdateSourceProperty method applies the IValueConverter, if it exists, and pushes the resulting value to the source property, as shown: 

C#
internal static void UpdateSourceProperty<T>(
    PropertyInfo sourceProperty,
    object dataContext,
    T value,
    IValueConverter valueConverter,
    string converterParameter)
{
    object newValue;
    
    if (valueConverter != null)
    {
        newValue = valueConverter.ConvertBack(value,
            sourceProperty.PropertyType,
            converterParameter,
            CultureInfo.CurrentCulture);
    }
    else
    {
        newValue = value;
    }
    
    sourceProperty.SetValue(dataContext, newValue);
}

Conclusion

In this article you saw an approach to implementing a data-binding system for Xamarin.Android. This system does not rely on third-party infrastructure; allowing you to quickly build layouts for your app without spending too much time writing plumbing code. The binding system allows for a view's ChangedEvent to be specified within a binding, and the system includes a view binder extensibility mechanism. The system supports binding to source properties, methods, and commands; with OneTime, OneWay, and TwoWay binding modes. 

I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.

History

January 2015

  • Initial publication.

License

This article, along with any associated source code and files, is licensed under The BSD License