Condividi tramite


Panoramica

Nell'ambito della compilazione .NET per Android, le risorse Android vengono elaborate, esponendo gli ID Android tramite un assembly generato_Microsoft.Android.Resource.Designer.dll. Ad esempio, dato il file Reources\layout\Main.axml con contenuto:

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

Quindi durante la fase di compilazione un _Microsoft.Android.Resource.Designer.dll assembly con contenuto simile al seguente:

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

In genere, l'interazione con Le risorse viene eseguita nel codice, usando le costanti del Resource tipo e il FindViewById<T>() metodo :

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

A partire da Xamarin.Android 8.4, esistono due modi aggiuntivi per interagire con le risorse Android quando si usa C#:

  1. Bindings
  2. Code-behind

Per abilitare queste nuove funzionalità, impostare $(AndroidGenerateLayoutBindings) Proprietà MSBuild su True nella riga di comando msbuild:

dotnet build -p:AndroidGenerateLayoutBindings=true MyProject.csproj

o nel file con estensione csproj:

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

Bindings

Un'associazione è una classe generata, una per ogni file di layout Android, che contiene proprietà fortemente tipizzate per tutti gli ID all'interno del file di layout. I tipi di associazione vengono generati nello spazio dei global::Bindings nomi, con nomi di tipo che rispecchiano il nome file del file di layout.

I tipi di associazione vengono creati per tutti i file di layout che contengono tutti gli ID Android.

Dato il file Resources\layout\Main.axmldi 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>

verrà quindi generato il tipo seguente:

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

Il tipo Xamarin.Android.Design.LayoutBinding di base dell'associazione non fa parte della libreria di classi .NET per Android, ma viene fornito con .NET per Android nel formato di origine e incluso automaticamente nella compilazione dell'applicazione ogni volta che vengono usate le associazioni.

Il tipo di associazione generato può essere creato intorno alle Activity istanze, consentendo l'accesso fortemente tipizzato agli ID all'interno del file di layout:

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

I tipi di associazione possono anche essere costruiti intorno View alle istanze, consentendo l'accesso fortemente tipizzato agli ID risorsa all'interno della visualizzazione o dei relativi elementi figlio:

var binding = new Binding.Main (some_view);

ID risorsa mancanti

Le proprietà sui tipi di associazione usano FindViewById<T>() ancora nell'implementazione. Se FindViewById<T>() restituisce null, il comportamento predefinito è che la proprietà generi un oggetto InvalidOperationException anziché restituire null.

Questo comportamento predefinito può essere sottoposto a override passando un delegato del gestore errori all'associazione generata nella relativa creazione di istanze:

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

Il OnLayoutItemNotFound() metodo viene richiamato quando non è stato possibile trovare un ID risorsa per un View oggetto o .Fragment

Il gestore deve restituire null, nel qual caso InvalidOperationException verrà generata o, preferibilmente, restituire l'istanza View o Fragment che corrisponde all'ID passato al gestore. L'oggetto restituito deve essere del tipo corretto corrispondente al tipo della proprietà Binding corrispondente. Viene eseguito il cast del valore restituito a tale tipo, pertanto se l'oggetto non è digitato correttamente, verrà generata un'eccezione.

code-behind

Code-Behind prevede la generazione in fase di compilazione di una partial classe che contiene proprietà fortemente tipizzate per tutti gli ID all'interno del file di layout.

Code-Behind compila in cima al meccanismo di associazione, richiedendo al tempo stesso che i file di layout "acconsentino" alla generazione code-behind usando il nuovo xamarin:classes attributo XML, ovvero un ;elenco separato da un elenco di nomi di classe completi da generare.

Dato il file Resources\layout\Main.axmldi layout 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>

in fase di compilazione verrà prodotto il tipo seguente:

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

Ciò consente un uso più "intuitivo" degli ID risorsa all'interno del layout:

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

Il OnLayoutItemNotFound gestore degli errori può essere passato come ultimo parametro di qualsiasi overload dell'attività SetContentView in uso:

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

Poiché Code-Behind si basa su classi parziali, tutte le dichiarazioni di una classe parziale devono essere usate partial class nella dichiarazione. In caso contrario, verrà generato un errore del compilatore C# CS0260 in fase di compilazione.

Personalizzazione

Il tipo Code Behind generato esegue sempre l'override Activity.SetContentView()di e per impostazione predefinita chiama base.SetContentView()sempre , inoltrando i parametri. Se non si vuole, è necessario eseguire l'override di uno dei OnSetContentView()partial metodi, impostando callBaseAfterReturn su :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);
  }
}

Codice generato di esempio

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

Attributi XML di layout

Molti nuovi attributi XML layout controllano il comportamento Binding e Code-Behind, che si trovano all'interno dello spazio dei xamarin nomi XML (xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools"). tra cui:

xamarin:classes

L'attributo xamarin:classes XML viene usato come parte di Code-Behind per specificare quali tipi devono essere generati.

L'attributo xamarin:classes XML contiene un ;elenco separato da -di nomi di classi completi che devono essere generati.

xamarin:managedType

