(MVC) MVC (2015 год)

Перемикач мови для сайту.

Перемикач мови сайту

На цієї сторінці я розповім про свій перемикач мови для мультимовних сайтів. Це невеличка, але важлива рюшечка сайту.

Перші три кроку ви бачите на скрінах нище: беремо каталог з прапорцями. Беремо базу з кодами прапорців та ім'ями файлів і робимо DBML-файлік (мапер Linq-to-SQL).

Тут є невеличка проблема, у мене цей каталог і база кочують із проекта в проект, тому там коди країн якісь ліві, не ті, що приносить браузер у HttpContext.Current.Request.Headers("Accept-Language"), можна було б зробити додаткову колонку з кодами мов, що приносить браузер, аде робити це нема часу. Тому що мов у світі багато - https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, табличка велика, а потрібно мені звичайно дві-три мови. Але ця невеличка проблема вирішується просто - нище у коді (стрічка 23 классу TopInfo) я просто додав свій мапер коду, що приносить браузер у Accept-Language відносно ключа табличці з именами прапорців. Якщо робити це точно, то краще було в призначити в цій табличці окрему колонку с кодом Accept-Language.



  • Наступним, четвертим кроком - десь на плашці сайту знаходимо місце для прапорців (OCMR у данному випадку - це ім'я мого проекту та ім'я моєї DLL):


       1:  <%@ Master Language="VB" Inherits="System.Web.Mvc.ViewMasterPage" %>
       2:   
       3:  <!DOCTYPE html>
       4:  <html>
    .....
      27:                          <td style="text-align: right; width:300px; vertical-align:top">
      28:                              <form enctype="multipart/form-data" method="post" name="lang1"  id="lang1" action="../../ChangeLang.ashx" >
      29:                              <%: OCMR.TopInfo.GetLang()%>
      30:                              </form>
      31:                          </td>
    ....
      61:  </body>
      62:  </html>

    Моя загальна думка у цьому випадку полягає в тому, щоб сформувати приблизно ось такий код:


    Перемикач мови сайту
  • Наступнім, п'ятим кроком - робимо ось такий хандлер ChangeLang.ashx. Сенс його діі поки що незрозумілий, бо незрозуміло як формуються імена кнопок з прапорцями (які ставляться у куку) - це буде зрозуміло далі.


       1:  Imports System.Web
       2:  Imports System.Web.Services
       3:   
       4:  Public Class ChangeLang
       5:      Implements System.Web.IHttpHandler
       6:   
       7:   
       8:      'context.Request.Form.AllKeys
       9:      '    (0): "US.x"
      10:      '    (1): "US.y"
      11:      '    (2): "__VIEWSTATE"
      12:      '    (3): "__VIEWSTATEGENERATOR"
      13:      Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
      14:          'Определяем имя кнопки от которой прилетел постбек
      15:          If context.Request.Form.AllKeys.Length >= 2 Then
      16:              Dim Name1 As String = context.Request.Form.AllKeys(0).Replace(".x", "")
      17:              Dim Name2 As String = context.Request.Form.AllKeys(1).Replace(".y", "")
      18:              If Name1 = Name2 Then
      19:                  'и ставим куку для TopInfo.GetLang
      20:                  Dim LangCook As HttpCookie = New HttpCookie("Lang", Name1)
      21:                  LangCook.Domain = System.Configuration.ConfigurationManager.AppSettings("LoginDomain")
      22:                  LangCook.Expires = Now.AddYears(1)
      23:                  context.Response.Cookies.Add(LangCook)
      24:                  context.Response.RedirectPermanent(context.Request.UrlReferrer.AbsolutePath)
      25:              End If
      26:          End If
      27:      End Sub
      28:   
      29:      ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
      30:          Get
      31:              Return False
      32:          End Get
      33:      End Property
      34:   
      35:  End Class

    Тут ви бачите ссилки на конфіг сайту, що в ньому повинно бути - це зрозуміло:


       1:  <?xml version="1.0"?>
       2:   
       3:  <configuration>
       4:    <appSettings>
    ....
      17:      <add key="LoginDomain" value="localhost"/>
      18:      <!-- Этот адрес нужен для активации логина и сброса пароля -->
      19:      <add key="HostingURL" value="localhost:52796"/>
      20:      <add key="NameURL" value="Open Community Media Room"/>
    ....
      34:      <add key="webpages:Version" value="1.0.0.0"/>
      35:      <add key="ClientValidationEnabled" value="true"/>
      36:      <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
      37:    </appSettings>
      38:    <connectionStrings>
      39:      <remove name="LocalSqlServer"/>
      40:      <add name="OCMR_ConnectionStrings" connectionString="server=AAA.BBB.CCC.DDD;Initial Catalog=Ocmr;User ID=Ocmr;Password=XXXXXXXXXXX;Max Pool Size=10000;" providerName="System.Data.SqlClient"/>
      41:      <add name="OCMR_FS_ConnectionStrings" connectionString="server=AAA.BBB.CCC.DDD;Initial Catalog=Ocmr_FS;User ID=Ocmr_FS;Password=XXXXXXXXXXXX;Max Pool Size=10000;" providerName="System.Data.SqlClient"/>
      42:       </connectionStrings>
      43:    <system.web>
      44:      <compilation debug="true" targetFramework="4.0">
      45:   
      46:      </compilation>
      47:   
      48:      <authentication mode="Forms">
      49:        <forms loginUrl="~/Account/LogOn" timeout="2880" />
      50:      </authentication>
      51:   
      52:      <pages>
      53:        <namespaces>
      54:          <add namespace="System.Web.Helpers" />
      55:          <add namespace="System.Web.Mvc" />
      56:          <add namespace="System.Web.Mvc.Ajax" />
      57:          <add namespace="System.Web.Mvc.Html" />
      58:          <add namespace="System.Web.Routing" />
      59:          <add namespace="System.Web.WebPages"/>
      60:        </namespaces>
      61:      </pages>
      62:    </system.web>
      63:   
      64:    <system.webServer>
      65:      <validation validateIntegratedModeConfiguration="false"/>
      66:      <modules runAllManagedModulesForAllRequests="true"/>
      67:    </system.webServer>
      68:   
      69:    <runtime>
      70:      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      71:        <dependentAssembly>
      72:          <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
      73:          <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="3.0.0.0" />
      74:        </dependentAssembly>
      75:      </assemblyBinding>
      76:    </runtime>
      77:  </configuration>

  • Наступним, шостим кроком робимо головний класс TopInfo, що формує MvcHtmlString кнопки з прапорцями, кожну із своїм ім'ям, що є ключом в табличці прапорців (а ідеально це ім'я було б тим кодом, що приносить браузер у реквесті у Request.Headers("Accept-Language").

    Цей код виходить з пропозиції, що браузер приносить у цьому параметрі стрічку, подібну "ru,en-US;q=0.7,en;q=0.3". Далее код анализирует куку, що ставить хандлер і прапорець з цим ім'ям виводиться звичайним стилем, а інші прапорці віводятсья напівпрозорим стилем з opacity.


       1:  Public Class TopInfo
       2:   
       3:   
       4:      Public Shared Function GetLang() As MvcHtmlString
       5:          'сначала смотрим был ли установлен язык хандлером ChangeLang.ashx
       6:          Dim Cookies As String() = HttpContext.Current.Request.Cookies.AllKeys
       7:          Dim CurrentLang As String = "US"
       8:          If Cookies.Length > 0 Then
       9:              If HttpContext.Current.Request.Cookies("Lang") IsNot Nothing Then
      10:                  If HttpContext.Current.Request.Cookies("Lang").Value <> "" Then
      11:                      CurrentLang = HttpContext.Current.Request.Cookies("Lang").Value
      12:                  End If
      13:              End If
      14:          End If
      15:   
      16:          Dim AcceptLanguageString As String = HttpContext.Current.Request.Headers("Accept-Language") '"ru,en-US;q=0.7,en;q=0.3"
      17:          If AcceptLanguageString IsNot Nothing Then
      18:              If AcceptLanguageString.Length > 0 Then
      19:                  Dim AcceptLanguageArr = AcceptLanguageString.Split(";")
      20:                  Dim SupportLangArr = AcceptLanguageArr(0).Split(",")
      21:                  'https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
      22:                  Dim AllFlag As System.Collections.Generic.List(Of Global.OCMR.Flag) = HttpContext.Current.Application("Flags")
      23:                  Dim AllSupportFlags As System.Collections.Generic.List(Of Global.OCMR.Flag) = (From X In AllFlag Select X Where X.Code = "UA" Or X.Code = "RU1" Or X.Code = "US").ToList
      24:                  If AllSupportFlags.Count > 0 Then
      25:                      Dim Result As New StringBuilder("")
      26:                      For i As Integer = 0 To AllSupportFlags.Count - 1
      27:                          If AllSupportFlags(i).Code = CurrentLang Then
      28:                              Result.Append("<input type='image' name='" & AllSupportFlags(i).Code & "' src='/Flag/" & AllSupportFlags(i).Flag & "'><br>")
      29:                          Else
      30:                              Result.Append("<input type='image' name='" & AllSupportFlags(i).Code & "' src='/Flag/" & AllSupportFlags(i).Flag & "' style='opacity: 0.2 ! important;'><br>")
      31:                          End If
      32:                      Next
      33:                      Return New MvcHtmlString(Result.ToString)
      34:                  End If
      35:              End If
      36:          End If
      37:          Return New MvcHtmlString("")
      38:      End Function
      39:   
      40:  End Class

  • І нарешті сьомий крок - це зробити кеш сайту з табличкою прапорців, щоб на кожному реквесті не смикати базу. Ще робиться ось так - в Global.asaх додаємо стрічку 29:


       1:  Public Class MvcApplication
       2:      Inherits System.Web.HttpApplication
       3:   
    .....
      23:      Sub Application_Start()
    .....
      28:          'read cache data
      29:          ApplicationStart.GO()
      30:      End Sub
    .....
      40:   
      41:      Sub Session_Start()
      42:          'redirect to permanent login user
      43:          SessionStart.GO()
      44:      End Sub
      45:   
      46:  End Class

    І ось нарешті останній крок ціеї невеличкої головоломки - утворення кешу:

       1:  Public Class ApplicationStart
       2:      Public Shared Sub GO()
       3:          Dim db1 As New OCMRDataContext
       4:          Dim AllFlag = (db1.Flags).ToList
       5:          HttpContext.Current.Application("Flags") = AllFlag
       6:      End Sub
       7:  End Class


