From fa71d6864d7c8248fefe9848132ac09fb58af7b2 Mon Sep 17 00:00:00 2001 From: Andoni Morales Alastruey Date: Thu, 14 Mar 2019 01:22:45 +0100 Subject: [PATCH] Add support for preview of pages and view without XAML --- XAMLator.Client/DocumentParser.cs | 78 ++++++++++++------- XAMLator.Client/FormsViewClassDeclaration.cs | 33 +++++++- XAMLator.Server.Shared/Previewer.cs | 13 ++-- .../NoXAMLTestContentPage.cs | 18 +++++ .../NoXAMLTestContentView.cs | 13 ++++ .../NoXamlTestInheritanceContentView.cs | 7 ++ XAMLator.Server.Tests/TestWorkspace.cs | 22 +++++- .../ViewWithoutXAMLPreviewFeature.cs | 55 ++++++++++++- .../XAMLator.Server.Tests.csproj | 3 + 9 files changed, 204 insertions(+), 38 deletions(-) create mode 100644 XAMLator.Server.Tests/NoXAMLTestContentPage.cs create mode 100644 XAMLator.Server.Tests/NoXAMLTestContentView.cs create mode 100644 XAMLator.Server.Tests/NoXamlTestInheritanceContentView.cs diff --git a/XAMLator.Client/DocumentParser.cs b/XAMLator.Client/DocumentParser.cs index 6090a48..c750a0f 100644 --- a/XAMLator.Client/DocumentParser.cs +++ b/XAMLator.Client/DocumentParser.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace XAMLator.Client { @@ -23,34 +24,33 @@ namespace XAMLator.Client SemanticModel semanticModel) { XAMLDocument xamlDocument = null; - FormsViewClassDeclaration xamlClass = null; - bool created; + FormsViewClassDeclaration formsViewClass = null; // FIXME: Support any kind of types, not just Xamarin.Forms views - if (!fileName.EndsWith(".xaml") && !fileName.EndsWith(".xaml.cs")) + if (!fileName.EndsWith(".xaml") && !fileName.EndsWith(".xaml.cs") && !fileName.EndsWith(".cs")) { return null; } // Check if we have already an instance of the class declaration for that file - if (!FormsViewClassDeclaration.TryGetByFileName(fileName, out xamlClass)) + if (!FormsViewClassDeclaration.TryGetByFileName(fileName, out formsViewClass)) { if (fileName.EndsWith(".xaml")) { xamlDocument = XAMLDocument.Parse(fileName, text); // Check if we have an instance of class by namespace - if (!FormsViewClassDeclaration.TryGetByFullNamespace(xamlDocument.Type, out xamlClass)) + if (!FormsViewClassDeclaration.TryGetByFullNamespace(xamlDocument.Type, out formsViewClass)) { - xamlClass = await CreateFromXaml(xamlDocument); + formsViewClass = await CreateFromXaml(xamlDocument); } } else { - xamlClass = await CreateFromCodeBehind(fileName, syntaxTree, semanticModel); + formsViewClass = await CreateFromCodeBehind(fileName, syntaxTree, semanticModel); } } - if (xamlClass == null) + if (formsViewClass == null) { return null; } @@ -59,19 +59,19 @@ namespace XAMLator.Client if (fileName.EndsWith(".xaml") && xamlDocument == null) { xamlDocument = XAMLDocument.Parse(fileName, text); - await xamlClass.UpdateXaml(xamlDocument); + await formsViewClass.UpdateXaml(xamlDocument); } - // The document is code behind - if (fileName.EndsWith(".xaml.cs")) + // The document is code behind or a view without XAML + if (fileName.EndsWith(".cs")) { - var classDeclaration = FormsViewClassDeclaration.FindClass(syntaxTree, xamlClass.ClassName); - if (xamlClass.NeedsClassInitialization) + var classDeclaration = FormsViewClassDeclaration.FindClass(syntaxTree, formsViewClass.ClassName); + if (formsViewClass.NeedsClassInitialization) { - xamlClass.FillClassInfo(classDeclaration, semanticModel); + formsViewClass.FillClassInfo(classDeclaration, semanticModel); } - xamlClass.UpdateCode(classDeclaration, semanticModel); + formsViewClass.UpdateCode(classDeclaration, semanticModel); } - return xamlClass; + return formsViewClass; } static async Task CreateFromXaml(XAMLDocument xamlDocument) @@ -92,8 +92,12 @@ namespace XAMLator.Client SyntaxTree syntaxTree, SemanticModel semanticModel) { - string xaml = null, xamlFilePath = null, codeBehindFilePath = null; - XAMLDocument xamlDocument; + string xaml = null; + string xamlFilePath = null; + string codeBehindFilePath = null; + string className = null; + ClassDeclarationSyntax classDeclaration; + XAMLDocument xamlDocument = null; codeBehindFilePath = fileName; var xamlCandidate = fileName.Substring(0, fileName.Length - 3); @@ -104,16 +108,34 @@ namespace XAMLator.Client } // FIXME: Handle XF views without XAML - // Parse the XAML file - xamlDocument = XAMLDocument.Parse(xamlFilePath, xaml); - - var className = xamlDocument.Type.Split('.').Last(); - var classDeclaration = FormsViewClassDeclaration.FindClass(syntaxTree, className); - var xamlClass = new FormsViewClassDeclaration(classDeclaration, semanticModel, - codeBehindFilePath, xamlDocument); - - await xamlClass.UpdateXaml(xamlDocument); - return xamlClass; + if (xamlFilePath != null) + { + // Parse the XAML file + xamlDocument = XAMLDocument.Parse(xamlFilePath, xaml); + className = xamlDocument.Type.Split('.').Last(); + classDeclaration = FormsViewClassDeclaration.FindClass(syntaxTree, className); + } + else + { + try + { + classDeclaration = FormsViewClassDeclaration.FindFormsViewClass(syntaxTree, semanticModel); + } + catch (Exception ex) + { + Log.Exception(ex); + Log.Error("The class is not a Xamarin.Forms View or Page"); + return null; + } + } + var formsClass = new FormsViewClassDeclaration(classDeclaration, semanticModel, + codeBehindFilePath, xamlDocument); + + if (xamlDocument != null) + { + await formsClass.UpdateXaml(xamlDocument); + } + return formsClass; } } } \ No newline at end of file diff --git a/XAMLator.Client/FormsViewClassDeclaration.cs b/XAMLator.Client/FormsViewClassDeclaration.cs index de13a2b..dc8c2c1 100644 --- a/XAMLator.Client/FormsViewClassDeclaration.cs +++ b/XAMLator.Client/FormsViewClassDeclaration.cs @@ -96,7 +96,10 @@ namespace XAMLator.Client string codeBehindFilePath, XAMLDocument xaml) { this.codeBehindFilePath = codeBehindFilePath; - this.xamlFilePath = xaml.FilePath; + if (xaml != null) + { + xamlFilePath = xaml.FilePath; + } StyleSheets = new Dictionary(); UpdateCode(classDeclarationSyntax, model); classesCache.Add(this); @@ -111,7 +114,10 @@ namespace XAMLator.Client public FormsViewClassDeclaration(string codeBehindFilePath, XAMLDocument xaml) { this.codeBehindFilePath = codeBehindFilePath; - this.xamlFilePath = xaml.FilePath; + if (xaml != null) + { + xamlFilePath = xaml.FilePath; + } StyleSheets = new Dictionary(); Namespace = xaml.Type.Substring(0, xaml.Type.LastIndexOf('.')); ClassName = xaml.Type.Split('.').Last(); @@ -479,5 +485,28 @@ namespace XAMLator.Client .OfType() .Single(c => c.Identifier.Text == className); } + + internal static ClassDeclarationSyntax FindFormsViewClass(SyntaxTree syntaxTree, SemanticModel model) + { + return syntaxTree.GetRoot() + .DescendantNodes() + .OfType() + .First(c => IsViewOrPage(model.GetDeclaredSymbol(c))); + } + + static bool IsViewOrPage(INamedTypeSymbol type) + { + do + { + var name = type.ToString(); + if (name == "Xamarin.Forms.Page" || name == "Xamarin.Forms.View" || + name == "Xamarin.Forms.ContentPage" || name == "Xamarin.Forms.ContentView") + { + return true; + } + type = type.BaseType; + } while (type != null); + return false; + } } } diff --git a/XAMLator.Server.Shared/Previewer.cs b/XAMLator.Server.Shared/Previewer.cs index b4835e9..4da977e 100644 --- a/XAMLator.Server.Shared/Previewer.cs +++ b/XAMLator.Server.Shared/Previewer.cs @@ -68,16 +68,19 @@ namespace XAMLator.Server protected virtual Page CreateViewFromResult(EvalResult res) { - if (res.HasResult) + Page page; + if (!res.HasResult) { - return res.Result as Page; + res.Result = TypeActivator(res.ResultType); } - var result = TypeActivator(res.ResultType); - Page page = result as Page; - if (page == null && result is View view) + if (res.Result is View view) { page = new ContentPage { Content = view }; } + else + { + page = res.Result as Page; + } if (page != null) { if (viewModelsMapping.TryGetValue(res.ResultType, out object viewModel)) diff --git a/XAMLator.Server.Tests/NoXAMLTestContentPage.cs b/XAMLator.Server.Tests/NoXAMLTestContentPage.cs new file mode 100644 index 0000000..6e0fcf0 --- /dev/null +++ b/XAMLator.Server.Tests/NoXAMLTestContentPage.cs @@ -0,0 +1,18 @@ +using Xamarin.Forms; + +namespace XAMLator.Server.Tests +{ + public class NoXAMLTestContentPage : ContentPage + { + public NoXAMLTestContentPage() + { + Content = new StackLayout + { + Children = { + new Label { Text = "Hello ContentPage" } + } + }; + } + } +} + diff --git a/XAMLator.Server.Tests/NoXAMLTestContentView.cs b/XAMLator.Server.Tests/NoXAMLTestContentView.cs new file mode 100644 index 0000000..d8c2a7d --- /dev/null +++ b/XAMLator.Server.Tests/NoXAMLTestContentView.cs @@ -0,0 +1,13 @@ +using Xamarin.Forms; + +namespace XAMLator.Server.Tests +{ + public class NoXAMLTestContentView : ContentView + { + public NoXAMLTestContentView() + { + Content = new Label { Text = "Hello ContentView" }; + } + } +} + diff --git a/XAMLator.Server.Tests/NoXamlTestInheritanceContentView.cs b/XAMLator.Server.Tests/NoXamlTestInheritanceContentView.cs new file mode 100644 index 0000000..e1c5c06 --- /dev/null +++ b/XAMLator.Server.Tests/NoXamlTestInheritanceContentView.cs @@ -0,0 +1,7 @@ +namespace XAMLator.Server.Tests +{ + public class NoXamlTestInheritanceContentView : NoXAMLTestContentView + { + } +} + diff --git a/XAMLator.Server.Tests/TestWorkspace.cs b/XAMLator.Server.Tests/TestWorkspace.cs index e9b2d51..d41b8c8 100644 --- a/XAMLator.Server.Tests/TestWorkspace.cs +++ b/XAMLator.Server.Tests/TestWorkspace.cs @@ -18,6 +18,9 @@ namespace XAMLator.Server.Tests CopyFile(TestProjectDir, "TestPage.xaml", tempDir); CopyFile(TestProjectDir, "TestPage.xaml.cs", tempDir); CopyFile(TestAutogenDir, "TestPage.xaml.g.cs", tempDir); + CopyFile(TestProjectDir, "NoXAMLTestContentPage.cs", tempDir); + CopyFile(TestProjectDir, "NoXAMLTestContentView.cs", tempDir); + CopyFile(TestProjectDir, "NoXAMLTestInheritanceContentView.cs", tempDir); workspace = new AdhocWorkspace(); var solution = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default); workspace.AddSolution(solution); @@ -29,6 +32,13 @@ namespace XAMLator.Server.Tests AddDocument(testProject, tempDir, "TestPage.xaml.cs"); AddDocument(testProject, tempDir, "TestPage.xaml"); AddDocument(testProject, tempDir, "TestPage.xaml.g.cs"); + AddDocument(testProject, tempDir, "NoXAMLTestContentPage.cs"); + AddDocument(testProject, tempDir, "NoXAMLTestContentView.cs"); + AddDocument(testProject, tempDir, "NoXAMLTestInheritanceContentView.cs"); + // Define a stub type for ContentPage and ConentView so they resolve to a type and not to an ErrorType + // as the Xamarin.Forms types are not defined in this workspae + AddDocument(testProject, tempDir, "ContentPage.cs", @"namespace Xamarin.Forms { class ContentPage: Page {}}"); + AddDocument(testProject, tempDir, "ContentPage.cs", @"namespace Xamarin.Forms { class ContentView: View {}}"); } public void Dispose() @@ -65,13 +75,23 @@ namespace XAMLator.Server.Tests } Document AddDocument(Project testProject, string dir, string fileName) + { + return AddDocument(testProject, dir, fileName, ReadFile(dir, fileName)); + } + + Document AddDocument(Project testProject, string dir, string fileName, string text) + { + return AddDocument(testProject, dir, fileName, SourceText.From(text)); + } + + Document AddDocument(Project testProject, string dir, string fileName, SourceText content) { DocumentInfo documentInfo = DocumentInfo.Create( DocumentId.CreateNewId(testProject.Id), fileName, new List { dir }, SourceCodeKind.Regular, - TextLoader.From(TextAndVersion.Create(ReadFile(dir, fileName), VersionStamp.Create())), + TextLoader.From(TextAndVersion.Create(content, VersionStamp.Create())), Path.Combine(dir, fileName)); return workspace.AddDocument(documentInfo); } diff --git a/XAMLator.Server.Tests/ViewWithoutXAMLPreviewFeature.cs b/XAMLator.Server.Tests/ViewWithoutXAMLPreviewFeature.cs index fa0946d..b6c2481 100644 --- a/XAMLator.Server.Tests/ViewWithoutXAMLPreviewFeature.cs +++ b/XAMLator.Server.Tests/ViewWithoutXAMLPreviewFeature.cs @@ -1,10 +1,61 @@ using System; +using System.Threading.Tasks; +using NUnit.Framework; +using Xamarin.Forms; + namespace XAMLator.Server.Tests { - public class ViewWithoutXAMLPreviewFeature + [TestFixture] + public class ViewWithoutXAMLPreviewFeature : XAMLatorFeatureBase { - public ViewWithoutXAMLPreviewFeature() + [Test] + public async Task The_user_changes_a_content_page_class_without_xaml_and_its_previewed() { + await When_the_code_changes("NoXAMLTestContentPage.cs", @" + using Xamarin.Forms; + namespace XAMLator.Server.Tests { + public class NoXAMLTestContentPage : ContentPage { + public NoXAMLTestContentPage() { + Content = new Label { Text = ""Hello ContentPage"" }; + } + } + }"); + var label = (previewer.PreviewedPage as ContentPage).Content as Label; + Assert.AreEqual(PreviewState.Preview, previewer.State); + Assert.AreEqual("Hello ContentPage", label.Text); + } + + [Test] + public async Task The_user_changes_a_conent_view_class_without_xaml_and_its_previewed() + { + await When_the_code_changes("NoXAMLTestContentView.cs", @" + using Xamarin.Forms; + namespace XAMLator.Server.Tests { + public class NoXAMLTestContentView : ContentView { + public NoXAMLTestContentView() { + Content = new Label { Text = ""Hello!"" }; + } + } + }"); + var label = ((previewer.PreviewedPage as ContentPage).Content as ContentView).Content as Label; + Assert.AreEqual(PreviewState.Preview, previewer.State); + Assert.AreEqual("Hello!", label.Text); + } + + [Test] + public async Task The_user_changes_a_view_page_class_with_inheritance_without_xaml_and_its_previewed() + { + await When_the_code_changes("NoXAMLTestInheritanceContentView.cs", @" + using Xamarin.Forms; + namespace XAMLator.Server.Tests { + public class NoXAMLTestInheritanceContentView : NoXAMLTestContentView { + public NoXAMLTestInheritanceContentView() { + } + } + }"); + var label = ((previewer.PreviewedPage as ContentPage).Content as ContentView).Content as Label; + Assert.AreEqual(PreviewState.Preview, previewer.State); + Assert.AreEqual("Hello ContentView", label.Text); } } } diff --git a/XAMLator.Server.Tests/XAMLator.Server.Tests.csproj b/XAMLator.Server.Tests/XAMLator.Server.Tests.csproj index 4141209..d8d94ee 100644 --- a/XAMLator.Server.Tests/XAMLator.Server.Tests.csproj +++ b/XAMLator.Server.Tests/XAMLator.Server.Tests.csproj @@ -212,6 +212,9 @@ + + +