SPA-page на Classic ASP.NET та jQuery.
Single Page Application звичайно пишуть на jQuery - це перше що потрібно зрозуміти при розмові про SPA. Незважаючи на на якусь божевільну істерику щодо більш нових фрейморків для SPA (React, Angular та інші) у цілому світі існує не більше 500 сайтів на React - проти десятков мільйонів сайтів на jQuery. Ось тут э повний перелік сайтів на React https://github.com/facebook/react/wiki/Sites-Using-React - і це незважаючи на те, що 75 рекламних агенцій розкручує цю технологію!
Але ж, більшість ASP.NET сайтів сьогодні пишеться саме на ASP.NET MVC. Чому ж так? Бо технологія ASP.NET Classic набагато краща! Проблема в тому, я вважаю, що мікрософт зупинив розвиток ASP.NET Classic і почав якийсь свої експерименти з MVC. Реально MVC не дає ні яких переваг за вилученням того, що верстка робиться швидше, у всьому іншому, на мій погляд, переваги має класична ASP.NET. Погано, що мікрософт не має ніякої стратегії розвитку своїх технологій, якщо б Мікрософт забажав розвивати ASP.NET, то було б непогано у першу чергу додати можливість розташування декількох тегів FORM, переробити об'єктну модель, щоб не возитися з EVENTTARGET, додати можливість налаштовувати контроли единим CSS-файлом, та багато іншіх ще побажань. Але Мікрософт зупинив розвиток цієї технології, він взагалі рухається по принципу "крок вперед, два назад". І зараз Мікрософт повернувся до ідеології, яка існувала до ASP.NET, тобто майже до рівня ASP (без слова .NET), знов ми маємо лапшу із тегов і кода, але до цієї лапши Мікрософт чомусь додав URL-редіректор - мотивуючи це рішення тим що з редіректором ГУГЛ працює краще (що звичайно, повний абсурд). Усю цю абсурдну кашу технологій Мікрософт заявив як "нову" технологію MVC, замість того, щоб розвивати ASP.NET.
Сьогодні, коли люди пишуть в Інеті про SPA-page, то в більшості випадків мається на увазі лише сайти на MVC. Ну з дурнями важно сваритися, бо більшість з них до того нерозумні, що навіть не розуміють що існує дуже багато засобів доступу до даних, наприклад EntityFramework - це сучасний, але далеко не кращий ORM-фреймворк, крім нього існує ще двадцять не менш поширених ORM і ще з десяток більш зручних засобів роботи з даними - взагалі без ORM. Ось тут я хотів зробити табличку порівняння можливостей доступу до даних, та так і не встиг ії доробити до кінця - Класифікація засобів роботи з даними.
Якщо подивися з іншого боку, то при будуванні SPA-page існує мабуть з десяток основних засобів будування веб-сервісів. RestFULL - це самий модерновий з них, але й найменш поширений. Нажаль дурні нічого цього не розуміють, їм здається, що якщо вони прочитали якийсь букварик про Angular, RestFUL, EntityFramework та MVC - то вони знають все про будування сучасних сайтів на ASP.NET, у тому числі про будування SPA-page. А чим же ми займалися все життя, дозвольте спитати? Як же ми програмували на ASP.NET 15 років до моменту появи EntityFramework/Angular/RestFULL/MVC? Як робили всі програмісти робили ті ж самі SPA-page? Відгадка дуже проста - раніше всі вибирали більш привабливі та більш поширені технології SPA-сторінок, ніж MVC-Entyty-Angular-Resfull. Наприклад у більшості випадків ще років п'ять тому програмісти вибирали такий стек ASP.NET технологій - класику ASP.NET, SqlDataSource, WebApi на хандлерах та jQuery. Саме цей стек технологій SPA-page я зараз опишу на цієї сторінці.
Зверніть будь ласка увагу, що це не означає, що я наприклад, не розумію і не вмію писати SPA-page у інших стеках, наприклад на MVC - увесь інет перевантажений прикладами коду таких сторінок, навіть у мене на сайті повно такого коду для MVC, ось тут наприклад Декілька моїх останніх тестових проєктів.. Але щось мені не попадався код SPA-page для класики ASP.NET, тому я і вирішив написати цю сторінку.
Отже, друзі, якщо ви використовуєте MVC, то кожний метод контроллеру фактично і є веб-сервісом, тобто ви можете по AJAX запросити безпосередньо будь-яку PartialView. Тобто ось тут, на першому скрині у стрічках 28-33 викликається метод контроллеру AddData (стрічки 16-18 на другому скрині). Само по собі Partial View на третьому скрині. А далі, коди відробляє AJAX-постбек у 17-й стрічці на третьому скрині, то POST-дата повертаються у контролер у POST-метод (стрічці 42-53 на третьому скріні). Якщо ж Partial View потребує в собі відображення якихось даних, то вона може бути зроблена більш складним чином, тобто як на першому скрині у стрічках 7-25. Якщо для відображення даних потрібні запроси у SQL - то це можливо зробити або безпосередньо на формі (як я зробив це у 17-й стрічці на першому скрині), або потрібно було б додавати Model у 13-й стрічці на другому скрині.
Оце, друзі, й є увесь патерн AJAX-програмування та програмування SPA-page у MVC. Хоча про наявності бажання про це можно написати книжку на 1000 сторінок.
У цього MVC-патерну існує дуже цікавий варіант використання. За допомогою ViewEngineResult.View.Render(ViewContext, Writer) - як це описано у мене ось тут Кешування вхідної сторінки сайту за допомогою System.Web.Mvc.ViewEngines. - можливо поєднати View з даними та записати взагалі підсумковий HTML будь-куди. Наприклад на діск, щоб зробити один раз кеш сторінки, а потім кожного разу не відпрацьовувати на сервері увесь ASP.NET-конвейер, а відразу віддати в браузер готовий HTML.
Інший варіант застосування класу ViewEngineResult.View.Render - це обхід усього проєкту, усіх View та завантаження даних у всі View. Я використовую це у моєї власної CMS як систему тестів коду і View Аналіз MVC-сайту за допомогою System.Reflection, замість більш поширених підходів, описаних тут - Unit-тести для ASP.NET MVC.
Але все що написано вище - стосується лише MVC, але як же робити SPA-page на Classic ASP.NET? Техніка трохи нагадує останній патерн з ViewEngineResult.View.Render. Я покажу ії на прикладі одного з моїх двадцяти проєктів 2016-го року - Опис двадцяти моїх дрібних фрілансерских проєктів 2016-го року, а саме на проєкті 8. Сайт для мобільників.
Все починається з того, що клас контрола поширюється ось таким методом:
1: Imports Microsoft.VisualBasic
2:
3: Public Class OneObjectFindCommon
4: Inherits System.Web.UI.UserControl
5: Implements IObject_id
6:
7: Public Property Object_id() As String Implements IObject_id.Object_id
8: Get
9: Return ViewState("Object_id")
10: End Get
11: Set(ByVal value As String)
12: ViewState("Object_id") = value
13: End Set
14: End Property
15:
16: Public Function GetHtml(Optional Patch As String = "~/OneObject_Find_Mobile.ascx") As String
17: Dim page = New Page()
18: Dim control = page.LoadControl(Patch)
19: Dim OneObject_Find_Mobile As OneObjectFindCommon = DirectCast(control, OneObjectFindCommon)
20: OneObject_Find_Mobile.Object_id = Me.Object_id
21: OneObject_Find_Mobile.OnLoad(New System.EventArgs)
22: page.Controls.Add(control)
23: Return RenderControlToHtml(control)
24: End Function
25:
26: Private Function RenderControlToHtml(control As Control) As String
27: Dim builder = New StringBuilder()
28: control.RenderControl(New HtmlTextWriter(New IO.StringWriter(builder)))
29: Return builder.ToString()
30: End Function
31: End Class
Далі контрол, який повинен працювати по AJAX-наслідується від цього класу і має можливість користуватися цім методом.
Далі ми розміщуємо AJAX-контрол на формі, у данному випадку ви бачите його статичну об'яву на формі. Спочатку цей контрол декілька разів він буде викликаний статично, тобто без AJAX, ще в процесі первинної підготовки сторінки на сервері до першого рендерінгу в браузер.
Тобто ASPX цієї сторінки виглядає ось так:
1: <%@ Page Language="VB" MasterPageFile="~/M1.master" AutoEventWireup="false" CodeFile="Object_list.aspx.vb" Inherits="Object_list" %>...
6: <%@ Register src="FindObjectTarget_Mobile.ascx" tagname="FindObjectTarget_Mobile" tagprefix="uc4" %>...
64: <asp:Repeater ID="DataList2" runat="server" >
65: <HeaderTemplate>
66: <ul class="objects">
67: </HeaderTemplate>
68: <ItemTemplate>
69: <uc5:OneObject_Find_Mobile ID="OneObject_Find_Mobile1" runat="server" />
70: </ItemTemplate>
71: <FooterTemplate>
72: </ul>
73: </FooterTemplate>
74: </asp:Repeater>
...
115: SelectCommand="GetObjectPage" SelectCommandType="StoredProcedure">
116: <SelectParameters>
117: <asp:Parameter Name="CountryID" Type="String" />
118: <asp:Parameter Name="KurortID" Type="String" />
119: <asp:Parameter Name="PriceType" Type="Int32" />
120: <asp:Parameter Name="Type" Type="Int32" />
121: <asp:Parameter Name="From" Type="Int32" />
122: <asp:Parameter Name="WithBan" Type="Int32" />
123: <asp:Parameter Name="To" Type="Int32" />
124: <asp:Parameter Name="PageNum" Type="Int32" />
125: <asp:Parameter DefaultValue="<%$ appSettings:PageSize %>" Name="PageSize" Type="Int32" />
126: </SelectParameters>...
До речі, рівень доступу до даних у цьому сайті побудований без використання ORM, необхідності в ньому у цьому проєкті я не бачу взагалі. А ця процедура виглядає ось так:
1: ALTER procedure [dbo].[GetObjectPage]
2: @CountryID nvarchar(50) = N'0',
3: @KurortID nvarchar(50) = N'0',
4: @PriceType int = 1,
5: @Type int = 0,
6: @From int = 0,
7: @To int = 0,
8: @WithBan int = 0,
9: @PageSize as int = NULL, --10 - размер странички пейждинга (если опущено - пейджинга нету)
10: @PageNum as int = NULL --0 - текущий номер странички (если опущено - пейджинга нету)
11: as
12: WITH All_ as
13: (
14: SELECT ROW_NUMBER() OVER (order by IsBest desc, OrderBy desc) as [ROW_NUMBER], AllObjects.* from AllObjects with (nolock)
15: where (@KurortID=N'0' or Rtrim(Ltrim(ToKurort))=@KurortID)
16: and (@CountryID=N'0' or Rtrim(Ltrim(Country_ID)) = @CountryID)
17: and (@Type=0 or ToObjType=@Type)
18: and IsActive=1
19: /****** and (@WithBan=1 or (IsModerBan is NULL))******/
20: and (IsModerBan is NULL)
21: and (@To=0 or dbo.Convert1(PriceFrom, ToPriceType, @PriceType)<=@To)
22: and (@From=0 or dbo.Convert1(PriceTo, ToPriceType, @PriceType)>=@From)
23:
24: )
25: SELECT ID from All_ WITH(NOLOCK)
26: WHERE [ROW_NUMBER]>ISNULL(@PageSize,1)*ISNULL(@PageNum,0) and [ROW_NUMBER]<=ISNULL(@PageSize,1)*ISNULL(@PageNum+1,1000)
27:
А необхідний нам фрагмент кода сторінки виглядає ось так:
2: Partial Class Object_list
3: Inherits MobilePage...
40: Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
41: If Me.IsMobile Then
42: If Not IsPostBack Then...
52: GetObjectPage.SelectParameters("PageNum").DefaultValue = 0
53: Dim DV2 As Data.DataView = GetObjectPage.Select(New DataSourceSelectArguments)
54: If DV2 IsNot Nothing Then
55: DataList2.DataSource = DV2
56: DataList2.DataBind()
Результатом роботи цього коду буде ось така перша сторінка.
Але це всього лише прелюдія до безпосередньо SPA-page. SPA-page починається саме є цього скрипта, який я зробив замість звичайного пейджингу для декстопного варіанта.
1: function getUrlParameter(sParam) {
2: var sPageURL = decodeURIComponent(window.location.search.substring(1)),
3: sURLVariables = sPageURL.split('&'),
4: sParameterName,
5: i;
6:
7: for (i = 0; i < sURLVariables.length; i++) {
8: sParameterName = sURLVariables[i].split('=');
9:
10: if (sParameterName[0] === sParam) {
11: return (sParameterName[1] === undefined ? true : (sParameterName[1] === "" ? "0" : sParameterName[1]));
12: }
13: else return "0";
14: }
15: };
16:
17: $(document).ready(function () {
18: var pagesize = $("#ctl00_ContentPlaceHolder1_PageSize");
19: var showobjcount = $("#ctl00_ContentPlaceHolder1_ShowObjCount");
20: var curpagenumber = $("#ctl00_ContentPlaceHolder1_CurPageNumber");
21: var countryid = getUrlParameter("Country");
22: var cityid = getUrlParameter("City");
23: var typeid = getUrlParameter("Type");
24: var pricetypeid = getUrlParameter("PriceType");
25: var fromid = getUrlParameter("From");
26: var toid = getUrlParameter("To");
27: var xxxid = getUrlParameter("xxx");
28: function addoneobjtolist(objid) {
29: $.ajax({
30: type: "GET",
31: url: "GetNewObjectPage.ashx?objectid=" + objid,
32: dataType: "html",
33: success: function (response) {
34: var lastli = $(".objects");
35: lastli.append(response);
36: },
37: failure: function (response) {
38: alert(JSON.stringify(response));
39: }
40: });
41: };
42: var getnextpage = function nextpage() {
43: var nextpagenum = Number(curpagenumber.text());
44: $.ajax({
45: type: "POST",
46: url: "Object_list.aspx/GetObjListToNextPage",
47: data: '{"PageNum":"' + nextpagenum + '","CountryID":"' + countryid + '","KurortID":"' + cityid + '","Type":"' + typeid + '","PriceType":"' + pricetypeid + '","From":"' + fromid + '","To":"' + toid + '"}',
48: contentType: "application/json; charset=utf-8",
49: dataType: "json",
50: success: function (response) {
51: var objarr = JSON.parse(response.d);
52: if (objarr.length > 0) {
53: curpagenumber.text(nextpagenum + 1);
54: showobjcount.text(Number(showobjcount.text()) + objarr.length);
55: for (var i = 0; i < objarr.length; i++) {
56: addoneobjtolist(objarr[i]);
57: };
58: };
59: },
60: failure: function (response) {
61: alert(JSON.stringify(response));
62: }
63: });
64: };
65: $(".but-more").bind("click", getnextpage);
66: });
Цей скріпт не якби-то двох-кроковий. Верхня частина там (до 15-ї стрічки) взагалі не має прямого відношення до функціоналу мобільного пейждера, це просто парсінг URL, щоб зрозуміти які параметрі відправляти на сервер. А ось щоб зрозуміти смислову частину цього мого скрипта, спочатку подивимося на обмін даними сторінки.
Тобто, як ви бачите, першим кроком вичитується з сервера перелік об'єктів (JSON з GUID), а потім вичитується вже HTML кожного об'єкту.
Одна з головних проблем будування SPA-page - яким чином побудувати WEB-API. Я зробив це у цьому проєкті двома шляхами. Перше звернення методом POST зроблено до веб-сервісу GetObjListToNextPage, який робиться у звичайному класичному ASP.NET як WebMethod, а ось друге звернення (методом GET) зроблено до хандлеру GetNewObjectPage.ashx.
Подивимося спочатку на Веб-метод, його код розташований на цієї ж сторінці і виглядає ось так:
2: Partial Class Object_list
3: Inherits MobilePage
4:
5: <System.Web.Services.WebMethod()> _
6: Public Shared Function GetObjListToNextPage(CountryID As String, KurortID As String, Type As String, _
7: PriceType As String, From As String, [To] As String, _
8: PageNum As Integer) As String
9: Try
10: Dim GetObjectPage As New SqlDataSource
11: GetObjectPage.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings("SQLServer_ConnectionStrings").ConnectionString
12: GetObjectPage.SelectCommand = "GetObjectPage"
13: GetObjectPage.SelectCommandType = SqlDataSourceCommandType.StoredProcedure
14: GetObjectPage.SelectParameters.Add("CountryID", CountryID)
15: GetObjectPage.SelectParameters.Add("KurortID", KurortID)
16: GetObjectPage.SelectParameters.Add("Type", Type)
17: GetObjectPage.SelectParameters.Add("PriceType", PriceType)
18: GetObjectPage.SelectParameters.Add("From", [From])
19: GetObjectPage.SelectParameters.Add("To", [To])
20: GetObjectPage.SelectParameters.Add("WithBan", 0)
21: GetObjectPage.SelectParameters.Add("PageNum", PageNum)
22: GetObjectPage.SelectParameters.Add("PageSize", System.Configuration.ConfigurationManager.AppSettings("PageSize"))
23: Dim Arr1 = New Newtonsoft.Json.Linq.JArray()
24: Dim DV As Data.DataView = GetObjectPage.Select(New DataSourceSelectArguments)
25: If DV IsNot Nothing Then
26: For i As Integer = 0 To DV.Count - 1
27: Arr1.Add(DV(i)("ID"))
28: Next
29: Else
30: Return ("Error1")
31: End If
32: Return Arr1.ToString()
33: Catch ex As Exception
34: Return "Error2" & ex.Message.Replace("""", "").Replace("'", "")
35: End Try
36:
37: End Function
Як бачите, ніякого WebApi нам непотрібно, як непотрібно і ніяких ORM. Так робився код ASP.NET ще з 2002-го року. І сьогодні цей засіб працює і нічим не гірше самих наймодернових технологій. Тобто пройшло 15-ть років, а ніякого скорочення коду і ніякого полегшення програмування не наступило, тільки ускладнення та погіршення. Чи можливо ли на якомусь новітньому фреймфорці зробити все те ж саме так просто і швидко? Не впевнений. Можливо, лише якщо застосувати Linq-to-SQL замість простого SqlDataSource - лише тоді код генерацій параметрів зробиться автоматично. Але це дає виграш лише на декілька символов "GetObjectPage.SelectParameters.Add" які непотрібно було б повторювати декілька разів. Але що - їх важко повторити? Цей повтор у стрічках 14-22 повністю автоматичній, але використання ORM взагалі у цьому випадку непотрібне.
І остання, та найбільш важлива крапка цього коду - як я отримав повний HTML з даними - саме з цього я почав цю сторінку. А отримав я ії у хандлері GetNewObjectPage.ashx дуже просто, скористувавшись методом GetHtml, який я додав до базового класу OneObjectFindCommon, методи якого наслідує контрол OneObject_Find_Mobile, розташований на цієї сторінці Object_list.aspx.
1: <%@ WebHandler Language="VB" Class="GetNewObjectPage" %>
2:
3: Imports System
4: Imports System.Web
5:
6: Public Class GetNewObjectPage : Implements IHttpHandler
7:
8: Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
9: context.Response.ContentType = "text/html"
10: Dim Guid1 As Guid
11: Try
12: 'check prm
13: Guid1 = New Guid(context.Current.Request.QueryString("ObjectID"))
14: Catch ex As Exception
15: context.Response.Write("error")
16: Exit Sub
17: End Try
18: Dim X As New OneObjectFindCommon
19: X.Object_id = Guid1.ToString
20: context.Response.Write(X.GetHtml)
21: End Sub
22:
23: Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
24: Get
25: Return False
26: End Get
27: End Property
28:
29: End Class
Таким чином, ось вам, друзі - патерн SPA-page, який чудово працює вже десь приблизно 15-ть років. І досі ніхто так і не придумав більш простого для порозуміння і швидкого для програмування патерну будування SPA-page.
|