ASP.NET Core Blazor 5:Blazor表单和数据

  本章将描述 Blazor 为处理 HTML 表单提供的特性,包括对数据验证的支持。

1 准备工作

  继续使用上一章项目。
  创建 Blazor/Forms 文件夹并添加一个名为 EmptyLayout.razor 的 Razor 组件。本章使用这个组件作为主要的布局。

@inherits LayoutComponentBase

<div class="m-2">
    @Body
</div>

  为 Blazor/Forms 文件夹添加 FormSpy.razor,这个组件用来显示表单元素和旁边正在编辑的值。

<div class="container-fluid no-gutters">
    <div class="row">
        <div class="col">
            @ChildContent
        </div>
        <div class="col">
            <table class="table table-sm table-striped table-bordered">
                <thead>
                    <tr><th colspan="2" class="text-center">Data Summary</th></tr>
                </thead>
                <tbody>
                    <tr><th>ID</th><td>@PersonData?.PersonId</td></tr>
                    <tr><th>Firstname</th><td>@PersonData?.Firstname</td></tr>
                    <tr><th>Surname</th><td>@PersonData?.Surname</td></tr>
                    <tr><th>Dept ID</th><td>@PersonData?.DepartmentId</td></tr>
                    <tr><th>Location ID</th><td>@PersonData?.LocationId</td></tr>
                </tbody>
            </table>            
        </div>
    </div>
</div>

@code {

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public Person PersonData { get; set; }
}

  Blazor/Forms 文件夹添加 Editor.razor,此组件将用于创建和编辑 Person 对象。

@page "/forms/edit/{id:long}"
@layout EmptyLayout

<h4 class="bg-primary text-center text-white p-2">Edit</h4>

<FormSpy PersonData="PersonData">
   <h4 class="text-center">Form Placeholder</h4>
   <div class="text-center">
       <NavLink class="btn btn-secondary" href="/forms">Back</NavLink>
   </div>
</FormSpy>


@code
{

    [Inject]
    public NavigationManager NavManager { get; set; }

    [Inject]
    DataContext Context { get; set; }

    [Parameter]
    public long Id { get; set; }

    public Person PersonData { get; set; } = new Person();

    protected async override Task OnParametersSetAsync()
    {
        PersonData = await Context.People.FindAsync(Id);
    }
}

  代码中的组件使用 @layout 表达式覆盖默认布局并选择 EmptyLayout。并排布局用于在占位符旁边显示 PersonTable 组件,在这里将添加表单。
最后,在 Blazor/Forms 文件夹中创建 List.razor,该组件以表的形式向用户显示 Person 对象列表。

@page "/forms"
@page "/forms/list"
@layout EmptyLayout

<h5 class="bg-primary text-white text-center p-2">People</h5>

<table class="table table-sm table-striped table-bordered">
    <thead>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Dept</th>
            <th>Location</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @if (People.Count() == 0)
        {
            <tr><th colspan="5" class="p-4 text-center">Loading Data...</th></tr>
        }
        else
        {
            @foreach (Person p in People)
            {
                <tr>
                    <td>@p.PersonId</td>
                    <td>@p.Surname, @p.Firstname</td>
                    <td>@p.Department.Name</td>
                    <td>@p.Location.City</td>
                    <td>
                        <NavLink class="btn btn-sm btn-warning"
                                 href="@GetEditUrl(p.PersonId)">
                            Edit
                        </NavLink>
                    </td>
                </tr>
            }
        }
    </tbody>
</table>

@code 
{
    [Inject]
    public DataContext Context { get; set; }

    public IEnumerable<Person> People { get; set; } = Enumerable.Empty<Person>();

    protected override void OnInitialized()
    {
        People = Context.People.Include(p => p.Department).Include(p => p.Location);
    }

    string GetEditUrl(long id) => $"/forms/edit/{id}";
}

  请求 http://localhost:5000/forms,这将生成一个数据表。单击其中一个 Edit 按钮,将看到表单占位符和显示所选 Person 对象当前属性值的摘要。

