概述

作为适用于 Android 版本的 .NET 的一部分,将处理 Android 资源,并通过生成的_Microsoft.Android.Resource.Designer.dll程序集公开 Android ID。 例如,给定包含内容的文件 Reources\layout\Main.axml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android">
  <Button android:id="@+id/myButton" />
  <fragment
      android:id="@+id/log_fragment"
      android:name="commonsamplelibrary.LogFragment"
  />
  <fragment
      android:id="@+id/secondary_log_fragment"
      android:name="CommonSampleLibrary.LogFragment"
  />
</LinearLayout>

然后在生成期间 _Microsoft.Android.Resource.Designer.dll ,包含类似于以下内容的程序集:

namespace _Microsoft.Android.Resource.Designer;

partial class Resource {
  partial class Id {
    public static int myButton               {get;}
    public static int log_fragment           {get;}
    public static int secondary_log_fragment {get;}
  }
  partial class Layout {
    public static int Main                   {get;}
  }
}

传统上,使用类型和FindViewById<T>()方法中的常量Resource在代码中执行与资源交互:

partial class MainActivity : Activity {

  protected override void OnCreate (Bundle savedInstanceState)
  {
    base.OnCreate (savedInstanceState);
    SetContentView (Resource.Layout.Main);
    Button button = FindViewById<Button>(Resource.Id.myButton);
    button.Click += delegate {
        button.Text = $"{count++} clicks!";
    };
  }
}

从 Xamarin.Android 8.4 开始,使用 C# 时,还有其他两种方法可以与 Android 资源进行交互:

  1. 绑定
  2. 代码隐藏

若要启用这些新功能,请设置 $(AndroidGenerateLayoutBindings) 在 msbuild 命令行上将 MSBuild 属性设置为 True 以下任一:

dotnet build -p:AndroidGenerateLayoutBindings=true MyProject.csproj

或在 .csproj 文件中:

<PropertyGroup>
    <AndroidGenerateLayoutBindings>true</AndroidGenerateLayoutBindings>
</PropertyGroup>

绑定

绑定是生成的类,每个 Android 布局文件一个,其中包含布局文件中所有 ID强类型属性。 绑定类型将生成到 global::Bindings 命名空间中,其类型名称反映了布局文件的文件名。

绑定类型是为包含任何 Android ID 的所有布局文件创建的。

给定 Android 布局文件 Resources\layout\Main.axml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools">
  <Button android:id="@+id/myButton" />
  <fragment
      android:id="@+id/fragmentWithExplicitManagedType"
      android:name="commonsamplelibrary.LogFragment"
      xamarin:managedType="CommonSampleLibrary.LogFragment"
  />
  <fragment
      android:id="@+id/fragmentWithInferredType"
      android:name="CommonSampleLibrary.LogFragment"
  />
</LinearLayout>

然后,将生成以下类型:

// Generated code
namespace Binding {
  sealed class Main : global::Xamarin.Android.Design.LayoutBinding {

    [global::Android.Runtime.PreserveAttribute (Conditional=true)]
    public Main (
      global::Android.App.Activity client,
      global::Xamarin.Android.Design.OnLayoutItemNotFoundHandler itemNotFoundHandler = null)
        : base (client, itemNotFoundHandler) {}

    [global::Android.Runtime.PreserveAttribute (Conditional=true)]
    public Main (
      global::Android.Views.View client,
      global::Xamarin.Android.Design.OnLayoutItemNotFoundHandler itemNotFoundHandler = null)
        : base (client, itemNotFoundHandler) {}

    Button __myButton;
    public Button myButton => FindView (global::Xamarin.Android.Tests.CodeBehindFew.Resource.Id.myButton, ref __myButton);

    CommonSampleLibrary.LogFragment __fragmentWithExplicitManagedType;
    public CommonSampleLibrary.LogFragment fragmentWithExplicitManagedType =>
      FindFragment (global::Xamarin.Android.Tests.CodeBehindFew.Resource.Id.fragmentWithExplicitManagedType, __fragmentWithExplicitManagedType, ref __fragmentWithExplicitManagedType);

    global::Android.App.Fragment __fragmentWithInferredType;
    public global::Android.App.Fragment fragmentWithInferredType =>
      FindFragment (global::Xamarin.Android.Tests.CodeBehindFew.Resource.Id.fragmentWithInferredType, __fragmentWithInferredType, ref __fragmentWithInferredType);
  }
}

绑定的基类型不是适用于 Android 类库的 .NET 的一部分,Xamarin.Android.Design.LayoutBinding而是随源形式的 .NET for Android 一起提供,并在使用绑定时自动包含在应用程序的生成中。

