为 Xamarin.Android 构建数据绑定库






4.88/5 (19投票s)
一个强大而轻量级的数据绑定库,用于绑定 Xamarin.Android 中的 XML 布局文件。
引言
数据绑定是 MVVM 模式的关键组成部分。在 WPF、UWP 或 Xamarin Forms 中使用 XAML 时,我最喜欢的一点就是 XAML 丰富的数据绑定功能。不过,数据绑定在许多其他 UI 堆栈中并非原生支持。虽然 Xamarin.Forms 在不断缩小与平台特定技术之间的差距,但如果您想创建高度定制的 UI,并且不愿意编写自定义渲染器,那么在 Xamarin.Android 或 Xamarin.iOS(或两者)中构建 UI 可能会更好。然而,进入 Xamarin.Android 的世界,您很快就会发现数据绑定支持并非内置。事实上,Google 的 Android 数据绑定库 中提供的数据绑定功能在这里并不支持。有一些第三方库为 Xamarin.Android 提供了数据绑定功能,例如 MVVMCross。然而,我这个项目的目的是创建一个轻量级且功能丰富的库,既可以作为独立库使用,也可以集成到 Calcium MVVM Toolkit 中。
背景
当我开始这个项目时,我发现 Thomas Lebrun 已经完成了一些 出色的工作。Thomas 构建了一个 Xamarin.Android 数据绑定库的雏形。我决定在此基础上进行开发。
本文中的数据绑定代码可以作为独立库使用,也可以通过 Calcium Nuget 包进行引用。我建议引用 Calcium Nuget 包以接收更新和错误修复。不过,您需要下载本文附带的示例来查看示例应用程序和单元测试。
我的实现中包含的一些功能包括:
- 绑定到方法、事件和属性。
- 绑定路径内无限的源嵌套。例如,您可以为源对象定义路径为“Child1.Child2.Child3.Text”。运行时,这会解析为数据上下文的 `Child1` 属性的 `Child2` 属性的 `Child3` 属性的 `Text` 属性。您应该明白了这个意思。如果路径中的任何父级实现了 `INotifyPropertyChanged`(并且绑定模式为 `OneWay` 或 `TwoWay`),那么当子级被替换时,绑定会被重新应用。这允许您切换 ViewModel 等。
- 能够移除绑定。这允许您将视图与 ViewModel 分离,从而防止内存泄漏。
- 集合支持。例如,这允许您通过 `IValueConverter` 将 `ListView` 绑定到集合,并在绑定中指定布局(数据模板)。
- 指定触发源更新的视图事件。这允许指定触发将目标值推送到源的视图事件。这支持多个视图属性,而无需为每个视图依赖一个主要事件。
- 用于添加新视图类型的扩展点。这允许您将视图事件与属性关联,这样您就不需要在每个绑定中指定它。
入门
示例应用程序演示了各种绑定场景。
图 1:绑定示例应用程序
示例项目中的 `Main` 活动的布局文件名为 Main.axml,位于 `Resources/layout` 目录下。
为 Activity 分配 Data Context
示例应用程序的 `MainActivity` 类包含一个 `ViewModel` 属性。请参见列表 1。在 `MainActivity` 的 `OnCreate` 方法中,它调用 `XmlBindingApplicator.ApplyBindings` 方法,该方法解析 XML 布局文件,将主视图分解为子视图,并将 ViewModel 的属性与视图的属性连接起来,反之亦然。
列表 1:MainActivity 类
[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();
}
}
绑定 ViewModel 属性
绑定系统在绑定表达式中支持六个关键字。
- 目标
- 源
- 模式
- 值转换器 (ValueConverter)
- ConverterParameter
- ChangeEvent
前五个与 WPF、Silverlight 和 UWP 类似。`Target` 是视图对象。`Source` 是数据上下文(又名 ViewModel)。`Mode` 可以是三个值之一:`OneWay`(默认)、`TwoWay` 或 `OneTime`。如果设置为 `OneWay`,只有数据上下文中的更改会推送到视图;视图属性的更改不会反映在数据上下文中。`Mode` 设置为 `OneTime` 是一种轻量级的选项,因为它不订阅 `PropertyChanged` 事件(在源或路径对象中)。`ValueConverter` 是 `IValueConverter` 类型的简称,它位于您的项目或引用的项目中。
`ChangeEvent` 是此数据绑定库特有的。`ChangeEvent` 标识用于触发源属性更新的视图事件。您将在本文后面看到它的使用示例。让我们继续看一些例子。
`MainViewModel` 类包含一个名为 `SampleText` 的属性。此属性绑定到 `Main.axml` 布局文件中 `EditText` 视图的 `Text` 属性,如下所示。
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/Hello"
local:Binding="{Source=SampleText, Target=Text, Mode=TwoWay}" />
该绑定是 `TwoWay` 绑定。当 `TextChanged` 事件被触发时,通过 `EditText` 视图对文本的更改会被推送到 ViewModel。
一个 `TextView`(位于 `EditText` 视图下方)与 ViewModel 的 `SampleText` 属性有一个 `OneWay` 绑定。当用户通过 `EditText` 视图进行更改时,更改会反映在 `TextView` 中。请参见下图。
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/Hello"
local:Binding="{Source=SampleText, Target=Text, Mode=OneWay}" />
也支持嵌套属性。`Main.axml` 中的一个 `TextView` 绑定到 ViewModel 的 `Foo` 属性的 `Text` 属性,如下所示。
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/Hello"
local:Binding="{Source=Foo.Text, Target=Text, Mode=OneWay}" />
下一个示例演示了将 `CheckBox` 的 `Checked` 属性绑定到 ViewModel 属性。
<CheckBox
android:layout_width="fill_parent"
android:layout_height="wrap_content"
local:Binding="{Source=CheckBox1Checked, Target=Checked, Mode=TwoWay}" />
您还可以指定视图上用于触发更新的事件。您可以通过使用绑定的 `ChangedEvent` 属性来做到这一点,如下所示。
<CheckBox
android:layout_width="fill_parent"
android:layout_height="wrap_content"
local:Binding="{Source=CheckBox1Checked, Target=Checked,
Mode=TwoWay, ChangedEvent=CheckedChange}" />
您可以通过将命令指定为绑定的源来将命令绑定到视图,如下面的示例所示。另请注意,可以在单个绑定定义中包含多个绑定表达式。它们用分号分隔。
<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}" />
绑定系统支持在视图事件触发时调用 ViewModel 上的方法。在下面的示例中,按钮的 `Click` 事件用于调用 ViewModel 的 `HandleClick` 方法。
<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}" />
在指定要调用的方法时,该方法可以有一个或零个参数。如果方法带有一个参数,则该参数是视图的数据上下文。这在绑定列表项时很有用。
在布局文件中使用 `Binding` 属性要求您将资源添加到 `Resources/values` 目录。在示例中,此文件名为 `BindingAttributes.xml`,并且包含一个 `declare-stylable` 资源,如下所示。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BindingExpression">
<attr name="Binding" format="string"/>
</declare-styleable>
</resources>
在任何希望使用绑定系统的布局文件的顶部,添加本地类型的命名空间别名,如下所示。
<?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>
绑定到集合
在 Android 中实现列表显示的方式与基于 XAML 的框架截然不同。在 Android 中,您需要编写相当多的基础代码。您的视图需要一个适配器,该适配器负责膨胀布局并为该项设置视图的相关属性。
我提出的方法利用了一个自定义的 `BindableListAdapter`。请参见列表 2。`BindableListAdapter` 是一个 `ListAdapter`,它依赖于自定义的 `ApplicationContextHolder` 类来检索应用程序的 `Context`。`Context` 对象用于在 `GetView` 方法中膨胀每个视图的布局。`XmlBindingApplicator` 将视图绑定到其数据上下文。
列表 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;
}
}
每次想显示列表时,都不必再创建适配器,这让您离 XAML 的乐趣更近了一步。就像在 XAML 中一样,您只需要定义列表项的布局。这类似于 XAML 中的 `DataTemplate`。
在示例应用程序中,`ListItemRow.axml` 文件定义了列表中的每个项是如何显示的。请参见列表 3。这是一个最简单的示例。布局包含一个带有两个 `TextView` 元素的 `RelativeLayout`。第一个 `TextView` 绑定到数据上下文(一个 `Post` 对象)的 `Title`。第二个 `TextView` 绑定到数据上下文的描述。
列表 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>
`Main.axml` 布局在底部包含一个 `ListView`。`ListView` 的绑定属性中有两个绑定定义。请参见列表 4。
第一个绑定定义将源 `Posts` 集合绑定到视图的 `Adapter` 属性。当然,`Adapter` 属性不是集合类型,但 `ListAdapterConverter` 负责将集合转换为适配器。每个项的布局文件使用绑定的 `ConverterParameter` 来指定。在这种情况下,它是 `AndroidBindingExampleApp.Resource+Layout.ListItemRow`。
`ConverterParameter` 指定了布局 ID 的整数常量值的命名空间限定路径。不,这个 '+' 符号不是格式问题。'+' 符号表示 `Layout` 类是 `Resource` 类的内部类。
第二个绑定定义包含对 `ItemClick` 事件的订阅。当用户点击一个项时,会调用 ViewModel 的 `HandleItemClick` 方法。
注意:在现代应用程序中,`RecyclerView` 可能比 `ListView` 更好的选择,但我希望在示例应用程序中尽量减少依赖项。
列表 4:绑定到源属性的 `ListView`。
<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}" />
Xamarin.Android 类库中没有 `IValueConverter`。这是一个自定义接口,与 WPF 和 Silverlight 中的接口类似。`ListAdapterConverter` 的任务是接收一个集合并实例化一个 `BindableListAdapter`。请参见列表 5。
`ListAdapterConverter` 的 `Convert` 方法(参见列表 5)使用绑定集合的泛型参数来构建 `BindableListAdapter` 的泛型实例。例如,如果 `Convert` 接收到一个类型为 `ObservableCollection
此方法中有大量的反射操作。因此,为了性能起见,布局 ID 的 `FieldInfo` 对象被缓存在一个字典中。在检查此方法时,我意识到还可以进行其他缓存来提高性能。我以后可能会对其进行增强。
列表 5:ListAdapterConverter 类
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();
}
}
总而言之,`ListView` 的绑定集合被传递给值转换器,后者创建一个适配器。然后,适配器使用绑定 `ConverterParameter` 指定的布局 ID 来执行所有渲染。
利用视图绑定扩展性
不指定 `ChangedEvent` 属性的属性绑定依赖于视图事件和视图属性之间的关联。例如,`TextView` 的 `TextChanged` 事件与 `TextView` 的 `Text` 属性相关联。如果该视图触发了 `TextChanged` 事件,那么使用该视图 `Text` 属性的任何 `TwoWay` 绑定的源属性都会被更新。`ViewBinderRegistery` 中包含各种预配置的视图绑定。
`ViewBinderRegistery` 类允许您覆盖或添加新的绑定行为。您可以通过注册一个 `IViewBinder` 对象来创建一个新的绑定行为。您可以实现 `IViewBinder` 接口,该接口为您提供了对绑定方式的极大控制,例如,允许您订阅多个视图事件。或者,您可以选择使用 `ViewEventBinder` 类,这是最简单的方法,因为它已经处理了绑定到单个事件的所有基础工作。
在使用 `ViewEventBinder` 时,您需要提供用于从控件添加和删除事件处理程序的动作,以及一个 `Func`,该 `Func` 用于解析当事件触发时推送到 ViewModel 的新值。
下面的示例展示了如何为 `RatingBar` 视图创建一个视图绑定器。
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);
要添加新的视图绑定器或覆盖现有的视图绑定器,请使用 `XmlBindingApplicator` 类的 `SetViewBinder` 方法。
绑定 - 幕后
`XmlBindingApplicator` 负责加载 XML 布局文件,并将文件中的元素与 Activity 视图中的相应视图匹配。`XmlBindingApplicator` 类依赖于 `BindingApplicator` 类将每个目标视图与其数据上下文(ViewModel)连接起来。大部分工作发生在 `BindingApplicator` 类的递归 `Bind` 方法中。请参见列表 6。`Bind` 方法拆分并遍历源路径。它订阅源对象的 `NotifyPropertyChanged` 属性,直到到达源对象,然后设置绑定。任何事件订阅还会导致一个移除 `Action` 添加到一个全局列表(针对所有使用 `XmlBindingApplicator` 实例绑定的视图)和一个局部列表。当源路径中的一个对象被替换时,这些局部列表就会生效,此时会调用局部移除操作列表中的所有操作,并重新构建绑定。当 `Activity` 取消绑定到 `View` 时,`XmlBindingApplicator` 会调用全局列表中所有的移除操作。
列表 6:BindingApplicator.Bind 方法
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);
}
}
}
对于 `TwoWay` 绑定,当触发 `View` 事件时,它会调用 `ViewValueChangedHandler` 类的 `HandleViewValueChanged` 方法。此方法使用 `newValueFunc` 来检索原始值(在应用任何 `IValueConverters` 之前),如下所示。
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;
}
}
`UpdateSourceProperty` 方法应用 `IValueConverter`(如果存在),并将结果值推送到源属性,如下所示:
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);
}
结论
在本文中,您看到了实现 Xamarin.Android 数据绑定系统的一种方法。该系统不依赖于第三方基础结构,使您能够快速构建应用程序布局,而无需花费过多时间编写基础代码。该绑定系统允许在绑定中指定视图的 `ChangedEvent`,并且该系统包含一个视图绑定器扩展机制。该系统支持绑定到源属性、方法和命令,并支持 `OneTime`、`OneWay` 和 `TwoWay` 绑定模式。
我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。
历史
2015 年 1 月
- 首次发布。