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
[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:
<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:
<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:
<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:
<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:
<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.
<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:
<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:
="1.0"="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:
="1.0"="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
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
<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.
<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
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:
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
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)
{
propertyBinding[0] = new PropertyBinding
{
SourceProperty = sourceProperty,
TargetProperty = targetProperty,
Converter = converter,
ConverterParameter = bindingExpression.ConverterParameter,
View = bindingExpression.View
};
{
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);
}
}
var bindingEvent = bindingExpression.View.GetType().GetEvent(bindingExpression.Target);
if (bindingEvent != null)
{
if (sourceProperty != null)
{
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.");
}
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);
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
{
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)
{
throw new InvalidOperationException(
$"Method {sourceMethod.Name} should not have zero or one parameter "
+ $"to be called when event {bindingEvent.Name} 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
{
if (sourceProperty == null)
{
throw new InvalidOperationException(
$"Source property {bindingExpression.Source} does not exist "
+ $"on {currentContext?.GetType().Name ?? "null"}.");
}
SetTargetProperty(sourceProperty, currentContext, bindingExpression.View,
targetProperty, converter, bindingExpression.ConverterParameter);
if (bindingExpression.Mode == BindingMode.TwoWay)
{
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
{
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;
}
var removeActionCount = localRemoveActions.Count;
for (int j = position; j < removeActionCount; j++)
{
var removeAction = localRemoveActions[j];
try
{
removeAction();
}
catch (Exception ex)
{
}
localRemoveActions.Remove(removeAction);
globalRemoveActions.Remove(removeAction);
}
propertyBinding[0] = null;
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:
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)
{
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:
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