Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Xamarin.Android.Build.Tasks] Code-Behind, Take 2! (#1465)
The original code-behind approach in 7c31899 and d09b86a was deemed as "bad", for two primary reasons: 1. The "mirroring" of the `id` hierarchy into C# would result in "brittle" code, as moving elements in Layout `.axml` could result in semantic C# changes. 2. We thought that "code behind" shouldn't be *required*; rather, there should be a way to make use of the "code behind" logic and infrastructure without requiring partial classes. Rework things, allowing one to find and access layout elements (widgets) without having to call the `FindViewById()` or `FindFragment()` APIs, and instead access a C# property which will do all the work of finding and returning the right widget. Two approaches are available: 1. Bindings 2. Code Behind For both approaches, assume a Layout `.axml` file such as: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> <Button android:id="@+id/myButton" /> <fragment xamarin:managedType="CommonSampleLibrary.LogFragment" android:name="commonsamplelibrary.LogFragment" android:id="@+id/log_fragment" /> <fragment android:name="CommonSampleLibrary.LogFragment" android:id="@+id/secondary_log_fragment" /> </LinearLayout> Bindings ======== Bindings involve processing the `.axml` files and generating a stand-alone class with properties to access the widgets. All the generated classes are placed into the global `Binding` namespace and their names follow the base name of the layout being bound. Therefore, a class for layout named `Main.axml` will be named `Binding.Main` and a class for layout `my_other_layout.xml` will be named `Binding.my_other_layout`. The generated file will contain the API: // Generated code: namespace Binding { sealed partial class Main : global::Xamarin.Android.Design.LayoutBinding { public override int ResourceLayoutID {get;} public Main (global::Xamarin.Android.Design.ILayoutBindingClient client); public Button myButton {get;} public CommonSampleLibrary.LogFragment log_fragment {get;} public global::Android.App.Fragment secondary_log_fragment {get;} } } Note that each `//*/@android:id` within the Layout `.axml` file results in a property of the same name within the Binding class. The main advantage of this approach is that no modifications to layout files are necessary. The Binding class can be instantiated in a number of ways: * By passing instance of an activity *after* setting the activity layout in the usual way: SetContentView (Resource.Layout.Main); // as normal var binding = new Binding.Main (this) * By passing instance of a *View* to the constructor after loading the correct layout into the view: var binding = new Binding.my_other_layout (some_view); * By changing the `Activity` layout setting code to call a new overload of the `SetContentView()` method: var binding = SetContentView<Binding.Main> (); After the binding is instantiated, one can find the widgets by simply accessing the correct property: Button btn = binding.myButton; `Xamarin.Android.Design.LayoutBinding` is a new class added to make it possible to instantiate bindings for both Activities and Views. Code Behind =========== The new Code Behind approach now uses the above Bindings types to work. It is similar in spirit to the previous approach in that it generates a `partial` activity class which defines a number of properties, within the Activity, to support accessing the layout widgets. In order for this to work, it is first necessary to modify the associated layout by adding two attributes to the root element of the layout: * XML namespace declaration: xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools" * Specification of *full* type names for activities which will use the generated code (a semicolon-separated list; at least one type is required): xamarin:classes="Xamarin.Android.Tests.CodeBehindBuildTests.MainActivity;Xamarin.Android.Tests.CodeBehindBuildTests.AnotherMainActivity" This would result in a `.axml` file such as: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xamarin="http://schemas.xamarin.com/android/xamarin/tools" xamarin:classes="Xamarin.Android.Tests.CodeBehindBuildTests.MainActivity"> <Button android:id="@+id/myButton" /> <fragment xamarin:managedType="CommonSampleLibrary.LogFragment" android:name="commonsamplelibrary.LogFragment" android:id="@+id/log_fragment" /> <fragment android:name="CommonSampleLibrary.LogFragment" android:id="@+id/secondary_log_fragment" /> </LinearLayout> (Note changes to the previous `<LinearLayout/>` example, far above.) With the added XML attributes in place, all the activities mentioned in the `/*/@xamarin:classes` attribute have to be modified by adding the `partial` modifier to the class declaration, and the build system will generate `partial` classes for each specified type, providing C# property access to each element with a `//*/@android:id` value: // Generated code: namespace Xamarin.Android.Tests.CodeBehindBuildTests { partial class MainActivity { public override void SetContentView (global::Android.Views.View view); public override void SetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params); public override void SetContentView (int 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 {get;} public CommonSampleLibrary.LogFragment log_fragment {get;} public global::Android.App.Fragment secondary_log_fragment {get;} } } User-written code would continue to use `SetContentView()`, and could access the Widget properties after `SetContentView()` is called: // User code namespace Xamarin.Android.Tests.CodeBehindBuildTests { partial class MainActivity { protected override void OnCreate(Bundle state) { SetContentView (Resource.Layout.Main); myButton.Click += delegate { myButton.Text = $"{count++} clicks!"; }; } } } The `OnSetContentView()` partial methods may be implemented in your code to customize `SetContentView()` behavior since it's no longer possible to override them in your "main" activity code. The current implementation of the generated code-behind class is similar to: namespace Xamarin.Android.Tests.CodeBehindBuildTests { 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); } public override void SetContentView (global::Android.Views.View view, global::Android.Views.ViewGroup.LayoutParams @params) { // Code similar to above } public override void SetContentView (int layoutResID) { // Code similar to above } 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 log_fragment => __layout_binding?.log_fragment; public global::Android.App.Fragment secondary_log_fragment => __layout_binding?.secondary_log_fragment; } }
- Loading branch information