Вы находитесь на странице: 1из 40

Условие задачи

Домашнее задание 10. Требуется реализовать веб-приложение на ASP.NET Core c использованием средств MVC и Razor для просмотра и
редактирования списков студентов, хранящихся в базе данных. Требования к поддерживаемой функциональности аналогичны указанным
в домашнем задании к занятию 6. Повторное использование кода для доступа к базе данных из вашего решения этой домашней работы
допускается и, более того, приветствуется. В этой работе, веб-приложение должно
содержать следующие страницы и функции:

Страница 1. Домашняя страница «Списки студентов». Содержит списки студентов по


группам:
● Напротив каждого студента кнопки (или ссылки) «Редактировать» и «Удалить»;
● Рядом с названием группы количество студентов в группе в скобках и кнопка
(ссылка) «Добавить студента»;
● Нажатие на кнопку (ссылку) «Удалить» удаляет студента, без подтверждения,
страница перезагружается;
● При нажатии на «Редактировать» или «Добавить студента» -- происходит переход
на страницу «Редактирование/добавление студента». Форма на странице
редактирования должна быть пред-заполнена данными соответствующего студента.

Страница 2. «Редактирование/добавление студента». Содержит форму для


редактирования данных о студенте:
● Поле «Группа» – выпадающий список;
● Поле «ФИО студента» – строка ввода текста; заполнено, если производится
редактирование, и пустое, если производится добавление;
● Кнопка (ссылки) «Сохранить» производит сохранение изменений и возврат на
домашнюю страницу;
● При нажатии на кнопку «Сохранить» в начале должна быть осуществлена проверка
заполнил ли пользователь поле «ФИО студента». В случае, если поле не заполнено,
сохранение не производится, остается открытой страница редактирования и рядом с
полем отображается сообщение об ошибке: «ФИО студента не может быть пустым».
● Кнопка (ссылка) «Отмена» осуществляет возврат на домашнюю страницу без
сохранения изменений.

Обе страницы должны иметь общий дизайн. Заголовок страниц должен содержать название и логотип приложения (можно использовать
произвольное изображение).
Домашнее задание 6. Необходимо написать консольное приложение, позволяющее вести учёт состава учебных групп студентов на
физическом факультете, используя реляционную базу данных для хранения этой информации.

Приложение должно позволять выводить списки студентов по группам, а также добавлять, удалять студентов из группы. Списки студентов
по умолчанию пустые и будут редактироваться в приложении.

Приложение должно быть рассчитано на работу с произвольным количеством групп, но для удобства, следующие группы должны быть
автоматически добавлены в базу данных при первом запуске приложения средствами Entity Framework:
● радиофизика,
● микроэлектроника,
● общая физика.

Алгоритм работы приложения:


1. При запуске выводятся списки студентов по группам: на первой строке выводится название группы и в скобках количество
студентов в группе, далее на каждой строке по одному студенту текущей группы (ФИО). Студенты отсортированы по фамилиям.
Затем таким же образом выводятся следующие группы.
2. Выводится вопрос, какое действие нужно совершить:
● Вариант «1 – добавить студента» запускает цепочку последовательных вопросов, после каждого из которых пользователь
должен ввести ответ:
○ Выберете номер группы: (далее следует вывод всех групп через запятую в формате <номер> - <название>)
○ Введите ФИО студента – строковый ввод
● Вариант «2 - удалить студента», вопросы:
○ Выберете номер группы: (список групп)
○ Выберите номер студента: (далее следует вывод всех студентов группы по одному на строке в формате <номер> -
<ФИО>)
После получения ответов производится запись в базу данных и возврат к пункту 1.
В предыдущей серии (1/2)

Факты о предметной области:


● Факультет, группы, студенты;
● Факультет только один;
● О группе известно ее название;
● О студенте известно его ФИО;
● Группы относятся к факультету;
● Студенты распределены по группам;
● Студент может относится только к одной группе.
В предыдущей серии (2/2)

Функции консольного приложения:


● Вывести список студентов в каждой группе;
● Добавить студента в группу;
● Удалить студента из группы.
Доступ к данным (сущности)

public class Group


{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Student> Students { get; set; }
}

public class Student


{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string FathersName { get; set; }
public int GroupId { get; set; }

public FullName FullName


{
get
{
return new FullName(FirstName, LastName, FathersName);
}
set
{
FirstName = value.FirstName;
LastName = value.LastName;
FathersName = value.FathersName;
}
}
}
Доступ к данным (объекты-значения)

