Успешное редактирование модели без множества скрытых полей
Короче говоря: как мне успешно отредактировать запись в БД без необходимости включать каждое отдельное поле для модели в окне редактирования?
ОБНОВИТЬ
Так что у меня есть пункт в БД (статья). Я хочу редактировать статью. Статья, которую я редактирую, имеет много свойств (Id, CreatedBy, DateCreated, Title, Body). Некоторые из этих свойств никогда не нужно изменять (например, Id, CreatedBy, DateCreated). Поэтому в моем редактируемом представлении я хочу только поля ввода для полей, которые можно изменить (например, заголовок, тело). Когда я реализую вид редактирования таким образом, привязка модели завершается неудачно. Любые поля, для которых я не предоставил входные данные, устанавливаются в какое-то значение по умолчанию (например, DateCreated устанавливается в 01/01/0001 12:00:00 утра). Если я предоставляю входные данные для каждого поля, все работает нормально, и статья редактируется, как и ожидалось. Я не знаю, правильно ли говорить, что "привязка модели не удалась" обязательно, так же как и то, что "система заполняет поля неверными данными, если поле ввода для них не было предоставлено в представлении редактирования".
Как я могу создать представление редактирования таким образом, чтобы мне нужно было указывать поля ввода только для полей, которые могут / нуждаются в редактировании, чтобы при вызове метода редактирования в контроллере поля, такие как DateCreated, заполнялись правильно и не задавались по умолчанию, неправильное значение? Вот мой метод Edit в его нынешнем виде:
[HttpPost]
public ActionResult Edit(Article article)
{
// Get a list of categories for dropdownlist
ViewBag.Categories = GetDropDownList();
if (article.CreatedBy == (string)CurrentSession.SamAccountName || (bool)CurrentSession.IsAdmin)
{
if (ModelState.IsValid)
{
article.LastUpdatedBy = MyHelpers.SessionBag.Current.SamAccountName;
article.LastUpdated = DateTime.Now;
article.Body = Sanitizer.GetSafeHtmlFragment(article.Body);
_db.Entry(article).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View(article);
}
// User not allowed to edit
return RedirectToAction("Index", "Home");
}
И Edit View, если это поможет:
. . .
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Article</legend>
<p>
<input type="submit" value="Save" /> | @Html.ActionLink("Back to List", "Index")
</p>
@Html.Action("Details", "Article", new { id = Model.Id })
@Html.HiddenFor(model => model.CreatedBy)
@Html.HiddenFor(model => model.DateCreated)
<div class="editor-field">
<span>
@Html.LabelFor(model => model.Type)
@Html.DropDownListFor(model => model.Type, (SelectList)ViewBag.Categories)
@Html.ValidationMessageFor(model => model.Type)
</span>
<span>
@Html.LabelFor(model => model.Active)
@Html.CheckBoxFor(model => model.Active)
@Html.ValidationMessageFor(model => model.Active)
</span>
<span>
@Html.LabelFor(model => model.Stickied)
@Html.CheckBoxFor(model => model.Stickied)
@Html.ValidationMessageFor(model => model.Stickied)
</span>
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Body)
</div>
<div class="editor-field">
@* We set the id of the TextArea to 'CKeditor' for the CKeditor script to change the TextArea into a WYSIWYG editor. *@
@Html.TextAreaFor(model => model.Body, new { id = "CKeditor", @class = "text-editor" })
@Html.ValidationMessageFor(model => model.Body)
</div>
</fieldset>
. . .
Если бы я пропустил эти два входа:
@Html.HiddenFor(model => model.CreatedBy)
@Html.HiddenFor(model => model.DateCreated)
когда вызывается метод Edit, им устанавливаются значения по умолчанию. CreatedBy имеет значение Null, Created установлено на 01/01/0001 12:00:00
Почему они не установлены в значения, как они в настоящее время установлены в БД?
5 ответов
После еще нескольких исследований я наткнулся на некоторые инструменты, которые помогают в процессе ViewModel - один из них AutoMapper, а другой InjectValues. Я пошел с InjectValues прежде всего потому, что он может не только "сгладить" объекты (объект карты a -> b), но и "разровнять" их (объект карты b -> a), чего, к сожалению, в AutoMapper не хватает вне коробка - то, что мне нужно сделать, чтобы обновить значения внутри БД.
Теперь вместо того, чтобы отправлять мою модель Article со всеми ее свойствами в мои представления, я создал ArticleViewModel, содержащий только следующие свойства:
public class ArticleViewModel
{
public int Id { get; set; }
[MaxLength(15)]
public string Type { get; set; }
public bool Active { get; set; }
public bool Stickied { get; set; }
[Required]
[MaxLength(200)]
public string Title { get; set; }
[Required]
[AllowHtml]
public string Body { get; set; }
}
Когда я создаю статью, вместо отправки объекта Article (с каждым свойством) я отправляю View "более простую" модель - мой ArticleViewModel:
//
// GET: /Article/Create
public ActionResult Create()
{
return View(new ArticleViewModel());
}
Для метода POST мы берем ViewModel, который мы отправили в View, и используем его данные для создания новой статьи в БД. Мы делаем это, "разжимая" ViewModel на объекте Article:
//
// POST: /Article/Create
public ActionResult Create(ArticleViewModel articleViewModel)
{
Article article = new Article(); // Create new Article object
article.InjectFrom(articleViewModel); // unflatten data from ViewModel into article
// Fill in the missing pieces
article.CreatedBy = CurrentSession.SamAccountName; // Get current logged-in user
article.DateCreated = DateTime.Now;
if (ModelState.IsValid)
{
_db.Articles.Add(article);
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
ViewBag.Categories = GetDropDownList();
return View(articleViewModel);
}
Заполненные "недостающие фрагменты" - это свойства статьи, которые я не хотел устанавливать в представлении, и их не нужно обновлять в представлении "Правка" (или вообще, в этом отношении).
Метод Edit почти такой же, за исключением того, что вместо отправки новой ViewModel в View мы отправляем ViewModel, предварительно заполненную данными из нашей БД. Мы делаем это, извлекая статью из БД и выравнивая данные в ViewModel. Во-первых, метод GET:
//
// GET: /Article/Edit/5
public ActionResult Edit(int id)
{
var article = _db.Articles.Single(r => r.Id == id); // Retrieve the Article to edit
ArticleViewModel viewModel = new ArticleViewModel(); // Create new ArticleViewModel to send to the view
viewModel.InjectFrom(article); // Inject ArticleViewModel with data from DB for the Article to be edited.
return View(viewModel);
}
Для метода POST мы хотим взять данные, отправленные из View, и обновить им статью, хранящуюся в БД. Чтобы сделать это, мы просто обращаем процесс сглаживания путем "разглаживания" ViewModel к объекту Article - так же, как мы делали для POST-версии нашего метода Create:
//
// POST: /Article/Edit/5
[HttpPost]
public ActionResult Edit(ArticleViewModel viewModel)
{
var article = _db.Articles.Single(r => r.Id == viewModel.Id); // Grab the Article from the DB to update
article.InjectFrom(viewModel); // Inject updated values from the viewModel into the Article stored in the DB
// Fill in missing pieces
article.LastUpdatedBy = MyHelpers.SessionBag.Current.SamAccountName;
article.LastUpdated = DateTime.Now;
if (ModelState.IsValid)
{
_db.Entry(article).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View(viewModel); // Something went wrong
}
Нам также нужно изменить строго типизированные представления Create & Edit, чтобы ожидать ArticleViewModel вместо Article:
@model ProjectName.ViewModels.ArticleViewModel
И это все!
Итак, в итоге, вы можете реализовать ViewModels, чтобы передавать только куски ваших моделей в ваши представления. Затем вы можете обновить только эти части, передать ViewModel обратно в контроллер и использовать обновленную информацию в ViewModel для обновления фактической модели.
Еще один хороший способ без модели представления
// POST: /Article/Edit/5
[HttpPost]
public ActionResult Edit(Article article0)
{
var article = _db.Articles.Single(r => r.Id == viewModel.Id); // Grab the Article from the DB to update
article.Stickied = article0.Stickied;
// Fill in missing pieces
article.LastUpdatedBy = MyHelpers.SessionBag.Current.SamAccountName;
article.LastUpdated = DateTime.Now;
if (ModelState.IsValid)
{
_db.Entry(article0).State = EntityState.Unchanged;
_db.Entry(article).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View(article0); // Something went wrong
}
Посмотреть пример модели:
public class ArticleViewModel {
[Required]
public string Title { get; set; }
public string Content { get; set; }
}
Обязательный пример
public ActionResult Edit(int id, ArticleViewModel article) {
var existingArticle = db.Articles.Where(a => a.Id == id).First();
existingArticle.Title = article.Title;
existingArticle.Content = article.Content;
db.SaveChanges();
}
Это простой пример, но вы должны взглянуть на ModelState, чтобы проверить, нет ли в модели ошибок, проверить авторизацию и переместить этот код из контроллера в классы обслуживания, но это еще один урок.
Это исправленный метод редактирования:
[HttpPost]
public ActionResult Edit(Article article)
{
// Get a list of categories for dropdownlist
ViewBag.Categories = GetDropDownList();
if (article.CreatedBy == (string)CurrentSession.SamAccountName || (bool)CurrentSession.IsAdmin)
{
if (ModelState.IsValid)
{
var existingArticle = _db.Articles.First(a => a.Id = article.Id);
existingArticle.LastUpdatedBy = MyHelpers.SessionBag.Current.SamAccountName;
existingArticle.LastUpdated = DateTime.Now;
existingArticle.Body = Sanitizer.GetSafeHtmlFragment(article.Body);
existingArticle.Stickied = article.Stickied;
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View(article);
}
// User not allowed to edit
return RedirectToAction("Index", "Home");
}
В дополнение к ответу AutoMapper также может быть использован для его разматывания. Использование AutoMapper для раскрепощения DTO
Используйте ViewModels.
Благодаря моему постоянному поиску решения этой проблемы, я считаю, что использование таких вещей, как "ViewModels" - это путь. Как поясняется в сообщении Джимми Богарда, ViewModels - это способ "показать фрагмент информации из одного объекта".
asp.net-mvc-view-model-Patterns направил меня на правильный путь; Я все еще проверяю некоторые из внешних ресурсов, которые автор опубликовал для дальнейшего понимания концепции ViewModel (одним из них является пост в блоге Джимми).