Обзор

В рамках сборки .NET для Android ресурсы 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, существует два дополнительных способа взаимодействия с ресурсами Android при использовании C#:

  1. Привязки
  2. Код за пределами

Чтобы включить эти новые функции, задайте $(AndroidGenerateLayoutBindings) Свойство MSBuild в True командной строке msbuild:

dotnet build -p:AndroidGenerateLayoutBindings=true MyProject.csproj

или в вашем .csproj-файле:

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

Привязки

Привязка — это созданный класс, один на файл макета Android, который содержит строго типизированные свойства для всех идентификаторов в файле макета. Типы привязки создаются в пространстве имен global::Bindings с именами типов, которые соответствуют именам файлов макета.

Типы привязки создаются для всех файлов макета, содержащих все идентификаторы Android.

Учитывая файл макета 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 для Android, но поставляется с .NET для Android в виде исходного кода и включается в сборку приложения автоматически при каждом использовании привязок.

Созданный тип привязки может быть построен вокруг Activity экземпляров, что позволяет строго типизированный доступ к идентификаторам в файле макета.

// 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 экземпляров, позволяя строго типизированный доступ к идентификаторам ресурсов в представлении или его дочерних элементах:

var binding = new Binding.Main (some_view);

Отсутствующие идентификаторы ресурсов

Свойства типов привязки по-прежнему используют 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 или Fragment.

Обработчик должен возвращать либо null, в этом случае будет выброшено InvalidOperationException, или, предпочтительно, возвращать экземпляр View или Fragment, соответствующий идентификатору, переданному обработчику. Возвращаемый объект должен иметь правильный тип, соответствующий типу соответствующего свойства Binding. Возвращаемое значение приводится к такому типу, поэтому если объект неправильно типизирован, исключение будет выброшено.

Code-Behind

Code-Behind включает создание partial класса во время сборки, содержащего строго типизированные свойства для всех идентификаторов в файле макета .

Code-Behind основан на механизме привязки, требуя, чтобы файлы макета активировали генерацию Code-Behind, используя новый XML-атрибут xamarin:classes, который является списком, разделенным с помощью ;, полных имен классов, которые необходимо создать.

Учитывая файл Resources\layout\Main.axmlмакета Android:

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

Это позволяет использовать более интуитивно понятные идентификаторы ресурсов в макете:

// 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 в объявлении, в противном случае ошибка компилятора C# CS0260 будет создана во время сборки.

Настройка

Созданный тип кода всегда переопределяет 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-атрибуты макета управляют привязкой и Code-Behind поведением, которые находятся в 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 Атрибут макета используется для явного указания управляемого типа для предоставления привязанного идентификатора как. Если тип не указан, его будут выводить из декларирующего контекста, например, <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 мини-приложения:

Эти классы несовместимы друг с другом, поэтому при создании кода привязки для <fragment> элементов в файлах макета необходимо учитывать особое внимание. .NET для Android должен выбрать одну Fragment реализацию в качестве используемой по умолчанию, если <fragment> элемент не имеет определенного типа (управляемого или другого). Генератор кода привязки использует $(AndroidFragmentType) Свойство MSBuild для этой цели. Свойство может быть переопределено пользователем, чтобы указать тип, отличный от типа по умолчанию. Свойство задано Android.App.Fragment по умолчанию и переопределяется пакетами NuGet AndroidX.

Если созданный код не компилируется, файл макета следует изменить, указав управляемый тип фрагмента.

Выбор и обработка макета программной части

Отбор

По умолчанию генерация связанного программного кода отключена. Чтобы включить обработку всех макетов в любом из Resource\layout* каталогов, содержащих по крайней мере один элемент с атрибутом //*/@android:id, установите для свойства $(AndroidGenerateLayoutBindings) значение True на командной строке msbuild:

dotnet build -p:AndroidGenerateLayoutBindings=true MyProject.csproj

или в вашем .csproj-файле:

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

Кроме того, вы можете оставить код отключенным глобально и включить его только для определенных файлов. Чтобы включить Code-Behind для определенного .axml файла, измените файл на действие сборки @(AndroidBoundLayout) изменив .csproj файл и заменив AndroidResource на AndroidBoundLayout:

<!-- 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. Если конфликтующие мини-приложения содержат как View, так и Fragment, тип свойства будет global::System.Object

Созданный код

Если вам интересно, как выглядит сгенерированный код для ваших макетов, пожалуйста, загляните в папку obj\$(Configuration)\generated в каталоге вашего решения.