可以围绕 Activity 实例创建生成的绑定类型,从而允许对布局文件中的 ID 进行强类型访问:

// User-written code
partial class MainActivity : Activity {

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

    SetContentView (Resource.Layout.Main);
    var binding     = new Binding.Main (this);
    Button button   = binding.myButton;
    button.Click   += delegate {
        button.Text = $"{count++} clicks!";
    };
  }
}

还可以围绕View实例构造绑定类型,从而允许对视图或其子级中的资源 ID 进行强类型访问:

var binding = new Binding.Main (some_view);

缺少资源 ID

绑定类型的属性仍在其实现中使用 FindViewById<T>() 。 如果 FindViewById<T>() 返回 null,则默认行为是使属性引发而不是 InvalidOperationException 返回 null

通过将错误处理程序委托传递给生成的绑定在其实例化时,可以重写此默认行为:

// User-written code
partial class MainActivity : Activity {

  Java.Lang.Object? OnLayoutItemNotFound (int resourceId, Type expectedViewType)
  {
     // Find and return the View or Fragment identified by `resourceId`
     // or `null` if unknown
     return null;
  }

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

    SetContentView (Resource.Layout.Main);
    var binding     = new Binding.Main (this, OnLayoutItemNotFound);
  }
}

OnLayoutItemNotFound()找不到某个View资源 ID 或Fragment找不到资源 ID 时,将调用该方法。

处理程序必须返回,null在这种情况下,InvalidOperationException将引发或最好返回View与传递给处理程序的 ID 对应的或Fragment实例。 返回的对象 必须是 与相应 Binding 属性的类型匹配的正确类型。 返回的值将强制转换为该类型,因此,如果对象未正确键入,将引发异常。

代码隐藏

代码隐藏涉及生成类partial的生成时间,该类包含布局文件中所有 ID强类型属性。

代码隐藏在绑定机制之上生成,同时要求布局文件使用新的 xamarin:classes XML 属性“选择加入”到代码隐藏生成,这是 ;要生成的完整类名的分隔列表。

给定 Android 布局文件 Resources\layout\Main.axml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools"
    xamarin:classes="Example.MainActivity">
  <Button android:id="@+id/myButton" />
  <fragment
      android:id="@+id/fragmentWithExplicitManagedType"
      android:name="commonsamplelibrary.LogFragment"
      xamarin:managedType="CommonSampleLibrary.LogFragment"
  />
  <fragment
      android:id="@+id/fragmentWithInferredType"
      android:name="CommonSampleLibrary.LogFragment"
  />
</LinearLayout>

在生成时,将生成以下类型:

// Generated code
namespace Example {
  partial class MainActivity {
    Binding.Main __layout_binding;

    public override void SetContentView (global::Android.Views.View view);
    void SetContentView (global::Android.Views.View view,
                         global::Xamarin.Android.Design.LayoutBinding.OnLayoutItemNotFoundHandler onLayoutItemNotFound);

    public override void SetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params);
    void SetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params,
                         global::Xamarin.Android.Design.LayoutBinding.OnLayoutItemNotFoundHandler onLayoutItemNotFound);

    public override void SetContentView (int layoutResID);
    void SetContentView (int layoutResID,
                         global::Xamarin.Android.Design.LayoutBinding.OnLayoutItemNotFoundHandler onLayoutItemNotFound);

    partial void OnSetContentView (global::Android.Views.View view, ref bool callBaseAfterReturn);
    partial void OnSetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params, ref bool callBaseAfterReturn);
    partial void OnSetContentView (int layoutResID, ref bool callBaseAfterReturn);

    public Button myButton => __layout_binding?.myButton;
    public CommonSampleLibrary.LogFragment fragmentWithExplicitManagedType => __layout_binding?.fragmentWithExplicitManagedType;
    public global::Android.App.Fragment fragmentWithInferredType => __layout_binding?.fragmentWithInferredType;
  }
}

这允许在布局中更“直观”地使用资源 ID:

// User-written code
partial class MainActivity : Activity {
  protected override void OnCreate (Bundle savedInstanceState)
  {
    base.OnCreate (savedInstanceState);

    SetContentView (Resource.Layout.Main);

    myButton.Click += delegate {
        button.Text = $"{count++} clicks!";
    };
  }
}

可以将 OnLayoutItemNotFound 错误处理程序作为活动重载 SetContentView 的最后一个参数传递:

// User-written code
partial class MainActivity : Activity {
  protected override void OnCreate (Bundle savedInstanceState)
  {
    base.OnCreate (savedInstanceState);

    SetContentView (Resource.Layout.Main, OnLayoutItemNotFound);
  }

  Java.Lang.Object? OnLayoutItemNotFound (int resourceId, Type expectedViewType)
  {
    // Find and return the View or Fragment identified by `resourceId`
    // or `null` if unknown
    return null;
  }
}

由于 Code-Behind 依赖于分部类,因此分部类的所有声明都必须在其声明中使用partial class,否则将在生成时生成 CS0260 C# 编译器错误。

自定义

生成的代码隐藏类型 始终 重写 Activity.SetContentView(),默认情况下,它 始终 调用 base.SetContentView(),转发参数。 如果不需要,则应重写其中OnSetContentView()partial一种方法,设置为:callBaseAfterReturn false

// Generated code
namespace Example
{
  partial class MainActivity {
    partial void OnSetContentView (global::Android.Views.View view, ref bool callBaseAfterReturn);
    partial void OnSetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params, ref bool callBaseAfterReturn);
    partial void OnSetContentView (int layoutResID, ref bool callBaseAfterReturn);
  }
}

生成代码示例

// Generated code
namespace Example
{
  partial class MainActivity {

    Binding.Main? __layout_binding;

    public override void SetContentView (global::Android.Views.View view)
    {
      __layout_binding = new global::Binding.Main (view);
      bool callBase = true;
      OnSetContentView (view, ref callBase);
      if (callBase) {
        base.SetContentView (view);
      }
    }

    void SetContentView (global::Android.Views.View view, global::Xamarin.Android.Design.LayoutBinding.OnLayoutItemNotFoundHandler onLayoutItemNotFound)
    {
      __layout_binding = new global::Binding.Main (view, onLayoutItemNotFound);
      bool callBase = true;
      OnSetContentView (view, ref callBase);
      if (callBase) {
        base.SetContentView (view);
      }
    }

    public override void SetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params)
    {
      __layout_binding = new global::Binding.Main (view);
      bool callBase = true;
      OnSetContentView (view, @params, ref callBase);
      if (callBase) {
        base.SetContentView (view, @params);
      }
    }

    void SetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params, global::Xamarin.Android.Design.LayoutBinding.OnLayoutItemNotFoundHandler onLayoutItemNotFound)
    {
      __layout_binding = new global::Binding.Main (view, onLayoutItemNotFound);
      bool callBase = true;
      OnSetContentView (view, @params, ref callBase);
      if (callBase) {
        base.SetContentView (view, @params);
      }
    }

    public override void SetContentView (int layoutResID)
    {
      __layout_binding = new global::Binding.Main (this);
      bool callBase = true;
      OnSetContentView (layoutResID, ref callBase);
      if (callBase) {
        base.SetContentView (layoutResID);
      }
    }

    void SetContentView (int layoutResID, global::Xamarin.Android.Design.LayoutBinding.OnLayoutItemNotFoundHandler onLayoutItemNotFound)
    {
      __layout_binding = new global::Binding.Main (this, onLayoutItemNotFound);
      bool callBase = true;
      OnSetContentView (layoutResID, ref callBase);
      if (callBase) {
        base.SetContentView (layoutResID);
      }
    }

    partial void OnSetContentView (global::Android.Views.View view, ref bool callBaseAfterReturn);
    partial void OnSetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params, ref bool callBaseAfterReturn);
    partial void OnSetContentView (int layoutResID, ref bool callBaseAfterReturn);

    public  Button                          myButton                         => __layout_binding?.myButton;
    public  CommonSampleLibrary.LogFragment fragmentWithExplicitManagedType  => __layout_binding?.fragmentWithExplicitManagedType;
    public  global::Android.App.Fragment    fragmentWithInferredType         => __layout_binding?.fragmentWithInferredType;
  }
}

布局 XML 属性