2 使用 Blazor 表单组件

  Blazor 提供的表单组件:

名称 描述
EditForm 此组件将呈现连接起来进行数据验证的表单元素
InputText 此组件呈现一个绑定到 C# 字符串属性的输入元素
InputCheckbox 此组件呈现一个输入元素,它的类型属性是 checkbox,并且绑定到 C# bool 属性
InputDate 此组件呈现一个输入元素,该元素的类型属性为date,并绑定到C# DateTime 或DateTimeOffset属性
InputNumber 此组件呈现一个输入元素,其类型属性为 number,并绑定到 C# int、long、float、double 或 decimal 值
InputTextArea 此组件呈现一个绑定到 C#字符串属性的 textarea 组件

  EditFomm 组件必须用于任何其他组件才能工作。Blazor/Forms 文件夹的 Editorrazor 文件中使用表单组件,添加一个 EditForm 和两个 ImputText 组件,来表示 Person 类定义的两个属性。

<FormSpy PersonData="PersonData">
    <EditForm Model="PersonData">
        <div class="form-group">
            <label>Person ID</label>
            <InputNumber class="form-control"
            @bind-Value="PersonData.PersonId" disabled />
        </div>
        <div class="form-group">
            <label>Firstname</label>
            <InputText class="form-control" @bind-Value="PersonData.Firstname" />
        </div>
        <div class="form-group">
            <label>Surname</label>
            <InputText class="form-control" @bind-Value="PersonData.Surname" />
        </div>
        <div class="form-group">
            <label>Dept ID</label>
            <InputNumber class="form-control"
            @bind-Value="PersonData.DepartmentId" />
        </div>
        <div class="text-center">
            <NavLink class="btn btn-secondary" href="/forms">Back</NavLink>
        </div>
    </EditForm>
</FormSpy>

  EditForm 组件呈现一个表单元素,Model 属性用于向 EditForm 提供表单用于编辑和验证的对象。
  名称以 Input 开头的组件用于显示单个模型属性的 input 或 textarea 元素。这些组件定义了一个名为 Value 的自定义绑定,该绑定使用@bind-Value 属性与模型属性关联。属性级组件必须与它们呈现给用户的属性类型相匹配。
  重启并请求 http://localhost:5000/forms/edit/2,将看到显示的三个输入元素。编推值并通过按 Tab 键移动焦点,将在更新窗口的右侧看到汇总数据。

2.1 创建自定义表单组件

  Blazor 仅为 input 和 textarea 元素提供内置组件。不过创建一个集成到 Blazor 表单特性的自定义组件是一个简单的过程。在 Blazor/Forms 文件夹中添加一个名为 CustomSelect.razor 的Razor 组件。

@typeparam TValue
@inherits InputBase<TValue>

<select class="form-control @CssClass" value="@CurrentValueAsString"
@onchange="@(ev => CurrentValueAsString = ev.Value as string)">
    @ChildContent
    @foreach (KeyValuePair<string, TValue> kvp in Values)
    {
        <option value="@kvp.Value">@kvp.Key</option>
    }
</select>

