Aperçu

Dans le cadre de la build .NET pour Android, les ressources Android sont traitées, exposant des ID Android via un assembly généré _Microsoft.Android.Resource.Designer.dll . Par exemple, étant donné le fichier Reources\layout\Main.axml avec le contenu :

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

Ensuite, pendant la compilation, un assembly avec un contenu _Microsoft.Android.Resource.Designer.dll similaire à :

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

Traditionnellement, l’interaction avec les ressources est effectuée dans le Resource code, à l’aide des constantes du type et de la FindViewById<T>() méthode :

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

À compter de Xamarin.Android 8.4, il existe deux façons supplémentaires d’interagir avec les ressources Android lors de l’utilisation de C# :

  1. Liaisons
  2. Code-Behind

Pour activer ces nouvelles fonctionnalités, configurez la $(AndroidGenerateLayoutBindings) Propriété MSBuild à définir dans la ligne de commande MSBuild :

dotnet build -p:AndroidGenerateLayoutBindings=true MyProject.csproj

ou dans votre fichier .csproj :

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

Liaisons

Une liaison est une classe générée, une par fichier de mise en page Android, qui contient des propriétés strictement typées pour tous les ids dans le fichier de mise en page. Les types de liaison sont générés dans l’espace de noms global::Bindings, et leurs noms de types reflètent le nom du fichier de mise en page.

Les types de liaison sont créés pour tous les fichiers de mise en page qui contiennent des ID Android.

Étant donné le fichier Resources\layout\Main.axml de layout Android :

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

ensuite, le type suivant sera généré :

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

Le type de base de la liaison ne Xamarin.Android.Design.LayoutBinding fait pas partie de la bibliothèque de classes .NET pour Android, mais plutôt fourni avec .NET pour Android sous forme source et inclus automatiquement dans la build de l’application chaque fois que des liaisons sont utilisées.

Le type de liaison généré peut être créé autour des instances Activity, ce qui permet un accès fortement typé aux ID dans le fichier de mise en page :

// 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!";
    };
  }
}

Les types de liaison peuvent également être construits autour des instances View, ce qui permet un accès fortement typé aux ID de ressources au sein de la vue ou de ses enfants.

var binding = new Binding.Main (some_view);

ID de ressource manquant

Les propriétés sur les types de liaison utilisent toujours FindViewById<T>() dans leur implémentation. Si FindViewById<T>() retourne null, le comportement par défaut est que la propriété lève un InvalidOperationException au lieu de renvoyer null.

Ce comportement par défaut peut être remplacé en transmettant la délégation d'un gestionnaire d'erreurs à la liaison générée lors de son instanciation.

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

La OnLayoutItemNotFound() méthode est appelée lorsqu’un ID de ressource pour un View ou un objet Fragment est introuvable.

Le gestionnaire doit retourner soit null, auquel cas le InvalidOperationException sera levé ou, de préférence, renvoyer l’instance View ou Fragment qui correspond à l’ID passé au gestionnaire. L’objet retourné doit être du type correct correspondant au type de la propriété Binding correspondante. La valeur retournée est convertie en ce type. Par conséquent, si l’objet n’est pas correctement typé, une exception est levée.

code sous-jacent

Code-Behind implique la génération lors de la compilation d’une partial classe qui contient des propriétés fortement typées pour tous les identifiants dans le fichier de mise en page.

Code-Behind s'appuie sur le mécanisme de liaison, tout en exigeant que les fichiers de mise en page "optent pour" la génération avec Code-Behind en utilisant le nouvel attribut XML xamarin:classes, qui doit contenir une liste séparée par des virgules de noms complets de classes à générer.

Étant donné le fichier de mise en page 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>

au moment de la compilation, le type suivant sera produit :

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

Cela permet une utilisation plus « intuitive » des ID de ressources dans la disposition :

// 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!";
    };
  }
}

Le OnLayoutItemNotFound gestionnaire d'erreurs peut être passé comme dernier paramètre de toute surcharge de SetContentView que l'activité utilise.

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

Comme Code-Behind s’appuie sur des classes partielles, toutes les déclarations d’une classe partielle doivent être utilisées partial class dans leur déclaration, sinon une erreur du compilateur CS0260 C# sera générée au moment de la génération.

Personnalisation

Le type Code Behind généré remplace Activity.SetContentView() , et par défaut, il appelle base.SetContentView() , en transférant les paramètres. Si ce n’est pas souhaité, l’une des OnSetContentView()partial méthodes doit être remplacée, en définissant callBaseAfterReturn sur 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);
  }
}

Exemple de code généré

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

Attributs XML de disposition

De nombreux nouveaux attributs XML de disposition contrôlent la liaison et le comportement du code-behind, qui se trouvent dans l'espace de noms XML xamarin (xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools"). Voici quelques-uns des éléments suivants :

xamarin:classes

L’attribut xamarin:classes XML est utilisé dans le cadre de Code-Behind pour spécifier les types à générer.

L’attribut xamarin:classes XML contient une liste ;-séparée des noms de classes complets qui doivent être générés.

xamarin:managedType

