65.9K
CodeProject 正在变化。 阅读更多。
Home

为 Xamarin.Android 构建数据绑定库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (19投票s)

2016 年 1 月 10 日

BSD

11分钟阅读

viewsIcon

52583

downloadIcon

699

一个强大而轻量级的数据绑定库,用于绑定 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` 的值,那么 `Convert` 方法将创建一个 `BindableListAdapter`。回想一下,`ConverterParameter` 是指向布局文件 ID 的路径。这由 `BindableListAdapter` 在其 `GetView` 方法中使用。

此方法中有大量的反射操作。因此,为了性能起见,布局 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 月

  • 首次发布。
© . All rights reserved.