@code
{
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public IDictionary<string, TValue> Values { get; set; }

    [Parameter]
    public Func<string, TValue> Parser { get; set; }

    protected override bool TryParseValueFromString(
        string value, out TValue result, out string validationErrorMessage)
    {
        try
        {
            result = Parser(value);
            validationErrorMessage = null;
            return true;
        }
        catch
        {
            result = default(TValue);
            validationErrorMessage = "The value is not valid";
            return false;
        }
    }
}

  表单组件的基类是 InputBase,其中通用类型参数是组件表示的模型属性类型。基类负责大部分工作,并提供 CurrentValueAsString 属性,该属性用于在用户选择新值时在事件处理程序中提供当前值:value="@CurrentValueAsString" @onchange="@(ev => CurrentValueAsString = ev.Value as string)"。在准备数据验证的过程中,该组件包括 CssClass 属性的值,在select 元素的 class 属性中@CssClass
  必须实现抽象的 TryParseValueFromString 方法,以便基类能够在 HTML 元素使用的字符串值和 C#模型属性的相应值之间进行映射。这里不想将自定义 select 元素实现为任何特定的 C# 数据类型,因此使用@typeparam 表达式来定义通用类型参数。Values 属性用于接收将显示给用户的字典映射字符串值和用作 C# 值的 TValue 值。该方法接收两个out 参数,这些参数用于设置解析值以及解析器验证错误消息,如果存在问题,该错误消息将显示给用户。由于正在使用泛型类型,因此 Parser 属性接收一个函数,调用该函数,以将字符串值解析为 TValue 值。

  在 BlazorFomms 文件央的 Edior.razor 文件中使用自定义表单元素,因此用户可为 Person 类定义的 Departmentld 和 Locationld 属性选择值。

@page "/forms/edit/{id:long}"
@layout EmptyLayout

<h4 class="bg-primary text-center text-white p-2">Edit</h4>

<FormSpy PersonData="PersonData">
    <EditForm Model="PersonData">
        <div class="form-group">
            <label>Firstname</label>
            <InputText class="form-control" @bind-Value="PersonData.Firstname" />
        </div>
        <div class="form-group">
            <label>Surname</label>
            <InputText class="form-control" @bind-Value="PersonData.Surname" />
        </div>
        <div class="form-group">
            <label>Dept ID</label>
            <CustomSelect TValue="long" Values="Departments" Parser="@(str=>long.Parse(str))"
            @bind-Value="PersonData.DepartmentId">
                <option selected disabled value="0">choose a Department</option>
            </CustomSelect>
        </div>
        <div class="form-group">
            <label>Location ID</label>
            <CustomSelect TValue="long" Values="Departments" Parser="@(str=>long.Parse(str))"
            @bind-Value="PersonData.LocationId">
                <option selected disabled value="0">choose a Location</option>
            </CustomSelect>
        </div>
        <div class="text-center">
            <NavLink class="btn btn-secondary" href="/forms">Back</NavLink>
        </div>
    </EditForm>
</FormSpy>


@code
{

    [Inject]
    public NavigationManager NavManager { get; set; }

    [Inject]
    DataContext Context { get; set; }

    [Parameter]
    public long Id { get; set; }

    public Person PersonData { get; set; } = new Person();

    public IDictionary<string, long> Departments { get; set; }
        = new Dictionary<string, long>();
    public IDictionary<string, long> Locations { get; set; }
        = new Dictionary<string, long>();

    protected async override Task OnParametersSetAsync()
    {
        PersonData = await Context.People.FindAsync(Id);
        Departments = await Context.Departments
            .ToDictionaryAsync(d => d.Name, d => d.Departmentid);
        Locations = await Context.Locations
            .ToDictionaryAsync(l => $"{l.City}, {l.State}", l => l.LocationId);
    }
}

  使用 Entity Framework Core ToDictionaryAsync 方法从 Department 和 Location 数据创建值和标签的集合,并使用它们配置 CustomSelect 组件。重启请求 http://localhost:5000/forms/edit/2,当选择一个新值时,CustomSelect 组件将更新 CurrentValueAsString 属性,TryParseValueFromString 方法被调用,其结果用于更新 Value 绑定。

2.2 验证表单数据

  blazor 提供了使用标准属性执行验证的组件。

名称 描述
DataAnnotationsValidator 此组件将应用于模型类的验证属性集成到 Blazor 表单特性中
ValidationMessage 此组件显示单个属性的验证错误消息
ValidationSummary 此组件显示整个模型对象的验证错误消息

  验证组件生成分配给类的元素,可以用 CSS 样式化这些元素。

