Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.Sign up
XML literals Enhancements to be more supportive to Vazor: the built-in VB syntax Razor! #397
In this proposal aspnet/AspNetCore#8674 (please all VB.NET fans, support it), I suggested to use xml literals to implement ASP.NET MVC razor in VB.NET instead of vbhtml files, like this:
Friend Class View1 Dim Model As IEnumerable(Of MvcMusicStore.Models.Genre) Public Sub New(model As IEnumerable(Of MvcMusicStore.Models.Genre)) Me.Model = model ViewBag.Title = "Store" End Sub 'ViewBag.Title = "Store" Public ReadOnly Property Razor Get Return _ <html> <h3> Browse Genres</h3> <p> Select from <%= Model.Count() %> genres: </p> <ul> <%= (From genre In Model Select <li><%= genre.Name %></li>) %> </ul> </html> End Get End Property End Class
In this code, I used LinQ because I need to return a value to be embedded in the xml literal. I tried another way:
<ul> <%= (Iterator Function() For Each genre In Model Yield <li><%= genre.Name %></li> Next End Function)() %> </ul>
Using inline lambdas like this, makes it possible to evaluate any expression with any VB code we want, but I think this can be simplified and shortened as this:
<ul> <%= For Each genre In Model Yield <li><%= genre.Name %></li> Next %> </ul>
This is more like a vbhtml razor page code!
Where [Iterator] is added if Yield is used in code.
<ul> <%= Select Case Model.Count Case 1 Return <li>One Item</li> Case 2 Return <li>Two Items</li> Case Else Return <li>Many Items</li> End Select %> </ul>
Which is a shortcut for:
<ul> <%= (Function() Select Case Model.Count Case 1 Return <li>One Item</li> Case 2 Return <li>Two Items</li> Case Else Return <li>Many Items</li> End Select End Function)() %> </ul>
I hope these proposal are taken seriously. VB.NET is too powerful and has many precious hidden treasures, and it is really unfortunate that MS decided to left VB.NET behind to save some effort and money, while VB.NET could have been saved too much time , effort and money spent to develop Razor syntax, while it is already supported in VB syntax!
Finally: A working VB.NET ASP.NET MVC Core Razor sample!
services.AddTransient(Of IConfigureOptions(Of MvcViewOptions), VBRazor.VBRazorMvcViewOptionsSetup)() services.AddSingleton(Of IViewEngine, VBRazor.VBRazorViewEngine)()
The VBRazor is just a VB class that implements the IVBRazor Interface:
Public Interface IVBRazor ReadOnly Property Razor As String End Interface
The Razor property uses the xml literals to compose the HTML code and returns it as a string.. Example:
Imports VbRazor Public Class IndexView Implements IVBRazor Dim students As List(Of Student) Public Sub New(students As List(Of Student)) Me.students = students End Sub Public ReadOnly Property Razor As String Implements IVBRazor.Razor Get Dim x = <html> <h3> Browse Students</h3> <p>Select from <%= students.Count() %> students:</p> <ul> <%= (Iterator Function() For Each std In students Yield <li><%= std.Name %></li> Next End Function)() %> </ul> </html> Return x.ToString() End Get End Property End Class
To use the IndexView from the Controller, I passed it to the View method as the model data in the action method, and passed the actual model data to its constructor:
Public Function Index() As IActionResult Return View(New IndexView(Students)) End Function
This was really easy, but needs more work, so I hope you start contribute to this project to make it a real productive tool!
The second thing to do, is to add intellisense support for html attributes in xml literals in VB!
This is a really interesting direction.
My main question at this point is what benefit the infrastructure of Razor Views brings?
If you took the base concept, using XML Literals instead of .cshtml, and didn't try to use any of the rest of the pattern, could you get an even simpler version of MVC.
For example, could you give the .vb file responsibility for calling a common subroutine for header and footer (possibly with a delegate for the body), then the call from the controller is a simple method call - and it becomes just a matter of naming conventions (rather than physical file location) how you find the view. In the simplest case, the view just has a unique name.
I'm explaining that to clarify that it is a serious question to ask what any part of razor brings to an XML literal based approach - beyond the controller routing and action results.
Or put another way - can an XML Literal approach be better/simpler than Razor.
(and regardless, I think there may be benefit in not calling it Razor as that is often associated with the interpretation of pages, which invites an unnecessary comparison).
There is a strange behaiviour of XElement: It is implicit conversion operator returns XElement.Value not XElement.ToString( ). This gives unexpected results such in this example:
Dim s = <p>Test</p> Console.WriteLine(s) ' <p>Test</p> Dim s2 As String = s Console.WriteLine(s2) 'Test
I fall into this in the Razor property, when I accidentally erased the .ToString() from the end of return statement, and got a plain text in the html page!
Public Interface IVBRazor Property ViewBag As Object Property ModelState As ModelStateDictionary Function Razor() As XElement End Interface
I added two new properties to pass the ViewBag and ModelState to the View
This is the modified VBRazorView.RenderAsync
Public Async Function RenderAsync(ByVal context As ViewContext) As Task Implements IView.RenderAsync Dim vbRazor = CType(context.ViewData.Model, IVBRazor) vbRazor.ViewBag = context.ViewBag vbRazor.ModelState = context.ModelState Dim r = Await Task.Run(AddressOf vbRazor.Razor.ToString).ConfigureAwait(False) context.Writer.Write(r) End Function
And this is the IndexView Class:
Imports Microsoft.AspNetCore.Mvc.ModelBinding Imports VbRazor Public Class IndexView Implements IVBRazor Dim students As List(Of Student) Public Sub New(students As List(Of Student)) Me.students = students End Sub Public Property ViewBag As Object Implements IVBRazor.ViewBag Public Property ModelState As ModelStateDictionary Implements IVBRazor.ModelState Public Function Razor() As XElement Implements IVBRazor.Razor ViewBag.Title = "VB Razor Sample" Return _ <html> <h3> Browse Students</h3> <p>Select from <%= students.Count() %> students:</p> <ul> <%= (Iterator Function() For Each std In students Yield <li><%= std.Name %></li> Next End Function)() %> </ul> <script> var x = 5; document.writeln("students count = <%= students.Count() %>"); </script> </html> End Function End Class
I didn't upload these changes to the repo, since I am still experimenting.
I made changes to use a layout View Class.. I used the same layout provided in the C# project template to create this layout class
Things are easy. The layout has a Body property that uses to insert the body in the XML representation of the layout:
<main role="main" class="pb-3"> <%= Body %> </main>
It is the job of each View to render itself as the body of the layout. This is done by the RenderView function:
Public Function RenderView() As XElement Implements IRazor.RenderView Dim layout As New LayoutView(Razor(), ViewBag, ModelState) Return layout.Razor End Function
Note: We need a command to create new View with the basic code like this one:
Imports Microsoft.AspNetCore.Mvc.ModelBinding Imports VbRazor Public Class <<ViewName>> Implements IRazor Dim <<ModelVarName>> As <<ModelType>> Public Sub New(<<ModelVarName>> As <<ModelType>>) Me.<<ModelVarName>> = <<ModelVarName>> End Sub Public Property ViewBag As Object Implements IRazor.ViewBag Public Property ModelState As ModelStateDictionary Implements IRazor.ModelState Public Function RenderView() As XElement Implements IRazor.RenderView Dim layout As New LayoutView(Razor(), ViewBag, ModelState) Return layout.Razor End Function Public Function Razor() As XElement Implements IRazor.Razor ViewBag.Title = "VB Razor Sample" Return _ <div> ' Write you vbxml view here </div> End Function End Class
Note That I had to add a div or p tag to contain the vbxml code. I want to avoid this if possible but have no ideas now.
The same can be done to generate the prototype for the layout class.
Now, the output page is shown like this:
There is more work to do to render sections and other things. I will see also why home and privacy are not shown as links, but now I need to sleep :)
I want to use a new approach. I can generate a simplified cshtml file from the vbxml code , so the C# Razor an carry out all the work as usual! This will:
Public Function Vazor() As XElement ViewBag.Title = "VB Razor Sample" Return _ <vazor> <h3> Browse Students</h3> <p>Select from <%= students.Count() %> students:</p> <ul> <%= (Iterator Function() For Each std In students Yield <li><%= std.Name %></li> Next End Function)() %> </ul> <script> var x = 5; document.writeln("students count = <%= students.Count() %>"); </script> </vazor> End Function
I want to generate this code inside the view class:
Dim Vazor_Value1 As String = students.Count().ToString() Dim Vazor_Value2 = <vb_xml> <%= (Iterator Function() For Each std In students Yield <li><%= std.Name %></li> Next End Function)() %> </vb_xml>.ToString(). Replace("<vb_xml>" + vbCrLf, ""). Replace(vbCrLf + "</vb_xml>", "") Public Sub PassData() ViewBag.Vazor_Value1 = Vazor_Value1 ViewBag.Vazor_Value2 = Vazor_Value2 End Sub
and PassData() must be called somewhere (say at the end of the constructor of the view class).
@KathleenDollard , @AnthonyDGreen
@VBAndCs Do you imagine the vb and cs code existing in the same project? That is probably not going to work.
To answer some specific questions, you could use a custom build task as part of the build - but this won't happen as part of design time build - no IntelliSense.
I would try to avoid parsing the Vazor code yourself if you can just use the XML.
Generating code and adding to an existing code file during the build is tricky - has anyone else done that? It's been a super long time as most of my generation was prior to build in a separate step.
What problems did you find with the straight XML Literal approach? I imagined that we could hand back HTML and bypass large parts of the Razor complexity. Of course with some pretty involved headers and such.
Typically, they are not in the same project, because C# in cshtml files is just a script and Razor (which is another project) is responsible for compiling it. This already working and I tried it :
Yes, I want to do this. Or, it can be done after saving the changes of each view class (that is free of bugs), so we grantee that the chml files are in place b4 the build starts.
1- Need to resolve paths to js and image files and other components.
As a heavy user of XML literals I definitely support this suggestion
Dim TaskInfoEntries = <order-summary-tasks> <%= From task In Me.GetPendingTasks() Select task.ToXML() %> </order-summary-tasks>
By combinining LINQ queries and function calls you can express pretty much anything you want. Since XML literals already support taking collections of
Some food for thought!
<ul> <%= (Function() Select Case Model.Count Case 1 Return <li>One Item</li> Case 2 Return <li>Two Items</li> Case Else Return <li>Many Items</li> End Select End Function)() %> </ul>
So, there is nothing you can'y do.
I saw this chtml
So, I tried to write it with xml literals. I got this:
Dim value = "Value 1" Dim html = GetVazorContent( <Vazor> <p><%= value %></p> <%= (Function() value = "Value 2" Return "" End Function)() %> <p><%= value %></p> </Vazor>)
It is a long workaround just to set a value inside xml!
<Vazor> <p><%= value %></p> <%= value = "Value 2" Return "" %> <p><%= value %></p> </Vazor>)
It is still feels strange!
<Vazor> <p><%= value %></p> <%= Let value = "Value 2" End Let %> <p><%= value %></p> </Vazor>)
And when it is just one statement, we can do it in one line as in LinQ:
<Vazor> <p><%= value %></p> <%= Let value = "Value 2"%> <p><%= value %></p> </Vazor>)
Or another thought: to be compatible with
<Vazor> <p><%= value %></p> <%= Set value = "Value 2" End Set %> <p><%= value %></p> </Vazor>)
And when it is just one statement:
<Vazor> <p><%= value %></p> <%= Set value = "Value 2"%> <p><%= value %></p> </Vazor>)
Another issue with Xaml literals, is that it doesn't allow write the & even inside quotes!
I hope to allow escaping & like this & or any other way.
can you give more context?
I don't do much XAML, but I do a fair bit of XML, and I'd normally expect the XML for that to look more like
Ok. VB accepts
But it worked! The
But, there is still a problem:
but inside quotes, the string vb_amp must inserted directly not because
VB XML literals are following XML character entities, which is a restricted list and doesn't include things like
HTML defines a bunch of extra ones like
So the bigger problem is that VB XML literals are only XML 1.0, and don't support DTDs, and what you're after is adding HTML5 support (the HTML5 DTD) to your XML.
I would say that there is a good argument for expanding VB's XML literals to support this, but that may require expanding LINQ to XML to support them properly (LINQ to XML has only limited DTD support).
Interestingly, good 'ol System.Xml has it - an XmlEntityReference class.
I guess the System.Xml.Linq folks didn't see the value . But then I think LINQ was focused on reading XML, not writing it, whereas System.Xml was from the days when XML was still cool and so needs to do both.
I prefer good solutions to cheap ones. Expanding XML support to include DTD's would be a much more powerful option and allow the XML literal to 'pretty print' any markup built on top of XML, including HTML5.
I just don't know 1) if it would need LINQ-to-XML expanded, or if there another workaround (e.g. deserializing any DTD separately) and 2) how the compiler converts from literals to LINQ-to-XML, so I don't know how much work there is.
To be honest, I think &&& is awful. I don't want to be counting punctuation when figuring out code.
If there were going to be a 'cheap' solution, I think I'd prefer to see any XML identity that literals don't understand 'ignored'. So & > > ' and " are understood and handled. Anything else that looks like an XML IDENTITY should be ignored (e.g. © , etc) and passed through for serialization (which is presumably what's happening to your code).
But once again, I don't know how much work is involved.
I'd love to start digging into the compiler myself, but unfortunately, being self-employed means that earning takes precedence over learning, and I don't currently have the luxury of time to commit to learning a whole new set of tricks.
(edit: fixed those identities!)
If someone has the time to put together a proposal on this, that would be very helpful. I'm not sure we need full DTD support, but understand the request for more than a one-off solution for ampersand. Is it only literals (where a "don't escape this" gesture might be enough). Is there more in XHTML that will affect the XML literal MVC ("Vazor") work. Doesn't need to be fancy, just the start of a list of problems like &xcopy
I am excited to announce that: My Work Is Complete!
To use the Vazor IndexView calss, we map it in the Home.Index acctiom method and pass the unique name generated by the mapper to the view method like this:
and that is all!
This image shows the rendered Page resulted of:
I will wrap somethings up and publish this work at a new repo called Vazor. It seems like a few lines of code, but it is a big step for VB.NET : )
So, the next level is to work on xml and intellisense. This is where I can't go on alone. I need the help of the team an community.
I moved the vxml code to a pratial class to be in its own file, whicvh I named IndexView.vbxml.vb
Partial Public Class IndexView Private Function GetVbXml() As XElement Implements Vazor.IVazorView.GetVbXml ViewBag.Title = "Vazor Sample" Return _ <vbxml> <h3> Browse Students</h3> <p>Select from <%= students.Count() %> students:</p> <ul> <%= (Iterator Function() For Each std In students Yield <li><%= std.Name %></li> Next End Function)() %> </ul> <script> var x = 5; document.writeln("students count = <%= students.Count() %>"); </script> </vbxml> End Function End Class
I wanted to put the signature of the GetVbXml method in the other prat of the class, but I figured out that partial functions are now allowed! Why?
Anyway, this is not the only thing I want. I want to be able to insert the mehod body alone in the partial file without the header, So, we can have a pure vbxml code, while VB still treats it as the code of the body of the GetVbXml function! This is how I want to see in the Indexview.vbxml.vb file:
<vbxml> <h3> Browse Students</h3> <p>Select from <%= students.Count() %> students:</p> <ul> <%= (Iterator Function() For Each std In students Yield <li><%= std.Name %></li> Next End Function)() %> </ul> <script> var x = 5; document.writeln("students count = <%= students.Count() %>"); </script> </vbxml>
and this is what the how should the function look like:
Private Function GetVbXml() As XElement Implements Vazor.IVazorView.GetVbXml ViewBag.Title = "Vazor Sample" Return Partial "IndexView.vbxml.vb" End Funcrion
In fact this new use of Partial keyword can be generalized as
Partial " code file that contains the Expression"
so we can use it anywhere in the class, such as:
The implementation should be easy: the compiler just substitute the partial part with the expression. But the work needed is to make the editor treat the partial file as a part of the main class in design time.
Vazor processes the view in two stages:
In fact, we can add a third stage in the middle, to do ant thing we want with the html page. for example, I decided to add this sort of data templates, which uses the model as the data source by default:
<ul> <li ForEach="m"> <p>Id: <m.Id/></p> <p>Name: <m.Name/></p> <p>Grade: <m.Grade/></p> </li> </ul>
Note: you can use any var name other than m.
The above code is a shortcut for:
<ul> <%= (Iterator Function() For Each std In students Yield <li> <p>Id: <%= std.ID %></p> <p>Name: <%= std.Name %></p> <p>Grade: <%= std.Grade %></p> </li> Next End Function)() %> </ul>
I manage to do it by using Reflection in the function ParseTemplate:
ParseTemplate is an extension method to the XElement class, and it only receives the model object (I assume it is an IEnumerable(Of T)), so, It can be used as this in our IndexView class in the sample project:
In Vazor, we have endless possibilities to add any features we want in the middle stage, to make vbxml code easier and more powerful. So, if you have any ideas, please share it.
@DualBrain @KathleenDollard @ericmutta @gilfusion @jasonmalinowski
I didn't use the virtual IFileProvider, because the routing system used in Razor pages opens the cshtml pages directly to get info about the model class!
@page @model IndexModel <div> @Html.Raw(Model.VbXml) </div>
And that is all!
Note: The start pages and layout must be pure cshtml. any part of them that contains C# code can be moved to a section or a partial view hence we can use the vbxml trick with it.
So, here is the fact: Vazor is just a technique not a real app!
Now You can start to use VB.NET Web apps in production, and write pages with vbxml code!
All we need is a template for VB MVC Core and VB Razor Pages apps. And I hope VB team provide pull some strings with the ASP.NET Core team to add it in VS.NET 2019. This vbxml technique is built on the existing Razor, and will benefit from any future development of it (like Blazor), without adding any cost or effort on the ASP team! There will be some coast on VB team of course if they agreed to enhance xml literals, which is not a pressing matter at the moment, because every thing is already up and running!
VB was a real player on .net core from the beginning, but the coach didn't see his potential and kept away with no reason at all!
@page @model IndexModel <div> @Html.Partial(Model.ViewName + ".cshtml") </div>
Note that I have to map the each vbxml instance to have a unique name, which means that the partial view has no static name. So I put name returned by the mapper in the ViewName property, in the OnGet method of the IndexModel class:
Public Class IndexModel : Inherits PageModel Public Property ViewName As String Public Sub OnGet() Dim iv = IndexView.CreateInstance(Students, ViewData) ViewName = Vazor.VazorViewMapper.Add(iv) End Sub End Class
Note I reported the unexpected behavior of the Razor Pages with the razor pages, and hope they solve this issue.
Now, we can write MVC and Razor Pages apps using VB.NET, and create the views in three ways:
This is the most flexible view engine ever, yet it came out of the box!
Unfortunately, the xsd schema for XML literals doesn't work currently in Roslyn (seems it was forgotten while moving to Roslyn) as reported here: dotnet/roslyn#34816
I have a new idea to implement Vazor view, called ZML. vbxml code is not compatable with Tag Helpers. I used workarounds to make it work, but the resulted code is longer than it should! So, I decided to expand my data template idea but in another direction: Writing structural code as xml tags, so we have a new ZML Razor layer built on tip of C# Razor!
I faced a new xml literals limitation: Is refuses to use > and < in attr values in many cases! This is a limitation of XML specification itself which doesn't exist in HTML5!
Dim x = <if condition="a>3 and y<5"> <p>x = 4</p> </if>
I use this workaround:
x = <if condition=<%= "a>3 and y<5" %>> <p>x = 4</p> </if>
But with all xml literals limitations, I find using ZML pages better:
XElement has a formating issue with xml containing literal strings as explained here: dotnet/corefx#36871
The thing to remember is that XML has rules, which VB's literals have to comply with, otherwise their usefulness is grossly undermined.
As programmers, we sometimes have to live with external constraints, and learn how to deal with them. This is an example of one.