Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] Fragments & CodeBehind
Browse files Browse the repository at this point in the history
While trying to repro Issue dotnet#1296, I had a brilliant idea: let's use
the new CodeBehind support from 7c31899!

Everything then fell apart.

For starters, Issue dotnet#1296 deals with Fragments, and 7c31899 didn't
support the presence of `<fragment/>` within Layout files.

Plumb support for that, by adding:

	partial class MainActivity {
	  partial void OnLayoutFragmentNotFound<T> (int resourceId, ref T type)
	    where T : global::Android.App.Fragment;
	}

Another issue was noticed as well: all the line numbers in the `#line`
pragmas were always `1`. This was narrowed down to a bug in
`GetLineInfo()`.

Then there's an issue with where I was using the CodeBehind file:
within the `src/Mono.Android/Test` project, which is in the
`Xamarin.Android.RuntimeTests` namespace. This prompted all manner of
namespace resolution failures:

	error CS0234: The type or namespace name 'App' does not exist in the namespace 'Xamarin.Android' (are you missing an assembly reference?)
	...

Fix this by using `CodeTypeReferenceOptions.GlobalReference` as much
as is practical (which isn't enough), and by "kludging" up the
`CodeNamespaceImport` construction so that we instead emit:

	using global::System;

Finally, the generated codebehind had some weird nesting going on:

	partial class MainActivity {
	  public __first_text_view_Views first_text_view {get;}
	  public sealed partial class __first_text_view_Views {
	    public __second_text_view_Views second_text_view {get;}
	    public sealed partial class __second_text_view_Views {
	      public sealed partial class __csharp_simple_fragment_Views {
	        public sealed partial class __csharp_partial_assembly_Views {
	        }
	      }
	    }
	  }
	}

It was bizarre *and* unusable: the `second_text_view` property --
corresponding to `<TextView android:id="@+id/second_text_view" />` --
was *nested within* a generated `MainActivity.__first_text_view_Views`
type, and I'm not sure why that existed. Furthermore, it *can't* work,
as the generated
`MainActivity.__first_text_view_Views.__CreateClass___second_text_view_Views()`
method depends on a constructor which doesn't exist:

	private @__second_text_view_Views @__CreateClass___second_text_view_Views() {
	  // There *is* a __second_text_view_Views(Activity) constructor,
	  // but not __second_text_view_Views(__first_text_view_Views).
	  return new @__second_text_view_Views(this);
	}

I "solved" (?) this by updating `LoadWidgets()` so that it always used
`widgetRoot` for all recursive calls to `LoadWidgets()`, so that there
is only ever one root. This removed all the nested types.
  • Loading branch information
jonpryor committed Feb 14, 2018
1 parent 0dee27d commit c2f3c99
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 18 deletions.
2 changes: 2 additions & 0 deletions src/Mono.Android/Test/Mono.Android-Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@
<AndroidResource Include="Resources\drawable\android_focused.png" />
<AndroidResource Include="Resources\drawable\android_normal.png" />
<AndroidResource Include="Resources\drawable\android_button.xml" />
<AndroidResource Include="Resources\layout\FragmentFixup.axml" />
<AndroidResource Include="Resources\layout\Main.axml" />
<AndroidResource Include="Resources\xml\XmlReaderResourceParser.xml" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
Expand Down
31 changes: 31 additions & 0 deletions src/Mono.Android/Test/Resources/layout/FragmentFixup.axml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<fragment
android:name="Xamarin.Android.RuntimeTests.MyFragment"
android:id="@+id/csharp_simple_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<fragment
android:name="xamarin.android.runtimetests.MyFragment"
android:id="@+id/csharp_legacy_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<fragment
android:name="Xamarin.Android.RuntimeTests.MyFragment, Mono.Android-Tests"
android:id="@+id/csharp_partial_assembly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<fragment
android:name="Xamarin.Android.RuntimeTests.MyFragment, Mono.Android-Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
android:id="@+id/csharp_full_assembly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
34 changes: 28 additions & 6 deletions src/Mono.Android/Test/Resources/layout/Main.axml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,37 @@
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:tools="http://schemas.xamarin.com/android/tools"
tools:class="Xamarin.Android.RuntimeTests.MainActivity"
>
<TextView
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/first_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:editable="false"
android:background="@color/uicolor_lightGrayColor"
/>
/>
<TextView
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/second_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:editable="false"
android:background="@color/WhiterShadeOfPale"
/>
/>
<fragment
android:name="Xamarin.Android.RuntimeTests.MyFragment"
android:id="@+id/csharp_simple_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<fragment
android:name="Xamarin.Android.RuntimeTests.MyFragment, Hello"
android:id="@+id/csharp_partial_assembly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<fragment
android:name="Xamarin.Android.RuntimeTests.MyFragment, Hello, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
android:id="@+id/csharp_full_assembly"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
using System.Reflection;
using Android.App;
using Android.OS;
using Android.Views;
using Android.Widget;
using Xamarin.Android.NUnitLite;

namespace Xamarin.Android.RuntimeTests
{
[Activity (Label = "runtime", MainLauncher = true,
Name="xamarin.android.runtimetests.MainActivity")]
public class MainActivity : TestSuiteActivity
public partial class MainActivity : TestSuiteActivity
{
protected override void OnCreate (Bundle bundle)
{
// Note; for testing <fragment/> fixup only.
// The actual view is set/replaced in `TestSuiteActivity.OnCreate()`
SetContentView (Resource.Layout.FragmentFixup);

first_text_view.Click += delegate {
// ignore
};

// tests can be inside the main assembly
AddTest (Assembly.GetExecutingAssembly ());
// or in any reference assemblies
Expand All @@ -20,5 +30,17 @@ protected override void OnCreate (Bundle bundle)
base.OnCreate (bundle);
}
}

#if __ANDROID_11__
public class MyFragment : Fragment {

public override View OnCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
return new TextView (Activity) {
Text = "via fragment!",
};
}
}
#endif // ANDROID_15
}

