Add support for preview of pages and view without XAML

This commit is contained in:
Andoni Morales Alastruey 2019-03-14 01:22:45 +01:00
Родитель 5ad8c1d888
Коммит fa71d6864d
9 изменённых файлов: 204 добавлений и 38 удалений

Просмотреть файл

@ -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<FormsViewClassDeclaration> 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;
}
}
}

Просмотреть файл

@ -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<string, string>();
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<string, string>();
Namespace = xaml.Type.Substring(0, xaml.Type.LastIndexOf('.'));
ClassName = xaml.Type.Split('.').Last();
@ -479,5 +485,28 @@ namespace XAMLator.Client
.OfType<ClassDeclarationSyntax>()
.Single(c => c.Identifier.Text == className);
}
internal static ClassDeclarationSyntax FindFormsViewClass(SyntaxTree syntaxTree, SemanticModel model)
{
return syntaxTree.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.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;
}
}
}

Просмотреть файл

@ -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))

Просмотреть файл

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

Просмотреть файл

@ -0,0 +1,13 @@
using Xamarin.Forms;
namespace XAMLator.Server.Tests
{
public class NoXAMLTestContentView : ContentView
{
public NoXAMLTestContentView()
{
Content = new Label { Text = "Hello ContentView" };
}
}
}

Просмотреть файл

@ -0,0 +1,7 @@
namespace XAMLator.Server.Tests
{
public class NoXamlTestInheritanceContentView : NoXAMLTestContentView
{
}
}

Просмотреть файл

@ -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<string> { 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);
}

Просмотреть файл

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

Просмотреть файл

@ -212,6 +212,9 @@
<Compile Include="XAMLatorFeatureBase.cs" />
<Compile Include="UsageFeature.cs" />
<Compile Include="CSSUpdateFeature.cs" />
<Compile Include="NoXAMLTestContentPage.cs" />
<Compile Include="NoXAMLTestContentView.cs" />
<Compile Include="NoXamlTestInheritanceContentView.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="TestPage.xaml">