Asp.net mvc 成功的模型编辑,没有一堆隐藏字段

Asp.net mvc 成功的模型编辑,没有一堆隐藏字段,asp.net-mvc,viewmodel,model-binding,valueinjecter,Asp.net Mvc,Viewmodel,Model Binding,Valueinjecter,简而言之:如何成功编辑DB条目,而不需要在编辑视图中包含模型的每个字段 更新 所以我在DB中有一个条目(一篇文章)。我想编辑一篇文章。我编辑的文章有许多属性(Id、CreatedBy、DateCreated、Title、Body)。其中一些属性永远不需要更改(如Id、CreatedBy、DateCreated)。因此,在我的编辑视图中,我只希望输入可以更改的字段(如标题、正文)。当我实现这样的编辑视图时,模型绑定失败。我没有为其提供输入的任何字段都会设置为某些“默认”值(例如DateCreate

简而言之:如何成功编辑DB条目,而不需要在编辑视图中包含模型的每个字段

更新
所以我在DB中有一个条目(一篇文章)。我想编辑一篇文章。我编辑的文章有许多属性(Id、CreatedBy、DateCreated、Title、Body)。其中一些属性永远不需要更改(如Id、CreatedBy、DateCreated)。因此,在我的编辑视图中,我只希望输入可以更改的字段(如标题、正文)。当我实现这样的编辑视图时,模型绑定失败。我没有为其提供输入的任何字段都会设置为某些“默认”值(例如DateCreated设置为01/01/0001 12:00:00am)。如果我为每个领域提供输入,一切都会正常进行,文章也会按预期进行编辑。我不知道说“模型绑定必然失败”是否正确,甚至“如果在编辑视图中没有为字段提供输入字段,那么系统会用不正确的数据填充字段。”

如何创建编辑视图,使我只需要为可以/需要编辑的字段提供输入字段,以便在调用控制器中的编辑方法时,正确填充DateCreated等字段,而不将其设置为某些默认的、不正确的值?以下是我目前的编辑方法:

    [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");   
    }
以及编辑视图(如果有帮助):

. . .
@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>
. . .
当调用Edit方法时,它们被设置为默认值。CreatedBy设置为Null,Created设置为01/01/0001 12:00:00am

为什么它们没有设置为当前在数据库中设置的值?

查看模型示例:

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");   
}

使用视图模型

通过我不断的研究来找到解决这个问题的方法,我相信使用这些被称为“视图模型”的东西是可行的。正如Jimmy Bogard在一篇文章中所解释的,ViewModels是一种“显示单个实体的信息片段”的方法


让我走上了正确的道路;我仍在查阅作者发布的一些外部资源,以便进一步掌握ViewModel的概念(Jimmy的博客文章就是其中之一)。

在进一步研究之后,我发现了一些有助于ViewModel过程的工具-一个是AutoMapper,另一个是InjectValue。我之所以选择InjectValue,主要是因为它不仅可以“展平”对象(映射对象a->b),还可以“取消展平”对象(映射对象b->a)-AutoMapper很遗憾没有现成的东西-为了更新DB中的值,我需要这样做

现在,我没有将我的文章模型及其所有属性发送到我的视图,而是创建了一个只包含以下属性的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; }
}
创建文章时,我不发送文章对象(每个属性),而是向视图发送“更简单”的模型-我的ArticleViewModel:

//
// GET: /Article/Create

public ActionResult Create()
{
    return View(new ArticleViewModel());
}
对于POST方法,我们使用发送到视图的ViewModel,并使用其数据在DB中创建一篇新文章。我们通过将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,而是发送一个用数据库中的数据预填充的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方法,我们希望获取从视图发送的数据,并用它更新存储在DB中的文章。要做到这一点,我们只需通过将ViewModel“取消平台化”到Article对象上来反转展平过程,就像我们在创建方法的后期版本中所做的那样:

    //
    // 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
    }
我们还需要更改强类型的“创建和编辑”视图,以期望使用ArticleViewModel而不是Article:

@model ProjectName.ViewModels.ArticleViewModel
就这样


总之,您可以实现ViewModels,将模型的一部分传递给视图。然后,您可以只更新这些部分,将ViewModel传递回控制器,并使用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
}

除了答案之外,AutoMapper还可以用于对其进行解冻。

因此,对于ViewModel,您仍然需要(在视图中)为每个字段提供输入,以便模型绑定器(至少是默认的)成功绑定到模型,对吗?在您的示例中,我需要为“标题”和“内容”字段提供输入。在这种情况下,我会发现简单地做我一直在做的事情更容易——在视图中为非更改字段(Id、DateCreated等)提供隐藏字段。这样我就不用为我的每一个模型创建视图模型了。@cbergman:这就是MVC的播放方式。隐藏字段可以很容易地操作和更改。使字段隐藏不会保护值不被更改。有人仍然可以更改隐藏字段并修改您不想修改的内容。请阅读这个最近的问题:我认为Model Binder能够通过其传递的ID找到某篇文章,填充每个字段本身(已经包含值),并且只修改视图中已编辑的每个字段。但显然(无论如何,我就是这样)如果它没有找到视图传递的该字段的输入,它将使用“默认”值填充该字段(例如,带有“01/01/0001 12:00:00am”的DateTime字段)。如果DateTime字段是DateCreated字段
// 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
}