名称 描述
validation-errors ValidationSummary 组件生成一个 ul 元素,该元素被分配给这个类,并且是验证消息摘要的顶级容器
validation-message ValidationSummary 组件使用为每个验证消息分配给这个类的 il 元素来填充它的 ul 元素。ValidationMessage 组件为这个类的属性级消息呈现一个分配给它的 div 元素

  Blazor Input* 组件将它们生成的 HTML 元素添加到下表描述的类中,以指示验证状态。这包括 ImnputBase类,从这个类派生了 CustomSelect 组件,它也是代码中 CssClass 属性的用途。

名称 描述
modifed 一旦用户编辑了值,元素就会添加到这个类中
valid 如果包含的值通过验证,则将元素添加到该类中
invalid 如果元素包含的值验证失败,则将元素添加到该类中

  将一个名为 blazorValidation.css 的 CSS 样式表添加到 wwwroot 文件夹中。

.validation-errors {
    background-color: rgb(220, 53, 69);
    color: white;
    padding: 8px;
    text-align: center;
    font-size: 16px;
    font-weight: 500;
}

div.validation-message {
    color: rgb(220, 53, 69);
    font-weight: 500
}

.modified.valid {
    border: solid 3px rgb(40, 167, 69);
}

.modified.invalid {
    border: solid 3px rgb(220, 53, 69);
}

  这些样式将错误消息格式化为红色,并对单个表单元素应用红色或绿色边框。导入 CSS 样式表并在Editor.razor 文件中应用验证组件。

@page "/forms/edit/{id:long}"
@layout EmptyLayout
<link href="~/blazorvalidation.css" rel="stylesheet" />
<h4 class="bg-primary text-center text-white p-2">Edit</h4>

<FormSpy PersonData="PersonData">
    <EditForm Model="PersonData">
        <DataAnnotationsValidator />
        <ValidationSummary />
        <div class="form-group">
            <label>Firstname</label>
            <ValidationMessage For="@(()=>PersonData.Firstname)" />
            <InputText class="form-control" @bind-Value="PersonData.Firstname" />
        </div>
        <div class="form-group">
            <label>Surname</label>
            <ValidationMessage For="@(()=>PersonData.Surname)" />
            <InputText class="form-control" @bind-Value="PersonData.Surname" />
        </div>
        <div class="form-group">
            <label>Dept ID</label>
            <ValidationMessage For="@(()=>PersonData.DepartmentId)" />
            <CustomSelect TValue="long" Values="Departments" Parser="@(str=>long.Parse(str))"
            @bind-Value="PersonData.DepartmentId">
                <option selected disabled value="0">choose a Department</option>
            </CustomSelect>
        </div>
        <div class="form-group">
            <label>Location ID</label>
            <ValidationMessage For="@(()=>PersonData.LocationId)" />
            <CustomSelect TValue="long" Values="Locations" Parser="@(str=>long.Parse(str))"
            @bind-Value="PersonData.LocationId">
                <option selected disabled value="0">choose a Location</option>
            </CustomSelect>
        </div>
        <div class="text-center">
            <NavLink class="btn btn-secondary" href="/forms">Back</NavLink>
        </div>
    </EditForm>
</FormSpy>

  DataAnnotationsValidator 和 ValidationSummary 组件在应用时没有任何配置属性。ValidationMessag 属性使用 For 属性配置,该属性接收一个的数,该函数返回组件所表示的属性。

  启用数据验证的最后一步是将属性应用到模型类,在 Models 文件夹的 Person.cs 文件中应用验证属性。

    public class Person
    {
        public long PersonId { get; set; }

        [Required(ErrorMessage = "A firstname is required")]
        [MinLength(3, ErrorMessage = "Firstnames must be 3 or more characters")]
        public string Firstname { get; set; }

        [Required(ErrorMessage = "A surname is required")]
        [MinLength(3, ErrorMessage = "Surnames must be 3 or more characters")]
        public string Surname { get; set; }

        [Required]
        [Range(1, long.MaxValue, ErrorMessage = "A department must be selected")]
        public long DepartmentId { get; set; }

        [Required]
        [Range(1, long.MaxValue, ErrorMessage = "A location must be selected")]
        public long LocationId { get; set; }

        public Department Department { get; set; }
        public Location Location { get; set; }
    }

  要查看验证组件的效果,重启请求 http://localhost:5000/forms/edit/2。除 Firstname 字段并通过按 Tab 键或单击另一个字段移动焦点。当焦点更改时,将执行验证,并显示错误消息。Editor 组件同时显示摘要消息和每个属性消息,因此相同的错误消息会显示两次从 Surname 字段中删除除前两个字符以外的所有字符,当更改焦点时将显示第二条验证消息。也有对其他属性的验证支持,但是 select 元素不允许用户选择无效的有效值。如果更改了一个值,select 元素将用绿色边框装饰,以指示有效的选择,但是在演示如何使用表单维件创建新的数据对象之前,看不到无效的响应。