public class FullName


{
public static bool TryParse(string str, out FullName name) { ... }

public FullName(string firstName, string lastName, string fathersName)


{
if (string.IsNullOrWhiteSpace(firstName)) throw new ArgumentException(nameof(firstName));
if (string.IsNullOrWhiteSpace(lastName)) throw new ArgumentException(nameof(lastName));
if (string.IsNullOrWhiteSpace(fathersName)) throw new ArgumentException(nameof(fathersName));

FirstName = firstName;
LastName = lastName;
FathersName = fathersName;
}

public string FirstName { get; }


public string LastName { get; }
public string FathersName { get; }

public override string ToString() => LastName + " " + FirstName + " " + FathersName;
}
Доступ к данным (контекст)

public class StudentsContext : DbContext


{
public DbSet<Group> Groups { get; private set; }
public DbSet<Student> Students { get; private set; }

protected override void OnModelCreating(ModelBuilder model)


{
base.OnModelCreating(model);
ConfigureGroup(model.Entity<Group>());
ConfigureStudent(model.Entity<Student>());
}

private void ConfigureGroup(EntityTypeBuilder<Group> group)


{
group.Property(t => t.Name).IsRequired().HasMaxLength(100);

group.HasMany(g => g.Students).WithOne().HasForeignKey(s => s.GroupId);

group.HasData(
new Group() { Id = 1, Name = "Радиофизика" },
new Group() { Id = 2, Name = "Микроэлектроника" },
new Group() { Id = 3, Name = "Общая физика" }
);
}

private void ConfigureStudent(EntityTypeBuilder<Student> student)


{
student.Property(t => t.FirstName).IsRequired().HasMaxLength(100);

student.Property(t => t.LastName).IsRequired().HasMaxLength(100);

student.Property(t => t.FathersName).IsRequired().HasMaxLength(100);

student.Ignore(t => t.FullName);


}
}
Доступ к данным (хранилище 1/2)

public class StudentStore


{
private readonly StudentsContext _db;

public StudentStore(StudentsContext db) { _db = db; }

public async Task<IReadOnlyList<Group>> AllGroupsAsync()


{
return await _db.Groups
.ToListAsync(); // вернет только группы, без студентов
}

public async Task<IReadOnlyList<Group>> AllGroupsWithStudentsAsync()


{
return await _db.Groups
.Include(g => g.Students) // чтобы вернуть студентов вместе с группами
.ToListAsync();
}

public Task<Student> StudentByIdOrDefaultAsync(int id)


{
return _db.Students
.FirstOrDefaultAsync(s => s.Id == id); // вернет null если студент не найден
}

// ... см. продолжение ниже


}
Доступ к данным (хранилище 2/2)

public class StudentStore


{
// ... см. начало выше

public async Task<int> AddStudentAsync(Student student)


{
_db.Students.Add(student);

await _db.SaveChangesAsync();
return student.Id;
}

public async Task<bool> UpdateStudentAsync(int id, Student student)


{
var existingStudent = await StudentByIdOrDefaultAsync(id);
if (existingStudent == null)
return false;

existingStudent.FullName = student.FullName;
existingStudent.GroupId = student.GroupId;

await _db.SaveChangesAsync();
return true;
}

public async Task DeleteStudentAsync(int id)


{
var existingStudent = await StudentByIdOrDefaultAsync(id);
if (existingStudent == null)
return; // такого студента нет, ошибку не возвращаем --
// мы все равно хотели от него избавиться
_db.Students.Remove(existingStudent);
await _db.SaveChangesAsync();
}
}
Анализ интерфейса
Сценарии использования (1/3)

Получение списка студентов:


Сценарии использования (2/3)

Редактирование данных о студенте:


Сценарии использования (3/3)

Удаление студента:
Контроллер (1/2)

public class StudentController : Controller


{
// СПИСОК СТУДЕНТОВ

// GET /student
// GET /student/index
[HttpGet]
public async Task<IActionResult> Index()
{
// ... получить списки всех групп и студентов
return View(...);
}

// ДОБАВЛЕНИЕ СТУДЕНТА

// GET /student/create - отображение формы


[HttpGet]
public async Task<IActionResult> Create()
{
// ... получить список всех групп (для выпадающего списка)
return View(...);
}

// POST /student/create - обработка присланной формы


[HttpPost]
public async Task<IActionResult> Create([FromForm]StudentFormVM vm)
{
if (ModelState.IsValid)
// ... сохранить данные студента
return RedirectToAction(nameof(Index));
else
return RedirectToAction(nameof(Create));
}

// ... см. продолжение ниже


}
Контроллер (2/2)

