Готуємо ASP.NET5, випуск №2 – повторимо ази для самих початківців

Друзі, перед вами другий випуск колонки про ASP,NET5, в якій ми знайомимося з різними цікавими речами світу з веб-розробки на новій версії відкритої платформи ASP.NET5.


Минулого разу ми говорили про нові підходи в роботі зі статичним контентом на платформі. В коментарях виникла пропозиція поговорити в наступних випусках про ази для веб-розробників, які тільки починають користуватися ASP.NET і занурюватися в тему. Ми прислухалися до вас і пропонуємо в цьому випуску матеріал від Андрія Веселова ( StealthDogg) – експерта веб-розробки, автора багатьох статей по темі ASP.NET і Microsoft MVP.

Зустрічайте введення в ази ASP.NET5 – контролери, уявлення і моделі.
Примітка Дана стаття актуальна для фінальної версії Visual Studio 2015 з ASP.NET5 Beta5. Для оновлення на нові версії ASP.NET скористайтеся примітками до релізу.
Робота з контролерами
Давайте подивимося на роботу контролера і дій шаблону MVC на прикладі невеликого веб-додатки.
aspnetcolumngithubРада! Ви можете спробувати все самостійно або завантаживши вихідний код з GitHub https://github.com/vyunev/AspColumnControllerDemo.

Створюємо проект

Для цього створимо проект ControllerDemo (Рис.1).


Рис.1 – Створення проекту веб-додатки ASP,NET5

У настроюваннях проекту виберемо шаблон Empty з розділу ASP.NET 5 Templates (Рис. 2).


Рис.2 – Вибір шаблону веб-додатки

Після створення проекту в Solution Explorer будуть відображені такі елементи (Рис. 3).


Рис.3. – Структура шаблону

Тепер необхідно підключити ASP.NET MVC 6 в проект.

1. У файлі project.json вкажемо залежність від бібліотеки Microsoft.AspNet.Mvc.

"dependencies": {
"Microsoft.AspNet.Server.IIS":"1.0.0-beta5",
"Microsoft.AspNet.Server.WebListener":"1.0.0-beta5",
"Microsoft.AspNet.Mvc": "6.0.0-beta5"
},

2. Потім необхідно включити ASP.NET MVC 6 конвеєр Owin для обробки запитів і налаштувати маршрутизацію. Для цього в файлі Startup.cs додамо наступні рядки:

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}

public void Configure(IApplicationBuilder app)
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template:"{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
});
}
}

Проект підготовлений і можна приступати до створення контролерів і дій.

Контролери та дії

Як правило, контролери розміщують у спеціальній папці Controllers. ASP.NET MVC 6 дозволяє використовувати і інші папки, т. к. шукає їх по базового класу. Проте в даному прикладі зупинимося на класичному варіанті розташування файлів. За допомогою контекстного меню створимо папку Controllers ("Add > New Folder") і в неї новий клас HomeController ("Add > New item… " і виберемо Class).

Примітка Ім'я класу контролера завжди складається з імені контролера (в даному випадку "Home") і закінчення "Controller".
Рада. В подальшому для створення контролерів можна використовувати готовий шаблон MVC Controller Class. Але зараз, в цілях вивчення, створимо звичайний порожній клас.
Базовим класом для всіх контролерів веб-додатки є Controller. Тому успадкуємо створений клас від нього:

public class HomeController : Controller
{
}

Клас Controller є не тільки маркером контролерів, але надає основну функціональність для роботи з отриманим запитом та формування відповіді. У даний момент варто скасувати такі його властивості, як:

  • ViewBag і ViewData — призначені для передачі довільних даних подання;
  • Request — містить дані вихідного запиту;
  • Response — дозволяє створити відповідь для клієнта.
Крім того, він надає радий корисних методів, які будуть розглянуті трохи пізніше.