120 changes: 109 additions & 11 deletions src/Xamarin.Android.Build.Tasks/Tasks/GenerateLayoutCodeBehind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public bool IsLeaf {
public Widget Parent { get; set; }
public bool IsInaccessible { get; set; }
public bool IsRoot { get; set; }
public bool IsFragment { get; set; }

public void AddChild (Widget child)
{
Expand Down Expand Up @@ -121,7 +122,7 @@ public override bool Execute ()

void GetLineInfo (IXmlLineInfo linfo, out int line, out int column)
{
if (linfo == null || linfo.HasLineInfo ()) {
if (linfo == null || !linfo.HasLineInfo ()) {
line = column = 1;
return;
}
Expand Down Expand Up @@ -167,7 +168,6 @@ bool GenerateLayoutMembers (CodeTypeDeclaration mainClass, string fileName)
LoadWidgets (fileName, subtree, lineInfo, androidNS, root, globalIdCache);
}
}

}
}
}
Expand All @@ -194,8 +194,30 @@ void LoadWidgets (string fileName, XmlReader reader, IXmlLineInfo lineinfo, str
}
}
while (reader.Read ()) {
LoadWidgets (fileName, reader, lineinfo, androidNS, root ?? widgetRoot, globalIdCache);
LoadWidgets (fileName, reader, lineinfo, androidNS, widgetRoot, globalIdCache);
}
}

static readonly Dictionary<string, Func<Widget, XmlReader, Widget>> WidgetCreators = new Dictionary<string, Func<Widget, XmlReader, Widget>> {
["fragment"] = CreateFragmentWidget,
};

const string AndroidXmlNamespace = "http://schemas.android.com/apk/res/android";
const string AndroidXmlName = "name";

static Widget CreateFragmentWidget (Widget w, XmlReader e)
{
var type = e.GetAttribute (AndroidXmlName, AndroidXmlNamespace);
if (type == null) {
return null;
}

w.IsFragment = true;

var c = type.IndexOf (',');
w.Type = c < 0 ? type : type.Substring (0, c);
w.Type = w.Type;
return w;
}

Widget CreateWidget (string fileName, XmlReader e, IXmlLineInfo lineInfo, string id, Widget parent)
Expand All @@ -220,6 +242,11 @@ Widget CreateWidget (string fileName, XmlReader e, IXmlLineInfo lineInfo, string
Column = column
};

Func<Widget, XmlReader, Widget> creator;
if (WidgetCreators.TryGetValue (e.LocalName, out creator)) {
return creator (ret, e);
}

return ret;
}

