У меня есть приложение 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
перед тем, как попытаться сохранить его в контексте базы данных.
Вот несколько важных моментов, которые стоит учесть для исправления вашей проблемы:
-
Проверка Свойств: Убедитесь, что вы присваиваете все необходимые поля перед их использованием. В вашем коде, если пользователь отмечает чекбокс
CheckedBox
, вы заполняете толькоBranchName
,PhoneNumber
,Address
и устанавливаетеShippingAddress
вnull
. Однако, если чекбокс не установлен, у вас есть участок кода, где вы также устанавливаетеPhoneNumber
иAddress
, но не устанавливаетеBranchName
. Это может стать причиной появленияNullReferenceException
, еслиBranchName
это обязательное поле в базе данных. - Логика в Контроллере: Ниже приведена обновленная логика вашего контроллера с учетом вышеописанных моментов:
[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");
}
-
Возврат Модели на Ошибку: В случае ошибки валидации вам следует возвращать не просто
View()
, аView(client)
, чтобы сохраняли информацию, введённую пользователем в форме. Это улучшит пользовательский интерфейс, так как пользователю не придется заполнять форму заново при возникновении ошибки. - Проверка Модельных Свойств: Возможно, стоит добавить валидационные атрибуты на уровень модели
Client
, чтобы убедиться, что вы обрабатываете все требования и исключения. ЕслиBranchName
должен быть обязательным в вашем приложении, необходимо это явно указать.
Таким образом, убедитесь, что все необходимые свойства объекта newClient
установлены, прежде чем вы попытаетесь сохранить его в контексте базы данных. Убедитесь также, что вы обрабатываете возможные ошибки с правильным возвращением состояния модели, когда возникают ошибки валидации.