概觀

作為 .NET for Android 建置的一部分,Android 資源會被處理,透過產生的組合語言公開 _Microsoft.Android.Resource.Designer.dll。 例如,給定檔案內容 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;}
  }
}

傳統上,與資源的互動通常在程式碼中完成,並使用Resource的型別和FindViewById<T>()方法的常數:

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 屬性可設定為 True msbuild 命令列中的任一:

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);
  }
}

綁定的基類型 Xamarin.Android.Design.LayoutBinding並不屬於 .NET for Android 類別庫的一部分,而是以原始碼形式隨 .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);
  }
}

當找不到 a OnLayoutItemNotFound() 或 a View 的資源 ID 時,會呼叫此Fragment方法。

處理器 必須 回傳 null,在這種情況下會拋出 InvalidOperationException,或更佳的做法是回傳與傳給處理器的 ID 對應的 ViewFragment 實例。 回傳的物件 必須 是正確型別,且與相應的 Binding 屬性類型相符。 回傳的值會被轉換成該類型,因此如果物件未被正確類型化,會拋出一個例外狀況。

Code-Behind

Code-Behind 涉及建置時間產生 partial 一個類別,該類別包含 layout 檔案中所有 id 的強型別屬性。

Code-Behind 建立在綁定機制之上,並且需要版面檔案透過新的 xamarin:classes XML 屬性「選擇加入」以進行 Code-Behind 產生。該屬性是一個以 ; 分隔的完整類別名稱清單。

給定 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# 編譯錯誤。

自訂

產生的 Code Behind 型別 總是 覆蓋 Activity.SetContentView(),且在預設下 總是 呼叫 base.SetContentView(),並轉發參數。 若不希望如此,則應重寫其中一個OnSetContentView()partial方法,設為callBaseAfterReturnfalse

// 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 屬性

許多新的 Layout XML 屬性控制綁定與後端代碼行為,這些屬性位於 xamarin XML 命名空間(xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools")。 這些包括:

xamarin:classes

xamarin:classes XML 屬性作為 Code-Behind 的一部分,用來指定應產生哪些類型。

xamarin:classes XML 屬性包含以;分隔的完整類別名稱清單,應予以產生。

xamarin:managedType

xamarin:managedType layout 屬性用來明確指定要暴露綁定 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.

然而,如果上述嘗試失敗,你需要修改使用該未映射型別的小工具的版面,將 xamarin XML 命名空間宣告加入版面的根元素,並將 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 小工具實現:

這些類別 彼此不 相容,因此在產生 layout 檔案中元素的綁定程式碼 <fragment> 時必須特別小心。 如果該Fragment元素沒有指定特定類型(或受管理型別),.NET for Android 必須選擇一個<fragment>實作作為預設實作。 使用綁定碼產生器 $(AndroidFragmentType) MSBuild 的屬性是為了這個目的。 使用者可以覆寫該屬性,以指定與預設不同的類型。 該屬性預設值為Android.App.Fragment,並被AndroidX NuGet套件覆蓋。

若產生的程式碼無法建置,則必須修改版面檔案,指定該片段的管理型態。

程式碼後方版面選擇與處理

選擇

預設情況下,程式碼背後產生是被禁用的。 若要啟用任何目錄中至少包含該Resource\layout*屬性元素的版面配置//*/@android:id的處理,請在 msbuild 命令列中將 MSBuild 屬性設$(AndroidGenerateLayoutBindings)True以下任一:

dotnet build -p:AndroidGenerateLayoutBindings=true MyProject.csproj

或者在你的 .csproj 檔案中:

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

或者,你也可以在全域範圍內禁用「程式碼後置」功能,僅對特定檔案啟用它。 要啟用特定 .axml 檔案的 Code-Behind,請將該檔案的 編譯動作 改為 @(AndroidBoundLayout) 透過編輯你的 .csproj 檔案並替換 AndroidResourceAndroidBoundLayout

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

加工業

版面依名稱分組,來自 不同Resource\layout* 目錄的同名範本組成一個群組。 這些群組被當作單一佈局來處理。 在這種情況下,不同配置中屬於同一群組的兩個小工具之間可能會產生型別衝突。 在這種情況下,產生的屬性無法擁有完全相同的元件類型,而是「衰減型」的類型。 衰減演算法如下:

  1. 若所有衝突的小工具皆為 View 衍生函數,則屬性類型為 Android.Views.View

  2. 若所有衝突類型皆為 Fragment 衍生型,則性質類型為 Android.App.Fragment

  3. 若衝突的元件同時包含 a ViewFragment,則屬性型態為 global::System.Object

產生的程序代碼

如果你對生成的程式碼如何對應你的版面配置感興趣,請查看解決方案目錄中的 obj\$(Configuration)\generated 資料夾。