disclaimer : the article is a response to criticism (which collapsed in
Habré ), revealing the potential of IML in the example of the popular
ToDo MVC application.
Got cuffs
Criticism is an extremely mild word in relation to the discussion that arose in my previous article, because it was more like a beating in which there were extremely unpleasant comments (in the photo of the top delusional), but also objective ones:
- The code on AngularJs is not a fountain - it is difficult to refute, although they were all from the official site and the popular manual.
- Weak examples - the emphasis was on tasks, not scenarios, but I agree that a more comprehensive solution more fully reveals the potential (I suggested some of our projects that are open to open source, but they were ignored)
- Don't know AngularJs? - for obvious reasons, this greatly hurt the developers AngularJs
- The JS topic is a serious mistake, because without using asp.net mvc, it is difficult to understand the delights of typed TextBoxFor and other extensions.
Why ToDo?
In the comments, we suggested trying to implement “Todo MVC” as evidence of the IML capabilities, and now we will see what happens. First of all, the
demo version, which has one thing different from those presented for the js framework, is that the storage is not local storage, but the database, as well as the
source code , which we will analyze further in the post. In the process of implementation, I built all the logic (basement calculations, hiding elements, etc.) on the client, although on real tasks it is easier (sometimes necessary) to update the “point-like” elements that, having IML code, know how to calculate and display .
')
Code review
The storytelling style this time will not be a comparison of one solution with another (otherwise the volume of material will be large), but an overview of the code that will be obtained when implementing the todo application. I mentioned above that in the IML implementation there is also a server part, but in order to equalize the tasks to be solved for a more objective comparison, we will focus only on the client side.
What it consists of
The code was divided into 3 view
- Index - the main page (and in fact only for the browser)
- Todo_List_Tmpl - template for building a central list
- Todo_Footer_Tmpl - a template for building a basement with indicators

