Skip to content

Commit 48f2a7a

Browse files
committed
Code-behind fragment and widget access fixes
Fragment handling code partially taken from dotnet#1302 Fragments are treated as normal widget, with the difference that they have to be found using `FragmentManager.FindFragmentById` instead of the usual `Activity.FindViewById` method. Each generated class also gets a property named `Widget` which returns the actual Android widget as found in the layout file. This allows us to keep hierarchical nature of the XML code (thus handling nested widgets with duplicate ids gracefully) while being able to access the parent widget itself. The commit introduces a new attribute called `tools:managedType` which is used to specify the element associated property's type. We cannot use `tools:class` for this purpose since the layout root element already uses it to specify the name of the generated code-behind class and if the element had `android:id` on it, we would end up using the activity's type for the root element's property in the generated code, instead of its actual type.
1 parent 351ca70 commit 48f2a7a

File tree

2 files changed

+312
-46
lines changed

2 files changed

+312
-46
lines changed

Documentation/guides/LayoutCodeBehind.md

Lines changed: 181 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,45 @@ dateupdated: 2018-01-29
1111
Xamarin.Android supports the auto generation of "Code Behind" classes. These classes
1212
can reduce the amount code a developer writes. You can end up replacing code like
1313

14-
SetContentView (Resource.Layout.Main);
15-
var button = FindViewById<Button> (Resource.Id.myButton);
16-
button.Click += delegate {
17-
};
14+
```csharp
15+
SetContentView (Resource.Layout.Main);
16+
var button = FindViewById<Button> (Resource.Id.myButton);
17+
button.Click += delegate {
18+
};
19+
```
1820

1921
with
2022

21-
InitializeContentView ();
22-
myButton.Click += delegate {
23-
};
23+
```csharp
24+
InitializeContentView ();
25+
myButton.Click += delegate {
26+
};
27+
```
2428

29+
or, with nested layouts:
30+
31+
```csharp
32+
InitializeContentView ();
33+
myParentLayout.myButton.Widget.Click += delegate {
34+
};
35+
```
2536

2637
<a name="" class="injected"/></a>
2738

2839
# Preparing to use Code Behind
2940

3041
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.
42+
An ``axml/xml`` file that you want to associate with an activity needs to be modified to
43+
include a few extra xml attributes on the root layout element.
44+
45+
Additionally, **only*** elements which have the `android:id` attribute will be accessible via
46+
the generated code.
47+
3348

34-
xmlns:tools="http://schemas.xamarin.com/android/tools"
35-
tools:class="$(Namespace).$(ClassName)"
49+
```xml
50+
xmlns:tools="http://schemas.xamarin.com/android/tools"
51+
tools:class="$(Namespace).$(ClassName)"
52+
```
3653

3754
The `class` attribute defines the Namespace and ClassName of the code which will be
3855
generated. For example if you have a layout for your `MainActivity` you would set
@@ -42,35 +59,46 @@ qualified name, not just the class name on its own.
4259
The next thing we need to do is to make the `MainActivity` a `partial` class. This
4360
allows the genereted code to extend the current class which you have written.
4461
So
45-
public class MainActivity : Activity {
46-
}
62+
63+
```csharp
64+
public class MainActivity : Activity {
65+
}
66+
```
4767

4868
will become
4969

50-
public partial class MainActivity : Activity {
51-
}
70+
```csharp
71+
public partial class MainActivity : Activity {
72+
}
73+
```
5274

5375
You then need to make sure you initialize the layout properties by calling
5476
`InitializeContentView ()` in the `OnCreate()` method of your activity.
5577

56-
protected override void OnCreate (Bundle bundle)
57-
{
58-
base.OnCreate (bundle);
59-
InitializeContentView ();
60-
}
78+
```csharp
79+
protected override void OnCreate (Bundle bundle)
80+
{
81+
base.OnCreate (bundle);
82+
InitializeContentView ();
83+
}
84+
```
6185

6286
For those of you familiar with System.Windows.Forms this is akin
6387
to `InitializeComponent`. Once this has been done you can now access
6488
your layout items via the properties.
6589

66-
myButton.Click += delegate {
67-
};
90+
```csharp
91+
myButton.Click += delegate {
92+
};
93+
```
6894

6995
There is a partial method available which can be implemented to handle
7096
situations where the View is not found. The method is
7197

72-
void OnLayoutViewNotFound<T> (int resourceId, ref T type)
73-
where T : global::Android.Views.View;
98+
```csharp
99+
void OnLayoutViewNotFound<T> (int resourceId, ref T type)
100+
where T : global::Android.Views.View;
101+
```
74102

75103
If `FindViewById` returns `null` then the `OnLayoutViewNotFound` method
76104
will be called (if it is implemented). This is done BEFORE we throw the
@@ -79,6 +107,135 @@ situation in a manner which fits the app they are writing. For example
79107
you might want to switch to a backup view, or just log some additional
80108
diagnostic information.
81109

