Валидация ViewModel в ASP.NET

Вопросы и ответы

У меня есть приложение ASP.NET MVC 8. В настоящее время я пытаюсь предоставить администратору возможность создавать новых клиентов для их электронной коммерции. У них есть простая форма, которая выглядит следующим образом:

NewClient.cshtml


@using Ecommerce.Models
@model NewClientViewModel
@using Microsoft.AspNetCore.Identity
@using Ecommerce.ViewModel

<h1>Создание нового клиента</h1>
<div>
    <form asp-action="NewClient" asp-controller="Admin" method="post">
        <div>
            <label asp-for="Email">Электронная почта</label>
            <input asp-for="Email" class="form-control" />
            <span asp-validation-for="Email" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="FirstName">Имя</label>
            <input asp-for="FirstName" class="form-control" />
            <span asp-validation-for="FirstName" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="LastName">Фамилия</label>
            <input asp-for="LastName" class="form-control" />
            <span asp-validation-for="LastName" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="Password">Пароль</label>
            <input asp-for="Password" class="form-control" />
            <span asp-validation-for="Password" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="BranchName">Название филиала</label>
            <input asp-for="BranchName" class="form-control" />
            <span asp-validation-for="BranchName" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="PhoneNumber">Номер телефона</label>
            <input asp-for="PhoneNumber" class="form-control" />
            <span asp-validation-for="PhoneNumber" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="Address">Адрес</label>
            <input asp-for="Address" class="form-control" />
            <span asp-validation-for="Address" class="text-danger"></span>
        </div>
        <div class="form-check">
            <input  asp-for="CheckedBox" class="form-check-input" type="checkbox" id="flexCheckDefault">
            <label class="form-check-label" for="flexCheckDefault">
                Если адрес совпадает с адресом доставки, отметьте здесь
            </label>
        </div>
        <div>
            <label asp-for="ShippingAddress">Адрес доставки</label>
            <input asp-for="ShippingAddress" class="form-control" />
            <span asp-validation-for="ShippingAddress" class="text-danger"></span>
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-primary">Создать клиента</button>
        </div>
    </form>
</div>

В случае, если адрес совпадает с адресом доставки, они должны установить флажок на ‘true’. Вот модель, представление и контроллер, который обрабатывает POST-запрос:

NewClientViewModel.cs


[Required]
public string Email { get; set; }

[Required]
[Display(Name = "Имя")]
public string FirstName { get; set; }

[Required]
[Display(Name = "Фамилия")]
public string LastName { get; set;}

[Required]
public string Password { get; set; }

[Required]
[Display(Name = "Название филиала")]
public string BranchName { get; set; }

[Required]
[Display(Name = "Номер телефона")]
public string PhoneNumber { get; set; }

[Required]
public string Address { get; set; }

[AllowNull]
[Display(Name = "Адрес доставки")]
public string? ShippingAddress { get; set; }

public bool CheckedBox { get; set; } = false;

Client.cs


public class Client
{
    public int Id { get; set; }
    public string BranchName { get; set; }
    public string PhoneNumber { get; set; }
    public string Address { get; set; }
    [AllowNull] // <-- добавил это, чтобы протестировать, будет ли работать этот атрибут. Это не дало результата
    // По моему пониманию, добавление вопросительного знака после типа должно разрешать null
    public string? ShippingAddress { get; set; }
}

AdminController.cs

    

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> NewClient(NewClientViewModel client)
{
    if(!ModelState.IsValid)
        return View();

    User newClientUser = new User
    {
        UserName = client.Email,
        Email = client.Email,
        FirstName = client.FirstName,
        LastName = client.LastName
    };

    Client newClient = new Client();
    // Если пользователь ставит галочку, мы попадаем в этот блок
    // устанавливаем адрес доставки в null. В противном случае делаем наоборот
    if(client.CheckedBox)
    {
        newClient.BranchName = client.BranchName;
        newClient.PhoneNumber = client.PhoneNumber;
        newClient.Address = client.Address;
        newClient.ShippingAddress = null;
    }
    else
    {
        newClient.PhoneNumber = client.PhoneNumber;
        newClient.Address = client.Address;
        newClient.ShippingAddress = client.ShippingAddress;
    }

    IdentityResult result = await _userManager.CreateAsync(newClientUser, client.Password);
    if(!result.Succeeded)
    {
        _logger.LogInformation("Неудачная попытка");

        ModelState.AddModelError(string.Empty, "Упс, что-то пошло не так!");
        return View();
    }
    await _userManager.AddToRoleAsync(newClientUser, Roles.Customer.ToString());
    // Следующая строка вызывает исключение null reference
    await _dbContext.Clients.AddAsync(newClient);
    await _dbContext.SaveChangesAsync();

    return RedirectToAction("Dashboard", "Admin");
}