Через безпосередню обробку запиту в контролері відповідає дія, до якому можна виділити наступні частини:

  1. Вказівку конкретного типу запиту, використовуючи атрибути [HttpGet], [HttpPut], [HttpPost], [HttpPatch], [HttpDelete]. Так само можна вказати кілька типів, використовуючи [HttpMethod]. При цьому запити інших типів будуть ігноруватися. Якщо атрибути відсутні, то дія буде викликано для будь-якого запиту.
  2. Результат дії — його тип це реалізація IActionResult. Всі дії повинні повернути екземпляр класу, що реалізує вказаний інтерфейс. По суті, він відповідає за формування відповіді, який буде відправлений клієнту. Це може бути HTML-сторінка, файл, переадресація або код помилки.
    Базовий клас Controller містить методи, які підходять у більшості стандартних випадків:

    1. View() — створення відповіді (html-сторінки) з використанням уяви;
    2. Content() — призначений для передачі довільної текстової рядка клієнта, наприклад, це може бути згенерований CSS та JavaScript;
    3. File() — дозволяє повернути клієнту файл;
    4. Json() — перетворює об'єкт до формату JSON і повертає його клієнту;
    5. Redirect(), RedirectPermanent(), RedirectToAction(), RedirectToRoute() — переадресують клієнта за новою адресою.
  3. Назва дії, за яким буде відбуватися його виклик. За замовчуванням вважається, що рядок запиту відповідає формату: www.site.com/<Controller>/<Action>
  4. Метод дії може мати параметри. Значення для них ASP.NET MVC постарається взяти з рядка запиту. Зіставлення параметрів дії і запитів здійснюється по іменах.
  5. І нарешті, тіло методу, в якому готуються необхідні дані і повертається результат.
Настав час додати перша дія — Index:

public class HomeController : Controller
{
// GET: /Home/
public IActionResult Index()
{
return this.Content("Hello ASP.NET MVC 6.");
}
}

Якщо тепер запустити веб-додаток, то задана рядок "Hello ASP.NET MVC 6." буде відображена у браузері.

Давайте розберемо що саме сталося:

  • Браузер відправив на сайт запит виду http://localhost:[port]/
    * Примітка: Порт для запуску в режимі налагодження автоматично вибирається при створенні проекту з вільних.
  • Оскільки ні контролер, ні дія явно не вказані, то маршрутизація ASP.NET MVC використовує варіанти задані за замовчуванням: Home і Index.
  • Був створений екземпляр класу HomeController.
  • У нього було знайдено та виконано дію Index.
  • Метод Index() викликав метод Content() для формування відповіді, що складається з вказаного рядка.
  • Механізм ASP.NET MVC отримав відповідь і переслав його браузеру клієнта.
  • Браузер відобразив отриману в якості відповіді рядок.
Якщо запитуваний контролер або дія не будуть знайдені, то буде відправлено відповідь "404. Сторінка не знайдена".

Трохи ускладнимо і створимо конструктор класу і ще одну дію:

public class HomeController : Controller
{
private readonly string _time;

public HomeController()
{
this._time = DateTime.Now.ToString("G");
}

// GET: /Home/
public IActionResult Index()
{
return this.Content("Hello ASP.NET MVC 6");
}

// GET: /Home/Echo/
public IActionResult Echo(string message)
{
return this.Content($"{this._time} > {message}");
}
}

Тепер в якості відповіді буде повертатися час створення контролера і передана в запиті рядок. Це дозволить переконатися, що новий примірник HomeController створюється при кожному запиті.

Для виклику створеного дії необхідно звернутися по шляху, що включає імена контролера і самої дії. Запустимо веб-додаток і введемо в браузері наступну адресу:

http://localhost:[port]/Home/Echo?message=Hello

Асинхронність — це просто

Розглянемо, як легко ASP.NET MVC 6 дозволяє створювати асинхронні методи. Переробимо дію Echo. Для симуляції виклику нікого асинхронного методу скористаємося Task.Delay():

public async Task<IActionResult> Echo(string message)
{
await Task.Delay(100);
return this.Content($"{this._time} > {message}");
}

Потрібно тільки замінити тип результату з IActionResult на Task<IActionResult> і додати модифікатор async. Така дія може використовувати виклики асинхронних методів, наприклад, для звернення до різних джерел даних або сервісів.

Трохи практики

І в завершенні, два невеликих практичних завдання:

  1. Напишіть дію, так само виводить значення параметрів, але які передаються запиті тільки типу POST.
    Підказка. Для формування запиту можна використовувати звичайну HTML-сторінку з формою. Файл додайте до вже існуючої папки wwwroot, яка призначена для будь-якого статичного вмісту сайту (html-сторінки, файли і т. д.)
  2. Створіть дію, яке відповідає тільки на Get запит:
    • повертає файл;
    • повертає об'єкт у форматі Json (для простоти можна використовувати анонімний клас);

    • переадресує на іншу сторінку.
