本章将描述 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;
}
}
}
表单组件的基类是 InputBasevalue="@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
名称 | 描述 |
---|---|
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 连接来显示多个组件。这将导致多个组件共享单个依赖注入范围,一个组件所做的更改将影响其他组件,即使这些更改没有写入数据库。
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
@inherits OwningComponentBase<DataContext>
......
DataContext Context => Service;
有作用域的服务可通过名为 Service 的属性使用。在这个例子中,指定 DataContext 作为基类的类型参数。
无论使用哪个基类,结果都是Editor组件有自己的依赖注入作用域和自己的DataContext对象。List 组件没有修改,因此它将接收请求范围的 DataContext 对象。
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 査询表示为 IQueryablehttp://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 按钮,将允许处理数据库中的数据。