Skip to content

Conversation

@dellis1972
Copy link
Contributor

@dellis1972 dellis1972 commented Jan 25, 2018

This commit is based on work done by @grendello. It adds support
for the generation of code behind for layout files. This will
make it easier for users to write code since they will no longer
need to use FindViewbyId manually. The generated code will
automatically provide properties for the various UI elements
in the layout file.

For example if you have a Button with an id of @id/myButton you
will see a property is available on your activity myButton. You
can then replace code like

var button = FindViewById<Button> (Resource.Id.myButton);
button.Click += delegate {
};

with

myButton.Click += delegate {
};

much nicer eh :).

There are a couple of caviats. The main one is your layout class
MUST be a partial class. So

public class MainActivity : Activity {
}

becomes

public partial class MainActivity : Activity {
}

Next is you need to add two items to your root layout of your
axml/xml files.

xmlns:tools="http://schemas.android.com/tools"
tools:class="$(Namespace).$(ClassName)"

where $(Namespace) and $(ClassName) are replaced with the
approprite values. Note these MUST match the full namespace/classname
of the layout class.

The following is a sample of the kind of code which will be
generated by this new system.

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

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;

		#line 1 "/Users/dean/Documents/Sandbox/Xamarin/xamarin-android/bin/TestDebug/temp/CheckCodeBehindIsGenerated/Resources/layout/Main.axml"
		private Button @__myButton;

		#line default
		#line hidden

		partial void OnLayoutViewNotFound<T> (int resourceId, ref T type) where T : global::Android.Views.View;

		#line 1 "/Users/dean/Documents/Sandbox/Xamarin/xamarin-android/bin/TestDebug/temp/CheckCodeBehindIsGenerated/Resources/layout/Main.axml"
		public Button myButton {
			get {
				if (@__myButtonFunc == null) {
					@__myButtonFunc = this.@__Create_myButton;
				}
				return this.@__EnsureView<Button>(this.@__myButtonFunc, ref this.@__myButton);
			}
		}

		#line default
		#line hidden

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

@dellis1972 dellis1972 added do-not-merge PR should not be merged. Area: App+Library Build Issues when building Library projects or Application projects. labels Jan 25, 2018
@dellis1972 dellis1972 force-pushed the codebehind branch 2 times, most recently from 2ec27d3 to a2c6fbd Compare January 29, 2018 12:16
@dellis1972 dellis1972 changed the title CodeBehind PR [WIP] [Dont merge] [Xamarin.Android.Build.Tasks] Add Support for CodeBehind for layout files Jan 29, 2018
@dellis1972 dellis1972 requested a review from grendello January 29, 2018 12:17
@atsushieno
Copy link
Contributor

I don't think we should be extending any attributes in https://schemas.android.com/tools because it is basically "owned" by Google and they will arbitrarily add any attributes at their will. "class" is very general name and can easily cause future conflicts.


with