І не поспішайте заглядати в прикладений вихідний код, де є всі рішення. Постарайтеся зробити це самостійно.

Робота з уявленнями
Перейдемо до розгляду наступної складової шаблону MVC — уявлень.
aspnetcolumngithubРада! Ви можете спробувати все самостійно або завантаживши вихідний код з GitHub https://github.com/vyunev/AspColumnViewDemo.

Створюємо веб-додаток з поданням

Для цього створимо порожній ASP.NET MVC веб-додаток ViewDemo, аналогічно тому як це було зроблено в попередній частині. Тільки в цей раз контролер HomeController буде виглядати наступним чином:

public class HomeController : Controller
{
// GET: /Home/
public IActionResult Index()
{
return this.View();
}
}

Метод View() створює результат дії, використовуючи уявлення. Спробуємо запустити проект і відразу отримаємо повідомлення про помилку, оскільки в проекті ще немає уявлення для дії Index (Рис. 4).


Рис.4 – Повідомлення про помилку при відсутньому поданні

Якщо уважно прочитати текст, то можна помітити, що для вистав існує чітко визначене місце у структурі проекту. Це папка Views. В ній розміщуються вкладені папки з іменами відповідними іменами контролерів. В даному прикладі це буде Home. І вже в ній зберігаються файли представлень, імена файлів яких, в загальному випадку, збігаються з назвами дій, для яких вони призначені.

Крім того, є спеціальна папка Views/Shared. В ній розміщуються подання доступні для будь-яких дій в проекті.

Створимо зазначені вище папки Views та Home. Після цього, в останній, за допомогою пунктів контекстного меню "Add > New Item ...", додамо подання (MVC View Page) з ім'ям Index.cshtml. Розширення cshtml показує що буде використовуватися мова розмітки Razor. В результаті проект буде виглядати наступним чином (Рис. 5):


Рис.5 – Структура проекту з новим поданням

Додамо в Index.cshtml наступний код:

<h1>View Demo</h1>

Запустимо веб-додаток. При цьому відбудеться наступне:

  1. Буде викликано дію Index контролера Home (як це було описано в попередній статті).
  2. В дії Index, метод View() буде використовувати уявлення View/Home/Index.cshtml для створення результату (екземпляра класу реалізує IActionResult).
  3. Результат дії сформує HTML код у відповідь клієнту (в методі ExecuteResultAsync інтерфейсу IActionResult).
  4. Створена сторінка буде відправлена клієнту і відображено його браузером.
Метод View() шукає уявлення, використовуючи наступні шляхи:

  1. View/<Ім'я контролера>/<ім'я дії>.cshtml
  2. Виберіть View/Shared/<ім'я дії>.cshtml
Якщо обидва пошуку нічого не принесли, то виводиться повідомлення про помилку.

Існує перевантаження методу, що дозволяє явно вказувати ім'я подання. Наприклад, виклик

this.View("MyView") буде шукати файл View/<Ім'я контролера>/MyView.cshtml або View/Shared/MyView.cshtml. Це може бути корисно, наприклад, якщо дія повинна давати різний результат залежно від якогось умови.

Крім того, існує варіант View(), що дозволяє передавати дані подання. Це буде докладніше розглянуто далі. А зараз трохи розберемо можливості Razor.

Мова розмітки Razor

Базові конструкції

Razor допускає такі конструкції:

  • @{… } — дозволяє додати C# код в тіло подання.
  • @мінлива, @властивість або @метод() — вставляють значення рядка в HTML код. При цьому висновок буде екранований (тобто якщо рядок містить теги, то вони будуть виведені як звичайний текст, а не вставлені як теги).
  • Дозволені такі керуючі конструкції:
    @if { ... } else { ... }
    @switch( ... ) { ... }
    @for( ... ) { ... }
    @foreach( ... ) { ... }
    @while( ... ) { ... }
У них C# код може бути поєднаний з HTML кодом, наприклад:

@for(int i=0; i < 10;i++) {
<p>@i</p>
}