许多新的布局 XML 属性控制绑定和代码隐藏行为,这些行为位于 xamarin XML 命名空间(xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools")。 这些设置包括:

xamarin:classes

xamarin:classes XML 属性用作代码隐藏一部分,用于指定应生成哪些类型。

xamarin:classes XML 属性包含;应生成的完整类名分隔列表。

xamarin:managedType

布局 xamarin:managedType 属性用于显式指定托管类型以公开绑定 ID。 如果未指定,将从声明上下文推断类型,例如 <Button/> 将生成一个 Android.Widget.Button类型,并 <fragment/> 生成一个 Android.App.Fragment

xamarin:managedType 属性允许更显式的类型声明。

托管类型映射

通常,根据小组件名称使用它们来自的 Java 包,同样通常,此类类型的托管 .NET 名称在托管土地上具有不同的 (.NET 样式) 名称。 代码生成器可以执行许多非常简单的调整来尝试匹配代码,例如:

  • 将类型命名空间和名称的所有组件大写。 例如 java.package.myButton ,将变为 Java.Package.MyButton

  • 将类型命名空间的双字母组件大写。 例如 android.os.SomeType ,将变为 Android.OS.SomeType

  • 查找许多具有已知映射的硬编码命名空间。 目前列表包括以下映射:

    • android.view ->Android.Views
    • com.actionbarsherlock ->ABSherlock
    • com.actionbarsherlock.widget ->ABSherlock.Widget
    • com.actionbarsherlock.view ->ABSherlock.View
    • com.actionbarsherlock.app ->ABSherlock.App
  • 在内部表中查找许多硬编码类型。 目前,列表包括以下类型:

  • 硬编码命名空间 前缀的条带编号。 目前,列表包含以下前缀:

    • com.google.

但是,如果上述尝试失败,则需要修改使用具有此类未映射类型的小组件将 XML 命名空间声明添加到 xamarin 布局的根元素和 xamarin:managedType 需要映射的元素的布局。 例如:

<fragment
    android:id="@+id/log_fragment"
    android:name="commonsamplelibrary.LogFragment"
    xamarin:managedType="CommonSampleLibrary.LogFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
/>

将类型 CommonSampleLibrary.LogFragment 用于本机类型 commonsamplelibrary.LogFragment

可以通过简单地使用其托管名称命名类型来避免添加 XML 命名空间声明和 xamarin:managedType 属性,例如,可以按如下所示重新声明上述片段:

<fragment
    android:name="CommonSampleLibrary.LogFragment"
    android:id="@+id/secondary_log_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
/>

片段:特殊情况

Android 生态系统目前支持小组件的两个不同的 Fragment 实现:

这些类彼此不兼容,因此在为<fragment>布局文件中的元素生成绑定代码时必须格外小心。 如果元素没有指定任何特定类型(托管或其他类型),则适用于 Android 的 <fragment> .NET 必须选择一个Fragment实现作为要使用的默认实现。 绑定代码生成器使用 $(AndroidFragmentType) 用于该目的的 MSBuild 属性。 用户可以重写该属性,以指定与默认类型不同的类型。 该属性默认设置为 Android.App.Fragment ,并由 AndroidX NuGet 包重写。

如果生成的代码未生成,则必须通过指定相关片段的托管类型来修改布局文件。

代码隐藏布局选择和处理

选择

默认情况下禁用代码隐藏生成。 若要为包含至少具有该属性的单个元素//*/@android:id的任何Resource\layout*目录中的所有布局启用处理,请将 MSBuild 属性设置为 $(AndroidGenerateLayoutBindings) True msbuild 命令行中的任意一个:

dotnet build -p:AndroidGenerateLayoutBindings=true MyProject.csproj

或在 .csproj 文件中:

<PropertyGroup>
  <AndroidGenerateLayoutBindings>true</AndroidGenerateLayoutBindings>
</PropertyGroup>

或者,可以全局禁用代码隐藏,并仅针对特定文件启用它。 若要为特定.axml文件启用代码隐藏,请将该文件更改为具有生成操作@(AndroidBoundLayout)通过编辑.csproj文件并将其替换为AndroidResourceAndroidBoundLayout

<!-- This -->
<AndroidResource Include="Resources\layout\Main.axml" />
<!-- should become this -->
<AndroidBoundLayout Include="Resources\layout\Main.axml" />

Processing

布局按名称分组,不同目录中的类似名称模板Resource\layout*由单个组组成。 此类组的处理方式就像是单个布局一样。 在这种情况下,在属于同一组的不同布局中,两个小组件之间存在类型冲突。 在这种情况下,生成的属性将不能具有确切的小组件类型,而是“衰减”小组件类型。 衰减遵循以下算法:

  1. 如果所有冲突的小组件都是 View 派生组件,则属性类型将为 Android.Views.View

  2. 如果所有冲突类型都是 Fragment 派生类型,则属性类型将为 Android.App.Fragment

  3. 如果冲突的小组件同时包含 a View 和 a Fragment,则属性类型将为 global::System.Object

生成的代码

如果对生成的代码如何查找布局感兴趣,请查看 obj\$(Configuration)\generated 解决方案目录中的文件夹。