Index (consists of three elements)
Add form todo
@using (Html.When(JqueryBind.Submit) .DoWithPreventDefault() .Submit(options => { options.Url = Url.Dispatcher() .Push(new AddTodoCommand { ClientId = Selector.Incoding.Cookie(CookieManager.ClientId) }); }) .OnSuccess(dsl => { dsl.WithId(containerId).Core().Trigger.Incoding(); dsl.Self().Core().Form.Reset(); }) .AsHtmlAttributes() .ToBeginTag(Html, HtmlTag.Form)) { @Html.TextBoxFor(r => r.Title, new { placeholder = "What needs to be done?", autofocus = "" }) }
note: I immediately expect phrases in the style of “Yes, well, this is not serious, the code is much more, you need to copy everywhere, see how people do !!!” - to which I have an argument in the face of C # extensions, which allows wrapping IML constructs. Later in the article, alternative solutions to problems will be given (also repository on GibHub with revised code) using C # extensionsWhat is what?
- When (JqueryBind.Submit) - specify the target event
- DoWithPreventDefault - event behavior (cancel browser handler)
- Submit - submit the form via ajax
note: a couple of comments on the submitted implementation:
- Url where the form is sent is set in the options (and not through the form attribute of the action)
- ClientId can be rendered as Hidden, which, by InitIncoding, sets a value from Cookies to call Submit without parameters
- OnSuccess - we execute after successful completion
- Trigger Incoding to containerId - run all IML code for the Container element (description below)
Note: You can use more than one When, which allows you to bind to different events (with different IML code), so trigger Incoding runs all the chains.
- Form reset - reset the value of form elements
- AsHtmlAttributes - we collect IML code in a convenient for asp.net mvc format (RouteValueDictionary)
- ToBeginTag - we pack the received attributes in a form tag (the principle of work as Html.BeginForm)
note: you can use Html.BeginForm(“action”,”controller”,Post,iml.AsHtmlAttributes())
Add form TODO (alternative)
@using (Html.Todo().BeginForm(setting => { setting.TargetId = containerId; setting.Routes = new { ClientId = Selector.Incoding.Cookie(CookieManager.ClientId) }; })) { @Html.TextBoxFor(r => r.Title, new { placeholder = "What needs to be done?", autofocus = "" }) }
note: the code has become less and most importantly, now you can expand (validation, redirect after submit, etc.) method, to the needs of a specific project.Under the hood public class BeginFormSetting { public string TargetId { get; set; } public object Routes { get; set; } } public BeginTag BeginForm(Action configure) { var setting = new BeginFormSetting(); configure(setting); var url = new UrlHelper(HttpContext.Current.Request.RequestContext); return this.helper.When(JqueryBind.Submit) .DoWithPreventDefault() .Submit(options => { options.Url = url.Dispatcher() .Push(setting.Routes); }) .OnSuccess(dsl => { dsl.WithId(setting.TargetId).Core().Trigger.Incoding(); dsl.Self().Core().Form.Reset(); }) .AsHtmlAttributes() .ToBeginTag(this.helper, HtmlTag.Form); }
note: the code is familiar to most asp.net mvc developers, but it is worth noting that instead of the “usual” parameters, we pass an anonymous method that accepts the settings class. Container
@(Html.When(JqueryBind.InitIncoding | JqueryBind.IncChangeUrl) .Do() .AjaxGet(Url.Dispatcher() .Query(new { ClientId = Selector.Incoding.Cookie(CookieManager.ClientId), Type = Selector.Incoding.HashQueryString(r => r.Type) }) .AsJson()) .OnSuccess(dsl => { string urlTmpl = Url.Dispatcher() .Model(new GetTodoByClientQuery.Tmpl { FooterId = footerId }) .AsView("~/Views/Home/Todo_List_Tmpl.cshtml"); dsl.Self().Core().Insert.WithTemplateByUrl(urlTmpl).Html(); dsl.WithId(footerId).Core().Trigger.Incoding(); }) .AsHtmlAttributes(new { id = containerId }) .ToDiv())
What is what?
- When (JqueryBind.InitIncoding | IncChangeUrl) - we specify target events
- InitIncoding - triggered when an element first appears on the page (no matter ajax or usually)
- IncChangeUrl - triggered when hash changes
- Do - event behavior
- AjaxGet - specify the url, which will be executed ajax request
- ClientId - get the value from cookies
- Type - get the value from the Hash Query String
- OnSuccess - we execute after successful completion of AjaxGet
- Insert data to self by template - insert the data from the request (json) through the template (Todo_List_Tmpl below) into the current element.
Note: The template can be obtained via any available Selector, for example, Jquery.Id used to be the main one, but loading via ajax is preferable
- Trigger incoding to footerId - run all the IML code for the footer element (description below)
- AsHtmlAttributes - collect the IML code and set the value of the containerId (guid) to the Id attribute
note: using guid as Id guarantees the uniqueness of the element on the page, especially true for single page application
- ToDiv - we pack the received attributes in a tag div
Note: ToDiv is C # extensions over RouteValueDictionary, so you can easily write your desired version
Container (alternative way)
@Html.Todo().Container(setting => { setting.Id = containerId; setting.Url = Url.Dispatcher() .Query(new { ClientId = Selector.Incoding.Cookie(CookieManager.ClientId), Type = Selector.Incoding.HashQueryString(r => r.Type) }) .AsJson(); setting.Tmpl = Url.Dispatcher() .Model(new GetTodoByClientQuery.Tmpl { FooterId = footerId }) .AsView("~/Views/Home/Todo_List_Tmpl.cshtml"); setting.DependencyId = footerId; })
note: if in the future it will be necessary to add block ui or other actions, now it can be done centrallyUnder the hood public class ContainerSetting { public string Id { get; set; } public string Url { get; set; } public string Tmpl { get; set; } public string DependencyId { get; set; } } public MvcHtmlString Container(Action configure) { var setting = new ContainerSetting(); configure(setting); return helper.When(JqueryBind.InitIncoding | JqueryBind.IncChangeUrl) .Do() .AjaxGet(setting.Url) .OnSuccess(dsl => { dsl.Self().Core().Insert.WithTemplateByUrl(setting.Tmpl).Html(); dsl.WithId(setting.DependencyId).Core().Trigger.Incoding(); }) .AsHtmlAttributes(new { id = setting.Id }) .ToDiv(); }
Footer
@(Html.When(JqueryBind.None) .Do() .Direct(new FooterVm { AllCount = Selector.Jquery.Class("toggle").Length(), IsCompleted = Selector.Jquery.Class("toggle").Is(JqueryExpression.Checked), CompletedCount = Selector.Jquery.Class("toggle") .Expression(JqueryExpression.Checked) .Length(), })) .OnSuccess(dsl => { string urlTmpl = Url.Dispatcher() .Model(new TodoFooterTmplVm { ContainerId = containerId }) .AsView("~/Views/Home/Todo_Footer_Tmpl.cshtml"); dsl.Self().Core().Insert.Prepare().WithTemplateByUrl(urlTmpl).Html(); }) .AsHtmlAttributes(new { id = footerId }) .ToDiv())
- When (JqueryBind.None) - we specify target events
- None - When allows you to specify any user event as the string “MySpecialEvent”, but practice has shown that for many scenarios one is enough.
- Do - event behavior
- Direct - can be considered as an action stub that does not perform actions, but can work with data
- AllCount - we get the number of objects with the class “toggle”
note: you can use the Method extension (instead of Length) to call any jquery method, and also write C # extensions over JquerySelectorExtend
- IsCompleted - check for the presence of marked objects with the class “toggle”
note: who does not have enough features of ready-made jquery selector, then you can use Selector.Jquery.Custom (“your jquery selector”)
- CompletedCount - we get the number of marked objects with the class “toggle”
- OnSuccess - we execute after successful completion of AjaxGet
- Insert prepare data to self by template - insert the prepared (prepare) data from Direct through the template (Todo_Footer_Tmpl below) into the current element
note: prepare before inserting data performs selectors that are in the fields.
- AsHtmlAttributes - collect IML code
- ToDiv - we pack the received attributes in a tag div
Todo list tmpl
Markup template to build todo list
@using (var template = Html.Incoding().Template()) { <ul> @using (var each = template.ForEach()) { @using (each.Is(r => r.Active)) { @createCheckBox(true) } @using (each.Not(r => r.Active)) { @createCheckBox(false) } <li class="@each.IsInline(r=>r.Active,"completed")"> <label>@each.For(r=>r.Title)</label> </li> </ul> }
note: the source code is larger (remote element logic) than is presented in the example, but this is done for ease of explanation templateWhat is what?
- Html.Incoding (). Template () - open the context (as part of using) of the template building
- template.ForEach () - we start searching (within using) elements
- using (each.Is (r => r.Active)) - the previous version of the conditions was in “one line”, but it often happens that you need to perform more complex actions.
- createCheckBox is an anonymous C # function to create a checkbox (description below)
- each.IsInline (r => r.Active, ”completed”) - if the Active field is true, then return “completed”
Note: IsNotLine and IsLine are also available.
- each.For (r => r.Title) - display the value of the Title field
note: all accesses to the fields are based on the specified model (yes, I'm again about typing)
Other elements
Button del
@(Html.When(JqueryBind.Click) .Do() .AjaxPost(Url.Dispatcher().Push(new DeleteEntityByIdCommand { Id = each.For(r => r.Id), AssemblyQualifiedName = typeof(Todo).AssemblyQualifiedName })) .OnBegin(r => { r.WithSelf(s => s.Closest(HtmlTag.Li)).Core().JQuery.Manipulation.Remove(); r.WithId(Model.FooterId).Core().Trigger.Incoding(); r.WithId(toggleAllId).Core().Trigger.None(); }) .AsHtmlAttributes(new { @class = "destroy" }) .ToButton(""))
What is what ?
Button Del (alternative)
@Html.Todo().Verb(setting => { setting.Url = Url.Dispatcher().Push(new DeleteEntityByIdCommand { Id = each.For(r => r.Id), AssemblyQualifiedName = typeof(Todo).AssemblyQualifiedName }); setting.OnBegin = dsl => { dsl.WithSelf(s => s.Closest(HtmlTag.Li)).Core().JQuery.Manipulation.Remove(); dsl.WithId(Model.FooterId).Core().Trigger.Incoding(); dsl.WithId(toggleAllId).Core().Trigger.None(); }; setting.Attr = new { @class = "destroy" }; })
note: OnBegin takes Action, which makes it easy to scale your extensions by embedding IML into it. (more examples will follow)
Under the hood public class VerbSetting { public string Url { get; set; } public Action<IIncodingMetaLanguageCallbackBodyDsl> OnBegin { get; set; } public Action<IIncodingMetaLanguageCallbackBodyDsl> OnSuccess { get; set; } public object Attr { get; set; } public string Content { get; set; } } public MvcHtmlString Verb(Action<VerbSetting> configure) { var setting = new VerbSetting(); configure(setting); return this.helper.When(JqueryBind.Click) .Do() .AjaxPost(setting.Url) .OnBegin(dsl => { if (setting.OnBegin != null) setting.OnBegin(dsl); }) .OnSuccess(dsl => { if (setting.OnSuccess != null) setting.OnSuccess(dsl); }) .AsHtmlAttributes(setting.Attr) .ToButton(setting.Content); }
note: since Verb uses in several scenarios, you can easily do optional parameters, check them for null, and also set default values
Checkbox Completed
var createCheckBox = isValue => Html.When(JqueryBind.Change) .Do() .AjaxPost(Url.Dispatcher().Push(new ToggleTodoCommand { Id = each.For(r => r.Id) })) .OnBegin(dsl => { dsl.WithSelf(r => r.Closest(HtmlTag.Li)) .Behaviors(inDsl => { inDsl.Core().JQuery.Attributes.RemoveClass("completed"); inDsl.Core().JQuery.Attributes.AddClass("completed") .If(builder => builder.Is(() => Selector.Jquery.Self())); }); dsl.WithId(Model.FooterId).Core().Trigger.Incoding(); dsl.WithId(toggleAllId).Core().Trigger.None(); }) .AsHtmlAttributes(new {@class="toggle" }) .ToCheckBox(isValue);
note: within the razor page, you can use anonymous C # functions or Razor helper, which allows you to aggregate tasks of the same type.
What is what?
- When (JqueryBind.Change) - specify the target event
- Do - event behavior
- AjaxPost - specify the Url to which we make the ajax request
note: AjaxPost and AjaxGet is the “named” version of Ajax, which has many advanced settings
- OnBegin - we execute before the start of the action (AjaxPost)
- Remove class on closest LI - remove the class “completed” from the nearest LI
- Add class on closest LI if self is true - add the class “completed”
note: while the else feature is not implemented in IML, but in version 2.0 it is planned
- AsHtmlAttributes - we collect the IML code, and also set the value “toggle” to the class attribute
- ToCheckBox - we pack the received attributes into the input [type = checkbox] tag
Filter by type todo
@{ const string classSelected = "selected"; var createLi = (typeOfTodo,isFirst) => Html.When(JqueryBind.InitIncoding) .Do() .Direct() .OnSuccess(dsl => { var type = Selector.Incoding.HashQueryString(r => r.Type); if (isFirst) dsl.Self().Core().JQuery.Attributes.AddClass(classSelected).If(s => s.Is(() => type == "")); dsl.Self().Core().JQuery.Attributes.AddClass(classSelected).If(s => s.Is(() => type == typeOfTodo.ToString())); }) .When(JqueryBind.Click) .Do() .Direct() .OnSuccess(dsl => { dsl.WithSelf(r => r.Closest(HtmlTag.Ul).Find(HtmlTag.A)).Core().JQuery.Attributes.RemoveClass(classSelected); dsl.Self().Core().JQuery.Attributes.AddClass(classSelected); }) .AsHtmlAttributes(new { href = "#!".AppendToHashQueryString(new { Type = typeOfTodo }) }) .ToLink(typeOfTodo.ToString()); } <li> @createLi(GetTodoByClientQuery.TypeOfTodo.All,true) </li> <li> @createLi(GetTodoByClientQuery.TypeOfTodo.Active,false) </li> <li> @createLi(GetTodoByClientQuery.TypeOfTodo.Completed,false) </li>
note: another example of the implementation of anonymous functions within the razor view
What is what?
- When (JqueryBind.InitIncoding) - specify the target event
- Do - event behavior
- Direct - do nothing
- OnSuccess - we execute after successful completion
note: for Direct there is no difference between OnBegin or OnSuccess, but OnError and OnBreak work the same way as for the rest
- var type - we declare a variable, which we will later use in expressions
- isFirst is true And type is Empty - add a class if the current element is the first and the type is empty
- add class to self if type is current type - add a class to the current element if type equals typeOfTodo argument
- When (JqueryBind.Click) - specify the target event
- Do - event behavior
note: we do not cancel the link behavior, because we need the browser to update the location
- Direct - do nothing
- remove class - remove the selected class from all A, which are in the near UL
- add class to self - add the selected class to the current element
- AsHtmlAttributes - collect IML code, and also set the href attribute
Filter by type todo (alternative method)
<li> @Html.Todo().LiHash(setting => { setting.IsFirst = true; setting.SelectedClass = classSelected; setting.Type = GetTodoByClientQuery.TypeOfTodo.All; }) </li>
Unconditional advantages!
What are the advantages of IML, I tried to reveal in the last article, but it was not convincing, so I will try again:
- Typing - of course, everyone looks at their typing through their prism, someone thinks that they have to write more code (this is true), others lack the flexibility that is not found in non-typed languages, but IML is primarily C #, so those developers who chose it I think this plus will be appreciated.
- Powerful extensions - in the article I cited a few, but in practice there are a lot more of them, in order to back up my words I will bring a couple more:
- Drop down
@Html.For(r=>r.HcsId).DropDown(control => { control.Url = Url.Action("HealthCareSystems", "Shared"); control.OnInit = dsl => dsl.Self().Core().Rap().DropDown(); control.Attr(new { @class = "selectInput", style = "width:375px" }); })
Note: OnInit accepts Action, which makes it easy to scale your extensions by embedding IML into it.
Dialog
@Html.ProjectName().OpenDialog(setting => { setting.Url = Url.Dispatcher() .Model<GroupEditProviderOrderCommand>() .AsView("~/Views/ProviderOrder/Edit.cshtml"); setting.Content = "Edit"; setting.Options = options => { options.Title = "Edit Order"; }; })
note: for more flexibility, you can use Action as a field, for example, setting.Options is Action.
The list is endless, but the basic idea is that IML allows you to perform any tasks, and html extensions solves the problem with reuse.
- More powerful extensions
- Grid - fully built on IML (documentation will be available soon)
@(Html.ProjectName() .Grid<CTRPrintLogModel>() .Columns(dsl => { dsl.Template(@<text> <span>@item.For(r=>r.Comment)</span> </text>) .Title("Comment"); const string classVerticalTop = "vertical_top"; dsl.Bound(r => r.Time).Title("Time").HtmlAttributes(new { @class = classVerticalTop }); dsl.Bound(r => r.Version).Title("Type").HtmlAttributes(new { @class = classVerticalTop }); dsl.Bound(r => r.PrintDate).Title("Date"); dsl.Bound(r => r.Comment).Raw(); }) .AjaxGet(Url.RootAction("GetCTRPrintLogModel", "CTR")))
- Tabs
@(Html.Rap() .Tabs<Enums.CarePlanTabs>() .Items(dsl => { dsl.AddTab(Url.Action("RedFlags", "PatientRedFlag"), Enums.CarePlanTabs.RedFlags); dsl.AddTab(Url.Action("Goals", "IncGoal"), Enums.CarePlanTabs.SelfCareGoals); dsl.AddTab(Url.Action("Index", "IncAppointment"), Enums.CarePlanTabs.Appointments); }))
note: any developer familiar with html extensions can build such an element for the needs of their project
- Working with hash - in this article was considered only at the IncChangeUrl level, but we have:
- Hash.Fetch - inserts values from hash to elements (sandbox)
- Hash.Insert / Update - inserts values in hash from elements
- Hash.Manipulate - allows finely (set / remove by key) to adjust the current hash
- AjaxHash is an analogue of Submit, not for a form, but for Hash.
- Working with Insert - I didn’t have to use it to implement TODO, but in real projects everywhere
- Insert Generic - all the examples above were built on one model, but there are often scenarios when the data is a “container”; for these purposes, Insert has the ability to specify with which part of the model we work through For, as well as a template for each of them.
Html.When(JqueryBind.InitIncoding) .Do() .AjaxGet(Url.Action("FetchComplex", "Data")) .OnSuccess(dsl => { dsl.WithId(newsDivId).Core().Insert.For<ComplexVm>(r => r.News).WithTemplateByUrl(urlNewsTmpl).Html(); dsl.WithId(contactDivId).Core().Insert.For<ComplexVm>(r => r.Contacts).WithTemplateByUrl(urlContactsTmpl).Html(); }) .AsHtmlAttributes() .ToDiv()
- Work with validation (server, as a client) - many js framework have tools for validation, but IML, as mentioned above, has integration with the server and support for any validation engine (FluentValidation, standard MVC) without writing additional code.
- Command code
if (device != null) throw IncWebException.For<AddDeviceCommand>(r => r.Pin, "Device with same pin is already exist");
- View code
.OnError(dsl => dsl.Self().Core().Form.Validation.Refresh())
note: OnError handler must be attached to the element that calls action (submit, ajaxpost or etc)
- Fewer scripts - with enthusiasm for the js framework project requires writing a lot of js files, but IML has a fixed (plug-ins do not count) set of libraries
- Typed template - I'm talking about typing in general, but this is especially important for building templates.
- Replacing the template engine - choose any, and the syntax is the same
- Ready infrastructure - IML is part of the Incoding Framework and unlike the js framework, we have a complete (server / client testing) infrastructure for developing projects, which is tightly integrated with each other.
Conclusion
When implementing todo on IML, I adhered to the rule: fewer updates to the page, that is, recalculated everything on the client, but the practice (of our projects) shows that the server is often the bottleneck, because many actions are not possible or preferred by the client, for example
Impossible (due to performance):
- Paginated - if there are hundreds of thousands of records in the database, then this amount is not correctly transferred to the client
- Order - same reason
- Where is the same reason
Calculations may not be preferable, such as complex calculations (total amount of orders including tax) based on field values, it will be more convenient to send a request to the server (with field data) and insert the result.
Again IML))In the framework of IML, calculations can be solved in the following ways:
- Single value
var val = Selector.Incoding.AjaxGet(url); dsl.WithId(yourId).Core().JQuery.Attributes.Val(val);
- Data set
dsl.With(r => r.Name(s => s.Last)).Core().Insert.For<ContactVm>(r => r.Last).Val(); dsl.With(r => r.Name(s => s.First)).Core().Insert.For<ContactVm>(r => r.First).Val(); dsl.With(r => r.Name(s => s.City)).Core().Insert.For<ContactVm>(r => r.City).Val();
You can talk for a long time about the possibilities of IML (and the Incoding Framework), but the article turned out to be a big one, so those who want to continue studying our tool will be able to find materials online. I understand that proving that IML is capable of solving problems no worse than the popular js framework is extremely difficult, but in the following articles there will be an overview of the implementations of autocomplete, Tree View, grid and other complex tasks that will demonstrate even more possibilities.
PS As always, I am happy with critics and comments)))