- Turn a Razor Model into a JavaScript Model
- Note: This post is out of date, please see the updated version here
- TL;DR:
- What I wanted to do:
- The Model Problem…
- The Solution on the HTML side:
- The Solution on the JS/jQ side:
- In Conclusion:
- What could be better?
- Шаблонизация в JavaScript с использованием Razor
- Razor
- SharpKit
- MSBuild
- SharpKit Razor
- А теперь по-подробнее — с кодами, да побольше
Turn a Razor Model into a JavaScript Model
Note: This post is out of date, please see the updated version here
If you really want to read it, please be my guest but this method is unnecessary and @Html.Raw presents many XSS security issues.
TL;DR:
This one is a little different than my other posts. Basically, I wrote some code that I’m happy about that allows me to edit data in a very UX/UI friendly fashion by passing Razor model data into a JS function.
What I wanted to do:
This post isn’t going to be as long as my normal ones. I’m just excited to share some code I figured out today. I’ve been working on an issue that I came across while trying to create an Admin page. What I want to do is display all of my users on the page in a table like structure with edit and delete buttons for each one. So before I get into the problem and how I solved it, let’s look at what I wanted to achieve:
- Keep each user row template in it’s own partial view for better manageability
- Same goes for the edit user row template
- Adhere to as many best practices as possible
- Render scripts in the scripts section of the layout page, rather than wherever is convenient
- Use view models appropriately
- Pull view data from the Controller
- Valid HTML
The Model Problem…
Because I wanted to keep my views as independent as possible, I had a hard time juggling the actual model data that came from my Controller. Since sections from partial views aren’t rendered into the final view, getting the correct model data into a JavaScript function was tricky. I also wanted it to be as clean as possible. There were hacky methods I could have used but I’m trying to be better than that. There’s also a second problem. JavaScript essentially only handles one submit action for a form. In order for my user rows to have an Edit button AND a Delete button, I had to figure out how to handle that as well.
The Solution on the HTML side:
This is part of the partial view for a single user row:
@model UserManagementSystem.Models.UserViewModel
The extra divs allow me to display this form like a row in a table, but there’s some extra CSS that goes along with that to make it work. If you’d like me to go into detail about that, let me know and I might write another post about it but I don’t want to muddy this up any further.
If you’re familiar with ASP.NET MVC, much of the above code should be self explanatory. But if you’re not, the @model UserManagementSystem.Models.UserViewModel is a Razor line that holds the model data that came from the controller. The @Html.DisplayFor lines are also Razor syntax that are called HTML helpers that create the necessary HTML markup for the item(s) from the data model that are passed as parameters. Then there are the html input tags and button tags. HTML helpers do exist for creating these input tags BUT they have a nasty habit of automatically assigning IDs to the element that are not easily controllable. They work perfectly fine when you’re only displaying one item on a page, but in this case, I needed to have this form appear many times for different users on the same page. Ergo, I wrote those tags manually, without even needing to give each one and ID.
The part that matters is the onclick action for the edit button.
With that statement, I’m passing the button (which allows me to have access to all of the data attributes associated with the button) along with the model data that was passed into the view originally. @Html.Raw(Json.Encode(Model)) is another HTML helper line that uses Json.Encode to encode the model data as a JSON object. The @Html.Raw function prevents the Json.Encode function from automatically replacing certain symbols that may appear in your model data with more HTML friendly ones.
The Solution on the JS/jQ side:
Now the JavaScript/jQuery functions in the parent View:
@section scripts < function Edit(button, model) < var model = new UserViewModel(model); var formRef = $(button).attr("data-ref") $.ajax(< url: "@Url.Action("EditUser")", data: model, success: function (data) < var ref = "form[data-ref='"+formRef+"']"; $(ref).html(data); >>) > function UserViewModel(model) >
This is a pretty standard JS snippet. The Edit function creates a new JS UserViewModel based off the model data that was passed into the function, gets the data attribute in the button that tells us where to find the form we want to update with the new HTML data that is returned from our controller, and finally calls our controller action with an ajax function to get the EditUser view data that will replace the current HTML in the form.
Because this code exists within the root view, I can render this script as a section that will be rendered correctly in the _Layout shared view. If it were in the view that displays a user row, it would not be rendered at all since that particular view is rendered as a child. I could have put the code directly inside a block on that view BUT that code would be rendered before the jQuery framework was loaded into the document, throwing all sorts of errors. Why not just load the jQuery framework first? Well, that would increase the loading time of the page and I would violate the best practice of loading all JS at the end of the document.
In Conclusion:
Did I achieve what I set out to?
- Keep each user row template in it’s own partial view for better manageability
- Yes. I can change the user row view however I need to and as long as it is a child view to the nth-degree of the view that contains the necessary JS, it will work.
- Yes. I didn’t cover the edit user view because it’s very similar to the user row view
- Render scripts in the scripts section of the layout page, rather than wherever is convenient
- Yes. Since I was able to pass the required model data up to the parent view, the JS is not required to be in the same view as the model.
- Maybe. I think I did? If I didn’t, please let me know! It seemed a little superfluous to create a JS object to represent a model that was already being passed as a JSON object, but by doing so, I think I can ensure that only the data I specifically bind to in that JS object will be passed to my controller, even if extra data was maliciously inserted into the original model data somehow.
- Yes. All of my view data is pulled from the controller instead of cheating with @Html.Partial . Not that there is never a legitimate use for that, but I don’t feel like this was one of them.
- Yes. By using divs and CSS to display my data as a pseudo-table instead of using , I am able to keep my form markup valid. Also, because I wrote out my input tags instead of letting Visual Studio “help” me out, I avoided duplicate element IDs.
- Yes. Thank you ajax!
What could be better?
- This method requires some slightly obtrusive JS to handle the onclick event of the button. However, I can’t think of a non-obtrusive way that would be clean enough to warrant the change.
- While this solution hinges on @Html.Raw(Json.Encode()) , it feels a little hacky. It works well here, but I don’t know every security implication that comes along with it. If anyone wants to enlighten me, I would greatly appreciate that.
Шаблонизация в JavaScript с использованием Razor
В силу всё большего и большего усложнения веб-приложений на стороне клиента, хочется иметь шаблонизаторы, которые работали бы прямо на клиенте. И таких средств, надо сказать, появилось не мало. Но так как
я легких путей не ищувсе они мне не нравятся, я решил сделать свой с блэкджеком и дамами лёгкого поведения (я так понял, на Хабре жестко карают и банят, если этой фразы нет в посте).И вот я решил создать строготипизированный шаблонизатор на Razor.
Razor
Для тех, кто пропустил, Razor — это такой элегантный язык для генерации разметки, в который встраивается C# или VB. Он используется в ASP.NET MVC и WebMatrix.
Все его замечательные свойства расписывать не буду, так как за меня это уже сделал Scott Guthrie, а на Хабре даже перевели.
Важно то, что он строготипизированный, и имеет отличную поддержку в Visual Studio, включая IntelliSense.
Более того, т.к. я веб-проекты разрабатываю на .Net с использованием ASP.NET MVC, а представления описываю как раз на Razor’е, то для меня это идеальный вариант. Тем более, при должном старании, можно избавиться от повторного написания кода — использовать одни и те же шаблоны на сервере и на клиенте.
Использовать Razor вне MVC не является большой проблемой, но вот код, который он генерирует — C#, а нам нужен JavaScript. И вот тут мне на помощь пришел…
SharpKit
Этот замечательный инструмент, почему-то обойден вниманием Хабра. Он позволяет конвертировать код на языке C# в JavaScript.
Например из такого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("").text("Hello, " + name.ToUpper())); > > >
function Hello(name) < $(document.body).append($("").text("Hello, " + name.toUpperCase())); >;
На официальном сайте проекта есть сносная документация. А также можно поиграться конвертированием online.
SharpKit платный, но, совершенно точно, стоит своих денег. Для open-source проектов можно получить бесплатную лицензию. А для коммерческих, можно бесплатно конвертировать до 2500 строк JavaScript кода, что иногда вполне может хватить.
К слову сказать, хоть и хорошо разбираюсь в JavaScript, уже давно использую SharpKit, и вам советую. Все таки гораздо удобнее писать с типами, нормальным intellisense, и проверкой ошибок на этапе компиляции.Что-то я отвлекся, но, как вы поняли, именно с помощью SharpKit шаблончики на Razor, сначала превращенные в C#, превращаются в JavaScript.
MSBuild
Ну а для того, чтобы это все выполнялось во время сборки проекта, интегрировать все это дело решил через MSBuild, для чего реализовал задачу.
SharpKit Razor
Да, никакого оригинального названия не придумал.
Там пока только исходники, демка.
А теперь по-подробнее — с кодами, да побольше
Ну да, это же хабр, придется совсем все кишки распотрошить. Читать только тем, кто еще не все понял как все реализовано.
База для Razor
Поэтому делаем базовый класс такой, чтобы он мог исполняться (метод Execute), и писать в выходной поток данные (экранированные — метод Write, неэкранированные — метод WriteLiteral).
Далее, чтобы было удобно оперировать нашими классами представлений в общем виде, выделим также интерфейс IRenderingArea.
Получается у нас вот такое:public interface IRenderingArea < [JsProperty(NativeField = false)] object Model < get; set; >[JsProperty(NativeField = false)] string Result < get; >void Execute(); > public interface IRenderingArea: IRenderingArea < [JsProperty(NativeField = false)] new T Model < get; set; >>
Здесь интерфейсы сразу с типизированным и нетипизированным вариантом. С помощью JsProperty помечено, чтобы SharpKit превращал Result не в поле, а в функцию get_Result(). Так будет больше пространства для хитрых маневров.
Ну а сам базовый класс, будет таким:[JsType(JsMode.Prototype)] public abstract class HtmlArea: JsContext, IRenderingArea < 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(); > > protected virtual void Write(object value) < if (value != null) _result += EscapeValue(value.As().toString()); > protected virtual string EscapeValue(JsString value) < return value .replace("&", "&") .replace("", ">") .replace("\"", """) .replace("'", "'"); > protected virtual void WriteLiteral(string value) < _result += value; >public abstract void Execute(); >
Как можно видеть, здесь класс адаптирован для того, чтобы он использовался в JavaScript.
Экранирование выполняется тупой заменой нескольких спецсимволов.Чисто для справки, привожу получившийся JavaScript:
/*Generated by SharpKit v4.24.9000*/ 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; >;
Теперь, чтобы полученные классы представлений можно было бы запускать, и получать из них результат, необходим какой-то механизм.
Для начала, нужно определиться, какая информация нам нужна, чтобы запустить шаблон. Ссылаться на шаблоны по имени — можно, но как-то не кошерно в нашем строготипизированном мире C#. Поэтому, я решил, что мне просто нужна информация о том как создать экземпляр шаблона. Т.е. функция создания шаблона и будет информацией о шаблоне. И тогда, для того, чтобы запустить шаблон, добавляем специальное расширение:[JsType(JsMode.Prototype)] public static class AreaExtensions < [JsMethod(OmitOptionalParameters = true)] public static string Execute(this Func
view, T model = default(T)) < var area = view(); if (JsContext.JsTypeOf(model) != JsTypes.undefined) area.Model = model; area.Execute(); return area.Result; >> Это расширение просто создает экземпляр шаблона, устанавливает модель, запускает и извлекает результат.
Генерация классов
Далее в игру вступает движок Razor, который должен сгенерировать класс шаблона.
Тут все достаточно просто:Ой-ёй! Хабр больше не разрешает писать. Так что ждите продолжения.