public class StudentController : Controller


{
// ... см. начало выше

// РЕДАКТИРОВАНИЕ СТУДЕНТА

// GET /student/edit/1 - отображение формы


[HttpGet]
public async Task<IActionResult> Edit(int id)
{
// ... получить текущие данные студента
if (student == null) return NotFound();

return View(formVm);
}

// POST /student/edit - обработка присланной формы


[HttpPost]
public async Task<IActionResult> Edit([FromForm]StudentFormVM vm)
{
if (ModelState.IsValid)
{
// ... обновить данные студента
if (student == null) return NotFound();

return RedirectToAction(nameof(Index));
}
else
return RedirectToAction(nameof(Edit), new { id = student.Id });
}

// УДАЛЕНИЕ СТУДЕНТА

// POST /student/delete/1
[HttpPost]
public async Task<IActionResult> Delete(int id)
{
// ... удалить студента
return RedirectToAction(nameof(Index));
}
}
Список студентов (контроллер)

public class StudentController : Controller


{
private readonly StudentStore _studentStore;

public StudentController(StudentStore studentStore)


{
_studentStore = studentStore;
}

[HttpGet]
public async Task<IActionResult> Index()
{
var groups = await _studentStore.AllGroupsWithStudentsAsync();

return View(new StudentIndexVM()


{
AllGroups = IndexGroupVM.OfGroupsWithStudents(groups)
});
}
}

ViewModel -- объект предназначенный для передачи данных в представление. Содержит только те данные, которые нужны
🛈 конкретному представлению, в том формате, который удобен для этого представления.
Список студентов (внедрение зависимостей)

public class Startup


{
public void ConfigureServices(IServiceCollection services)
{
// ...

services.AddDbContext<StudentsContext>(opts =>
opts.UseSqlServer(Configuration.GetConnectionString("Default")));

services.AddTransient<StudentStore>();
}

// ...
}

public class StudentStore


{
private readonly StudentsContext _db;

public StudentStore(StudentsContext db)


{
_db = db;
}

// ...
}

public class StudentController : Controller


{
private readonly StudentStore _studentStore;

public StudentController(StudentStore studentStore)


{
_studentStore = studentStore;
}

// ...
}
Список студентов (модели представления)

public class StudentIndexVM


{
public IReadOnlyList<IndexGroupVM> AllGroups { get; set; }
}

public class IndexGroupVM


{
public static IReadOnlyList<IndexGroupVM> OfGroupsWithStudents(IReadOnlyList<Group> groups) =>
groups.OrderBy(g => g.Id).Select(OfGroupWithStudents).ToList();

public static IndexGroupVM OfGroupWithStudents(Group group)


{
var students = group.Students ?? Array.Empty<Student>();
return new IndexGroupVM()
{
Id = group.Id,
Name = group.Name,
Students = students.Select(IndexStudentVM.OfStudent).OrderBy(s => s.FullName).ToList()
};
}

public int Id { get; set; }


public string Name { get; set; }

public int TotalStudentCount => Students.Count; // чтобы не помещать логику в представление


public IReadOnlyList<IndexStudentVM> Students { get; set; }
}

public class IndexStudentVM


{
public static IndexStudentVM OfStudent(Student student) =>
new IndexStudentVM() { Id = student.Id, FullName = student.FullName.ToString() };

public int Id { get; set; }


public string FullName { get; set; }
}
Список студентов (представление 1/2)

@foreach (var group in Model.AllGroups)


{
<div class="group-item">
<div class="group">
<span class="group-name">@group.Id - @group.Name (@group.TotalStudentCount)</span>
<a asp-action="Create" asp-route-groupId="@group.Id" class="action">Добавить студента</a>
</div>

<div class="student-list">
@foreach (var student in group.Students)
{
<div class="student">
<span class="student-name">@student.FullName</span>
<a asp-action="Edit" asp-route-id="@student.Id" class="action">Редактировать</a>
<a asp-action="Delete" asp-route-id="@student.Id" class="action">Удалить</a>
</div>
<!-- 🤔 -->
}
</div>
</div>
}

