From c2f3c99ed619ea7ba1e1ffbce4d49420f393cfc9 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Tue, 13 Feb 2018 22:54:14 -0500 Subject: [PATCH] [Xamarin.Android.Build.Tasks] Fragments & CodeBehind While trying to repro Issue #1296, I had a brilliant idea: let's use the new CodeBehind support from 7c31899b! Everything then fell apart. For starters, Issue #1296 deals with Fragments, and 7c31899b didn't support the presence of `` within Layout files. Plumb support for that, by adding: partial class MainActivity { partial void OnLayoutFragmentNotFound (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 `` -- 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. --- .../Test/Mono.Android-Tests.csproj | 2 + .../Test/Resources/layout/FragmentFixup.axml | 31 +++++ .../Test/Resources/layout/Main.axml | 34 ++++- .../MainActivity.cs | 24 +++- .../Tasks/GenerateLayoutCodeBehind.cs | 120 ++++++++++++++++-- 5 files changed, 193 insertions(+), 18 deletions(-) create mode 100644 src/Mono.Android/Test/Resources/layout/FragmentFixup.axml diff --git a/src/Mono.Android/Test/Mono.Android-Tests.csproj b/src/Mono.Android/Test/Mono.Android-Tests.csproj index ef762d3d91d..0d107101d4e 100644 --- a/src/Mono.Android/Test/Mono.Android-Tests.csproj +++ b/src/Mono.Android/Test/Mono.Android-Tests.csproj @@ -97,6 +97,8 @@ + + diff --git a/src/Mono.Android/Test/Resources/layout/FragmentFixup.axml b/src/Mono.Android/Test/Resources/layout/FragmentFixup.axml new file mode 100644 index 00000000000..db6bb86933f --- /dev/null +++ b/src/Mono.Android/Test/Resources/layout/FragmentFixup.axml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/src/Mono.Android/Test/Resources/layout/Main.axml b/src/Mono.Android/Test/Resources/layout/Main.axml index 25d909baff7..97958e7bd90 100644 --- a/src/Mono.Android/Test/Resources/layout/Main.axml +++ b/src/Mono.Android/Test/Resources/layout/Main.axml @@ -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" > + /> + /> + + + diff --git a/src/Mono.Android/Test/Xamarin.Android.RuntimeTests/MainActivity.cs b/src/Mono.Android/Test/Xamarin.Android.RuntimeTests/MainActivity.cs index a69679300d4..d2412fa1267 100644 --- a/src/Mono.Android/Test/Xamarin.Android.RuntimeTests/MainActivity.cs +++ b/src/Mono.Android/Test/Xamarin.Android.RuntimeTests/MainActivity.cs @@ -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 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 @@ -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 } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateLayoutCodeBehind.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateLayoutCodeBehind.cs index a493d959c78..96bc08026fd 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateLayoutCodeBehind.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateLayoutCodeBehind.cs @@ -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) { @@ -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; } @@ -167,7 +168,6 @@ bool GenerateLayoutMembers (CodeTypeDeclaration mainClass, string fileName) LoadWidgets (fileName, subtree, lineInfo, androidNS, root, globalIdCache); } } - } } } @@ -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> WidgetCreators = new Dictionary> { + ["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) @@ -220,6 +242,11 @@ Widget CreateWidget (string fileName, XmlReader e, IXmlLineInfo lineInfo, string Column = column }; + Func creator; + if (WidgetCreators.TryGetValue (e.LocalName, out creator)) { + return creator (ret, e); + } + return ret; } @@ -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) }); @@ -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 globalIdCache) @@ -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))) @@ -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 (int resourceId, ref T type) where T : global::Android.Views.View;")); + klass.Members.Add (new CodeSnippetTypeMember ("\tpartial void OnLayoutFragmentNotFound (int resourceId, ref T type) where T : global::Android.App.Fragment;")); } CodeMemberMethod ImplementInitializeContentView (ITaskItem layoutFile) @@ -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")); @@ -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)") } ) ); @@ -653,6 +682,75 @@ CodeMemberMethod ImplementEnsureView () return method; } + CodeMemberMethod ImplementFindFragment (CodeTypeReference typeForParent, CodeTypeReference typeForOverloadCall = null, Func 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 (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 constructParentViewCall = null) { CodeMemberMethod method = CreateMethod ("__FindView", MethodAccessibility.Private, MethodScope.Final); @@ -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})\"") } ) ); @@ -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) @@ -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))); } } } \ No newline at end of file