2.3 处理表单事件

  EditForm 组件定义了允许应用程序响应用户操作的事件。

名称 描述
OnValidSubmit 当提交表单且表单数据通过验证时触发此事件
OnInvalidSubmit 当提交表单且表单数据验证失败时触发此事件
OnSubmit 此事件在表单提交和验证执行之前触发

  这些事件通过提交按钮来触发。向 Editor 组件中添加一个 submit 按钮来处理 EditForm 事件。

@page "/forms/edit/{id:long}"
@layout EmptyLayout
<link href="~/blazorvalidation.css" rel="stylesheet" />
<h4 class="bg-primary text-center text-white p-2">Edit</h4>
<h6 class="bg-info text-center text-white p-2">@FormSubmitMessage</h6>

<FormSpy PersonData="PersonData">
    <EditForm Model="PersonData" OnValidSubmit="HandleValidSubmit"
              OnInvalidSubmit="HandleInvalidSubmit">
        <DataAnnotationsValidator />
        ......
        <div class="text-center">
            <button type="submit" class="btn btn-primary">Submit</button>
            <NavLink class="btn btn-secondary" href="/forms">Back</NavLink>
        </div>
    </EditForm>
</FormSpy>


@code
{
    ......
    public string FormSubmitMessage { get; set; } = "Form Data Not Submitted";
    public void HandleValidSubmit() => FormSubmitMessage = "Valid Data Submitted";
    public void HandleInvalidSubmit() => FormSubmitMessage = "Invalid Data Submitted";
}

  重启请求 http://localhost:5000/forms/edit/2,清除 Firstname 字段,然后单击 Submit 按钮。除了验证错误之外,还将看到一条消息,指示提交的表单使用了无效数据。在字段中输入一个名称,再次单击 Submit,消息会更改。

3 使用 EF Core 与 Blazor

  Blazor 模型改变了 EF Core 的行为方式,如果习惯于编写常规的 ASE.NET Core 应用程序,那么这可能会导致意想不到的结果。接下来将解释这些问题以及如何避免可能出现的问题。

3.1 理解 EF Core 上下文范围问题

  第一个问题是,更改 Firstname 之后未提交,点击返回主列表后,主列表数据已经改变了。
  在 Blazor 应用程序中,路由系统响应 URL 更改,而不发送新的 HTTP 请求,这意味着只使用 Blazor 维护的到服务器的持久 HTTP 连接来显示多个组件。这将导致多个组件共享单个依赖注入范围,一个组件所做的更改将影响其他组件,即使这些更改没有写入数据库。
image.png
1.丢弃未保存的数据更改
  如果在组件之间共享上下文,那么可采用这种方法,并确保组件在销毁时放弃任何更改。 在 Blazor/Forms 文件夹的 Editor.razor 文件中丢弃未保存的数据更改。

@implements IDisposable
......
public void Dispose() => Context.Entry(PersonData).State = EntityState.Detached;