Обратите внимание на использование атрибутов asp-* (tag helpers) в элементах ссылок. Используйте их, или @Html.* (html
⚠ helpers) для того, чтобы указать ссылки на экшены, вместо “зашитых” в разметку URL-ов.
Список студентов (представление 2/2)

Нажатие на ссылку приводит к GET-запросу в браузере (при переходе на страницу):

<a asp-action="Delete" asp-route-id="@student.Id" class="action">Удалить</a>

Отправка формы на сервер приводит к GET-запросу или POST-запросу в браузере в зависимости от значения атрибута method:

<form method="POST" class="action-form">


<button class="button-as-link" asp-action="Delete" asp-route-id="@student.Id">Удалить</button>
</form>
О представлениях

Избегайте логики в представлениях.

Выносите общие стили в отдельные css файлы.

Используйте осмысленные имена CSS классов.

Не используйте <table>, <p>, <br/>, &nbsp; и другие для изменения положения


элементов на странице.

Избегайте указания стилей непосредственно на элементе через атрибут style.


Добавление студента (неожиданное требование)
Добавление студента (контроллер)

public class StudentController : Controller


{
[HttpGet]
public async Task<IActionResult> Create(int groupId)
{
var studentVm = FormStudentVM.NewStudent(groupId);
return View(await CreateStudentFormVMAsync(studentVm));
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([FromForm]StudentFormVM vm) // тип такой же, как тип модели представления в GET
{
var studentVm = vm.Student;

if (ModelState.IsValid)
{
await _studentStore.AddStudentAsync(studentVm.ToStudent());
return RedirectToAction(nameof(Index));
}
else
{

}
return View(await CreateStudentFormVMAsync(studentVm)); // возвращаем форму так же, как в GET-запросе, 🤔
}

private async Task<StudentFormVM> CreateStudentFormVMAsync(FormStudentVM studentVm)


{
var groups = await _studentStore.AllGroupsAsync();

return new StudentFormVM()


{
AllGroups = SelectListItemUtil.OfGroups(groups),
Student = studentVm
};
}
}
Добавление студента (модели представления)

public class StudentFormVM


{
public IReadOnlyList<SelectListItem> AllGroups { get; set; }
public FormStudentVM Student { get; set; }
}

public class FormStudentVM


{
public static FormStudentVM NewStudent(int groupId) =>
new FormStudentVM() { GroupId = groupId };

public int Id { get; set; }

[Required(ErrorMessage = "ФИО студента не может быть пустым")]


[FullNameRequired(ErrorMessage = "Пожалуйста, введите фамилию, имя и отчество, разделенные знаком пробела.")]
[MaxLength(100)]
public string FullName { get; set; }

[Required(ErrorMessage = "Пожалуйста, укажите группу")]


public int GroupId { get; set; }

public Student ToStudent()


{
return new Student()
{
Id = Id,
FullName = StudentsData.Data.FullName.Parse(FullName),
GroupId = GroupId
};
}
}
Добавление студента (представление)

<form class="student-form" method="POST">


<div class="form-title">Студент</div>

<input asp-for="Student.Id" type="hidden" /> <!-- чтобы ID студента пришел на сервер при отправке формы -->

<div class="control-group">
<label asp-for="Student.FullName" class="control-label"> <!-- asp-for чтобы при нажатии на label, фокус перешел в input -->
<abbr title="Фамилия Имя Отчество">ФИО</abbr>:
</label>
<input asp-for="Student.FullName" class="control" placeholder="Имя студента" />
<span asp-validation-for="Student.FullName" class="control-error"></span>
</div>

<div class="control-group">
<label asp-for="Student.GroupId" class="control-label">
Группа:
</label>
<select asp-for="Student.GroupId" asp-items="Model.AllGroups" class="control"></select>
<span asp-validation-for="Student.GroupId" class="control-error"></span>
</div>

<div class="form-actions">
<input class="action-button" type="submit" value="Сохранить" />
<a class="action-button" asp-action="Index">Отмена</a>
</div>
</form>
Выпадающие списки

public class StudentFormVM


{
public IReadOnlyList<SelectListItem> AllGroups { get; set; }
// ...
}

// Определен в Microsoft.AspNetCore.Mvc.Rendering
// https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.selectlistitem?view=aspnetcore-5.0

public class SelectListItem


{
public bool Disabled { get; set; }
public bool Selected { get; set; }
public string Text { get; set; }
public string Value { get; set; }

// ...
}

<div class="control-group">
<!-- ... -->
<select asp-for="Student.GroupId" asp-items="Model.AllGroups" class="control"></select>
<!-- ... -->
</div>
Валидация форм (1/3)

public class StudentController : Controller


{
[HttpPost]
public async Task<IActionResult> Create([FromForm]FormStudentVM studentVm)
{
if (ModelState.IsValid) // (1)
{
// данные верны
}
else
{
// данные некорректны
}
}
}

public class FormStudentVM


{
// (2)
[Required(ErrorMessage = "ФИО студента не может быть пустым")]
[FullNameRequired(ErrorMessage = "Пожалуйста, введите фамилию, имя и отчество, разделенные знаком пробела.")]
[MaxLength(100)]
public string FullName { get; set; }
}

<div class="control-group">
<label asp-for="Student.FullName" class="control-label">
<abbr title="Фамилия Имя Отчество">ФИО</abbr>:
</label>
<input asp-for="Student.FullName" class="control" placeholder="Имя студента" />
<span asp-validation-for="Student.FullName" class="control-error"></span> <-- (3) -->
</div>

Валидация входных данных на сервере обязательна, даже если эти данные уже валидировались на клиенте, или UI клиента не
⚠ позволяет ввести некорректные данные. Отправка формы -- это обычный POST-запрос, который можно сделать напрямую, в
обход UI отображаемого в браузере.
Валидация форм (2/3)

// Определены в Microsoft.AspNetCore.Mvc.*

public abstract class ControllerBase


{
public ModelStateDictionary ModelState { get; }
// ...
}

public class ModelStateDictionary


{
public bool IsValid { get; }
public ModelStateEntry this[string key] { get; }

public void AddModelError(string key, string errorMessage);


public void SetModelValue(string key, object rawValue, string attemptedValue);

// ...
}

public abstract class ModelStateEntry


{
public ModelErrorCollection Errors { get; }
public object RawValue { get; set; }
public string AttemptedValue { get; set; }
public abstract IReadOnlyList<ModelStateEntry> Children { get; }
// ...
}
Валидация форм (3/3)

Нестандартный атрибут валидации:

public class FormStudentVM


{
// ...

[FullNameRequired(ErrorMessage = "Пожалуйста, введите фамилию, имя и отчество, разделенные знаком пробела.")]


public string FullName { get; set; }

// ...
}

public class FullNameRequiredAttribute : ValidationAttribute


{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value is string fullName)
{
if (FullName.TryParse(fullName, out var ignored))
{
return ValidationResult.Success;
}
else
{
return new ValidationResult(ErrorMessage);
}
}
else
{
return ValidationResult.Success;
}
}
}
Cross-Site Request Forgery (1/4)