Мой вопрос: почему я получаю исключение null reference, если я сделал модель клиента и ViewModel, чтобы разрешить null. Я также проверил в базе данных, что она примет null, если ничего не предоставлено. Что я упускаю, что не удается сохранить нового клиента в базу данных? Я могу явно указать newClient.ShippingAddress = null; на newClient.ShippingAddress = client.ShippingAddress;, однако я считаю, что это будет избыточным, если я предположительно говорю в модели, чтобы принимать null.

Ответ

Ваша проблема, связанная с получением исключения NullReferenceException, скорее всего, связана с тем, что вы не устанавливаете некоторые обязательные свойства вашего объекта newClient перед тем, как попытаться сохранить его в контексте базы данных.

Вот несколько важных моментов, которые стоит учесть для исправления вашей проблемы:

  1. Проверка Свойств: Убедитесь, что вы присваиваете все необходимые поля перед их использованием. В вашем коде, если пользователь отмечает чекбокс CheckedBox, вы заполняете только BranchName, PhoneNumber, Address и устанавливаете ShippingAddress в null. Однако, если чекбокс не установлен, у вас есть участок кода, где вы также устанавливаете PhoneNumber и Address, но не устанавливаете BranchName. Это может стать причиной появления NullReferenceException, если BranchName это обязательное поле в базе данных.

  2. Логика в Контроллере: Ниже приведена обновленная логика вашего контроллера с учетом вышеописанных моментов:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> NewClient(NewClientViewModel client)
{
    if (!ModelState.IsValid)
        return View(client); // Не забудьте вернуть модель

    User newClientUser = new User
    {
        UserName = client.Email,
        Email = client.Email,
        FirstName = client.FirstName,
        LastName = client.LastName
    };

    // Создайте новый объект Client
    Client newClient = new Client
    {
        BranchName = client.BranchName,
        PhoneNumber = client.PhoneNumber,
        Address = client.Address,
        ShippingAddress = client.CheckedBox ? null : client.ShippingAddress // Установите ShippingAddress в null, если CheckedBox - true
    };

    IdentityResult result = await _userManager.CreateAsync(newClientUser, client.Password);
    if (!result.Succeeded)
    {
        _logger.LogInformation("Failed attempt");
        ModelState.AddModelError(string.Empty, "Hmm something went wrong!");
        return View(client); // Вернуть модель с ошибками валидации
    }

    await _userManager.AddToRoleAsync(newClientUser, Roles.Customer.ToString());

    // Добавляем новый клиент в базу данных
    await _dbContext.Clients.AddAsync(newClient);
    await _dbContext.SaveChangesAsync();

    return RedirectToAction("Dashboard", "Admin");
}
  1. Возврат Модели на Ошибку: В случае ошибки валидации вам следует возвращать не просто View(), а View(client), чтобы сохраняли информацию, введённую пользователем в форме. Это улучшит пользовательский интерфейс, так как пользователю не придется заполнять форму заново при возникновении ошибки.

  2. Проверка Модельных Свойств: Возможно, стоит добавить валидационные атрибуты на уровень модели Client, чтобы убедиться, что вы обрабатываете все требования и исключения. Если BranchName должен быть обязательным в вашем приложении, необходимо это явно указать.

Таким образом, убедитесь, что все необходимые свойства объекта newClient установлены, прежде чем вы попытаетесь сохранить его в контексте базы данных. Убедитесь также, что вы обрабатываете возможные ошибки с правильным возвращением состояния модели, когда возникают ошибки валидации.

Оцените материал
Добавить комментарий

Капча загружается...