L'attributo xamarin:managedType layout viene usato per specificare in modo esplicito il tipo gestito per esporre l'ID associato come . Se non specificato, il tipo verrà dedotto dal contesto dichiarante, ad esempio <Button/> verrà generato un Android.Widget.Buttonoggetto e <fragment/> verrà generato un oggetto Android.App.Fragment.

L'attributo xamarin:managedType consente dichiarazioni di tipo più esplicite.

Mapping dei tipi gestiti

È piuttosto comune usare nomi di widget basati sul pacchetto Java da cui provengono e, altrettanto spesso, il nome .NET gestito di tale tipo avrà un nome diverso (stile.NET) nella terra gestita. Il generatore di codice può eseguire una serie di modifiche molto semplici per provare a trovare la corrispondenza con il codice, ad esempio:

  • Capitalizzare tutti i componenti dello spazio dei nomi e del nome del tipo. Ad esempio java.package.myButton , sarebbe diventato Java.Package.MyButton

  • In maiuscolo i componenti di due lettere dello spazio dei nomi del tipo. Ad esempio android.os.SomeType , sarebbe diventato Android.OS.SomeType

  • Cercare una serie di spazi dei nomi hardcoded con mapping noti. Attualmente l'elenco include i mapping seguenti:

    • android.view ->Android.Views
    • com.actionbarsherlock ->ABSherlock
    • com.actionbarsherlock.widget ->ABSherlock.Widget
    • com.actionbarsherlock.view ->ABSherlock.View
    • com.actionbarsherlock.app ->ABSherlock.App
  • Cercare diversi tipi hardcoded nelle tabelle interne. Attualmente l'elenco include i tipi seguenti:

  • Numero di prefissi dello spazio dei nomi hardcoded. Attualmente l'elenco include i prefissi seguenti:

    • com.google.

Se, tuttavia, i tentativi precedenti hanno esito negativo, sarà necessario modificare il layout che usa un widget con tale tipo non mappato per aggiungere sia la xamarin dichiarazione dello spazio dei nomi XML all'elemento radice del layout che xamarin:managedType all'elemento che richiede il mapping. Ad esempio:

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

Userà il CommonSampleLibrary.LogFragment tipo per il tipo commonsamplelibrary.LogFragmentnativo .

È possibile evitare di aggiungere la dichiarazione dello spazio dei nomi XML e l'attributo xamarin:managedType semplicemente denominando il tipo usando il relativo nome gestito, ad esempio il frammento precedente potrebbe essere dichiarato nuovamente come segue:

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

Frammenti: un caso speciale

L'ecosistema Android supporta attualmente due implementazioni distinte del Fragment widget:

Queste classi non sono compatibili tra loro e pertanto è necessario prestare particolare attenzione quando si genera codice di associazione per <fragment> gli elementi nei file di layout. .NET per Android deve scegliere un'implementazione Fragment come predefinita da usare se l'elemento <fragment> non ha alcun tipo specifico (gestito o in altro modo) specificato. Il generatore di codice di associazione usa il $(AndroidFragmentType) Proprietà MSBuild a tale scopo. La proprietà può essere sostituita dall'utente per specificare un tipo diverso da quello predefinito. La proprietà è impostata su per impostazione predefinita ed è sottoposta a Android.App.Fragment override dai pacchetti NuGet AndroidX.

Se il codice generato non viene compilato, il file di layout deve essere modificato specificando il tipo manged del frammento in questione.

Selezione ed elaborazione del layout code-behind

Selezione

Per impostazione predefinita, la generazione code-behind è disabilitata. Per abilitare l'elaborazione per tutti i layout in una delle Resource\layout* directory che contengono almeno un singolo elemento con l'attributo //*/@android:id , impostare la $(AndroidGenerateLayoutBindings) proprietà MSBuild su True nella riga di comando msbuild:

dotnet build -p:AndroidGenerateLayoutBindings=true MyProject.csproj

o nel file con estensione csproj:

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

In alternativa, è possibile lasciare il code-behind disabilitato a livello globale e abilitarlo solo per file specifici. Per abilitare Code-Behind per un file specifico.axml, modificare il file in modo che abbia un'azione di compilazione di@(AndroidBoundLayout) modificando il .csproj file e sostituendo AndroidResource con AndroidBoundLayout:

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

in lavorazione

I layout vengono raggruppati in base al nome, con modelli con nome simile a directory diverseResource\layout* che comprendono un singolo gruppo. Tali gruppi vengono elaborati come se fossero un singolo layout. È possibile che in questo caso si verifichi un conflitto di tipi tra due widget presenti in layout diversi appartenenti allo stesso gruppo. In questo caso, la proprietà generata non sarà in grado di avere il tipo di widget esatto, ma piuttosto una proprietà "decadita". Il decadimento segue l'algoritmo seguente:

  1. Se tutti i widget in conflitto sono View derivati, il tipo di proprietà sarà Android.Views.View

  2. Se tutti i tipi in conflitto sono Fragment derivati, il tipo di proprietà sarà Android.App.Fragment

  3. Se i widget in conflitto contengono sia un oggetto View che un Fragment, il tipo di proprietà sarà global::System.Object

Codice generato

Se si è interessati al modo in cui il codice generato cerca i layout, esaminare la cartella nella directory della obj\$(Configuration)\generated soluzione.