2.创建新的依赖注入范围
  若想保留其余部分使用的模型,就必须创建新的依赖注入范围。并让每个组件接收子级的 EF Core 上下文对象。这是通过使用 @ inherits 表达是将组件的基类设置为 OwningComponentBase 来完成的。
  OwningComponentBase 类定义了组件继承的 ScopedServices 属性,提供了一个可用于获取服务的 IServiceProvider 对象,该服务在一个特定于组件的生命周期的作用域中创建,该范围不会与其他任何组件共享。
  在 Blazor/Forms 文件夹的 Editor.razor 文件中使用新的范围。

@inherits OwningComponentBase
@using Microsoft.Extensions.DependencyInjection
......
DataContext Context => ScopedServices.GetService<DataContext>();

  注释掉了 Inject 属性,并通过获得 DataContext 服务来设置 Context 属性的值。Micosof.Extensions.DependencyIniection 名称空间包含扩展方法,这样 IServiceProvider 对象更容易获取服务。
  OwningComponentBase 类定义了一个额外的便利属性,来访问范围类型 T 的服务,如果组件只需要范围内单一的服务,该属性就可以很有用。

@inherits OwningComponentBase<DataContext>
......
DataContext Context => Service;

  有作用域的服务可通过名为 Service 的属性使用。在这个例子中,指定 DataContext 作为基类的类型参数。
  无论使用哪个基类,结果都是Editor组件有自己的依赖注入作用域和自己的DataContext对象。List 组件没有修改,因此它将接收请求范围的 DataContext 对象。
image.png

3.2 理解重复查询问题

  Blazor 尽可能高效地响应状态变化,但仍然必须呈现组件的内容,以确定应该发送到浏览器的变化,它会导致发送到数据库的查询数量急剧增加。在 Blazor/Foms 文件夹的 List.razor文件中添加一个按钮计数器来反映这个问题。

......
@layout EmptyLayout
...... 
<button class="btn btn-primary" @onclick="@(() => Counter++)">Increment</button>
<span class="h5">Counter:@Counter</span>

@code
{
    ......
    public int Counter { get; set; } = 0;
}

  请求 http://localhost:5000/forms。单击按钮并观察 ASP.NET Core 服务器的输出。每次单击该按钮时,都会调用事件处理程序,并向数据库发送一个新的数据库查询。
  每次呈现组件时,EFCore 都向数据库发送两个相同的请求,即使在没有执行数据操作的地方单击了 Increment 按钮,也是如此。当使用 EF Core 时,就会出现这个问题,而 Blazor 则加重了这个问题。

管理组件中的查询
  Blazor 和 EF Core 之间的交互对所有项目来说都不是问题,但是如果是的话,那么最好的方法是査询一次数据库,并且只对用户希望发生更新的操作再次进行査询。有些应用程产可能需要为用户提供显式选项来重新加载数据,特别是对于用户希望看到更新的应用程序。
  在 Blazor/Forms 文件夹的 List.razor 文件中控制查询。

<button class="btn btn-danger" @onclick="UpdateData">Update</button>
......
protected async override Task OnInitializedAsync()
{
    await UpdateData();
}
private async Task UpdateData() =>
    People = await Context.People.Include(p => p.Department)
        .Include(p => p.Location).ToListAsync<Person>();

  UpdateData 方法执行相同的査询,但应用 ToListAsync 方法,该方法强制对 EFCore 查询进行评估。结果分配给 People 属性,可以重复读取,而不触发其他查询。为了让用户控制数据,添加了一个按钮,当单击 UpdateData 方法时,该按钮会调用该方法。重启请求 http:/localhost:5000/forms,然后单击 Increment 按钮。监视服务器的输出,将看到只有在组件初始化时才进行査询。要显式触发查询,请单击Update 按钮。

  一些操作可能需要一个新的查询,这很容易执行。为了便于演示,向 List 组件添加了一个排序操作,该操作是使用和不使用新查询实现的。

<button class="btn btn-danger" @onclick="(()=>UpdateData())">Update</button>
<button class="btn btn-info" @onclick="SortWithQuery">Sort (With Query)</button>
<button class="btn btn-info" @onclick="SortWithoutQuery">Sort (No Query)</button>
......
protected async override Task OnInitializedAsync()
{
    await UpdateData();
}
private IQueryable<Person> Query =>
    Context.People.Include(p => p.Department).Include(p => p.Location);