Сайт https://good-guys.com позволяет переводить деньги между счетами пользователей по


номеру телефона.

Перевод денег осуществляется через простую форму:

<form action="/process-payment" method="POST">


<label>Email: <input type="text" name="phone-number" /></label>
<label>Amount: <input type="number" name="money-amount" /></label>
<label>Comments: <textarea name="comments"></textarea></label>
<input type="submit" value="Send!" />
</form>

Шаг 1.
Пользователь логиниться на сайте https://good-guys.com.
Сайт устанавливает аутентификационную куку, идентифицирующую пользователя.
Браузер будет отправлять эту куку вместе с любым запросом с доменом good-guys.com в
адресе.
Cross-Site Request Forgery (2/4)

Шаг 2.
Злоумышленник заманивает этого пользователя на свой сайт https://shady-biz.io.
На сайте показана внешне форма, никак не связанная с https://good-guys.com.
Пользователя подбивают отправить форму.

<form action="https://good-guys.com/process-payment" method="POST">


<input type="hidden" name="phone-number" value="+1 (212) 333-4455" />
<input type="hidden" name="money-amount" value="$100.00" />
<input type="hidden" name="comments" value="" />

<input type="submit" value="What?!" />


</form>

Итог.
Пользователь, сам того не подозревая, перевел деньги на номер злоумышленника.

Отправка формы с сайта https://shady-biz.io на сайт https://good-guys.com пройдет успешно и от имени пользователя, т.к. браузер отправит
легитимную авторизационную куку установленную сайтом good-guys.com при POST-запросе на это доменное имя.

https://en.wikipedia.org/wiki/Cross-site_request_forgery
Cross-Site Request Forgery (3/4)
Cross-Site Request Forgery (4/4)

