Due to the increasing complexity of web applications on the client side, I want to have template engines that work directly on the client. And I must say that such means appeared quite a few. But since
I don’t look for easy ways, I don’t like all of them, I decided to make my own with blackjack and ladies of easy virtue (as I understand it, Habré is severely punished and banned if this phrase is not in the post).
So I decided to create a strictly typed template engine on the Razor.
Razor
For those who missed, Razor is such an elegant language for generating markup, which is embedded in C # or VB. It is used in ASP.NET MVC and WebMatrix.
')
I will not paint all his wonderful properties, since Scott Guthrie has already
done this for me, and even
translated to Habré.
The important thing is that it is
strictly typed , and has excellent support in Visual Studio, including
IntelliSense .
Moreover, since I develop web projects on .Net using ASP.NET MVC, and I describe presentations on Razor, then for me this is an ideal option. Moreover, with due diligence, you can get rid of re-writing code - use the same templates on the server and on the client.
Using Razor outside of MVC is
not a big problem , but the code it generates is C #, and we need JavaScript. And here I came to the rescue ...
SharpKit
This wonderful tool is somehow
overlooked by Habr . It allows you to convert C # code to javascript.
For example from this
using SharpKit.JavaScript; using SharpKit.jQuery; namespace Namespace { [JsType(JsMode.Global)] public class MyPageClient : jQueryContext { public static void Hello(string name) { J(document.body).append(J("<h1/>").text("Hello, " + name.ToUpper())); } } }
will turn into
function Hello(name) { $(document.body).append($("<h1/>").text("Hello, " + name.toUpperCase())); };
On the
official website of the project there is a tolerable documentation. And also you can play around
converting online .
SharpKit paid, but, exactly, worth the money. For open-source projects you can get a free license. And for commercial ones, you can convert up to 2500 lines of JavaScript code for free, which can sometimes be enough.
By the way, although I am well versed in JavaScript, I have been using SharpKit for a long time, and I advise you. All the same, it is much more convenient to write with types, normal intellisense, and error checking at the compilation stage.
Something I was distracted, but, as you understood, it was with the help of SharpKit that the templates on Razor, first turned into C #, are turned into JavaScript.
Msbuild
Well, in order for this to be done during the project build, I decided to integrate the whole business through MSBuild, for which I implemented the task.
And so, I present the project
SharpKit Razor
Yes, I did not invent any original name.
Official site of the project on CodePlexEh, I hardly had a description in English, but
Google still does not understand me .
There is only the source code, demo.
And now in more detail - with codes, yes more
Well, yes, it's habr, you'll have to completely gut all guts. Read only to those who have not yet understood everything how everything is implemented.
Base for Razor
When the Razor engine processes the code, it’s from something like that
@inherit MyBase<string[]> <ul> @foreach (var item in Model) { <li>@item</li> } </ul>
on output generates a class of something like this:
namespace MyNamespace { public class MyView: MyBase<string[]> { public override void Execute() { WriteLiteral("<ul>\r\n"); foreach (var item in Model) { WriteLiteral("\t<li>"); Write(item); WriteLiteral("</li>\r\n"); } WriteLiteral("</ul>"); } } }
Therefore, we make the base class such that it can be executed (the Execute method) and write data to the output stream (screened - the Write method, unshielded - the WriteLiteral method).
Further, in order to make it convenient to operate with our classes of views in a general form, we also select the interface IRenderingArea.
It turns out that we have this:
public interface IRenderingArea { [JsProperty(NativeField = false)] object Model { get; set; } [JsProperty(NativeField = false)] string Result { get; } void Execute(); } public interface IRenderingArea<T>: IRenderingArea { [JsProperty(NativeField = false)] new T Model { get; set; } }
Here the interfaces are immediately with a typed and untyped version. With the help of JsProperty it is marked that SharpKit turns Result not into a field, but into the get_Result () function. So there will be more space for tricky maneuvers.
Well, the base class itself will be:
[JsType(JsMode.Prototype)] public abstract class HtmlArea<T>: JsContext, IRenderingArea<T> { private string _result = ""; public string Result { get { return _result; } } [JsField(Export = false)] private T _model; public T Model { get { return _model; } set { _model = value; } } [JsProperty(Export = false)] object IRenderingArea.Model { get { return Model; } set { Model = value.As<T>(); } } protected virtual void Write(object value) { if (value != null) _result += EscapeValue(value.As<JsObject>().toString()); } protected virtual string EscapeValue(JsString value) { return value .replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } protected virtual void WriteLiteral(string value) { _result += value; } public abstract void Execute(); }
As you can see, here the class is adapted to be used in JavaScript.
Escaping is done with a blunt replacement of several special characters.
Purely for reference, I cite the resulting javascript:
if(typeof(XWeb) == "undefined") XWeb = {}; if(typeof(XWeb.SharpKit) == "undefined") XWeb.SharpKit = {}; if(typeof(XWeb.SharpKit.Razor) == "undefined") XWeb.SharpKit.Razor = {}; XWeb.SharpKit.Razor.AreaExtensions = function() { }; XWeb.SharpKit.Razor.AreaExtensions.Execute = function(view,model) { var area=view(); if(typeof(model) != "undefined") area.set_Model(model); area.Execute(); return area.get_Result(); }; XWeb.SharpKit.Razor.HtmlArea = function() { this._result = ""; }; XWeb.SharpKit.Razor.HtmlArea.prototype.get_Result = function() { return this._result; }; XWeb.SharpKit.Razor.HtmlArea.prototype.get_Model = function() { return this._model; }; XWeb.SharpKit.Razor.HtmlArea.prototype.set_Model = function(value) { this._model = value; }; XWeb.SharpKit.Razor.HtmlArea.prototype.Write = function(value) { if(value != null) this._result += this.EscapeValue(value.toString()); }; XWeb.SharpKit.Razor.HtmlArea.prototype.EscapeValue = function(value) { return value.replace("&","&").replace("<","<").replace(">",">").replace("\"",""").replace("'","'"); }; XWeb.SharpKit.Razor.HtmlArea.prototype.WriteLiteral = function(value) { this._result += value; };
Now, in order for the resulting classes of representations to be launched, and to get the result from them, some mechanism is needed.
First, you need to decide what information we need to run the template. To refer to templates by name is possible, but somehow not kosher in our strictly typed world of C #. Therefore, I decided that I just needed information on how to create an instance of the template. Those. the template creation function will be the information about the template. And then, in order to run the template, add a special extension:
[JsType(JsMode.Prototype)] public static class AreaExtensions { [JsMethod(OmitOptionalParameters = true)] public static string Execute<T>(this Func<IRenderingArea<T>> view, T model = default(T)) { var area = view(); if (JsContext.JsTypeOf(model) != JsTypes.undefined) area.Model = model; area.Execute(); return area.Result; } }
This extension simply creates an instance of the template, sets the model, runs and retrieves the result.
Class generation
Next, the Razor engine comes into play, which should generate a template class.
It's all quite simple:
Oops! Habr no longer allows writing. So wait for the continuation.