private async Task UpdateData(IQueryable<Person> query = null) =>
    People = await (query ?? Query).ToListAsync<Person>();
public async Task SortWithQuery()
{
    await UpdateData(Query.OrderBy(p => p.Surname));
}
public async Task SortWithoutQuery()
{
    People = People.OrderBy(p => p.Firstname).ToList<Person>();
}

  EF Core 査询表示为 IQueryable对象,允许该査询在发送到数据库服务器之前与附加的 LINQ 方法组合。示例中的新操作都使用 LINQ OrderBy 方法,但其中一个将其应用于 IQueryable,然后对其进行评估,以使用 ToListAsync 方法发送查询。另一个操作将 OrderBy 方法应用于现有结果数据,对其进行排序,而不发送新的查询。要查看这两个操作,请重新请求 http://localhost:5000/forms,并单击 Sort 按钮。当单击 Sort (With Query)按钮时,将看到一条日志消息,指示查询已发送到数据库。

4 执行增删改查操作

4.1 创建 List 组件

  List 组件包含需要的基本功能。在 List.razor 中删除了前面部分中不再需要的一些特性,并派加了允许用户导航到其他函数的按钮。

<td class="text-center">
    <NavLink class="btn btn-sm btn-info"
             href="@GetDetailsUrl(p.PersonId)">
        Details
    </NavLink>
    <NavLink class="btn btn-sm btn-warning"
             href="@GetEditUrl(p.PersonId)">
        Edit
    </NavLink>
    <button class="btn btn-sm btn-danger"
    @onclick="@(() => HandleDelete(p))">
        Delete
    </button>
</td>
......
string GetEditUrl(long id) => $"/forms/edit/{id}";
string GetDetailsUrl(long id) => $"/forms/details/{id}";
public async Task HandleDelete(Person p)
{
    Context.Remove(p);
    await Context.SaveChangesAsync();
    await UpdateData();
}

  对象的创建、査看和编辑操作导航到其他 URL,但是删除操作由 List 组件执行,注意在保存更改后重新加载数据,以将更改反映给用户。

4.2 创建 Details 组件

为 Blazor/Forms 文件夹添加一个名为 Details.razor 的 Blazor 组件。此组件显示的所有输入元素都被禁用,这意味着不需要处理事件或处理用户输入。

@page "/forms/details/{id:long}"
@layout EmptyLayout
@inherits OwningComponentBase<DataContext>

<h4 class="bg-info text-center text-white p-2">Details</h4>

<div class="form-group">
    <label>ID</label>
    <input class="form-control" value="@PersonData.PersonId" disabled />
</div>
<div class="form-group">
    <label>Firstname</label>
    <input class="form-control" value="@PersonData.Firstname" disabled />
</div>
<div class="form-group">
    <label>Surname</label>
    <input class="form-control" value="@PersonData.Surname" disabled />
</div>
<div class="form-group">
    <label>Department</label>
    <input class="form-control" value="@PersonData.Department?.Name" disabled />
</div>
<div class="form-group">
    <label>Location</label>
    <input class="form-control"
           value="@($"{PersonData.Location?.City}, {PersonData.Location?.State}")"
           disabled />
</div>
<div class="text-center">
    <NavLink class="btn btn-info" href="@EditUrl">Edit</NavLink>
    <NavLink class="btn btn-secondary" href="/forms">Back</NavLink>
</div>

@code
{
    [Inject]
    public NavigationManager NavManager { get; set; }

    DataContext Context => Service;

    [Parameter]
    public long Id { get; set; }

    public Person PersonData { get; set; } = new Person();

    protected async override Task OnParametersSetAsync()
    {
        PersonData = await Context.People.Include(p => p.Department)
            .Include(p => p.Location).FirstOrDefaultAsync(p => p.PersonId == Id);
    }

    public string EditUrl => $"/forms/edit/{Id}";
}