Необхідно зазначити, що подання по суті є класом, що реалізує IRazorPage, зі своїми властивостями і методами. Наприклад, доступні ViewBag і ViewData, які містять значення, які були додані ще в дії.

Так само існує властивість Html, яке надає велике число допоміжних методів. Ось деякі з них:

  • @Нtml.Raw() — додає в HTML код текстове значення без екранування.
  • @Нtml.CheckBoxFor(), @Нtml.TextBoxFor() та інші методи для генерації коду різних HTML-елементів.
Ще одне допоміжне властивість — Url. Воно містить методи для роботи з шляхами у веб-додатку. Напевно, найпопулярніший з них це @ Url.Action(… ), який виводить шлях до зазначеного дії зазначеного контролера, наприклад: <a href="@ Url.Action("Index","Home")"> Home page</a>.

Необхідно так само відзначити спеціальний файл Views/_ViewStart.cshtml. Його наявність не обов'язково, але якщо він присутній у проекті, то його вміст виконується перед кожним поданням. З його допомогою можна задати загальний для всіх уявлень шаблон сторінок.

Шаблони сторінок

Як правило, веб-додаток містить десятки сторінок. Практично кожній з них буде відповідати своє уявлення, а значить і свій файл з HTML-кодом. При цьому багато сторінок мають однаковий шаблон і розрізняються лише частиною з основним вмістом (контентом).

Щоб не вставляти копію HTML-коду сторінки в кожен файл представлення, можна скористатися розміткою (layout). Для цього необхідно:

  1. Створити файл з загальною розміткою в View/Shared
  2. _ViewStart.cshtml або дію вказується файл розмітки за допомогою властивості Layout.
    Підказка: Якщо шаблон вказано в файлі і в поданні, то буде використовуватися той, що вказано в поданні. Таким чином можна задати шаблон за замовчуванням в _ViewStart.cshtml, а при необхідності змінити його подання.
  3. Місце, де код представлення буде вставлений шаблон, зазначається викликом @RenderBody().