110+
Another partial method exists to handle fragments:
111+
112+
```csharp
113+
void OnLayoutFragmentNotFound<T> (int resourceId, ref T type)
114+
where T : global::Android.App.Fragment;
115+
```
116+
117+
It works in exactly the same way as `OnLayoutViewNotFound` above, just for fragments.
118+
119+
## Generated code structure
120+
121+
The generated code-behind is laid out in a hierarchical fashion, reflecting the parent-child
122+
relationship found in the layout file. The way it is done is that each element which has any
123+
child elements **with** the `android:id` attribute (that is, ones which will also have code
124+
generated for them) will have a nested class generated for it which will have a property for
125+
each child element as well as the `Widget` property which refers to this element's actual
126+
Android widget/view. Each element which does **not** have any child elements with the
127+
`android:id` attribute, however, will become a *leaf node* and will have an associated property
128+
in its parent widget's class directly typed to the actual Android type (e.g. `TextView`) instead
129+
of the class described before. For instance, given this layout:
130+
131+
```xml
132+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.xamarin.com/android/tools"
133+
tools:class="MyActivity">
134+
<ScrollView android:id="@+id/myScrollView">
135+
<TextView android:id="@+id/myTextView"/>
136+
</ScrollView>
137+
</LinearLayout>
138+
```
139+
140+
The code-behind will have this rough structure (class names are different to keep the documentation clear):
141+
142+
```csharp
143+
myScrollView_Class myScrollView {
144+
get { return new myScrollView_Class (this); }
145+
}
146+
147+
class myScrollView_Class
148+
{
149+
public ScrollView Widget {
150+
get { /* ... */ }
151+
}
152+
153+
public TextView myTextView {
154+
get { /* ... */ }
155+
}
156+
157+
public myScrollView_Class (MyActivity parent) {}
158+
}
159+
```
160+
161+
So in order to access the widgets you'd use code similar to:
162+
163+
```csharp
164+
InitializeContentView ();
165+
myScrollView.Widget.Fling (100);
166+
myScrollView.myTextView.AutoSizeMaxTextSize = 40;
167+
```
168+
169+
### Code structure rationale
170+
171+
It may seem that it would be simpler to generate code which would put properties returning the layout elements directly in the
172+
Activity partial class instead of outputing a seemingly complex nested class structure. This approach would work if it wasn't
173+
for the following:
174+
175+
1. Android allows duplicate `android:id` values for **sibling** elements
176+
2. Android allows duplicate `android:id` values anywhere withn the layout tree
177+
3. Many layouts reuse XML in the form of fragments
178+
4. Many layouts reuse XML in the form of includes (using the `<include>` element)
179+
180+
`1.` means that there's direct access (via `FindViewById` or with code-behind) to the **first** element with that `id` **only**.
181+
The rest of elements can be accessed only by enumerating the child collection. This is how it works in Android and we do not deviate
182+
from the Android approach currently.
183+
184+
`2.` works in Android by walking down the element hierarchy (using `FindViewById`) until we find the direct parent of the element we seek and,
185+
despite being tedious, this approach creates no conflicts and issues with accessing the elements with the same `id`
186+
187+
If we "flattened" the hierarchy, however, we would create the issue ourselves as suddenly we'd have `id` conflicts where there would have been
188+
none before. Additionally, we wouldn't be able to generate code to directly access the farther elements, similar to `1.`. Or we could but we would
189+
have to come up with a scheme to generate unique names for our properties for instance by appending a monotonously increasing integer to the base name,
190+
e.g. given the base `id` of `myTextView` we would have properties named `myTextView`, `myTextView1`, `myTextView2` and so on.
191+
192+
It may not seem to be a big problem, after all there's a clearly defined naming convention that is predictable. But, is it? What happens if one element
193+
with the shared `id` in the middle is removed? The elements following it are renumbered and suddenly our code works subtly differently - where `myTextView2`
194+
was used to refer to the 3rd control, now it not only does not exist (causing a build error for the **third** instance of the element) but it is now silently
195+
referred to by `myTextView1` which might again introduce subtle issues to the way the code works.
196+
197+
What happens when the layouts containing the "duplicate" `id`s are reordered? We have no compilation error as in the scenario above, it's much worse - suddenly
198+
and quietly the code works differently, because the properties refer to **different** widgets (and thus layouts) but with the same `id`s!
199+
200+
`3.` and `4.` make the situation worse as they can introduce a number of "duplicate" `id` values all over the place and cause the `1.` and `2.` issues.
201+
202+
The hierarchical approach generates code that's inherently object-oriented, reflects the structure of the layout and in case of removing of elements will
203+
generate a compile-time error, while in case of reordering of elements it will keep working correctly as long as the `id` "path" doesn't change (i.e. the
204+
involved elements keep their `id` values from the root all the way to the leaf child). The only slightly awkward aspect is the necessity to introduce the
205+
`Widget` property in each wrapper class in order to enable referring to the element itself and not just its children. However, since the usage and naming is
206+
consistent, this is simply a matter of getting used to the convention.
207+
208+
## Managed types
209+
210+
By default each element for which we generate code-behind has its managed type set to
211+
its local name, for instance
212+
213+
```xml
214+
<TextView android:id=""@+id/textView" />
215+
```
216+
217+
Will generate a property named `textView` of type `TextView`. It works fine in most cases
218+
but sometimes you might find code which either refers to a custom widget using the package
219+
name or a fragment which uses the case-insensitive `android:name` attribute syntax, for
220+
instance:
221+
222+
```xml
223+
<fragment
224+
android:name="commonsamplelibrary.LogFragment"
225+
android:id="@+id/log_fragment" />
226+
```
227+
228+
In this case the generated property would have the managed type `commonsamplelibrary.LogFragment`,
229+
however the actual managed type fully qualified name is `CommonSampleLibrary.LogFragment` and thus
230+
the generated code would fail to compile. The solution is to add the `tools:managedType` attribute
231+
which specifies the element's (all elements support this attribute) managed type.
232+
233+
One may wonder why we didn't reuse the `tools:class` attribute to specify the managed type? It is
234+
because that attribute is used to specify the code-behind partial class name on the root element of
235+
the layout and should the element had the `android:id` attribute present we'd end up with generated
236+
code that would use the **activity** type for the element's associated property instead of its
237+
actual type and the code wouldn't build.
238+
82239
# How it works
83240
84241
There are a couple of new MSBuild Tasks which generate the code behind.

0 commit comments

Comments
 (0)