public class StudentController : Controller


{
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([FromForm]StudentFormVM vm)
{
// ...
}

// ...
}

Подробнее: https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery
Подтверждение повторной отправки формы

Шаг 1. Открываем форму редактирования, вводим неправильные данные, нажимаем “Сохранить”.

Шаг 2. Пытаемся обновить страницу в браузере (F5).


Шаблон Post-Redirect-Get

public class StudentController : Controller


{
[HttpGet]
[ImportModelState]
public async Task<IActionResult> Create(int groupId)
{
var studentVm = FormStudentVM.NewStudent(groupId);
return View(await CreateStudentFormVMAsync(studentVm));
}

[HttpPost]
[ValidateAntiForgeryToken]
[ExportModelState]
public async Task<IActionResult> Create([FromForm]StudentFormVM vm)
{
var studentVm = vm.Student;

if (ModelState.IsValid)
{
await _studentStore.AddStudentAsync(studentVm.ToStudent());
return RedirectToAction(nameof(Index));
}
else
{
return View(await CreateStudentFormVMAsync(studentVm));

return RedirectToAction(nameof(Create), new { id = studentVm.Id });


}
}
}

[ImportModelState] и [ExportModelState] -- нестандартные MVC фильтры, которые сохраняют и восстанавливают ModelState


🛈 в TempData (стандартное средство MVC для передачи данных от одного запроса к другому через Cookie).

Все подробности реализации тут: https://andrewlock.net/post-redirect-get-using-tempdata-in-asp-net-core/


Редактирование студента (контроллер)

public class StudentController : Controller


{
[HttpGet]
[ImportModelState]
public async Task<IActionResult> Edit(int id)
{
var student = await _studentStore.StudentByIdOrDefaultAsync(id);
if (student == null)
return NotFound();

var studentVm = FormStudentVM.OfExistingStudent(student);


return View(await CreateStudentFormVMAsync(studentVm)); // модель такая же как для добавления Студента
}

[HttpPost]
[ValidateAntiForgeryToken]
[ExportModelState]
public async Task<IActionResult> Edit([FromForm] StudentFormVM vm) // модель такая же как для добавления Студента
{
var studentVm = vm.Student;

if (ModelState.IsValid)
{
var isUpdated = await _studentStore.UpdateStudentAsync(studentVm.Id, studentVm.ToStudent());
if (!isUpdated)
return NotFound();

return RedirectToAction(nameof(Index));
}
else
{
return RedirectToAction(nameof(Edit), new { id = studentVm.Id });
}
}
}
Редактирование студента (повторное использование представления)

Create.cshtml

@model StudentsWeb.Models.StudentForm.StudentFormVM
@{ ViewData["Title"] = "Новый студент"; }

<partial name="_StudentForm" model="@Model" view-data="@ViewData" />

Edit.cshtml

@model StudentsWeb.Models.StudentForm.StudentFormVM
@{ ViewData["Title"] = "Редактирование студента"; }

<partial name="_StudentForm" model="@Model" view-data="@ViewData" />

_StudentForm.cshtml

@model StudentsWeb.Models.StudentForm.StudentFormVM

<form class="student-form" method="POST">


<!-- ... сама форма точно такая же как была показана выше ... -->
</form>
Общий заголовок для всех страниц

_ViewStart.cshtml

@{ Layout = "_Layout"; }

_Layout.cshtml

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cтуденты - @ViewData["Title"]</title>
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<div class="header">
<div class="logo"></div>
<div class="title">Списки студентов</div>
</div>

<div class="content">
@RenderBody() <!-- сюда будет подставлена разметка представления -->
</div>

@RenderSection("Scripts", required: false)


</body>
</html>
Удаление студента (контроллер)

public class StudentController : Controller


{
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
await _studentStore.DeleteStudentAsync(id);
return RedirectToAction(nameof(Index));
}
}
Удаление студента (или, почему POST, а не DELETE?)

GET:
<a asp-action="Delete" asp-route-id="@student.Id" class="action">Удалить</a>

POST:
<form method="POST" class="action-form">
<button class="button-as-link" asp-action="Delete" asp-route-id="@student.Id">Удалить</button>
</form>

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method

https://softwareengineering.stackexchange.com/questions/114156/why-are-there-are-no-put-and-delete-methods-on-html-forms

Вам также может понравиться