Ще одне невеличке пояснення застосованою мною технології - зверніть увагу що я сформував повний синхронний постбек, (на відміну від постбека AJAX!) - обробив його у хандлері і зробив редірект на Request.UrlReferrer.AbsolutePath, який приніс браузер. Щоб порозуміти цей мій вибір я поясню два альтернативних варіанту відправки постбеку, які не можна використовуватии у данному випадку. Як приклад використуемо html select / option одного мого сайту.

Взагалі повний постбек звичайно відправляють (по application/x-www-form-urlencoded) десь приблизно так:


   1:  <form enctype="application/x-www-form-urlencoded" method="post" name="Comm3"  id="Comm3" action='/Home/ChangeCommunity' >
   2:       <% Dim Commnunity As System.Collections.Generic.List(Of System.Web.Mvc.SelectListItem) = Application("Community")%>
   3:       <%: Html.DropDownList("Community", Commnunity, New With {.style = "width:200px"})%>
   4:  </form>
   5:  <script language="javascript" type="text/javascript">
   6:       $('#Community').change(function () {
   7:           var selectedValue = $('#Community').val();
   8:           $('form#Comm3').submit();
   9:       });
  10:  </script>

І обробляють у контроллері десь приблизно так:


   1:  Namespace OCMR
   2:      Public Class HomeController
   3:          Inherits System.Web.Mvc.Controller
   4:   
   5:          Function Index() As ActionResult
   6:              Return View()
   7:          End Function
   8:   
   9:   
  10:          Function ChangeCommunity(Community As String) As ActionResult
  11:              If Community IsNot Nothing Then
  12:                  Return RedirectToActionPermanent("Index")
  13:              End If
  14:          End Function
  15:   