L’attribut xamarin:managedType de disposition est utilisé pour spécifier explicitement le type managé pour exposer l’ID lié comme. S’il n’est pas spécifié, le type sera déduit du contexte déclarant, par exemple <Button/> résultera en un Android.Widget.Button, et <fragment/> résultera en un Android.App.Fragment.

L’attribut xamarin:managedType permet d’obtenir des déclarations de type plus explicites.

Mappage de type managé

Il est très courant d’utiliser des noms de widgets basés sur le package Java dont ils proviennent et, aussi souvent, le nom .NET managé de ce type aura un autre nom (style .NET) dans le terrain managé. Le générateur de code peut effectuer plusieurs ajustements très simples pour essayer de faire correspondre le code, par exemple :

  • Mettre en majuscule tous les composants de l’espace de noms et du nom de type. Par exemple java.package.myButton , devenir Java.Package.MyButton

  • Mettre en majuscule les composants à deux lettres de l’espace de noms de type. Par exemple android.os.SomeType , devenir Android.OS.SomeType

  • Recherchez un certain nombre d’espaces de noms codés en dur qui ont des mappages connus. Actuellement, la liste inclut les mappages suivants :

    • android.view ->Android.Views
    • com.actionbarsherlock ->ABSherlock
    • com.actionbarsherlock.widget ->ABSherlock.Widget
    • com.actionbarsherlock.view ->ABSherlock.View
    • com.actionbarsherlock.app ->ABSherlock.App
  • Recherchez un certain nombre de types codés en dur dans des tables internes. Actuellement, la liste comprend les types suivants :

  • Nombre de préfixes d’espace de noms codés en dur. Actuellement, la liste inclut les préfixes suivants :

    • com.google.

Si, toutefois, les tentatives ci-dessus échouent, vous devez modifier la disposition qui utilise un widget avec un tel type non mappé pour ajouter la xamarin déclaration d’espace de noms XML à l’élément racine de la disposition et à xamarin:managedType l’élément nécessitant le mappage. Par exemple:

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

Va utiliser le type CommonSampleLibrary.LogFragment pour le type natif commonsamplelibrary.LogFragment.

Vous pouvez éviter d’ajouter la déclaration d’espace de noms XML et l’attribut xamarin:managedType en nommant simplement le type à l’aide de son nom managé, par exemple le fragment ci-dessus peut être redéclaré comme suit :

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

Fragments : un cas spécial

L’écosystème Android prend actuellement en charge deux implémentations distinctes du Fragment widget :

Ces classes ne sont pas compatibles les unes avec les autres, et un soin particulier doit donc être pris lors de la génération de code de liaison pour <fragment> les éléments dans les fichiers de disposition. .NET pour Android doit choisir une Fragment implémentation comme implémentation par défaut à utiliser si l’élément <fragment> n’a pas de type spécifique (géré ou autre) spécifié. Le générateur de code de liaison utilise le $(AndroidFragmentType) Propriété MSBuild à cet effet. La propriété peut être remplacée par l’utilisateur pour spécifier un type différent de celui par défaut. La propriété est définie sur Android.App.Fragment par défaut et est écrasée par les packages NuGet AndroidX.

Si le code généré ne se compile pas, le fichier de mise en page doit être modifié en spécifiant le type géré du fragment en question.

Sélection et traitement de la disposition du code-behind

Sélection

Par défaut, la génération de code-behind est désactivée. Pour activer le traitement pour tous les agencements dans n'importe lequel des Resource\layout* répertoires contenant au moins un élément avec l'attribut //*/@android:id, définissez la propriété MSBuild $(AndroidGenerateLayoutBindings) sur True dans la ligne de commande msbuild :

dotnet build -p:AndroidGenerateLayoutBindings=true MyProject.csproj

ou dans votre fichier .csproj :

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

Vous pouvez également laisser le code-behind désactivé globalement et l’activer uniquement pour des fichiers spécifiques. Pour activer Code-Behind pour un fichier particulier .axml , modifiez le fichier pour qu’il dispose d’une action de génération de @(AndroidBoundLayout) en modifiant le fichier .csproj et en remplaçant le AndroidResource par AndroidBoundLayout:

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

Traitement

Les mises en page sont regroupées par nom, avec des modèles portant le même nom provenant de différents répertoires et formant ainsi un seul groupe. Ces groupes sont traités comme s’ils étaient une disposition unique. Dans ce cas, il est possible qu’il y ait un conflit de type entre deux widgets trouvés dans des dispositions différentes appartenant au même groupe. Dans ce cas, la propriété générée ne sera pas en mesure d’avoir le type de widget exact, mais plutôt un type de widget « décomposé ». La décomposition suit l’algorithme ci-dessous :

  1. Si tous les widgets en conflit sont View des dérivés, le type de propriété sera Android.Views.View

  2. Si tous les types en conflit sont Fragment des dérivés, le type de propriété sera Android.App.Fragment

  3. Si les widgets en conflit contiennent à la fois un View et un Fragment, le type de propriété sera global::System.Object

Code généré

Si vous êtes intéressé par à quoi ressemble le code généré pour vos mises en page, veuillez consulter le dossier obj\$(Configuration)\generated dans votre répertoire de solution.