myButton.Click += delegate {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code indentation is inconsistent within the file. Here, it's 8 spaces, while later below it's 4 spaces.

Indentation should be consistent.

# How it works

There are a couple of new MSBuild Tasks which generate the code behind.
`CalculateLayoutCodeBehind` and `GenerateCodeBehindForLayout`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these are tasks, please use <CalculateLayoutCodeBehind/> and <GenerateCodeBehindForLayout/>. If these are targets, please use "the CalculateLayoutCodeBehind target", etc.

@dellis1972 dellis1972 removed the do-not-merge PR should not be merged. label Feb 7, 2018
@dellis1972 dellis1972 force-pushed the codebehind branch 2 times, most recently from fd3a940 to 43a42a9 Compare February 7, 2018 15:38
@garuma
Copy link
Contributor

garuma commented Feb 7, 2018

On the generated code, there is a slight optimization that should be made for the creator function.

In the current generated code as I see it in the description, anytime the property for views are called a new instance of Func<T> will be created even though it's not necessary most of the time.

2 options:

  • Generate an extra field caching the delegate instance
  • Change the code to use Lazy<T> and drop the custom null-check pattern with EnsureView, this benefit from the JIT optimizing that pattern. Exception will also be cached and re-thrown if that arises. Con is that it's an extra instance.

@jonpryor
Copy link
Contributor

jonpryor commented Feb 7, 2018

Proposed commit message when merging:

Support generating CodeBehind files for `.axml` layout files.
This feature removes the need to call `FindViewById()` manually, as
properties generated into the CodeBehind file do 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 have the same name as the
`//@tools:class` attribute value, and will contain a property for
each elememtn 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 (bool throwOnMissingView = false);

	  // Called if `myButton` can't be found and `throwOnMissingValue`==false
	  partial void OnLayoutViewNotFound (int resourceId, Type type);

	  // One property per //@android:id 
	  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 (throwOnMissingView:true);
	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.

    `InitializeContentView()` takes an optional
    `bool throwOnMissingView` parameter, which controls what happens
    if the View cannot be found when the wrapper property is used.
    When `false` (the default), the partial `OnLayoutViewNotFound()`
    method is invoked. When `true`, an `InvalidOperationException`
    is thrown.

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.

	//------------------------------------------------------------------------------
	// <auto-generated>
	//     This code was generated by a tool.
	//     Runtime Version:4.0.30319.42000
	//
	//     Changes to this file may cause incorrect behavior and will be lost if
	//     the code is regenerated.
	// </auto-generated>
	//------------------------------------------------------------------------------

	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 bool @__throwOnMissingView_LayoutCodeBehind;

			#line 1 "/Users/dean/Documents/Sandbox/Xamarin/xamarin-android/bin/TestDebug/temp/CheckCodeBehindIsGenerated/Resources/layout/Main.axml"
			private Button @__myButton;

			#line default
			#line hidden

			partial void OnLayoutViewNotFound (int resourceId, Type type);

			#line 1 "/Users/dean/Documents/Sandbox/Xamarin/xamarin-android/bin/TestDebug/temp/CheckCodeBehindIsGenerated/Resources/layout/Main.axml"
			public Button myButton {
				get {
					return this.@__EnsureView<Button>(this.@__Create_myButton, ref this.@__myButton);
				}
			}

			#line default
			#line hidden

			private void InitializeContentView(bool throwOnMissingView = false) {
				this.@__throwOnMissingView_LayoutCodeBehind = throwOnMissingView;
				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)) {
					if (@__throwOnMissingView_LayoutCodeBehind) {
						throw new System.InvalidOperationException($"View not found (ID: {resourceId})");
					}
					else {
						this.OnLayoutViewNotFound(resourceId, typeof(T));
					}
				}
				return view;
			}

			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)) {
					if (@__throwOnMissingView_LayoutCodeBehind) {
						throw new System.InvalidOperationException($"View not found (ID: {resourceId})");
					}
					else {
						this.OnLayoutViewNotFound(resourceId, typeof(T));
					}
				}
				return view;
			}

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

@jonpryor
Copy link
Contributor

jonpryor commented Feb 7, 2018

@dellis1972: while reviewing the commit message, a few issues popped up which aren't in the documentation, nor the commit message, nor...make a lot of sense to me.

In particular, the OnLayoutViewNotFound() method.

What do we want? Presumably this:

InitializeContentView();
myButton.Click += delegate {Console.WriteLine ("clicked!");};

But if myButton cannot be loaded...what instead will happen is a NullReferenceException.

Is this desirable?

Similarly, what happens if InitializeContentView() isn't called? I think we'd be in the same boat.

@jonpryor
Copy link
Contributor

jonpryor commented Feb 7, 2018

The documentation should be improved to at least mention InitializeContentView() and OnLayoutViewNotFound(), assuming we want to even keep OnLayoutViewNotFound().

OnLayoutViewNotFound() doesn't seem good to me. I'd rather that we just threw from the wrapper properties.

@dellis1972
Copy link
Contributor Author

OK the OnLayoutViewNotFound only needs to be implemented if InitializeContentView has true for throwOnMissingView.
So I'll document that , I guess the idea by the author would be to allow the user to handle the problem manually.

@dellis1972
Copy link
Contributor Author

I also think we should change the default for InitializeContentView (throwOnMissingView) to be true rather than false. This means there is less work for people to do.

Alternatively provide virtual method which people can override to stop the exception being thrown.

@jonpryor
Copy link
Contributor

