You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET MVC预算应用:如何动态增删多份收入表单?

实现ASP.NET MVC多收入表单动态增删功能

我正在使用ASP.NET MVC开发月度预算应用,需要允许用户同时创建多条收入记录,要求大屏最多显示5个表单,小屏最多3个。目前通过ViewModel维护收入表单列表,但在视图中实现动态增删遇到困难,希望实现用户点击"+"按钮添加新表单,点击"x"按钮删除表单的交互效果。

现有后台代码

// GET: Incomes/Create
public IActionResult Create()
{
    var model = new MultipleIncomeVM();
    ViewData["BudgetId"] = new SelectList(_context.Budgets, "BudgetId", "Name");
    ViewData["CategoryId"] = new SelectList(_context.Categories, "CategoryId", "Name");
    return View(model);
}

// POST: Incomes/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(MultipleIncomeVM incomevm)
{
    try
    {
        // Validate only the incomes with Amount greater than 0
        var validIncomes = incomevm.Incomes.Where(i => i.Amount > 0);

        if (ModelState.IsValid)
        {
            foreach (var i in validIncomes)
            {
                // Add each income
                _context.Add(i);
                await _context.SaveChangesAsync();

                // A new transaction
                var transaction = new Transaction
                {
                    Date = i.DateReceived,
                    Type = ControllersName.GetControllerName(this.GetType().Name),
                    Detail = i.Source,
                    Amount = i.Amount,
                    BudgetId = i.BudgetId,
                    CategoryId = i.CategoryId
                };

                _context.Add(transaction);
                await _context.SaveChangesAsync();
            }
            return RedirectToAction(nameof(Index));
        }
    }
    catch (DbUpdateException)
    {
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
    }

    ViewData["BudgetId"] = new SelectList(_context.Budgets, "BudgetId", "Name");
    ViewData["CategoryId"] = new SelectList(_context.Categories, "CategoryId", "Name");
    return View(incomevm);
}

ViewModel代码

namespace My_Budget.ViewModels
{
    public class MultipleIncomeVM
    {
        public List<Income> Incomes { get; set; } = new List<Income>
        {
            new Income { DateReceived = DateTime.Today }
        };
    }
}

现有视图代码

@model My_Budget.ViewModels.MultipleIncomeVM

@{
    ViewData["Title"] = "Create Incomes";
}

<h1>Create Multiple Incomes</h1>

<hr />
<div class="row">
    <div class="col-md-12">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>

            <table class="table table-bordered">
                <thead>
                    <tr>
                        <th>Source</th>
                        <th>Amount</th>
                        <th>Date Received</th>
                        <th>Category</th>
                        <th></th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    @for (int i = 0; i < Model.Incomes.Count; i++)
                    {
                        <tr>
                            <td>
                                <input asp-for="@Model.Incomes[i].Source" class="form-control" />
                                <span asp-validation-for="@Model.Incomes[i].Source" class="text-danger"></span>
                            </td>
                            <td>
                                <input asp-for="@Model.Incomes[i].Amount" class="form-control" />
                                <span asp-validation-for="@Model.Incomes[i].Amount" class="text-danger"></span>
                            </td>
                            <td>
                                <input asp-for="@Model.Incomes[i].DateReceived" class="form-control" type="date" />
                                <span asp-validation-for="@Model.Incomes[i].DateReceived" class="text-danger"></span>
                            </td>
                            <td>
                                <select asp-for="@Model.Incomes[i].CategoryId" class="form-control" asp-items="ViewBag.CategoryId"></select>
                                <span asp-validation-for="@Model.Incomes[i].CategoryId" class="text-danger"></span>
                            </td>
                            <td>
                                <i class="bi bi-plus-circle" id="add-form"></i>
                                <i class="bi bi-x-circle" disabled></i>
                            </td>
                            <td>
                                @* Hidden value *@
                                <select asp-for="@Model.Incomes[i].BudgetId" hidden class="form-control" asp-items="ViewBag.BudgetId" ></select>
                            </td>
                        </tr>
                    }
                </tbody>
            </table>

            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

已尝试的操作

  • 在ViewModel中创建列表存储多条收入记录
  • 可初始展示多表单,但不知如何在视图中处理动态增删

解决方案

步骤1:修改视图结构,添加模板行

在视图的<tbody>中添加隐藏的模板行,用于新增表单时克隆,同时调整现有行的类和按钮属性:

<tbody>
    @for (int i = 0; i < Model.Incomes.Count; i++)
    {
        <tr class="income-row">
            <td>
                <input asp-for="@Model.Incomes[i].Source" class="form-control" />
                <span asp-validation-for="@Model.Incomes[i].Source" class="text-danger"></span>
            </td>
            <td>
                <input asp-for="@Model.Incomes[i].Amount" class="form-control" />
                <span asp-validation-for="@Model.Incomes[i].Amount" class="text-danger"></span>
            </td>
            <td>
                <input asp-for="@Model.Incomes[i].DateReceived" class="form-control" type="date" />
                <span asp-validation-for="@Model.Incomes[i].DateReceived" class="text-danger"></span>
            </td>
            <td>
                <select asp-for="@Model.Incomes[i].CategoryId" class="form-control" asp-items="ViewBag.CategoryId"></select>
                <span asp-validation-for="@Model.Incomes[i].CategoryId" class="text-danger"></span>
            </td>
            <td>
                <i class="bi bi-plus-circle add-btn" style="cursor:pointer;color:green;"></i>
                <i class="bi bi-x-circle remove-btn" style="cursor:pointer;color:red;" @(i == 0 ? "disabled" : "")></i>
            </td>
            <td>
                <select asp-for="@Model.Incomes[i].BudgetId" hidden class="form-control" asp-items="ViewBag.BudgetId"></select>
            </td>
        </tr>
    }
    <!-- 隐藏的模板行 -->
    <tr id="income-template" style="display:none;">
        <td>
            <input name="Incomes[__INDEX__].Source" class="form-control" />
            <span class="text-danger field-validation-valid" data-valmsg-for="Incomes[__INDEX__].Source" data-valmsg-replace="true"></span>
        </td>
        <td>
            <input name="Incomes[__INDEX__].Amount" class="form-control" />
            <span class="text-danger field-validation-valid" data-valmsg-for="Incomes[__INDEX__].Amount" data-valmsg-replace="true"></span>
        </td>
        <td>
            <input name="Incomes[__INDEX__].DateReceived" class="form-control" type="date" />
            <span class="text-danger field-validation-valid" data-valmsg-for="Incomes[__INDEX__].DateReceived" data-valmsg-replace="true"></span>
        </td>
        <td>
            <select name="Incomes[__INDEX__].CategoryId" class="form-control" asp-items="ViewBag.CategoryId"></select>
            <span class="text-danger field-validation-valid" data-valmsg-for="Incomes[__INDEX__].CategoryId" data-valmsg-replace="true"></span>
        </td>
        <td>
            <i class="bi bi-plus-circle add-btn" style="cursor:pointer;color:green;"></i>
            <i class="bi bi-x-circle remove-btn" style="cursor:pointer;color:red;"></i>
        </td>
        <td>
            <select name="Incomes[__INDEX__].BudgetId" hidden class="form-control" asp-items="ViewBag.BudgetId"></select>
        </td>
    </tr>
</tbody>

步骤2:添加JavaScript实现动态增删

在Scripts区域添加以下代码,实现增删逻辑、响应式限制和验证绑定:

@section Scripts {
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
    <script>
        $(function() {
            // 获取当前屏幕允许的最大表单数量
            function getMaxRows() {
                return $(window).width() >= 768 ? 5 : 3;
            }

            // 新增表单逻辑
            $(document).on('click', '.add-btn', function() {
                const currentRows = $('.income-row').length;
                const maxRows = getMaxRows();
                if (currentRows >= maxRows) {
                    alert(`最多只能添加${maxRows}条记录`);
                    return;
                }

                // 克隆模板行并替换索引
                const template = $('#income-template').clone();
                template.removeAttr('id').show().addClass('income-row');
                const newIndex = currentRows;
                template.html(template.html().replace(/__INDEX__/g, newIndex));
                $('tbody').append(template);

                // 重新绑定客户端验证
                $.validator.unobtrusive.parse('form');
            });

            // 删除表单逻辑
            $(document).on('click', '.remove-btn', function() {
                const row = $(this).closest('.income-row');
                // 至少保留一条记录
                if ($('.income-row').length <= 1) {
                    alert('至少保留一条记录');
                    return;
                }
                row.remove();

                // 重新索引所有表单元素,保证模型绑定正确
                $('.income-row').each(function(index) {
                    $(this).find('input, select').each(function() {
                        const name = $(this).attr('name').replace(/Incomes\[\d+\]/, `Incomes[${index}]`);
                        $(this).attr('name', name);
                        // 更新验证消息的关联属性
                        const msgSpan = $(this).next('.text-danger');
                        if (msgSpan.length) {
                            msgSpan.attr('data-valmsg-for', name);
                        }
                    });
                });

                // 重新绑定客户端验证
                $.validator.unobtrusive.parse('form');
            });

            // 窗口大小变化时自动调整表单数量(可选)
            $(window).resize(function() {
                const currentRows = $('.income-row').length;
                const maxRows = getMaxRows();
                if (currentRows > maxRows) {
                    alert(`屏幕尺寸变化,最多只能显示${maxRows}条记录,已自动移除多余行`);
                    $('.income-row').slice(maxRows).remove();
                    // 重新索引表单元素
                    $('.income-row').each(function(index) {
                        $(this).find('input, select').each(function() {
                            const name = $(this).attr('name').replace(/Incomes\[\d+\]/, `Incomes[${index}]`);
                            $(this).attr('name', name);
                            const msgSpan = $(this).next('.text-danger');
                            if (msgSpan.length) {
                                msgSpan.attr('data-valmsg-for', name);
                            }
                        });
                    });
                    $.validator.unobtrusive.parse('form');
                }
            });
        });
    </script>
}

步骤3:可选调整ViewModel初始化

如果希望初始显示多条记录,修改ViewModel的默认列表:

public List<Income> Incomes { get; set; } = new List<Income>
{
    new Income { DateReceived = DateTime.Today },
    new Income { DateReceived = DateTime.Today }
};

核心说明

  • 模型绑定兼容:通过动态更新表单元素的name属性索引,确保ASP.NET MVC能正确绑定到MultipleIncomeVM.Incomes列表
  • 响应式限制:根据屏幕宽度自动切换最大允许的表单数量
  • 验证支持:每次增删后重新解析表单验证规则,保证客户端验证正常工作
  • 操作限制:禁止删除最后一条表单,避免提交空数据

内容的提问来源于stack exchange,提问作者NicolasMartinezK

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 01:54:57