Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
609 lines (470 sloc) 21.3 KB
id title dateupdated
11763499-79e9-4868-83e6-41f3061745d1
Layout CodeBehind
2018-05-14

Overview

The Xamarin.Android build processes Android resources, exposing Android IDs via a generated Resource.designer.cs file. For example, given the file Reources\layout\Main.axml with contents:

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

Then during build-time a Resource.designer.cs file will be generated:

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

Traditionally, interacting with Resources would be done in code, using the constants from the Resource type and the FindViewById<T>() method:

class MainActivity : Activity {

  // Code omitted for brevity

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

Starting with Xamarin.Android 8.4, there are two additional ways to interact with Android resources when using C#:

  1. Bindings
  2. Code-Behind

To enable these new features, set the $(AndroidGenerateLayoutBindings) MSBuild property to True either on the msbuild command line:

msbuild /p:AndroidGenerateLayoutBindings=true MyProject.csproj

or in your .csproj file:

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

Bindings

A binding is a generated class, one per Android layout file, which contains strongly typed properties for all of the ids within the layout file. Binding types are generated into the global::Bindings namespace, with type names which mirror the filename of the layout file.

Binding types are created for all layout files which contain any Android IDs.

Given the Android Layout file 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>

then following type will be generated:

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

The binding's base type, Xamarin.Android.Design.LayoutBinding is not part of the Xamarin.Android class library but rather shipped with Xamarin.Android in source form and included in the application's build automatically whenever bindings are used.

The generated binding type can be created around Activity instances, allowing for strongly-typed access to IDs within the layout file:

// User-written code
class MainActivity : Activity {

  // Code omitted for brevity

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

Binding types may also be constructed around View instances, allowing strongly-typed access to Resource IDs within the View or its children:

var binding = new Binding.Main (some_view);

Missing Resource IDs

Properties on binding types still use FindViewById<T>() in their implementation. If FindViewById<T>() returns null, then the default behavior is for the property to throw an InvalidOperationException instead of returning null.

This default behavior may be overridden by passing an error handler delegate to the generated binding on its instantiation:

// User-written code
class MainActivity : Activity {

  // Code omitted for brevity

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

The OnLayoutItemNotFound() method is invoked when a resource ID for a View or a Fragment could not be found.

The handler must return either null, in which case the InvalidOperationException will be thrown or, preferably, return the View or Fragment instance that corresponds to the ID passed to the handler. The returned object must be of the correct type matching the type of the corresponding Binding property. The returned value is cast to that type, so if the object isn't correctly typed an exception will be thrown.

Code-Behind

Code-Behind involves build-time generation of a partial class which contains strongly typed properties for all of the ids within the layout file.

Code-Behind builds atop the Binding mechanism, while requiring that layout files "opt-in" to Code-Behind generation by using the new xamarin:classes XML attribute, which is a ;-separated list of full class names to be generated.

Given the Android Layout file 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>

at build time the following type will be produced:

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

This allows for more "intuitive" use of Resource IDs within the 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!";
    };
  }
}

The OnLayoutItemNotFound error handler can be passed as the last parameter of whatever overload of SetContentView the activity is using:

// User-written code
Java.Lang.Object OnLayoutItemNotFound (int resourceId, Type expectedViewType)
{
  // Find and return the View or Fragment identified by `resourceId`
  // or `null` if unknown
  return null;
}
  
partial class MainActivity : Activity {
  protected override void OnCreate (Bundle savedInstanceState)
  {
    base.OnCreate (savedInstanceState);

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

As Code-Behind relies on partial classes, all declarations of a partial class must use partial class in their declaration, otherwise a CS0260 C# compiler error will be generated at build time.

Customization

The generated Code Behind type always overrides Activity.SetContentView(), and by default it always calls base.SetContentView(), forwarding the parameters. If this is not desired, then one of the OnSetContentView() partial methods should be overridden, setting callBaseAfterReturn to 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);
  }
}

Example Generated Code

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

Layout XML Attributes