Expand Down Expand Up @@ -401,7 +428,7 @@ CodeMemberMethod ImplementWidgetCreator (Widget widget, CodeExpression parent, L

CodeMethodInvokeExpression CreateFindViewInvoke (Widget widget, CodeExpression parent, CodeExpression parentView)
{
var findViewRef = new CodeMethodReferenceExpression (parent, "__FindView");
var findViewRef = new CodeMethodReferenceExpression (parent, widget.IsFragment ? "__FindFragment" : "__FindView");
findViewRef.TypeArguments.Add (new CodeTypeReference (widget.Type));

return new CodeMethodInvokeExpression (findViewRef, new CodeExpression [] { parentView, new CodeSnippetExpression (widget.ID) });
Expand All @@ -416,7 +443,7 @@ CodeMemberField CreateBackingField (Widget widget, string memberType)

CodeMemberField CreateBackingFuncField (Widget widget, string memberType)
{
return new CodeMemberField ($"Func<{memberType}>", $"__{widget.Name}Func");
return new CodeMemberField ($"global::System.Func<{memberType}>", $"__{widget.Name}Func");
}

bool HasUniqueId (Widget widget, Dictionary<string, int> globalIdCache)
Expand Down Expand Up @@ -484,7 +511,7 @@ ITaskItem GenerateCodeBehind (ITaskItem layoutFile, CodeGeneratorOptions generat
var ns = new CodeNamespace (namespaceName);
compileUnit.Namespaces.Add (ns);
foreach (string import in StandardImports)
ns.Imports.Add (new CodeNamespaceImport (import));
ns.Imports.Add (new CodeNamespaceImport ($"global::{import}"));

CodeTypeDeclaration mainClass = AddMainClass (layoutFile, ns, className);
if (!GenerateLayoutMembers (mainClass, Path.GetFullPath (layoutFile.ItemSpec)))
Expand Down Expand Up @@ -578,7 +605,9 @@ void AddCommonMembers (CodeTypeDeclaration klass, ITaskItem layoutFile)
klass.Members.Add (ImplementFindView (activityTypeRef));
klass.Members.Add (ImplementFindView (new CodeTypeReference ("Android.App.Fragment", CodeTypeReferenceOptions.GlobalReference), activityTypeRef, (CodeVariableReferenceExpression parentView) => new CodePropertyReferenceExpression (parentView, "Activity")));
klass.Members.Add (ImplementEnsureView ());
klass.Members.Add (ImplementFindFragment (activityTypeRef));
klass.Members.Add (new CodeSnippetTypeMember ("\tpartial void OnLayoutViewNotFound<T> (int resourceId, ref T type) where T : global::Android.Views.View;"));
klass.Members.Add (new CodeSnippetTypeMember ("\tpartial void OnLayoutFragmentNotFound<T> (int resourceId, ref T type) where T : global::Android.App.Fragment;"));
}

CodeMemberMethod ImplementInitializeContentView (ITaskItem layoutFile)
Expand Down Expand Up @@ -609,7 +638,7 @@ CodeMemberMethod ImplementEnsureView ()
method.TypeParameters.Add (typeParam);

var tRef = new CodeTypeReference (typeParam);
var funcRef = new CodeTypeReference (typeof (Func<>));
var funcRef = new CodeTypeReference (typeof (Func<>), CodeTypeReferenceOptions.GlobalReference);
funcRef.TypeArguments.Add (tRef);
method.Parameters.Add (new CodeParameterDeclarationExpression (funcRef, "creator"));

Expand All @@ -635,7 +664,7 @@ CodeMemberMethod ImplementEnsureView ()
var creatorVarRef = new CodeVariableReferenceExpression ("creator");
var argNullEx = new CodeThrowExceptionStatement (
new CodeObjectCreateExpression (
new CodeTypeReference (typeof (ArgumentNullException)),
new CodeTypeReference (typeof (ArgumentNullException), CodeTypeReferenceOptions.GlobalReference),
new [] { new CodeSnippetExpression ("nameof (creator)") }
)
);
Expand All @@ -653,6 +682,75 @@ CodeMemberMethod ImplementEnsureView ()
return method;
}

CodeMemberMethod ImplementFindFragment (CodeTypeReference typeForParent, CodeTypeReference typeForOverloadCall = null, Func<CodeVariableReferenceExpression, CodeExpression> constructParentViewCall = null)
{
CodeMemberMethod method = CreateMethod ("__FindFragment", MethodAccessibility.Private, MethodScope.Final);

var typeParam = new CodeTypeParameter ("T") {
Constraints = {
new CodeTypeReference ("Android.App.Fragment", CodeTypeReferenceOptions.GlobalReference),
},
};
method.TypeParameters.Add (typeParam);
method.Parameters.Add (new CodeParameterDeclarationExpression (typeForParent, "activity"));
method.Parameters.Add (new CodeParameterDeclarationExpression (typeof (int), "id"));

var tReference = new CodeTypeReference (typeParam);
method.ReturnType = tReference;

// T fragment = FragmentManager.FindFragmentById<T> (id);
var id = new CodeVariableReferenceExpression ("id");

var findByIdRef = new CodeMethodReferenceExpression (
new CodePropertyReferenceExpression (new CodeVariableReferenceExpression ("activity"), "FragmentManager"),
"FindFragmentById",
new [] { tReference }
);

var findByIdInvoke = new CodeMethodInvokeExpression (findByIdRef, new [] { id });
var viewVar = new CodeVariableDeclarationStatement (tReference, "fragment", findByIdInvoke);
method.Statements.Add (viewVar);

// if (view == null) {
// OnLayoutFragmentNotFound (resourceId, ref view);
// }
// if (view != null)
// return view;
// throw new System.InvalidOperationException($"Fragment not found (ID: {id})");

var viewVarRef = new CodeVariableReferenceExpression ("fragment");
var ifViewNotNull = new CodeConditionStatement (
new CodeBinaryOperatorExpression (viewVarRef, CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression (null)),
new CodeStatement [] { new CodeMethodReturnStatement (viewVarRef) }
);

var viewRefParam = new CodeDirectionExpression (FieldDirection.Ref, viewVarRef);
var viewNotFoundInvoke = new CodeMethodInvokeExpression (
new CodeThisReferenceExpression (),
"OnLayoutFragmentNotFound",
new CodeExpression [] { id, viewRefParam }
);

var ifViewNull = new CodeConditionStatement (
new CodeBinaryOperatorExpression (viewVarRef, CodeBinaryOperatorType.IdentityEquality, new CodePrimitiveExpression (null)),
new CodeStatement [] { new CodeExpressionStatement (viewNotFoundInvoke) }
);

method.Statements.Add (ifViewNull);
method.Statements.Add (ifViewNotNull);

var throwInvOp = new CodeThrowExceptionStatement (
new CodeObjectCreateExpression (
new CodeTypeReference (typeof (InvalidOperationException), CodeTypeReferenceOptions.GlobalReference),
new [] { new CodeSnippetExpression ("$\"Fragment not found (ID: {id})\"") }
)
);

method.Statements.Add (throwInvOp);

return method;
}

CodeMemberMethod ImplementFindView (CodeTypeReference typeForParent, CodeTypeReference typeForOverloadCall = null, Func<CodeVariableReferenceExpression, CodeExpression> constructParentViewCall = null)
{
CodeMemberMethod method = CreateMethod ("__FindView", MethodAccessibility.Private, MethodScope.Final);
Expand Down Expand Up @@ -729,7 +827,7 @@ CodeMemberMethod ImplementFindView (CodeTypeReference typeForParent, CodeTypeRef

var throwInvOp = new CodeThrowExceptionStatement (
new CodeObjectCreateExpression (
new CodeTypeReference (typeof (InvalidOperationException)),
new CodeTypeReference (typeof (InvalidOperationException), CodeTypeReferenceOptions.GlobalReference),
new [] { new CodeSnippetExpression ("$\"View not found (ID: {resourceId})\"") }
)
);
Expand All @@ -746,7 +844,7 @@ CodeMemberMethod CreateMethod (string methodName, MethodAccessibility access, Me

CodeMemberMethod CreateMethod (string methodName, MethodAccessibility access, MethodScope scope, Type returnType)
{
return CreateMethod (methodName, access, scope, new CodeTypeReference (returnType));
return CreateMethod (methodName, access, scope, new CodeTypeReference (returnType, CodeTypeReferenceOptions.GlobalReference));
}

CodeMemberMethod CreateMethod (string methodName, MethodAccessibility access, MethodScope scope, string returnType)
Expand Down Expand Up @@ -813,7 +911,7 @@ void MarkAsCompilerGenerated (CodeTypeMember member)

void AddCustomAttribute (CodeAttributeDeclarationCollection attributes, Type type)
{
attributes.Add (new CodeAttributeDeclaration (new CodeTypeReference (type)));
attributes.Add (new CodeAttributeDeclaration (new CodeTypeReference (type, CodeTypeReferenceOptions.GlobalReference)));
}
}
}

0 comments on commit c2f3c99

Please sign in to comment.