Отже, в папці View/Shared створимо файл шаблону HTML розмітки _Layout.cshtml (ім'я може бути довільним):

<html>
<head>
<meta charset="utf-8" />
<title>@this.ViewBag.Title</title>
</head>
<body>
<nav>Menu</nav>

<section id="mainContent">
@RenderBody()
</section>

<footer>Footer</footer>
</body>
</html>

Так само змінимо код подання:

@{
this.ViewBag.Title = "Home page";
this.Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>View Demo</h1>

Як добре видно, цей код:
  • Створює у ViewBag властивість Title зі значенням "Home page".
  • Встановлює Layout вказує на використовуваний шаблон HTML розмітки.
Тепер при запуску веб-додатка буде виведена сторінка з використанням визначеного шаблону. В її заголовок буде вставлено рядок "Home page".

Секції

Існує можливість для вставки коду з подання в шаблон сторінки поза коду блоку, створюваного @RenderBody(). Це секції, які працюють наступним чином:

  1. У коді шаблону вказуються місця для вставки секцій з допомогою @RenderSection(name, required) або @RenderSectionAsync(name, isRequired). Де name визначає унікальне ім'я секції, а required — є вона обов'язковою.
  2. Уявлення визначають вміст для секцій з допомогою конструкції @section name {… }
  3. При відсутності коду для обов'язкової секції, буде виведено повідомлення про помилку.
Варто відзначити, що секцій можуть розташовуватися не тільки в тілі сторінки, але і в її заголовку. Наприклад, це можна використовувати для підключення зовнішніх JavaScript бібліотек, необхідних тільки на окремих сторінках.

Перейдемо до прикладу. Визначимо необов'язкову секцію в шаблоні сторінки

<html>
<head>
<meta charset="utf-8" />
<title>@this.ViewBag.Title</title>

</head>
<body>
<nav>Menu</nav>

<section id="mainContent">
@RenderBody()
</section>

<footer>Footer</footer>

@RenderSection("scripts", required: false)
</body>
</html>

Тепер задамо код для даної секції в поданні. Для наочності викличемо alert():

@{
this.ViewBag.Title = "Home page";
}
<h1>View Demo</h1>

@section scripts {
<script>
alert("Demo");
</script> 
}

Запустимо проект і переконаємося JavaScript був доданий на сторінку і виконаний.

Часткові подання

Ще одна зручна можливість Razor — створення часткових уявлень. Це може використовуватися, як для зберігання повторюваних елементів сторінок (екранних діалогів, форм і т. д.), так і для логічного поділу великий сторінки на частини.

Використовувати часткові подання дуже просто:

  1. Створити часткове уявлення як звичайне
  2. Вказати в ньому Layout = null, щоб скасувати використання шаблону сторінки.
  3. Для вставки часткового подання використовувати Html.Partial() або Html.PartialAsync().
Як завжди, перейдемо до прикладу. У новій папці Views/Shared/Forms створимо файл часткового представления_ContactForm.cshtml з наступним вмістом:

<form>
Message: <input type="text" /><button type="submit">Send</button>
</form>

Виведемо його в уявленні Index:

@{
this.ViewBag.Title = "Home page";
}
<h1>View Demo</h1>

@Html.Partial("Forms/_ContactForm")

@section scripts {
<script>
alert("Demo");
</script> 
}

Зверніть увагу як заданий шлях до часткового подання. Його пошук йде за загальними правилами, і в даному випадку шлях зазначений щодо Shared. Файл з ним можна розмістити у будь-якій папці всередині View, він при цьому треба буде вказувати в параметрі методу вже повний шлях щодо кореня сайту. Наприклад, Html.Partial("~/Views/Shared/Forms/_ContactForm")

Залишається тільки запустити проект і переконатися в результаті.

Компоненти уявлень (View Components)

Говорячи про подання, необхідно скасувати таке нововведення ASP.NET MVC 6 як компоненти. По суті це розвиток ідеї часткових уявлень шляхом додавання до них власних контролерів. Однак, насамперед необхідно розібратися використанням моделей. І тоді можна буде повернутися до компонентів в наступних частинах.

Працюємо з моделями
Створені в попередніх частинах програми вже містять контролер і подання. Залишається розглянути як використовувати ще один компонент шаблону MVC — моделі.
aspnetcolumngithubРада! Ви можете спробувати все самостійно або завантаживши вихідний код з GitHub https://github.com/vyunev/AspColumnModelView.

Створюємо модель

Скористаємося створеним в минулій частині додатком ViewDemo і додамо в нього роботу з моделлю.

Для розміщення моделей в проекті створимо папку Models. Дане ім'я є рекомендованим, однак не обов'язковим. Їх код може бути розміщений в папках з іншими іменами. Крім того, можна використовувати вкладені папки для логічного групування класів.

Модель — це простий об'єкт (POCO). Немає необхідності наслідувати його від будь-якого класу або реалізовувати якийсь інтерфейс. Створимо модель, яка, для прикладу, буде містити ім'я користувача та поточний час. Для цього додамо в папку Models файл IndexPageModel.cs з наступним кодом:

using System;

namespace ViewDemo.Models
{
public class IndexPageModel
{
public string FirstName { get; set; }

public string LastName { get; set; }

public string FullName => $"{this.FirstName} {this.LastName}";

public DateTime CurrentTime { get; set; }
}
}

Модель готова. Зверніть увагу, що немає обмежень на клас моделі. Наприклад, в даному випадку присутня обчислюване властивість FullName. Проте не варто захоплюватися і переносити логіку програми в моделі. Краще залишати їх максимально простими, щоб полегшити подальший супровід програми.

Допрацьовуємо контролер

Отже, модель вже є. Необхідно створити екземпляр і записати в нього які-небудь значення. Для простоти скористаємося константами. Перейдемо до контролера Home і модифікуємо дію Index наступним чином:

public IActionResult Index()
{
var model = new IndexPageModel()
{
FirstName = "John",
LastName = "Doe",
CurrentTime = DateTime.Now
};

return View(model);
}

Для передачі моделі подання достатньо підставити її примірник в якості параметра методу View(). В даному прикладі використовуються константи для заповнення моделі. Зрозуміло в реальному проекті буде звернення до бізнес-логікою програми і отриманий результат буде вкладено в модель.

Модифікуємо подання

Відкриємо подання Index.cshtml. В першу чергу необхідно вказати, що воно буде використовувати певний клас моделі. Для цього в першому рядку файлу напишемо наступне:

@model ViewDemo.Models.IndexPageModel

Зверніть увагу, що ім'я класу зазначено разом з його простором імен.

Тепер можна використовувати властивість this.Model, в якому буде міститися передана модель, зазначеного типу. Виведемо дані на сторінку (повний код подання Index.cshtml):

@model ViewDemo.Models.IndexPageModel
@{
this.ViewBag.Title = "Home page";
}

<h1>Model Demo</h1>
<p><strong>Ім'я та прізвище:</strong>@this.Model.FullName</p>
<p><strong>Поточне дата і час:</strong>@this.Model.CurrentTime.ToString()</p>

Запустимо програму. При кожному оновленні сторінки буде виводитися зазначені в контролері ім'я, прізвище, а також поточні дата і час.

Отже, створювати та використовувати моделі не складніше ніж звичайні класи. Всю роботу по передачі її примірники подання бере на себе ASP.NET MVC. Крім того, в поданні властивість this.Model стає того ж типу, що і модель. Це дозволяє використовувати IntelliSense при створенні і редагуванні уявлень.

Свіжі новини
Як ви вже знаєте випущена Visual Studio 2015 з ASP.NET5 Beta5. Подробиці про те, що саме в реліз включено Visual Studio можна почитати в цьому блозі.

Випущено оновлення ASP.NET5 Beta6 з безліччю змін, поліпшень і виправлень помилок. Подробиці оновлення можна знайти на у цьому блозі. Ви можете завантажити оновлення по цьому посиланню.

Опубліковані плани з випуску релізів платформи протягом найближчих місяців до випуску фінальної версії ASP.NET5. Відповідно їм на нас чекають версії Beta7 і Beta8, після чого в листопаді ми отримаємо першу версію, готову до продакшну (RC1), фінальна версія вийде в першому кварталі 2016 року. Подробиці кожної версії можна знайти на за адресою.

Опубліковані доповіді конференції DevCon 2015, в тому числі з веб-розробки і теми ASP.NET.

Корисні посилання
Сама свіжа документація по ASP.NET5 розташована за адресою http://docs.asp.net/en/latest/.

Запрошуємо вас підключатися до живих трансляцій періодичного шоу ASP.NET 5 Community Standup, де розробники з команди Microsoft діляться останніми новинами про платформі. Записи доступні по цьому посиланню.

Познайомтеся з демонстраційним проектом PiDnx від Деміана Едвардса, який показує як запускати ASP.NET5 на Raspberry Pi 2 на базі Windows 10 IoT Core.

Вивчіть основи ASP.NET5 новим безкоштовним курсом віртуальної академії Microsoft.

Авторам
Друзі, якщо вам цікаво підтримати колонку своїм власним матеріалом, то прошу написати мені на vyunev@microsoft.com для того, щоб обговорити всі деталі. Ми шукаємо авторів, які можуть цікаво розповісти про ASP.NET та інші теми.

Про автора
Веселов Андрій
Senior Developer, CodeFirst, Ireland

Сертифікований розробник з 15-річним стажем, Microsoft MVP (ASP.Net/IIS). Останні 9 років займається веб-розробкою з використанням технологій Microsoft. Сфера основних професійних інтересів: ASP.NET і Azure. В даний час працює в компанії CodeFirst на посаді Senior Developer. Веде професійний блог.

Twiter: twitter.com/AndreyVeselov
Facebook: www.facebook.com/veselov.andrey
Блог: andrey.moveax.ru
Які подробиці ASP.NET5 вам цікаво дізнатися?

/>
/>


<input type=«checkbox» id=«vv68297»
class=«checkbox js-field-data»
name=«variant[]»
value=«68297» />
Публікування і запуск на Linux, OSX
<input type=«checkbox» id=«vv68299»
class=«checkbox js-field-data»
name=«variant[]»
value=«68299» />
Розробка з Visual Studio Code
<input type=«checkbox» id=«vv68301»
class=«checkbox js-field-data»
name=«variant[]»
value=«68301» />
Нові або змінені функції порівняно з ASP.NET4.5
<input type=«checkbox» id=«vv68303»
class=«checkbox js-field-data»
name=«variant[]»
value=«68303» />
Підсистема безпеки
<input type=«checkbox» id=«vv68305»
class=«checkbox js-field-data»
name=«variant[]»
value=«68305» />
Що інше, напишу в коментарях

Проголосувало 34 людини. Утрималося 5 чоловік.


Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.