jonpryor commented Feb 7, 2018

@dellis1972: I think you have it backwards: OnLayoutViewNotFound() only needs to be implemented if InitializeContentView() is called with throwOnMissingView:false. When throwOnMissingView:true is used, the exception will be thrown, and OnLayoutViewNotFound() won't be called.

Alternatively provide virtual method which people can override to stop the exception being thrown.

The partial method is better than a virtual method, as the class might not be subclassed; why require subclassing?

I also don't really see how OnLayoutViewNotFound() will be used. Suppose we have:

InitializeComponentView(throwOnMissingView:false);
SetContentView(Resource.Layout.CompletelyWrongLayout); // bwa-ha-ha-ha-ha-ha
myButton.Click += delegate {};

Assuming that Resource.Layout.CompletelyWrongLayout doesn't contain a myButton id, there is no myButton id. What should happen?

If OnLayoutViewNotFound() isn't "overridden", then we get a NullReferenceException on myButton.Click.

If it is overridden...what is it supposed to do?

partial void OnLayoutViewNotFound (int resourceId, Type type)
{
  // Uh...?
}

It could throw an exception, which is arguably the safest thing to do.

If it doesn't throw an exception...then it must "somehow" make the myButton property return a non-null value. If it doesn't, then we still have a NullReferenceException!

There's no straightforward way for the dev to "know" that Resource.id.myButton is backed by the __myButton field, short of looking at -- and understanding (!) -- the generated CodeBehind file. (Lol?)

partial void OnLayoutViewNotFound (int resourceId, Type type)
{
  if (resourceId == Resource.Id.myButton) {
    __myButton = ....wait, what?
  }
}

In short, I see no reason for OnLayoutViewNotFound() to exist. Assuming the whole reason it exists is to avoid a later NullReferenceException, I don't see how it can do that.

Meaning the only sane approach is to always throw.

Meaning we don't need a InitializeContentView(bool throwOnMissingView), we just need a InitializeContentView() (no parameters at all!).

@dellis1972
Copy link
Contributor Author

dellis1972 commented Feb 7, 2018 via email

@grendello
Copy link
Contributor

@jonpryor

Assuming that Resource.Layout.CompletelyWrongLayout doesn't contain a myButton id, there is no myButton id. What should happen?
If OnLayoutViewNotFound() isn't "overridden", then we get a NullReferenceException on myButton.Click.
If it is overridden...what is it supposed to do?

That's up to the developer. The idea was what @dellis1972 mentioned - to let the developer handle it in some way. It might be manually locating the view with FindViewById (we might miss some views for whatever reason - bug, corner case etc), creating the view dynamically or simply throwing an exception if all else fails. This is, IMO, a must-have flexibility with such generated code.

@jonpryor
Copy link
Contributor

jonpryor commented Feb 7, 2018

@grendello: then there needs to be a sane way to actually do that.

The current solution is not what I'd call "sane." Again, what should we expect developers to provide within the body of OnLayoutViewNotFound() which will allow the subsequent myButton property access to return a non-null value?

If we want the flexibility, then I suggest we instead do:

partial void OnLayoutViewNotFound<TView> (int resourceId, ref TView value);

CodeBehind-side, we'd thus have e.g.:

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)) {
		if (@__throwOnMissingView_LayoutCodeBehind) {
			throw new System.InvalidOperationException($"View not found (ID: {resourceId})");
		}
		else {
			this.OnLayoutViewNotFound(resourceId, ref view);
		}
	}
	return view;
}

Developer-override wise, now they can actually set the underlying field without "magically" knowing that __myButton actually exists:

partial void OnLayoutViewNotFound<TView> (int resourceId, ref TView value)
{
    if (resourceId == Resource.Id.myButton) {
        // In yada yada yada, we instead use some other resource:
        value = FindViewById<TView>(Resource.Id.myRelatedButton);
    }
}

@grendello
Copy link
Contributor

The idea was that this method is part of the same class so it has access to all the fields and can write them or throw if it chooses so. Your idea with a ref parameter adds a bit of complexity but it also makes it possible to use method variables instead of class fields. I like this modification. Possibly even add an unconditional throw after OnLayoutViewNotFound returns and view is still null? That would serve as an assert.