Але таким чином незрозуміло на якому конкретно прапорці зроблено клік. Або тільки кожній прапорець загорнути у дужки <form> та зробити багато-багато Action у контролері. Кода вийде у сто разів більше, html буде забруднений тегами <form> - тобто це можливий, але дуже дурний шлях вирішення цієї проблеми - замість описанного тут - отримати координати кліка на прапорцях по multipart/form-data.

Альтернативний шлях - відправити Postback по AJAX:


   1:  <form enctype="application/x-www-form-urlencoded" method="post" name="Comm3"  id="Comm3" action='/Home/ChangeCommunity' >
   2:       <% Dim Commnunity As System.Collections.Generic.List(Of System.Web.Mvc.SelectListItem) = Application("Community")%>
   3:       <%: Html.DropDownList("Community", Commnunity, New With {.style = "width:200px"})%>
   4:  </form>
   5:  <script language="javascript" type="text/javascript">
   6:       $('#Community').change(function () {
   7:            var selectedValue = $('#Community').val();
   8:                 $.post('<%: html.Action("ChangeCommunity", "Home") %>', { Community: selectedValue }, function (data) {
   9:                 //server response for AJAX request
  10:            });
  11:       });
  12:  </script>

Але справа у тому, що зробити повний редірект і відправити сторінку з самого початку з MVC-контроллера ASP.NET у відповідь на AJAX-реквест до серверу неможна. Буде добре знайоме усім програмистам повідомлення про заборону цього - Child actions are not allowed to perform redirect actions. - тобто у відповідь на такий AJAX-реквест до сервера можливо відправити або PartialView або Json.


Таким чином, шлях, описаний мною на ціей сторінці, мабуть, единий можливий для вирішення описанною тут проблеми.



< THANKS ME>