(MVC) MVC (2016 год)

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.



Comments ( )
Link to this page: //www.vb-net.com/SpaPageWithjQueryAndClassicAspNet/index.htm
< THANKS ME>