4.3 创建 Editor 组件

  其余特性将由 Editor 组件处理。代码清单删除了前面示例中不再需要的特性,并添加了对创建和编辑对象的支持,包括持久化数据。

@page "/forms/edit/{id:long}"
@page "/forms/create"
@layout EmptyLayout
@inherits OwningComponentBase<DataContext>

<link href="/blazorValidation.css" rel="stylesheet" />

<h4 class="bg-@Theme text-center text-white p-2">@Mode</h4>

<EditForm Model="PersonData" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    @if (Mode == "Edit")
    {
        <div class="form-group">
            <label>ID</label>
            <InputNumber class="form-control"
            @bind-Value="PersonData.PersonId" readonly />
        </div>
    }
    <div class="form-group">
        <label>Firstname</label>
        <ValidationMessage For="@(() => PersonData.Firstname)" />
        <InputText class="form-control" @bind-Value="PersonData.Firstname" />
    </div>
    <div class="form-group">
        <label>Surname</label>
        <ValidationMessage For="@(() => PersonData.Surname)" />
        <InputText class="form-control" @bind-Value="PersonData.Surname" />
    </div>
    <div class="form-group">
        <label>Deptartment</label>
        <ValidationMessage For="@(() => PersonData.DepartmentId)" />
        <CustomSelect TValue="long" Values="Departments"
                      Parser="@(str => long.Parse(str))"
        @bind-Value="PersonData.DepartmentId">
            <option selected disabled value="0">Choose a Department</option>
        </CustomSelect>
    </div>
    <div class="form-group">
        <label>Location</label>
        <ValidationMessage For="@(() => PersonData.LocationId)" />
        <CustomSelect TValue="long" Values="Locations"
                      Parser="@(str => long.Parse(str))"
        @bind-Value="PersonData.LocationId">
            <option selected disabled value="0">Choose a Location</option>
        </CustomSelect>
    </div>
    <div class="text-center">
        <button type="submit" class="btn btn-@Theme">Save</button>
        <NavLink class="btn btn-secondary" href="/forms">Back</NavLink>
    </div>
</EditForm>

@code
{

    [Inject]
    public NavigationManager NavManager { get; set; }

    DataContext Context => Service;

    [Parameter]
    public long Id { get; set; }

    public Person PersonData { get; set; } = new Person();

    public IDictionary<string, long> Departments { get; set; }
        = new Dictionary<string, long>();
    public IDictionary<string, long> Locations { get; set; }
        = new Dictionary<string, long>();

    protected async override Task OnParametersSetAsync()
    {
        if (Mode == "Edit")
        {
            PersonData = await Context.People.FindAsync(Id);
        }
        Departments = await Context.Departments
            .ToDictionaryAsync(d => d.Name, d => d.Departmentid);
        Locations = await Context.Locations
            .ToDictionaryAsync(l => $"{l.City}, {l.State}", l => l.LocationId);
    }

    public string Theme => Id == 0 ? "primary" : "warning";
    public string Mode => Id == 0 ? "Create" : "Edit";

    public async Task HandleValidSubmit()
    {
        if (Mode == "Create")
        {
            Context.Add(PersonData);
        }
        await Context.SaveChangesAsync();
        NavManager.NavigateTo("/forms");
    }
}

  添加了对新 URL 的支持,并使用引导 CSS 主题来区分创建新对象和编辑现有对象。删除了验证摘要,以便只显示属性级别的验证消息,并添加了通过 EF Core 存储数据的支持。与使用控制器或 Razor Pages 创建的表单应用程序不同,本例不必处理模型绑定,因为 Blazor。直接处理 EF Core 从初始数据库査询生成的对象。重启并请求 http://localhost:5000/forms。将看到 Person 对象列表,单击 Create、Details、Edi和 Delete 按钮,将允许处理数据库中的数据。

posted @ 2024-07-03 20:54  一纸年华  阅读(10)  评论(0编辑  收藏  举报