Many new Layout XML attributes control Binding and Code-Behind behavior, which are within the xamarin XML namespace (xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools"). These include:

xamarin:classes

The xamarin:classes XML attribute is used as part of Code-Behind to specify which types should be generated.

The xamarin:classes XML attribute contains a ;-separated list of full class names that should be generated.

xamarin:managedType

The xamarin:managedType layout attribute is used to explicitly specify the managed type to expose the bound ID as. If not specified, the type will be inferred from the declaring context, e.g. <Button/> will result in an Android.Widget.Button, and <fragment/> will result in an Android.App.Fragment.

The xamarin:managedType attribute allows for more explicit type declarations.

Managed type mapping

It is quite common to use widget names based on the Java package they come from and, equally as often, the managed .NET name of such type will have a different (.NET style) name in the managed land. The code generator can perform a number of very simple adjustments to try to match the code, such as:

  • Capitalize all the components of the type namespace and name. For instance java.package.myButton would become Java.Package.MyButton

  • Capitalize two-letter components of the type namespace. For instance android.os.SomeType would become Android.OS.SomeType

  • Look up a number of hard-coded namespaces which have known mappings. Currently the list includes the following namespaces:

    • android.view -> Android.Views
    • android.support.wearable.view -> Android.Support.Wearable.Views
    • android.support.constraint -> Android.Support.Constraints
    • com.actionbarsherlock -> ABSherlock
    • com.actionbarsherlock.widget -> ABSherlock.Widget
    • com.actionbarsherlock.view -> ABSherlock.View
    • com.actionbarsherlock.app -> ABSherlock.App
  • Look up a number of hard-coded types in internal tables. Currently the list includes the following types:

    • WebView -> Android.Webkit.WebView
  • Strip number of hard-coded namespace prefixes. Currently the list includes the following prefixes:

    • com.google.

If, however, the above attempts fail, you will need to modify the layout which uses a widget with such an unmapped type to add both the xamarin XML namespace declaration to the root element of the layout and the xamarin:managedType to the element requiring the mapping. For instance:

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

Will use the CommonSampleLibrary.LogFragment type for the native type commonsamplelibrary.LogFragment.

You can avoid adding the XML namespace declaration and the xamarin:managedType attribute by simply naming the type using its managed name, for instance the above fragment could be redeclared as follows:

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

Fragments: a special case

The Android ecosystem currently supports two distinct implementations of the Fragment widget:

* Android.App.Fragment
  The "classic" Fragment shipped with the base Android system
* Android.Support.V4.App.Fragment

And in the near future, the AndroidX project will introduce the third type:

* Androidx.Fragment.App.Fragment

All three of those classes are not compatible with each other and so special care must be taken when generating binding code for <fragment> elements in the layout files. Xamarin.Android must choose one Fragment implementation as the default one to be used if the <fragment> element does not have any specific type (managed or otherwise) specified. Binding code generator uses the AndroidFragmentType MSBuild property for that purpose. The property can be overriden by the user to specify a type different than the default one. The property is set to Android.App.Fragment by default, unless overriden by the support libraries or the future AndroidX libraries.

If the generated code does not build, the layout file must be amended by specifying the manged type of the fragment in question.

Code-behind layout selection and processing

Selection

By default code-behind generation is disabled. To enable processing for all layouts in any of the Resource\layout* directories that contain at least a single element with the //*/@android:id attribute, set the $(AndroidGenerateLayoutBindings) MSBuild property to True either on the msbuild command line:

msbuild /p:AndroidGenerateLayoutBindings=true MyProject.csproj

or in your .csproj file:

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

Alternatively, you can leave code-behind disabled globally and enable it only for specific files. To enable Code-Behind for a particular .axml file, change the file to have a Build action of @(AndroidBoundLayout) by editing your .csproj file and replacing AndroidResource with AndroidBoundLayout:

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

Processing

Layouts are grouped by name, with like-named templates from different Resource\layout* directories comprising a single group. Such groups are processed as if they were a single layout. It is possible that in such case there will be a type clash between two widgets found in different layouts belonging to the same group. In such case the generated property will not be able to have the exact widget type, but rather a "decayed" one. Decaying follows the algorithm below:

  1. If all of the conflicting widgets are View derivatives, the property type will be Android.Views.View

  2. If all of the conflicting types are Fragment derivatives, the property type will be Android.App.Fragment

  3. If the conflicting widgets contain both a View and a Fragment, the property type will be global::System.Object

Generated code

If you are interested in how the generated code looks for your layouts, please take a look in the obj\$(Configuration)\generated folder in your solution directory.