Skip to content

Commit 7c31899

Browse files
dellis1972jonpryor
authored andcommitted
[Xamarin.Android.Build.Tasks] Add Support for CodeBehind for layout files (#1238)
Support generating CodeBehind files for `.axml` layout files. This removes the need to call `FindViewById<T>()` manually, as properties generated into the CodeBehind file will call it instead. The CodeBehind files are generated for all `.axml` files with a build action of `@(AndroidResource)` *and* contain an XML namespace to `http://schemas.android.com/tools` and a `tools:class` attribute that contains the name of the `partial class` to generate: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" xmlns:tools="http://schemas.xamarin.com/android/tools" tools:class="UnnamedProject.MainActivity"> <Button android:id="@+id/myButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> </LinearLayout> The generated CodeBehind partial class will use the value of the `//@tools:class` attribute, and will contain a property for each element with an `//@android:id` attribute. The `//@android:id` attribute is the name of a generated field, while the containing element name is the type of the property. The above XML fragment would partially produce: partial class MainActivity { // Call instead of `SetContentView()` private void InitializeContentView (); // Called if `myButton` can't be found partial void OnLayoutViewNotFound<T> (int resourceId, ref T type) where T : global::Android.Views.View; // One property per //@android:id // Throws InvalidOperationException if it can't be found public Button myButton { get; } } This allows replacing the current code pattern of: SetContentView (Resource.Layout.NameOfLayoutFilename); var button = FindViewById<Button> (Resource.Id.myButton); button.Click += delegate { }; and instead rely on the CodeBehind properties: InitializeContentView (); myButton.Click += delegate { }; There are two other requirements to make this work: 1. All declarations of the class specified by the `//@tools:class` attribute must be `partial` classes, otherwise a CS0260 error will be produced at build time. 2. `InitializeContentView()` must be called instead of `SetContentView()`. `InitializeContentView()` itself calls `SetContentView()`, providing the `@(AndroidResource)` filename as the layout to use. This feature was originally prototyped by @grendello. The following is a sample of the kind of code which will be generated by this new system: namespace UnnamedProject { using System; using Android.App; using Android.Widget; using Android.Views; using Android.OS; // Generated from layout file 'Resources/layout/Main.axml' public partial class MainActivity { private Func<Button> @__myButtonFunc; private Button @__myButton; partial void OnLayoutViewNotFound<T> (int resourceId, ref T type) where T : global::Android.Views.View; public Button myButton { get { if (@__myButtonFunc == null) { @__myButtonFunc = this.@__Create_myButton; } return this.@__EnsureView<Button>(this.@__myButtonFunc, ref this.@__myButton); } } private void InitializeContentView() { this.SetContentView(Resource.Layout.Main); } private T @__FindView<T>(global::Android.Views.View parentView, int resourceId) where T : global::Android.Views.View { T view = parentView.FindViewById<T>(resourceId); if ((view == null)) { this.OnLayoutViewNotFound(resourceId, ref view); } if ((view != null)) { return view; } throw new System.InvalidOperationException($"View not found (ID: {resourceId})"); } private T @__FindView<T>(global::Android.App.Activity parentView, int resourceId) where T : global::Android.Views.View { T view = parentView.FindViewById<T>(resourceId); if ((view == null)) { this.OnLayoutViewNotFound(resourceId, ref view); } if ((view != null)) { return view; } throw new System.InvalidOperationException($"View not found (ID: {resourceId})"); } private T @__FindView<T>(global::Android.App.Fragment parentView, int resourceId) where T : global::Android.Views.View { return this.@__FindView<T>(parentView.Activity, resourceId); } private T @__EnsureView<T>(System.Func<T> creator, ref T field) where T : class { if ((field != null)) { return field; } if ((creator == null)) { throw new System.ArgumentNullException(nameof (creator)); } field = creator(); return field; } private Button @__Create_myButton() { return this.@__FindView<Button>(this, Resource.Id.myButton); } } }
1 parent 01ac172 commit 7c31899

File tree

6 files changed

+1120
-0
lines changed

6 files changed

+1120
-0
lines changed

Documentation/LayoutCodeBehind.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
id: 11763499-79e9-4868-83e6-41f3061745d1
3+
title: "Layout CodeBehind"
4+
dateupdated: 2018-01-29
5+
---
6+
7+
<a name="Overview" class="injected"></a>
8+
9+
# Overview
10+
11+
Xamarin.Android supports the auto generation of "Code Behind" classes. These classes
12+
can reduce the amount code a developer writes. You can end up replacing code like
13+
14+
SetContentView (Resource.Layout.Main);
15+
var button = FindViewById<Button> (Resource.Id.myButton);
16+
button.Click += delegate {
17+
};
18+
19+
with
20+
21+
InitializeContentView ();
22+
myButton.Click += delegate {
23+
};
24+
25+
26+
<a name="" class="injected"/></a>
27+
28+
# Preparing to use Code Behind
29+
30+
In order to make use of this new feature there are a few changes which are required.
31+
An axml/xml file that you want to associate with an activity needs to be modified to
32+
include a few extra xml attributes on the root layout element.
33+
34+
xmlns:tools="http://schemas.xamarin.com/android/tools"
35+
tools:class="$(Namespace).$(ClassName)"
36+
37+
The `class` attribute defines the Namespace and ClassName of the code which will be
38+
generated. For example if you have a layout for your `MainActivity` you would set
39+
the `tools:class` to `MyAppNamespace.MainActivity`. Note it should be the fully
40+
qualified name, not just the class name on its own.
41+
42+
The next thing we need to do is to make the `MainActivity` a `partial` class. This
43+
allows the genereted code to extend the current class which you have written.
44+
So
45+
public class MainActivity : Activity {
46+
}
47+
48+
will become
49+
50+
public partial class MainActivity : Activity {
51+
}
52+
53+
You then need to make sure you initialize the layout properties by calling
54+
`InitializeContentView ()` in the `OnCreate()` method of your activity.
55+
56+
protected override void OnCreate (Bundle bundle)
57+
{
58+
base.OnCreate (bundle);
59+
InitializeContentView ();
60+
}
61+
62+
For those of you familiar with System.Windows.Forms this is akin
63+
to `InitializeComponent`. Once this has been done you can now access
64+
your layout items via the properties.
65+
66+
myButton.Click += delegate {
67+
};
68+
69+
There is a partial method available which can be implemented to handle
70+
situations where the View is not found. The method is
71+
72+
void OnLayoutViewNotFound<T> (int resourceId, ref T type)
73+
where T : global::Android.Views.View;
74+
75+
If `FindViewById` returns `null` then the `OnLayoutViewNotFound` method
76+
will be called (if it is implemented). This is done BEFORE we throw the
77+
`InvalidOperationException`. This allows the deveoper to handle the
78+
situation in a manner which fits the app they are writing. For example
79+
you might want to switch to a backup view, or just log some additional
80+
diagnostic information.
81+
82+
# How it works
83+
84+
There are a couple of new MSBuild Tasks which generate the code behind.
85+
`<CalculateLayoutCodeBehind/>` and `<GenerateCodeBehindForLayout/>`.
86+
87+
`<CalculateLayoutCodeBehind/>` scans through the `@(AndroidResources)` of the
88+
project looking fo the `tools:class` attributes. Any layout file which does
89+
not have this on the very first element will be ignored.
90+
91+
`<GenerateCodeBehindForLayout/>` will then process the discovered files and
92+
geneerate the code behind files in `$(IntermediateOutputDir)generated`.
93+
These files are named by combining the name of the layout file along with
94+
the Namespace and ClassName from the `tools:class` attribute. So if we are
95+
creating code behind for `MyAppNamespace.MainActivity` for the `Main.axml`
96+
you will see an intermediate file named `Main-MyAppNamespace.MainActivity.g.cs`.
97+
Thes files will automatically be included in the `Compile` MSBuild ItemGroup if it
98+
exists.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System;
2+
using Microsoft.Build.Utilities;
3+
using Microsoft.Build.Framework;
4+
using System.Collections;
5+
using System.IO;
6+
using System.Xml;
7+
using System.Collections.Generic;
8+
9+
namespace Xamarin.Android.Tasks {
10+
11+
public class CalculateLayoutCodeBehind : Task {
12+
13+
readonly string LayoutDirSuffix = "layout";
14+
readonly string classSuffix = "class";
15+
readonly string toolsNamespace = "http://schemas.xamarin.com/android/tools";
16+
17+
/// <summary>
18+
/// Designer Specific Property. Can be used to calcualte the code behind
19+
/// file for one specific file. Saves having to process all the resources.
20+
/// </summary>
21+
/// <value>The specific file.</value>
22+
public ITaskItem SpecificFile { get; set; }
23+
24+
[Required]
25+
public ITaskItem [] ResourceFiles { get; set; }
26+
27+
[Output]
28+
public ITaskItem [] CodeBehindFiles { get; set; }
29+
30+
public override bool Execute ()
31+
{
32+
Log.LogDebugMessage ("CalculateLayoutCodeBehind Task");
33+
Log.LogDebugMessage (" SpecificFile: {0}", SpecificFile);
34+
Log.LogDebugTaskItems (" ResourceFiles:", ResourceFiles);
35+
36+
string partialClassNames = null;
37+
var codeBehindFiles = new List<ITaskItem> ();
38+
if (SpecificFile != null && File.Exists (SpecificFile.ItemSpec)) {
39+
string fileName = SpecificFile.ItemSpec;
40+
if (IsCodeBehindLayoutFile (fileName, out partialClassNames))
41+
CalculateCodeBehindFilenames (fileName, partialClassNames, codeBehindFiles);
42+
} else {
43+
foreach (var item in ResourceFiles) {
44+
string fileName = item.ItemSpec;
45+
if (!IsCodeBehindLayoutFile (fileName, out partialClassNames))
46+
continue;
47+
CalculateCodeBehindFilenames (fileName, partialClassNames, codeBehindFiles);
48+
}
49+
}
50+
51+
CodeBehindFiles = codeBehindFiles.ToArray ();
52+
if (CodeBehindFiles.Length == 0) {
53+
Log.LogDebugMessage (" No layout file qualifies for code-behind generation");
54+
}
55+
Log.LogDebugTaskItems (" CodeBehindFiles:", CodeBehindFiles);
56+
return !Log.HasLoggedErrors;
57+
}
58+
59+
void CalculateCodeBehindFilenames (string fileName, string partialClassNames, List<ITaskItem> codeBehindFiles)
60+
{
61+
string [] classes = partialClassNames?.Split (',');
62+
if (classes == null || classes.Length == 0)
63+
return;
64+
65+
foreach (string c in classes) {
66+
string cl = c?.Trim ();
67+
if (String.IsNullOrEmpty (cl))
68+
continue;
69+
70+
codeBehindFiles.Add(CreateCodeBehindTaskItem (fileName, cl));
71+
}
72+
}
73+
74+
ITaskItem CreateCodeBehindTaskItem (string fileName, string partialClassName)
75+
{
76+
var ret = new TaskItem (fileName);
77+
ret.SetMetadata("CodeBehindFileName", $"{Path.GetFileNameWithoutExtension (fileName)}-{partialClassName}.g.cs");
78+
ret.SetMetadata("ClassName", partialClassName);
79+
return ret;
80+
}
81+
82+
protected bool IsCodeBehindLayoutFile (string fileName, out string partialClassNames)
83+
{
84+
partialClassNames = null;
85+
if (String.IsNullOrEmpty (fileName) || !File.Exists(fileName))
86+
return false;
87+
88+
if (!Path.GetDirectoryName(fileName).EndsWith (LayoutDirSuffix, StringComparison.OrdinalIgnoreCase))
89+
return false;
90+
91+
if (!fileName.EndsWith(".axml", StringComparison.OrdinalIgnoreCase) && !fileName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
92+
return false;
93+
94+
using (var fs = File.OpenRead (fileName)) {
95+
using (var reader = XmlReader.Create (fs)) {
96+
while (reader.Read ()) {
97+
if (reader.NodeType != XmlNodeType.Element)
98+
continue;
99+
if (reader.IsStartElement ()) {
100+
if (reader.HasAttributes) {
101+
partialClassNames = reader.GetAttribute (classSuffix, toolsNamespace);
102+
if (!string.IsNullOrEmpty (partialClassNames)) {
103+
return true;
104+
}
105+
106+
}
107+
// only read the root element. if it doesn't have the data we need
108+
// its not an auto layout file.
109+
break;
110+
}
111+
}
112+
}
113+
}
114+
return false;
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)