@jonpryor
Copy link
Contributor

jonpryor commented Feb 7, 2018

That implies we should generate:

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)) {
		throw new System.InvalidOperationException($"View not found (ID: {resourceId})");
	}
	return view;
}

This also means that we don't need the throwOnMissingView parameter in InitializeContentView() either.

@grendello
Copy link
Contributor

A small nitpick :)

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

This looks cleaner IMHO :)

@grendello
Copy link
Contributor

@dellis1972 we could move this code from the latest proposed version of __FindView overloads to a common generic method:

if ((view == null)) {
   	this.OnLayoutViewNotFound(resourceId, ref view);
   	if (view != null)
   		return view;
   }
   
   throw new System.InvalidOperationException($"View not found (ID: {resourceId})");
}

This way the generated source code would be slightly, but always, smaller and leaner

Copy link
Contributor

@grendello grendello left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the OnLayoutViewNotFound changes and it'll be perfect :)

@jonpryor
Copy link
Contributor

jonpryor commented Feb 8, 2018

We also need to update the documentation about semantics and requirements. I'm currently assuming that InitializeComponentView() must be called first -- because I can't see how it would work, otherwise -- but that wasn't explicitly called out anywhere.

Documentation also needs to mention OnLayoutViewNotFound().

@dellis1972
Copy link
Contributor Author

dellis1972 commented Feb 8, 2018 via email

@jonpryor
Copy link
Contributor

jonpryor commented Feb 8, 2018

@dellis1972: we'll also want an updated copy of the generated codebehind file.

@dellis1972 dellis1972 force-pushed the codebehind branch 3 times, most recently from bcd7647 to cbfa8ff Compare February 8, 2018 17:28
…iles.

This commit is based on work done by @grendello. It adds support
for the generation of code behind for layout files. This will
make it easier for users to write code since they will no longer
need to use `FindViewbyId` manually. The generated code will
automatically provide properties for the various UI elements
in the layout file.

For example if you have a Button with an id of `@id/myButton` you
will see a property is available on your activity `myButton`. You
can then replace code like

	var button = FindViewById<Button> (Resource.Id.myButton);
	button.Click += delegate {
	};

with

	myButton.Click += delegate {
	};

much nicer eh :).

There are a couple of caviats. The main one is your layout class
MUST be a `partial` class. So

	public class MainActivity : Activity {
	}

becomes

	public partial class MainActivity : Activity {
	}

Next is you need to add two items to your root layout of your
axml/xml files.

	xmlns:tools="http://schemas.android.com/tools"
	tools:class="$(Namespace).$(ClassName)"

where `$(Namespace)` and `$(ClassName)` are replaced with the
approprite values. Note these MUST match the full namespace/classname
of the layout class.

The following is a sample of the kind of code which will be
generated by this new system.

	//------------------------------------------------------------------------------
	// <auto-generated>
	//     This code was generated by a tool.
	//     Runtime Version:4.0.30319.42000
	//
	//     Changes to this file may cause incorrect behavior and will be lost if
	//     the code is regenerated.
	// </auto-generated>
	//------------------------------------------------------------------------------

	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;

			#line 1 "/Users/dean/Documents/Sandbox/Xamarin/xamarin-android/bin/TestDebug/temp/CheckCodeBehindIsGenerated/Resources/layout/Main.axml"
			private Button @__myButton;

			#line default
			#line hidden

			partial void OnLayoutViewNotFound<T> (int resourceId, ref T type) where T : global::Android.Views.View;

			#line 1 "/Users/dean/Documents/Sandbox/Xamarin/xamarin-android/bin/TestDebug/temp/CheckCodeBehindIsGenerated/Resources/layout/Main.axml"
			public Button myButton {
				get {
					if (@__myButtonFunc == null) {
						@__myButtonFunc = this.@__Create_myButton;
					}
					return this.@__EnsureView<Button>(this.@__myButtonFunc, ref this.@__myButton);
				}
			}

			#line default
			#line hidden

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

ff
@jonpryor
Copy link
Contributor

jonpryor commented Feb 9, 2018

Build hung deploying to device. Restarted the build.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Area: App+Library Build Issues when building Library projects or Application projects.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants