65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET MVC 5 列表编辑器结合 Bootstrap 模态框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (53投票s)

2014年6月15日

CPOL

8分钟阅读

viewsIcon

353858

downloadIcon

17509

在此示例中,我将尝试演示如何在单个视图中实现一对多模型的编辑。

引言

我们的想法是显示标准的编辑视图,其中包含主对象的标准脚手架编辑器,并在此基础上添加编辑器列表,允许在同一页面上编辑子对象列表。

背景

通常,我们从 VS2013 Asp.Net MVC 模板和脚手架中获得的是针对简单模型的开箱即用功能。在创建了几个项目后,我决定创建一个发票示例。
我立即发现这不是一项简单的任务,直到我发现了神奇的 RenderAction Http 扩展。我试图让附加的示例非常简单易懂,但需要具备 Asp.Net MVC 的基本知识。

代码

首先,让我们创建一个名为 TestAjax 的标准新 Asp.Net 项目,选择 MVC 模板,核心引用设置为 MVC,并且不启用身份验证。

为了简单起见,我们创建两个模型:Person(人)和一个分配给每个 Person 的 Address(地址)列表。

在 Models 文件夹中添加两个类。

namespace TestAjax.Models
{
    public class Person
    {
        public int Id { get; set; }

        [Display(Name = "First Name")]
        [Required]
        [StringLength(255, MinimumLength = 3)]
        public string Name { get; set; }

        [Display(Name = "Last Name")]
        [Required]
        [StringLength(255, MinimumLength = 3)]
        public string  Surname { get; set; }

        public virtual ICollection<Address> Addresses { get; set; }
    }
}

 

namespace TestAjax.Models
{
    public class Address
    {
        public int Id { get; set; }

        [Required]
        [StringLength(255, MinimumLength = 3)]
        public string City { get; set; }
       
        [Display(Name = "Street Address")]
        public string Street { get; set; }

        [Phone]
        public string Phone { get; set; }

        public int PersonID { get; set; }
        public virtual Person Person  { get; set; }
    }
}

现在,模型已就位,我们来创建控制器。右键单击 Controllers 文件夹,选择 Add(添加),Controller... 菜单,然后在对话框中选择“MVC 5 Controller with views, using Entity Framework”(使用 Entity Framework 的带有视图的 MVC 5 控制器)。选择 Person 作为模型类,创建一个新的 DataDb context,并在对话框中设置所有复选框。重新生成项目,然后为 Address 创建另一个控制器,选择已创建的 DataDb context。

为了结束准备阶段,请删除 HomeController 和 Views 文件夹下的 Home 文件夹。在共享的 _Layout 视图中,我们将 Home、About、Contact 的标准菜单项替换为指向 People 的菜单:@Html.ActionLink("People", "Index", "People"),最后,在 App_Start\RouteConfig.cs 中,我们必须将 Home 替换为 People,这样我们的应用程序默认将指向 People 控制器。

现在,项目已启动并运行,我们将修改其呈现。

按钮中的 Bootstrap 图标

为了美化示例中的按钮,我决定使用 bootstrap glyphicons 来装饰它们。

由于 glyphicons 的语法,

<button type="button" class="btn btn-default btn-lg">
  <span class="glyphicon glyphicon-star"></span> Star
</button>

我添加了一个额外的辅助函数,以从 ActionLink 辅助函数生成正确的 HTML 输出。

在项目中,让我们创建一个 Helpers 文件夹,并在其中创建一个 MyHelper.cs 类。

辅助函数代码

// As the text the: "<span class='glyphicon glyphicon-plus'></span>" can be entered
        public static MvcHtmlString NoEncodeActionLink(this HtmlHelper htmlHelper,
                                             string text, string title, string action,
                                             string controller,
                                             object routeValues = null,
                                             object htmlAttributes = null)
        {
            UrlHelper urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);

            TagBuilder  builder = new TagBuilder("a");
            builder.InnerHtml = text;
            builder.Attributes["title"] = title;
            builder.Attributes["href"] = urlHelper.Action(action, controller, routeValues);
            builder.MergeAttributes(new RouteValueDictionary(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)));

            return MvcHtmlString.Create(builder.ToString());
        }

因此,现在要使用 ActionLink 输出带图标的按钮,我可以使用这种语法:

@Html.NoEncodeActionLink("<span class='glyphicon glyphicon-plus'></span>", "Add new Person", "Create", "People", routeValues: null, htmlAttributes: new { @class = "btn btn-primary" })

在 People 控制器的 Index 视图中,让我们添加@usingTestAjax.Helpers指令到页面顶部。然后,让我们用以下内容替换@Html.ActionLink("Create New", "Create")

<div class="pull-right">
 @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-plus'></span>", "Add new Person", "Create", "People", routeValues: null, htmlAttributes: new { @class = "btn btn-primary" })
</div>

以及 Edit、Details、Create 的 ActionLinks:

<div class="pull-right">
    @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-pencil'></span>", "Edit", "Edit", "People", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-default" })
    &nbsp;
    @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-search'></span>", "Details", "Details", "People", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-default" })
    &nbsp;
    @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-trash'></span>", "Delete", "Delete", "People", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-danger" })
</div>

现在我们的图标应该就位了。

列表

为了在 Person 的 Edit 视图中开发地址列表,我将尝试实现以下想法:

Html.RenderAction 将从不同的操作注入输出到视图。我们必须记住的一点是,子控制器不能“逃离”视图。因此,我们将所有子视图都实现为通过模态框进行就地编辑的 Partial。

首先,编辑 People 控制器的 Edit 视图。就在“Back to list”div 之前,让我们添加以下部分:

<div class="row">
    <div class="col-md-offset-2 col-md-10">
        @{ Html.RenderAction("Index", "Addresses", new { id = Model.Id }); }
    </div>
</div>

现在我们必须修改 Addresses Controller,因为它在 Index 操作中返回标准视图,而我们需要的是 PartialView。所以我尝试为 Index 操作输入这样的代码:

// GET: Addresses
[ChildActionOnly]
public async Task<ActionResult> Index(int id)
{
    ViewBag.PersonID = id;
    var addresses = db.Addresses.Where(a => a.PersonID == id);

    return PartialView("_Index", await addresses.ToListAsync());
}

启动后,我收到了一个错误“HttpServerUtility.Execute was blocked by waiting for the end of an asynchronous operation.”。在网上稍作研究后,我发现异步操作在此版本的 MVC 中仍然不允许在子操作中使用。因此,作为快速修复,让我们删除 Addresses Controller 和 Views 文件夹下的 Addresses 文件夹,然后再次脚手架化 Addresses Controller,但这次取消选中“Use async controller actions”(使用异步控制器操作)的选项。编辑后的 Index 操作现在应该看起来像:

[ChildActionOnly]
public ActionResult Index(int id)
{
    ViewBag.PersonID = id;
    var addresses = db.Addresses.Where(a => a.PersonID == id);

    return PartialView("_Index", addresses.ToList());
}

然后,我们需要将 Views\Addresses 中的“Index.cshtml”重命名为“_Index.cshtml”,然后就可以运行了。我用[ChildActionOnly]属性来装饰 Index 操作,因为我们不希望直接调用此方法。id 参数允许我们检索给定人员的地址。ViewBag.PersonID 将有助于稍后在 Index 中创建地址。

程序现在正在运行,但我们还没有实现地址的就地编辑,所以点击 Create 链接会带我们到另一个视图 - 你可以暂时移除 [ChildActionOnly] 属性并点击该链接来尝试。为了避免逃离视图,我们将使用 Bootstrap Modals 配合一些 JavaScript。

Bootstrap 模态框

我不是 JavaScript 专家,但不知怎的,我开发了以下脚本来显示 bootstrap modals

// modalform.js

$(function () {

    $.ajaxSetup({ cache: false });

    $("a[data-modal]").on("click", function (e) {

        // hide dropdown if any
        $(e.target).closest('.btn-group').children('.dropdown-toggle').dropdown('toggle');

       
        $('#myModalContent').load(this.href, function () {
           

            $('#myModal').modal({
                /*backdrop: 'static',*/
                keyboard: true
            }, 'show');

            bindForm(this);
        });

        return false;
    });

});

function bindForm(dialog) {
   
    $('form', dialog).submit(function () {
        $.ajax({
            url: this.action,
            type: this.method,
            data: $(this).serialize(),
            success: function (result) {
                if (result.success) {
                    $('#myModal').modal('hide');
                    //Refresh
                    location.reload();
                } else {
                    $('#myModalContent').html(result);
                    bindForm(dialog);
                }
            }
        });
        return false;
    });
}

脚本要正常工作需要三样东西:

1. 在视图中,这样的占位符:

<!-- modal placeholder-->
<div id='myModal' class='modal fade in'>
    <div class="modal-dialog">
        <div class="modal-content">
            <div id='myModalContent'></div>
        </div>
    </div>
</div>

2. 在想要放入模态框的视图中,这样的标记:

<div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
    <h4 class="modal-title" id="myModalLabel">Add new Address</h4>
</div>

@using (Html.BeginForm())
{
    <div class="modal-body">
        // Place Form  editors here
    </div>
    </div>
    <div class="modal-footer">
        <button class="btn" data-dismiss="modal">Cancel</button>
        <input class="btn btn-primary" type="submit" value="Add" />
    </div>
}

3. 支持模态框的操作视图必须返回:Json(new{ success =true}); 而不是 View();

注意:请注意,脚本需要调用 ActionLink 中的 data-modal 属性。

注意:最好为 data-dismiss 按钮添加type="button"属性 - 这样可以避免用户按 Enter 键接受对话框时 Bootstrap Modal 出现问题 - 在某些浏览器中,没有此属性的 Modal 会直接关闭。

有关更完整的、专注于 Bootstrap 3.1.1 Modals in MVC 5 的示例,请 访问此网站


在我们的项目中,我将 modalform.js 添加到 \Sripts 文件夹,并在 AppStart\BundleConfig.cs 文件中添加了以下行:

bundles.Add(new ScriptBundle("~/bundles/modalform").Include("~/Scripts/modalform.js"));

到 AppStart\BundleConfig.cs 文件中。

现在,编辑 "_Index.schtml" 以获得此布局:

@using TestAjax.Helpers
@model IEnumerable<TestAjax.Models.Address>

<!-- modal placeholder-->
<div id='myModal' class='modal fade in'>
    <div class="modal-dialog">
        <div class="modal-content">
            <div id='myModalContent'></div>
        </div>
    </div>
</div>

<div class="panel panel-default">
    <div class="panel-heading">
        <strong>Address List</strong>
    </div>

    <table class="table table-hover">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Street)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Phone)
            </th>
            <th>@Html.NoEncodeActionLink("<span class='glyphicon glyphicon-plus'></span>", "Add", "Create", "Addresses", routeValues: new { PersonId = ViewBag.PersonID }, htmlAttributes: new { data_modal = "", @class = "btn btn-primary pull-right" })</th>
        </tr>

        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.City)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Street)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Phone)
                </td>
                <td>
                    <div class="pull-right">
                        @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-pencil'></span>", "Edit", "Edit", "Addresses", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-default" })
                        &nbsp;
                        @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-trash'></span>", "Delete", "Delete", "Addresses", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-danger" })
                       
                    </div>
                </td>
            </tr>
        }
    </table>
</div>

在 People 控制器的 "Edit.cshtml" 的脚本部分添加对模态框的支持。

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    @Scripts.Render("~/bundles/modalform")
}

为了使用模态框,我们还必须修改 Addresses Controller 中的 Create 方法:

public ActionResult Create(int PersonID)
{
    Address address = new Address();
    address.PersonID = PersonID;

    return PartialView("_Create", address);
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,City,Street,Phone,PersonID")] Address address)
{
    if (ModelState.IsValid)
    {
        db.Addresses.Add(address);
        db.SaveChanges();
        return Json(new { success = true });
    }

    return PartialView("_Create");
}

最后,重命名后的 Addresses 的 "_Create.cshtml" 应该具有这样的形状:

@model TestAjax.Models.Address

<div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
    <h4 class="modal-title" id="myModalLabel">Add new Address</h4>
</div>

@using (Html.BeginForm())
{
    <div class="modal-body">

        @Html.AntiForgeryToken()
        <div class="form-horizontal">
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })

            <div class="form-group">
                @Html.LabelFor(model => model.City, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.City, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.City, "", new { @class = "text-danger" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.Street, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.Street, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.Street, "", new { @class = "text-danger" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.Phone, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.Phone, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.Phone, "", new { @class = "text-danger" })
                </div>
            </div>

        </div>
    </div>
    <div class="modal-footer">
        <button class="btn" data-dismiss="modal">Cancel</button>
        <input class="btn btn-primary" type="submit" value="Add" />
    </div>
}

现在,我们可以在不离开 Person Edit 视图的情况下添加新的 Address 行。

相同的模式应该用于创建 _Edit 和 _Delete Partial。

此外,我们可以创建一个 _List Partial 来显示在 Person 的 Details 视图中。

有关其余 Partial 的信息,请参阅附件下载,其中包含项目的完整源代码。

注意:重新生成项目以更新附件中的 Nuget 包。您应该在 Visual Studio 的 NuGet 设置中勾选“Allow Nuget to download missing packages”(允许 Nuget 下载缺失的包)和“Automatically check for missing packages during build”(构建时自动检查缺失的包)这两个选项。

问题

提出的解决方案有一个缺陷:

每次添加、更改或删除 Address 时,People Edit 视图都会刷新。

解决方案

解决上述问题的方案是**Ajaxify**它。

全页刷新来自我脚本中的这行:

location.reload();

为了修复它,我们必须准备一个新版本:

// modalform.js

$(function () {
    $.ajaxSetup({ cache: false });

    $("a[data-modal]").on("click", function (e) {
        // hide dropdown if any (this is used wehen invoking modal from link in bootstrap dropdown )
        //$(e.target).closest('.btn-group').children('.dropdown-toggle').dropdown('toggle');

        $('#myModalContent').load(this.href, function () {
            $('#myModal').modal({
                /*backdrop: 'static',*/
                keyboard: true
            }, 'show');
            bindForm(this);
        });
        return false;
    });
});

function bindForm(dialog) {
    $('form', dialog).submit(function () {
        $.ajax({
            url: this.action,
            type: this.method,
            data: $(this).serialize(),
            success: function (result) {
                if (result.success) {
                    $('#myModal').modal('hide');
                    $('#replacetarget').load(result.url); //  Load data from the server and place the returned HTML into the matched element
                } else {
                    $('#myModalContent').html(result);
                    bindForm(dialog);
                }
            }
        });
        return false;
    });
}

 

正如你所见,现在我们正在用id="replacetarget"使用从 Action 收到的 JSON 中的 url 来替换 DOM 元素的内容。

我们的项目需要进行一些重构才能使脚本正常工作。

首先,让我们将@脚本.Render("~/bundles/jquery")行从 Shared\_Layout.cshtml 移动到页面顶部的 区域。原因是我们将需要比以前更早地访问 jQuery。

现在,在 People 控制器的 Edit 视图中,让我们修改表单结束括号后的部分:

} // End of @using (Html.BeginForm())

<!-- modal placeholder-->
<div id='myModal' class='modal fade in'>
    <div class="modal-dialog">
        <div class="modal-content">
            <div id='myModalContent'></div>
        </div>
    </div>
</div>

<div class="row">
    <div class="col-md-offset-2 col-md-10" id="replacetarget">
        @{ Html.RenderAction("Index", "Addresses", new { id = Model.Id }); }
    </div>
</div>

<p>
    @Html.ActionLink("Back to List", "Index")

</p>


@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")

}

您可以看到,我们将模态框占位符移到了此处(已从 _Index partial 中移除),因为我们不希望这部分被脚本替换。就在 Html.RenderAction 之前,我添加了id="replacetarget"到 div。这是将被脚本替换的 div。脚本部分现在只包含验证。

在 _Index Partial 中,如上所述移除模态框块,并在页面末尾添加 @Scripts.Render("~/bundles/modalform")。在 标签内添加 modalform.js 的内容(因为 @Script.Render 在 Partial 中不起作用)。这就是为什么我们需要提前加载 jQuery。modalform 脚本放在这里是因为所有的

内容都将被 JavaScript 代码替换,所以我们需要每次发生这种情况时都重新创建它。

我们必须提供的最后一项是脚本中使用的 result.url JSON 数据。

深入 Addresses Controller 代码,并替换每个

returnJson(new{ success =true});

行用

字符串url = Url.Action("Index", "Addresses", new{ id = address.PersonID });

returnJson(new{ success =true, url = url });

我们在这里使用 Url.Action 辅助函数来使用我们当前正在编辑的 PersonID 构建正确的 URL。

最后,移除[ChildActionOnly] 属性,因为 Index 方法现在将由脚本调用,而不仅仅是 Person 的 Edit 视图中的 RenderAction。

与之前一样,代码可在此上方下载。

关注点

从下载中您可以了解到:

  • 如何处理视图中的就地动态列表。
  • 如何在 Bootstrap Modals 中显示表单。
  • 如何使用 Bootstrap glyph icons 装饰 Bootstrap 按钮。
  • 如何突出显示 Bootstrap 活动导航栏菜单项。
  • 如何使用 Ajax 只刷新页面的一部分。

历史

2015-02-15 添加了两个小的修复。

问题:模态对话框有时不再是模态了。感谢 agpulidoc 在评论中提供的解决方案。

修复:Partials 不支持 @script 区域 - 直接替换为 script 或引入扩展。

问题:当验证在模态框中报告错误时,Submit Save Person 不工作 - 在评论中发现。

修复:在模态框脚本中,当 !result.success 时 - 应该调用 bindForm(dialog); 而不是 bindForm();

2014-01-17 添加了 Ajax 部分。

2014-01-15 已修订。

2014-01-14 初始版本。

© . All rights reserved.