C#9 和 .NET5 高级教程(十六)
二十八、WPF 通知、验证、命令和 MVVM
本章将通过介绍支持模型-视图-视图模型(MVVM)模式的功能来结束您对 WPF 编程模型的研究。第一部分介绍了模型-视图-视图模型模式。接下来,您将了解 WPF 通知系统及其通过可观察模型和可观察集合实现的可观察模式。让 UI 中的数据准确地描述数据的当前状态可以自动显著改善用户体验,并减少在旧技术(如 WinForms)中实现相同结果所需的手动编码。
在可观察模式的基础上,您将研究向应用中添加验证的机制。验证是任何应用的一个重要部分——不仅让用户知道有什么地方出错了,还让他们知道什么地方出错了。为了通知用户错误是什么,您还将学习如何将验证合并到视图标记中。
接下来,您将更深入地研究 WPF 命令系统,并创建自定义命令来封装程序逻辑,就像您在第二十五章中使用内置命令一样。创建定制命令有几个优点,包括(但不限于)支持代码重用、逻辑封装和关注点分离。
最后,您将在一个示例 MVVM 应用中将所有这些结合在一起。
介绍模型-视图-视图模型
在深入研究 WPF 中的通知、验证和命令之前,最好理解一下本章的最终目标,即模型-视图-视图模型模式(MVVM)。MVVM 源自马丁·福勒的表示模型模式,它利用了本章中讨论的 XAML 特有的能力,使你的 WPF 开发更快更干净。名称本身描述了模式的主要组成部分:模型、视图、视图模型。
模型
模型是数据的对象表示。在 MVVM,模型在概念上与来自数据访问层(DAL)的模型相同。有时候是同一个物理班,但是这个没有要求。当你阅读这一章时,你将学会如何决定你是否可以使用你的 DAL 模型或者你是否需要创建新的模型。
模型通常通过数据注释和INotifyDataErrorInfo接口利用内置(或自定义)验证,并被配置为可观察的,以与 WPF 通知系统相结合。在本章的后面你会看到所有这些。
景色
视图是应用的 UI,它被设计得非常轻量级。想想免下车餐馆的菜单板。该板显示菜单项和价格,并且它有一个机制,以便用户可以与后端系统通信。然而,该电路板没有内置任何智能,除非它是专门的用户界面逻辑,例如在天黑时开灯。
应该怀着同样的目标发展 MVVM 观点。任何智能都应该内置到应用的其他地方。代码隐藏文件中唯一的代码(例如MainWindow.xaml.cs)应该与操作 UI 直接相关。它不应该基于业务规则或任何需要保留以备将来使用的东西。虽然这不是 MVVM 的主要目标,但是开发良好的 MVVM 应用通常只有很少的代码隐藏。
视图模型
在 WPF 和其他 XAML 技术中,视图模型有两个用途。
-
视图模型为视图所需的所有数据提供了一站式服务。这并不意味着视图模型负责获取实际数据;相反,它只是一种将数据从数据存储区移动到视图的传输机制。通常,视图和视图模型之间存在一对一的关联,但是存在架构差异,并且您的里程可能会有所不同。
-
第二项工作是充当视图的控制器。就像菜单板一样,视图模型接受用户的指示,并将该调用转发给相关代码,以确保采取正确的操作。这些代码通常以自定义命令的形式出现。
贫血模型或贫血视图模型
在 WPF 的早期,当开发人员仍然在研究如何最好地实现 MVVM 模式时,有关于在哪里实现验证和可观察模式的重要(有时是激烈的)讨论。一个阵营(贫血模型阵营)认为所有的都应该在视图模型中,因为将这些功能添加到模型中打破了关注点的分离。另一个阵营(贫血视图模型阵营)认为应该全部放在模型中,因为这样可以减少代码的重复。
真正的答案当然是视情况而定。当INotifyPropertyChanged、IDataErrorInfo和INotifyDataErrorInfo在模型类上实现时,这确保了相关代码接近代码的目标(正如你将在本章中看到的),并且对于每个模型只实现一次。也就是说,有时候你的view model类本身也需要被开发成可观察的。最终,您需要确定什么对您的应用最有意义,而不会使您的代码过于复杂或牺牲 MVVM 的好处。
Note
有多种 MVVM 框架可用于 WPF,如 MVVMLite、Caliburn。Micro 和 Prism(尽管 Prism 不仅仅是一个 MVVM 框架)。本章讨论 MVVM 模式和 WPF 支持实现该模式的特性。读者朋友们,我让你们来研究不同的框架,并选择最符合你的应用需求的框架。
WPF 约束通知系统
WinForms 绑定系统的一个显著缺点是缺少通知。如果视图中表示的数据是以编程方式更新的,则 UI 也必须以编程方式刷新,以使它们保持同步。这导致了对控件上的Refresh()的大量调用,为了安全起见,通常比绝对必要的调用更多。虽然包含太多对Refresh()的调用通常不是一个严重的性能问题,但是如果没有包含足够多的调用,用户的体验可能会受到负面影响。
内置于基于 XAML 的应用中的绑定系统纠正了这个问题,它使您能够将数据对象和集合作为可观察对象开发到通知系统中。每当一个属性的值在一个可观察的模型上改变或者集合在一个可观察的集合上改变(例如,项目被添加、删除或者重新排序),一个事件被引发(或者NotifyPropertyChanged或者NotifyCollectionChanged)。绑定框架自动侦听这些事件的发生,并在它们触发时更新绑定的控件。更好的是,作为开发人员,您可以控制哪些属性会引发通知。听起来很完美,对吧?嗯,这不是完全完美。如果您全部手动完成,那么为可观察的模型设置这个过程会涉及到相当多的代码。幸运的是,有一个开源框架使它变得更简单,您很快就会看到这一点。
可观察模型和集合
在本节中,您将创建一个使用可观察模型和集合的应用。首先,创建一个名为 WpfNotifications 的新 WPF 应用。该应用将是一个主从表单,允许用户使用ComboBox选择特定的汽车,然后该汽车的详细信息将显示在下面的TextBox控件中。通过用以下标记替换默认网格来更新MainWindow.xaml:
<Grid IsSharedSizeScope="True" Margin="5,0,5,5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
SharedSizeGroup="CarLabels"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Vehicle"/>
<ComboBox Name="cboCars" Grid.Column="1"
DisplayMemberPath="PetName" />
</Grid>
<Grid Grid.Row="1" Name="DetailsGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
SharedSizeGroup="CarLabels"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" Content="Id"/>
<TextBox Grid.Column="1" Grid.Row="0" />
<Label Grid.Column="0" Grid.Row="1" Content="Make"/>
<TextBox Grid.Column="1" Grid.Row="1" />
<Label Grid.Column="0" Grid.Row="2" Content="Color"/>
<TextBox Grid.Column="1" Grid.Row="2" />
<Label Grid.Column="0" Grid.Row="3" Content="Pet Name"/>
<TextBox Grid.Column="1" Grid.Row="3" />
<StackPanel Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="4"
HorizontalAlignment="Right" Orientation="Horizontal" Margin="0,5,0,5">
<Button x:Name="btnAddCar" Content="Add Car" Margin="5,0,5,0" Padding="4, 2" />
<Button x:Name="btnChangeColor" Content="Change Color" Margin="5,0,5,0"
Padding="4, 2"/>
</StackPanel>
</Grid>
</Grid>
您的窗口将类似于图 28-1 。
图 28-1。
显示汽车详细信息的主-详细信息窗口
Grid控件上的IsSharedSizeScope标签设置子网格来共享维度。标有SharedSizeGroup的ColumnDefinitions将自动调整到相同的宽度,无需任何编程。在这个例子中,如果Pet Name标签变得更长,那么Vehicle列(在另一个Grid控件中)的大小将与之匹配,保持窗口的外观整洁。
接下来,在解决方案资源管理器中右键单击项目名称,选择添加➤新文件夹,并将文件夹命名为Models。在这个新文件夹中,创建一个名为Car的类。这里列出了初始类:
public class Car
{
public int Id { get; set; }
public string Make { get; set; }
public string Color { get; set; }
public string PetName { get; set; }
}
添加绑定和数据
下一步是为控件添加绑定语句。请记住,数据绑定语句围绕数据上下文,这可以在控件本身或父控件上设置。这里,您将在DetailsGrid上设置上下文,因此包含的每个控件都将继承该数据上下文。将DataContext设置为ComboBox的SelectedItem属性。将保存细节控件的Grid更新为以下内容:
<Grid Grid.Row="1" Name="DetailsGrid"
DataContext="{Binding ElementName=cboCars, Path=SelectedItem}">
DetailsGrid中的文本框将显示所选汽车的个别属性。向TextBox控件添加适当的文本属性和相关绑定,如下所示:
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Path=Id}" />
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Path=Make}" />
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding Path=Color}" />
<TextBox Grid.Column="1" Grid.Row="3" Text="{Binding Path=PetName}" />
最后,将数据添加到ComboBox中。在MainWindow.xaml.cs中,创建一个新的Car记录列表,并将ComboBox的ItemsSource设置到列表中。此外,为Notifications.Models名称空间添加using语句。
using WpfNotifications.Models;
//omitted for brevity
public partial class MainWindow : Window
{
readonly IList<Car> _cars = new List<Car>();
public MainWindow()
{
InitializeComponent();
_cars.Add(new Car {Id = 1, Color = "Blue", Make = "Chevy", PetName = "Kit"});
_cars.Add(new Car {Id = 2, Color = "Red", Make = "Ford", PetName = "Red Rider"});
cboCars.ItemsSource = _cars;
}
}
运行应用。您将看到车辆选择器有两辆车可供选择。选择其中一个,文本框将自动填充车辆详细信息。更改其中一辆车的颜色,选择另一辆车,然后返回到您编辑的车辆。你会看到新的颜色确实仍然附着在车辆上。这没什么了不起的。在前面的例子中,您已经看到了 XAML 数据绑定的强大功能。
以编程方式更改车辆数据
虽然前面的例子像预期的那样工作,但是如果数据以编程方式改变,用户界面将而不是反映这些变化,除非你编写应用来刷新数据。为了演示这一点,为btnChangeColor Button添加一个事件处理程序,如下所示:
<Button x:Name="btnChangeColor" Content="Change Color" Margin="5,0,5,0"
Padding="4, 2" Click="BtnChangeColor_OnClick"/>
在BtnChangeColor_Click()事件处理程序中,使用ComboBox的SelectedItem属性从汽车列表中定位选中的记录,并将颜色改为Pink。代码如下所示:
private void BtnChangeColor_OnClick(object sender, RoutedEventArgs e)
{
_cars.First(x => x.Id == ((Car)cboCars.SelectedItem)?.Id).Color = "Pink";
}
运行应用,选择一辆车,然后单击“改变颜色”按钮。没有明显的变化。选择另一辆车,然后回到最初选择的车。现在您将看到更新后的值。这对用户来说不是一个好的体验!
现在给btnAddCar按钮添加一个事件处理程序,如下所示:
<Button x:Name="btnAddCar" Content="Add Car" Margin="5,0,5,0" Padding="4, 2"
Click="BtnAddCar_OnClick" />
在BtnAddCar_Click事件处理程序中,向Car列表添加一条新记录。
private void BtnAddCar_Click(object sender, RoutedEventArgs e)
{
var maxCount = _cars?.Max(x => x.Id) ?? 0;
_cars?.Add(new Car { Id=++maxCount,Color="Yellow",Make="VW",PetName="Birdie"});
}
运行应用,点击 Add Car 按钮,检查ComboBox的内容。尽管您知道列表中有三辆汽车,但只显示了两辆!为了纠正这两个问题,您将把Car类转换成一个可观察的模型,并使用一个可观察的集合来保存所有的Car实例。
可观测模型
通过在您的Car模型类上实现INotifyPropertyChanged接口,解决了您的模型属性上的数据更改和不在 UI 中显示的问题。INotifyPropertyChanged界面包含一个单独的事件:PropertyChangedEvent。XAML 绑定引擎为实现INotifyPropertyChanged接口的类上的每个绑定属性监听该事件。界面如下所示:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
将以下using语句添加到Car.cs类中:
using System.ComponentModel;
using System.Runtime.CompilerServices;
接下来,在类上实现INotifyPropertyChanged接口,如下所示:
public class Car : INotifyPropertyChanged
{
//Omitted for brevity
public event PropertyChangedEventHandler PropertyChanged;
}
PropertyChanged事件接受一个对象引用和一个PropertyChangedEventArgs类的新实例,如下例所示:
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs("Model"));
第一个参数是引发事件的对象实例。PropertyChangedEventArgs构造函数接受一个字符串,该字符串表示属性已被更改,需要更新。当引发事件时,绑定引擎在该实例上查找绑定到命名属性的任何控件。如果将String.Empty传递给PropertyChangedEventArgs,那么实例的所有绑定属性都会更新。
您可以控制在自动更新中登记哪些属性。只有那些在 setter 中引发PropertyChanged事件的属性会被自动更新。这通常是模型类的所有属性,但是您可以根据应用的需求选择省略某些属性。一种常见的模式是创建一个帮助器方法(通常名为OnPropertyChanged())来代表属性引发事件,而不是直接在 setter 中为每个登记的属性引发事件,通常是在模型的基类中。将以下方法和代码添加到Car.cs类中:
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
接下来,更新Car类中的每个自动属性,使其拥有一个完整的 getter 和 setter 以及一个支持字段。当值改变时,调用OnPropertyChanged()帮助器方法。下面是更新后的Id属性:
private int _id;
public int Id
{
get => _id;
set
{
if (value == _id) return;
_id = value;
OnPropertyChanged();
}
}
确保对该类中的所有属性执行相同的操作,然后再次运行该应用。选择一辆车并点击“改变颜色”按钮。您将立即看到 UI 中显示的更改。第一个问题解决!
使用名称 of
C# 6 中增加的一个特性是nameof操作符,它提供传递给nameof方法的项目的字符串名称。您可以在 setters 中调用OnPropertyChanged(),就像这样:
public string Color
{
get { return _color; }
set
{
if (value == _color) return;
_color = value;
OnPropertyChanged(nameof(Color));
}
}
注意,当您使用nameof方法时,您不必从OnPropertyChanged()中移除CallerMemberName属性(尽管它变得没有必要)。最后,是使用nameof方法还是CallerMemberName属性,这取决于个人的选择。
可观察的集合
下一个要解决的问题是当集合的内容改变时更新 UI。这是通过实现INotifyCollectionChanged接口来完成的。像INotifyPropertyChanged接口一样,这个接口公开了一个事件,即CollectionChanged事件。与INotifyPropertyChanged事件不同,手工实现这个接口不仅仅是调用 setter 中的一个方法。您需要创建一个完整的List实现,并在列表发生变化时引发CollectionChanged事件。
使用 ObservableCollections 类
幸运的是,有一种比创建自己的集合类更简单的方法。ObservableCollection<T>类实现了INotifyCollectionChanged、INotifyPropertyChanged和Collection<T>,它是。NET 核心框架。没有额外的工作!为此,为System.Collections.ObjectModel添加一个using语句,然后将_cars的私有字段更新为:
private readonly IList<Car> _cars =
new ObservableCollection<Car>();
再次运行应用,然后单击添加汽车按钮。您将看到新记录适当地出现。
实现脏标志
可观测模型的另一个优点是跟踪状态变化的能力。使用 WPF 进行脏跟踪(当一个或多个对象的值发生变化时进行跟踪)相当简单。向Car类添加一个名为IsChanged的bool属性。确保像调用Car类中的其他属性一样调用OnPropertyChanged()。
private bool _isChanged;
public bool IsChanged {
get => _isChanged;
set
{
if (value == _isChanged) return;
_isChanged = value;
OnPropertyChanged();
}
}
您需要在OnPropertyChanged()方法中将IsChanged属性设置为true。当IsChanged更新时,你还需要确保你没有将IsChanged设置为true,否则你将遇到堆栈溢出异常!将OnPropertyChanged()方法更新如下(使用前面讨论的nameof方法):
protected virtual void OnPropertyChanged(
[CallerMemberName] string propertyName = "")
{
if (propertyName != nameof(IsChanged))
{
IsChanged = true;
}
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
打开MainWindow.xaml并给DetailsGrid增加一个额外的RowDefinition。将以下内容添加到包含一个Label和一个CheckBox的Grid的末尾,绑定到IsChanged属性,如下所示:
<Label Grid.Column="0" Grid.Row="5" Content="Is Changed"/>
<CheckBox Grid.Column="1" Grid.Row="5" VerticalAlignment="Center"
Margin="10,0,0,0" IsEnabled="False" IsChecked="{Binding Path=IsChanged}" />
如果您现在运行该应用,您会看到每一条记录都显示为已更改,即使您没有更改任何内容!这是因为对象创建会设置属性值,设置任何值都会调用OnPropertyChanged()。这将设置对象的IsChanged属性。要纠正这一点,将IsChanged属性设置为false,作为对象初始化代码中的最后一个属性。打开MainWindow.xaml.cs,将创建列表的代码改为如下:
_cars.Add(
new Car {Id = 1, Color = "Blue", Make = "Chevy", PetName = "Kit", IsChanged = false});
_cars.Add(
new Car {Id = 2, Color = "Red", Make = "Ford", PetName = "Red Rider", IsChanged = false});
再次运行应用,选择一辆车,然后单击“更改颜色”按钮。您将看到复选框和更新的颜色一起被选中。
通过 UI 交互更新源代码
您可能会注意到,如果在用户界面中键入文本,“已更改”复选框实际上不会被选中,直到您退出正在编辑的控件。这是因为TextBox绑定上的UpdateSourceTrigger属性。UpdateSourceTrigger决定了什么事件(比如改变值、跳转等等)。)使用户界面更新基础数据。有四种选择,如表 28-1 所示。
表 28-1。
UpdateSourceTrigger值
成员
|
生命的意义
|
| --- | --- |
| Default | 为控件设置默认值(例如,TextBox控件设置为LostFocus)。 |
| Explicit | 仅在调用UpdateSource方法时更新源对象。 |
| LostFocus | 当控件失去焦点时更新。这是TextBox控件的默认设置。 |
| PropertyChanged | 属性一改变就更新。这是CheckBox控件的默认设置。 |
TextBox的默认源触发器是LostFocus事件。通过将颜色TextBox的绑定更新为以下 XAML,将其更改为PropertyChanged:
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding Path=Color, UpdateSourceTrigger=PropertyChanged}" />
现在,当您运行应用并开始在颜色文本框中键入内容时,复选框会立即被选中。你可能会问为什么默认设置为TextBox控件的LostFocus。一个模型的任何确认(稍后介绍)都与UpdateSourceTrigger一起启动。对于TextBox,这可能会导致错误持续闪烁,直到用户输入正确的值。例如,如果验证规则不允许在一个TextBox中少于五个字符,错误将在每次击键时显示,直到用户输入五个或更多。在这些情况下,最好等待用户退出TextBox(在完成对文本的更改之后)来更新源代码。
包装通知和可观察项
对模型使用INotifyPropertyChanged和对列表使用ObservableCollections类可以通过保持数据和 UI 同步来改善用户体验。虽然这两个接口都不复杂,但它们确实需要更新您的代码。幸运的是,微软已经包含了ObservableCollection类来处理创建可观察集合的所有管道。同样幸运的是,Fody 项目的更新自动添加了INotifyPropertyChanged功能。有了这两个工具,没有理由不在您的 WPF 应用中实现 observables。
WPF 验证
既然您已经实现了INotifyPropertyChanged并且正在使用ObservableCollection,那么是时候为您的应用添加验证了。应用需要验证用户输入,并在输入的数据不正确时向用户提供反馈。本节涵盖了现代 WPF 应用最常见的验证机制,但这些仍然只是 WPF 内置功能的一部分。
当数据绑定试图更新数据源时,会发生验证。除了内置验证(如属性 setter 中的异常)之外,您还可以创建自定义验证规则。如果任何验证规则(内置的或定制的)失败,那么Validation类(稍后将讨论)就会发挥作用。
Note
对于本章中的每一节,您可以继续使用上一节中的同一项目,也可以为每个新节创建一个项目副本。在本章的回购中,每个部分都是一个不同的项目。
更新验证示例的样本
在本章的 repo 中,新项目(复制自上一个例子)被称为 WpfValidations。如果您使用的是上一节中的同一个项目,那么在将本节中列出的示例中的代码复制到您的项目中时,您只需要记下名称空间的变化。
验证类
在向项目添加验证之前,理解Validation类很重要。该类是验证框架的一部分,它提供了可用于显示验证结果的方法和附加属性。在处理验证错误时,Validation类有三个常用的主要属性(如表 28-2 所示)。在本节的剩余部分,您将使用其中的每一项。
表 28-2。
Validation班的主要成员
成员
|
生命的意义
|
| --- | --- |
| HasError | 附加的属性,指示验证规则在过程中的某个地方失败 |
| Errors | 所有活动ValidationError对象的集合 |
| ErrorTemplate | 当HasError设置为true时,变得可见并修饰绑定元素的控制模板 |
验证选项
如前所述,XAML 技术公司有几种将验证逻辑整合到应用中的机制。在接下来的小节中,您将研究三种最常用的验证选择。
异常时通知
虽然不应该使用异常来实施业务逻辑,但是异常可能并且确实会发生,并且应该适当地处理它们。如果代码中没有处理它们,用户应该会收到问题的视觉反馈。WinForms 的一个重要变化是,默认情况下,WPF 绑定异常不会作为异常传播给用户。但是,它们是使用装饰器(位于控件顶部的可视层)以可视方式指示的。
为了测试这一点,运行应用,从ComboBox中选择一个记录,并清除Id值。因为Id属性被定义为int(不是nullable int),所以需要一个数值。当您跳出Id字段时,绑定框架会向Id属性发送一个空字符串,由于空字符串不能转换为int,setter 中会抛出一个异常。通常,未处理的异常会向用户生成一个消息框,但在这种情况下,不会发生类似的情况。如果查看输出窗口的调试部分,您会看到以下内容:
System.Windows.Data Error: 7 : ConvertBack cannot convert value '' (type 'String'). BindingExpression:Path=Id; DataItem="Car" (HashCode=52579650); target element is 'TextBox' (Name=''); target property is 'Text' (type 'String') FormatException:'System.FormatException: Input string was not in a correct format.
异常的直观显示是控件周围的一个红色细框,如图 28-2 所示。
图 28-2。
默认错误模板
红框是Validation对象的ErrorTemplate属性,充当绑定控件的装饰器。虽然默认的错误装饰器显示确实有一个错误,但是没有任何迹象表明什么是错误的。好消息是ErrorTemplate是完全可定制的,你将在本章后面看到。
IDataErrorInfo
IDataErrorInfo接口为您向模型类添加定制验证提供了一种机制。这个接口直接添加到您的模型(或视图模型)类中,验证代码放在您的模型类中(最好是在分部类中)。这将验证代码集中在您的项目中,与 WinForms 项目形成鲜明对比,后者的验证通常在 UI 本身中完成。
这里显示的IDataErrorInfo接口包含两个属性:一个索引器和一个名为Error的字符串属性。注意,WPF 绑定引擎不使用Error属性。
public interface IDataErrorInfo
{
string this[string columnName] { get; }
string Error { get; }
}
您将很快添加Car分部类,但是首先您需要更新Car.cs类并将其标记为分部类。接下来,向Models目录添加另一个名为CarPartial.cs的文件。重命名该类Car,确保该类标记为partial,并添加IDataErrorInfo接口。最后,实现接口的 API。初始代码如下所示:
public partial class Car : IDataErrorInfo
{
public string this[string columnName] => string.Empty;
public string Error { get;}
}
对于选择加入到IDataErrorInfo接口的绑定控件,它必须将ValidatesOnDataErrors添加到绑定表达式中。将Make文本框的绑定表达式更新如下(并以同样的方式更新其余的绑定语句):
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Path=Make, ValidatesOnDataErrors=True}" />
一旦对绑定语句进行了更新,模型上的索引器就会在每次引发PropertyChanged事件时被调用。事件的属性名被用作索引器中的columnName参数。如果索引器返回string.Empty,那么框架假设所有的验证都通过了,并且不存在错误情况。如果索引器返回除string.Empty之外的任何内容,则认为该对象实例的属性存在错误,并且绑定到该类的特定实例上正在验证的属性的每个控件都被认为有错误,Validation对象的HasError属性被设置为true,并且为受影响的控件激活ErrorTemplate装饰器。
接下来,您将向CarPartial.cs中的索引器添加一些简单的验证逻辑。验证规则很简单。
-
如果
Make等于ModelT,设置误差等于"Too Old"。 -
如果
Make等于Chevy,Color等于Pink,则设置误差等于$"{Make}'s don't come in {Color}"。
首先为每个属性添加一个switch语句。为了避免在case语句中使用神奇的字符串,您将再次使用nameof方法。如果代码没有通过switch语句,返回string.Empty。接下来,添加验证规则。在适当的case语句中,添加一个基于前面列出的规则的属性值检查。在Make属性的case语句中,首先检查以确保值不是ModelT。如果是,则返回错误。如果通过,下一行将调用一个 helper 方法,如果违反了第二条规则,它将返回一个错误,否则它将返回string.Empty。在针对Color属性的case语句中,也调用 helper 方法。代码如下:
public string this[string columnName]
{
get
{
switch (columnName)
{
case nameof(Id):
break;
case nameof(Make):
return Make == "ModelT"
? “Too Old”
: CheckMakeAndColor();
case nameof(Color):
return CheckMakeAndColor();
case nameof(PetName):
break;
}
return string.Empty;
}
}
internal string CheckMakeAndColor()
{
if (Make == "Chevy" && Color == "Pink")
{
return $"{Make}'s don't come in {Color}";
}
return string.Empty;
}
运行应用,选择红色骑手车辆(福特),并将品牌更改为 ModelT。一旦您跳出该字段,就会出现红色的错误装饰。现在从下拉列表中选择 Kit(这是一辆 Chevy ),并单击 Change Color 按钮将颜色更改为粉红色。红色错误装饰立即出现在颜色字段中,但不会出现在生成文本框中。现在,将 Make 更改为 Ford,跳出文本框,注意红色装饰符没有消失!
这是因为索引器仅在属性的PropertyChanged事件被触发时运行。正如在“WPP 绑定通知系统”一节中所讨论的,当源对象的属性改变时,PropertyChanged事件被触发,这或者通过代码(比如单击改变颜色按钮)或者通过用户交互(时间通过UpdateSourceTrigger来控制)来实现。当您改变颜色时,Make属性没有改变,所以事件没有为Make属性触发。因为事件没有触发,索引器没有被调用,所以对Make属性的验证没有运行。
有两种方法可以解决这个问题。第一个是通过传入string.Empty而不是字段名来更改PropertyChangedEventArgs以更新每个绑定属性。如前所述,这会导致绑定引擎更新该实例上的每个属性的*。像这样更新Car.cs类中的OnPropertyChanged()方法:*
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (propertyName != nameof(IsChanged))
{
IsChanged = true;
}
//PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(string.Empty));
}
现在,当您运行相同的测试时,您会看到当 Make 和 Color 文本框中的一个被更新时,它们都用错误模板进行了修饰。那么,为什么不总是以这种方式引发事件呢?很大程度上是性能问题。刷新一个对象的所有属性可能会降低性能。当然,不测试是无法知道的,你的里程可能(很可能)会有所不同。
另一个解决方案是当一个字段发生变化时,为其他依赖字段引发PropertyChanged事件。使用这种机制的缺点是,你(或其他支持你的应用的开发者)必须知道Make和Color属性是通过验证码联系在一起的。
INotifyDataErrorInfo
中引入的INotifyDataErrorInfo接口。NET 4.5 建立在IDataErrorInfo接口的基础上,并增加了额外的验证功能。当然,随着额外的功率而来的是额外的工作!与您必须特别选择的先前的验证技术相比,这是一个巨大的转变,ValidatesOnNotifyDataErrors绑定属性默认为true,因此将该属性添加到您的绑定语句是可选的。
INotifyDataErrorInfo接口非常小,但是确实需要大量的管道代码来使其有效,您很快就会看到这一点。界面如下所示:
public interface INotifyDataErrorInfo
{
bool HasErrors { get; }
event EventHandler<DataErrorsChangedEventArgs>
ErrorsChanged;
IEnumerable GetErrors(string propertyName);
}
绑定引擎使用HasErrors属性来确定实例的任何属性上是否有任何错误。如果使用 null 或空字符串调用GetErrors()方法的propertyName参数,它将返回实例中存在的所有错误。如果一个propertyName被传递到方法中,那么只返回特定属性的错误。ErrorsChanged事件(类似于PropertyChanged和CollectionChanged事件)通知绑定引擎更新当前错误列表的 UI。
实现支持代码
在实现INotifyDataErrorInfo的时候,大部分代码通常会被推送到一个基础模型类中,所以只需要编写一次。从在CarPartial.cs类中用INotifyDataErrorInfo替换IDataErrorInfo开始,并添加接口成员(你可以把来自IDataErrorInfo的代码留在类中;您稍后将更新此内容)。
public partial class Car: INotifyDataErrorInfo, IDataErrorInfo
{
...
public IEnumerable GetErrors(string propertyName)
{
throw new NotImplementedException();
}
public bool HasErrors { get; }
public event
EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}
接下来,添加一个Dictionary<string,List<string>>来保存按属性名分组的任何错误。您还需要为System.Collections.Generic添加一个using语句。两者都显示在这里:
using System.Collections.Generic;
private readonly Dictionary<string,List<string>> _errors
= new Dictionary<string, List<string>>();
如果字典中有任何错误,HasErrors属性应该返回true。这很容易实现,如下所示:
public bool HasErrors => _errors.Any();
接下来,创建一个 helper 方法来引发ErrorsChanged事件(就像引发PropertyChanged事件一样),如下所示:
private void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this,
new DataErrorsChangedEventArgs(propertyName));
}
如前所述,如果参数为空,那么GetErrors()方法应该返回字典中的所有错误。如果传入一个propertyName值,它将返回为该属性找到的任何错误。如果参数不匹配(或者属性没有任何错误),那么该方法将返回 null。
public IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
{
return _errors.Values;
}
return _errors.ContainsKey(propertyName)
? _errors[propertyName]
: null;
}
最后一组助手将为一个属性添加一个或多个错误,或者清除一个属性(或所有属性)的所有错误。每当字典改变时,记得调用OnErrorsChanged() helper 方法。
private void AddError(string propertyName, string error)
{
AddErrors(propertyName, new List<string> { error });
}
private void AddErrors(
string propertyName, IList<string> errors)
{
if (errors == null || !errors.Any())
{
return;
}
var changed = false;
if (!_errors.ContainsKey(propertyName))
{
_errors.Add(propertyName, new List<string>());
changed = true;
}
foreach (var err in errors)
{
if (_errors[propertyName].Contains(err)) continue;
_errors[propertyName].Add(err);
changed = true;
}
if (changed)
{
OnErrorsChanged(propertyName);
}
}
protected void ClearErrors(string propertyName = "")
{
if (string.IsNullOrEmpty(propertyName))
{
_errors.Clear();
}
else
{
_errors.Remove(propertyName);
}
OnErrorsChanged(propertyName);
}
现在的问题是“这个代码是如何被激活的?”绑定引擎监听ErrorsChanged事件,如果绑定语句的错误集合发生变化,它将更新 UI。但是验证代码仍然需要一个触发器来执行。对此有两种机制,它们将在下面讨论。
使用 INotifyDataErrorInfo 进行验证
检查错误的一个地方是属性设置器,如下例所示,简化为只检查ModelT验证:
public string Make
{
get { return _make; }
set
{
if (value == _make) return;
_make = value;
if (Make == "ModelT")
{
AddError(nameof(Make), "Too Old");
}
else
{
ClearErrors(nameof(Make));
}
OnPropertyChanged(nameof(Make));
OnPropertyChanged(nameof(Color));
}
}
这种方法的主要问题是,您必须将验证逻辑与属性设置器结合起来,这使得代码更难阅读和支持。
将 idataerrorinfo 与 inotifydataerrorinfo 结合起来进行验证
在上一节中,您看到了可以将IDataErrorInfo添加到分部类中,这意味着您不必更新 setters。您还看到,当属性上的PropertyChanged被引发时,索引器会自动被调用。结合IDataErrorInfo和INotifyDataErrorInfo为您提供了来自INotifyDataErrorInfo的额外验证特性,以及IDataErrorInfo提供的与设置器的分离。
使用IDataErrorInfo的目的不是运行验证,而是确保每次在对象上引发PropertyChanged时,利用INotifyDataErrorInfo的验证代码都会被调用。因为没有使用IDataErrorInfo进行验证,所以总是从索引器返回string.Empty。将索引器和CheckMakeAndColor()帮助器方法更新为以下代码:
public string this[string columnName]
{
get
{
ClearErrors(columnName);
switch (columnName)
{
case nameof(Id):
break;
case nameof(Make):
CheckMakeAndColor();
if (Make == "ModelT")
{
AddError(nameof(Make), "Too Old");
hasError = true;
}
break;
case nameof(Color):
CheckMakeAndColor();
break;
case nameof(PetName):
break;
}
return string.Empty;
}
}
internal bool CheckMakeAndColor()
{
if (Make == "Chevy" && Color == "Pink")
{
AddError(nameof(Make), $"{Make}'s don't come in {Color}");
AddError(nameof(Color),
$"{Make}'s don't come in {Color}");
return true;
}
return false;
}
运行应用,选择雪佛兰,并改变颜色为粉红色。除了品牌和型号文本框周围的红色装饰外,您还会看到整个网格周围的红色框装饰,其中包含了Car细节字段(如图 28-3 所示)。
图 28-3。
更新的错误装饰器
这是使用INotifyDataErrorInfo的另一个好处。除了有错误的控件之外,定义数据上下文的控件也用错误模板装饰。
显示所有错误
Validation类上的Errors属性以ValidationError对象的形式返回特定对象上的所有验证错误。每个ValidationError对象都有一个ErrorContent属性,包含该属性的错误消息列表。这意味着您要显示的错误消息在列表中的这个列表中。为了正确显示它们,您需要创建一个保存显示数据的ListBox的ListBox。听起来有点递归,但是一旦看到就有道理了。
首先向DetailsGrid添加另一行,并确保Window的Height至少为 300。在最后一行添加一个ListBox,将ItemsSource绑定到DetailsGrid,使用Validation.Errors作为路径,如下所示:
<ListBox Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding ElementName=DetailsGrid, Path=(Validation.Errors)}">
</ListBox>
在ListBox中添加一个DataTemplate,在DataTemplate中添加一个与ErrorContent属性绑定的ListBox。在这种情况下,每个ListBoxItem的数据上下文是一个ValidationError对象,所以您不需要设置数据上下文,只需要设置路径。将绑定路径设置为ErrorContent,如下所示:
<ListBox.ItemTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding Path=ErrorContent}"/>
</DataTemplate>
</ListBox.ItemTemplate>
运行应用,选择雪佛兰,并设置颜色为粉红色。您将看到图 28-4 中显示的错误。
图 28-4。
显示错误集合
这仅仅触及了验证和显示生成的错误的表面,但是它应该会让您在开发改善用户体验的信息丰富的 ui 的道路上走得很好。
将支持代码移动到基类
正如您可能注意到的,现在在CarPartial.cs类中有很多代码。因为这个例子只有一个模型类,这并不可怕。但是,当您将模型添加到实际的应用中时,您不希望必须将所有的管道添加到模型的每个分部类中。最好的做法是将所有支持代码下推到一个基类。你现在就去做。
向名为BaseEntity.cs的Models文件夹添加一个新的类文件。增加System.Collections和System.ComponentModel的using语句。将该类公开,并添加INotifyDataErrorInfo接口,如下所示:
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace Validations.Models
{
public class BaseEntity : INotifyDataErrorInfo
}
将所有与INofityDataErrorInfo相关的代码从CarPartial.cs移到新的基类中。任何私有方法和变量都需要受到保护。接下来,从CarPartial.cs类中移除INotifyDataErrorInfo接口,并添加BaseEntity作为基类,如下所示:
public partial class Car : BaseEntity, IDataErrorInfo
{
//removed for brevity
}
现在,您创建的任何额外的模型类都将继承所有的INotifyDataErrorInfo管道代码。
通过 WPF 利用数据注释
WPF 也可以利用数据注释进行 UI 验证。让我们给Car模型添加一些数据注释。
向模型添加数据注释
打开Car.cs,为System.ComponentModel.DataAnnotations添加一条using语句。将[Required]和[StringLength(50)]属性添加到Make、Color和PetName中。Required属性添加了一个验证规则,即属性不能为空(当然,这对Id属性来说是多余的,因为它不是nullable int)。StringLength(50)属性增加了一条验证规则,即属性值不能超过 50 个字符。
检查基于数据注释的验证错误
在 WPF 中,您必须以编程方式检查基于数据注释的验证错误。基于注释的验证的两个关键类是ValidationContext和Validator类。ValidationContext类提供了检查类验证错误的上下文。Validator类允许您在ValidationContext中检查对象的基于属性的错误。
打开BaseEntity.cs,添加以下using语句:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
接下来,创建一个名为GetErrorsFromAnnotations()的新方法。这个方法是通用的,接受一个字符串属性名和一个类型为T的值作为参数,并返回一个字符串数组。确保该方法被标记为受保护。签名如下所示:
protected string[] GetErrorsFromAnnotations<T>(
string propertyName, T value)
{}
在该方法中,创建一个保存验证检查结果的List<ValidationResult>变量,并创建一个作用域为传递给该方法的属性名的ValidationContext。当你准备好这两个项目后,调用Validate.TryValidateProperty,它会返回一个bool。如果一切都通过(关于数据注释验证),它返回true。如果不是,它返回false并用错误填充List<ValidationResult>。完整的代码如下所示:
protected string[] GetErrorsFromAnnotations<T>(
string propertyName, T value)
{
var results = new List<ValidationResult>();
var vc = new ValidationContext(this, null, null)
{ MemberName = propertyName };
var isValid = Validator.TryValidateProperty(
value, vc, results);
return (isValid)
? null
: Array.ConvertAll(
results.ToArray(), o => o.ErrorMessage);
}
现在您可以更新CarPartial.cs中的索引器方法,根据数据注释检查任何错误。如果发现任何错误,将它们添加到支持INotifyDataErrorInfo的错误集合中。这使我们能够清理错误处理。在索引器方法的开始,清除列的错误。然后处理验证,最后是实体的定制逻辑。更新后的索引器代码如下所示:
public string this[string columnName]
{
get
{
ClearErrors(columnName);
var errorsFromAnnotations =
GetErrorsFromAnnotations(columnName,
typeof(Car)
.GetProperty(columnName)?.GetValue(this,null));
if (errorsFromAnnotations != null)
{
AddErrors(columnName, errorsFromAnnotations);
}
switch (columnName)
{
case nameof(Id):
break;
case nameof(Make):
CheckMakeAndColor();
if (Make == "ModelT")
{
AddError(nameof(Make), "Too Old");
}
break;
case nameof(Color):
CheckMakeAndColor();
break;
case nameof(PetName):
break;
}
return string.Empty;
}
}
运行应用,选择其中一辆车,并为颜色添加超过 50 个字符的文本。当超过 50 个字符的阈值时,StringLength数据注释会创建一个验证错误,并报告给用户,如图 28-5 所示。
图 28-5。
验证所需的数据注释
自定义错误模板
最后一个主题是创建一个在控件出错时应用的样式,并更新ErrorTemplate以显示更有意义的错误信息。正如你在第二十七章中学到的,控件可以通过样式和控件模板来定制。
首先在目标类型为TextBox的MainWindow.xaml的Windows.Resources部分添加一个新样式。接下来,当Validation.HasError属性被设置为true时,在设置属性的样式上添加一个触发器。要设置的属性和值是Background ( Pink)、Foreground ( Black)和Tooltip到ErrorContent。Background和Foreground设置器并不新鲜,但是设置ToolTip的语法需要一些解释。绑定指向应用该样式的控件,在本例中是TextBox。该路径是Validation.Errors集合的第一个ErrorContent值。标记如下所示:
<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="Background" Value="Pink" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
运行应用并创建一个错误条件。结果将类似于图 28-6 ,并带有显示错误信息的工具提示。
图 28-6。
显示自定义错误模板
以前的样式改变了任何有错误条件的TextBox的外观。接下来,您将创建一个定制的控件模板来更新Validation类的ErrorTemplate以显示一个红色的感叹号,并为感叹号设置工具提示。ErrorTemplate是一个装饰器,它位于控件的顶部。当刚刚创建的样式更新控件本身时,ErrorTemplate将位于控件之上。
在刚刚创建的样式中,在Style.Triggers结束标记之后立即放置一个 setter。您将创建一个控制模板,它由一个TextBlock(显示感叹号)和一个BorderBrush组成,包围包含错误的TextBox。在 XAML 有一个特殊的标签,这个标签上装饰着名为AdornedElementPlaceholder的ErrorTemplate。通过向该控件添加名称,可以访问与该控件相关联的错误。在这个例子中,您想要访问Validation.Errors属性,这样您就可以获得ErrorContent(就像您在Style.Trigger中所做的那样)。以下是 setter 的完整标记:
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock Foreground="Red" FontSize="20" Text="!"
ToolTip="{Binding ElementName=controlWithError,
Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"/>
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder Name="controlWithError" />
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
运行应用并创建一个错误条件。结果将类似于图 28-7 。
图 28-7。
显示自定义错误模板
完成验证
这就完成了您对 WPF 验证方法的了解。当然,你还可以做更多的事情。有关更多信息,请参考 WPF 文档。
创建自定义命令
与验证部分一样,您可以继续在同一个项目中工作,或者创建一个新项目并将所有代码复制到其中。我将创建一个名为 WpfCommands 的新项目。如果您正在使用同一个项目,请务必注意本节代码示例中的名称空间,并根据需要进行调整。
正如你在第二十五章中了解到的,命令是 WPF 不可或缺的一部分。命令可以挂接到 WPF 控件(比如Button和MenuItem控件)来处理用户事件,比如Click()事件。不是直接创建事件处理程序并将代码直接添加到代码隐藏文件中,而是在 click 事件触发时执行命令的Execute()方法。CanExecute()方法用于根据自定义代码启用或禁用控件。除了您在第二十五章中使用的内置命令之外,您还可以通过实现ICommand接口来创建自己的定制命令。通过使用命令而不是事件处理程序,您获得了封装应用代码以及基于业务逻辑自动启用和禁用控件的好处。
实现 ICommand 接口
作为对第二十五章的快速回顾,ICommand界面如下所示:
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
添加 ChangeColorCommand 命令
从改变颜色按钮开始,Button控件的事件处理程序将被替换为命令。首先在项目中添加一个新文件夹(名为Cmds)。添加一个名为ChangeColorCommand.cs的新类。公开类,实现ICommand接口。添加下面的using语句(第一个可能会有所不同,这取决于您是否为这个示例创建了一个新项目):
using WpfCommands.Models;
using System.Windows.Input;
您的类应该是这样的:
public class ChangeColorCommand : ICommand
{
public bool CanExecute(object parameter)
{
throw new NotImplementedException();
}
public void Execute(object parameter)
{
throw new NotImplementedException();
}
public event EventHandler CanExecuteChanged;
}
如果CanExecute()方法返回true,任何绑定控件都将被启用,如果它返回false,它们将被禁用。如果一个控件被启用(因为CanExecute()返回true)并被点击,那么Execute()方法将被触发。传递给这两个方法的参数来自基于绑定语句上设置的CommandParameter属性的 UI。CanExecuteChanged事件绑定到绑定和通知系统,通知 UICanExecute()方法的结果已经改变(很像PropertyChanged事件)。
在本例中,只有当参数不为空并且类型为Car时,更改颜色按钮才起作用。将CanExecute()方法更新如下:
public bool CanExecute(object parameter)
=> (parameter as Car) != null;
Execute()方法参数的值与CanExecute()方法的值相同。由于Execute()方法只能在对象类型为Car时执行,参数必须转换为Car类型并更新颜色,如下所示:
public void Execute(object parameter)
{
((Car)parameter).Color="Pink";
}
将命令附加到 CommandManager
命令类的最后一个更新是在命令管理器中键入命令。当Window第一次加载,然后当命令管理器指示它重新执行时,CanExecute()方法触发。每个命令类都必须选择加入命令管理器。这是通过更新关于CanExecuteChanged事件的代码来完成的,如下所示:
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
更新 MainWindow.xaml.cs
下一个变化是创建这个类的一个实例,Button可以访问它。现在,您将把它放在MainWindow的代码隐藏文件中(在本章的后面,您将把它移到一个view model)。打开MainWindow.xaml.cs并删除改变颜色按钮的Click事件处理程序。将下面的using语句添加到文件的顶部(同样,命名空间可能会根据您是仍在使用同一个项目还是开始了一个新项目而有所不同):
using WpfCommands.Cmds;
using System.Windows.Input;
接下来,添加一个名为ChangeColorCmd的公共属性,类型为ICommand,带有一个支持字段。在属性的表达式体中,返回支持属性(如果支持字段为空,确保实例化一个新的ChangeColorCommand实例)。
private ICommand _changeColorCommand = null;
public ICommand ChangeColorCmd
=> _changeColorCommand ??= new ChangeColorCommand());
更新 MainWindow.xaml
正如你在第二十五章中看到的,WPF 中的可点击控件(像Button控件)有一个Command属性,允许你给控件分配一个命令对象。首先,将代码隐藏中实例化的命令连接到btnChangeColor按钮。因为命令的属性在MainWindow类上,所以使用RelativeSourceMode绑定语法来访问包含Button的Window,如下所示:
Command="{Binding Path=ChangeColorCmd,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
Button仍然需要发送一个Car对象作为CanExecute()和Execute()方法的参数。这是通过CommandParameter房产转让的。您将此设置为cboCars ComboBox的SelectedItem,如下所示:
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}"
按钮的完整标记如下所示:
<Button x:Name="btnChangeColor" Content="Change Color" Margin="5,0,5,0"
Padding="4, 2" Command="{Binding Path=ChangeColorCmd,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}"/>
测试应用
运行应用。你会看到变色命令是而不是被激活,如图 28-8 所示,因为没有选择车辆。
图 28-8。
未选择任何内容的窗口
现在,选择一辆车;该按钮将被启用,点击它将改变颜色,正如所料!
创建 commandbars 类
如果对AddCarCommand.cs继续使用这种模式,将会有代码在类之间重复。这是一个好迹象,表明基类可以提供帮助。在Cmds文件夹中创建一个名为CommandBase.cs的新类,并为System.Windows.Input名称空间添加一个using。将类设置为 public 并实现ICommand接口。将类和Execute()和CanExecute()方法改为抽象。最后,添加来自ChangeColorCommand类的更新后的CanExecuteChanged事件。下面列出了完整的实现:
using System;
using System.Windows.Input;
namespace WpfCommands.Cmds
{
public abstract class CommandBase : ICommand
{
public abstract bool CanExecute(object parameter);
public abstract void Execute(object parameter);
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}
添加 AddCarCommand 类
将名为AddCarCommand.cs的新类添加到Cmds文件夹中。将该类公开,并添加CommandBase作为基类。将以下using语句添加到文件的顶部:
using System.Collections.ObjectModel;
using System.Linq;
using WpfCommands.Models;
该参数应该是一个ObservableCollection<Car>,所以在CanExecute()方法中检查以确保这一点。如果是,那么Execute()方法应该添加一辆额外的汽车,就像Click事件处理程序一样。
public class AddCarCommand :CommandBase
{
public override bool CanExecute(object parameter)
=> parameter is ObservableCollection<Car>;
public override void Execute(object parameter)
{
if (parameter is not ObservableCollection<Car> cars)
{
return;
}
var maxCount = cars.Max(x => x.Id);
cars.Add(new Car
{
Id = ++maxCount,
Color = "Yellow",
Make = "VW",
PetName = "Birdie"
});
}
}
更新 MainWindow.xaml.cs
添加一个名为AddCarCmd的公共属性,类型为ICommand,带有一个支持字段。在属性的表达式体中,返回支持属性(如果支持字段为空,确保实例化一个新的AddCarCommand实例)。
private ICommand _addCarCommand = null;
public ICommand AddCarCmd
=> _addCarCommand ??= new AddCarCommand());
更新 MainWindow.xaml
更新 XAML 以删除Click属性,并添加Command和CommandParameter属性。AddCarCommand将从cboCars组合框中接收汽车列表。整个按钮的 XAML 如下:
<Button x:Name="btnAddCar" Content="Add Car" Margin="5,0,5,0" Padding="4, 2"
Command="{Binding Path=AddCarCmd,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=ItemsSource}"/>
有了这些,您现在可以使用独立类中包含的可重用代码添加汽车和更新汽车的颜色。
正在更新 ChangeColorCommand
最后一步是更新ChangeColorCommand以继承CommandBase。将ICommand改为CommandBase,在两个方法中添加override关键字,删除CanExecuteChanged代码。真的就这么简单!下面列出了新代码:
public class ChangeColorCommand : CommandBase
{
public override bool CanExecute(object parameter)
=> parameter is Car;
public override void Execute(object parameter)
{
((Car)parameter).Color = "Pink";
}
}
中继命令
在 WPF,命令模式的另一个实现是RelayCommand。该模式使用委托来实现ICommand接口,而不是为每个命令创建一个新的类。这是一个轻量级的实现,因为每个命令都没有自己的类。RelayCommand通常在执行命令不需要重用的时候使用。
创建基本继电器命令
通常在两个类中实现。当CanExecute()和Execute()方法不需要任何参数时,使用基类RelayCommand,当需要参数时,使用RelayCommand<T>。您将从基本的RelayCommand类开始,它利用了CommandBase类。将名为RelayCommand.cs的新类添加到Cmds文件夹中。将该类公开,并添加CommandBase作为基类。添加两个类级变量来保存Execute()和CanExecute()委托。
private readonly Action _execute;
private readonly Func<bool> _canExecute;
创建三个构造函数。第一个是默认构造函数(RelayCommand<T>-派生类需要),第二个是带Action参数的构造函数,第三个是带Action参数和Func参数的构造函数,如下所示:
public RelayCommand(){}
public RelayCommand(Action execute) : this(execute, null) { }
public RelayCommand(Action execute, Func<bool> canExecute)
{
_execute = execute
?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
最后,实现CanExecute()和Execute()覆盖。如果Func为空,则CanExecute()返回true;或者如果不为空,则执行并返回true。Execute()执行Action参数。
public override bool CanExecute(object parameter)
=> _canExecute == null || _canExecute();
public override void Execute(object parameter) { _execute(); }
创建继电器命令
将名为RelayCommandT.cs的新类添加到Cmds文件夹中。这个类几乎是基类的翻版,只是委托都带有一个参数。使该类成为公共的和通用的,并添加RelayCommand作为基类,如下所示:
public class RelayCommand<T> : RelayCommand
添加两个类级变量来保存Execute()和CanExecute()委托:
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
创建两个构造函数。第一个带一个Action<T>参数,第二个带一个Action<T>参数和一个Func<T,bool>参数,如下所示:
public RelayCommand(Action<T> execute):this(execute, null) {}
public RelayCommand(
Action<T> execute, Func<T, bool> canExecute)
{
_execute = execute
?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
最后,实现CanExecute()和Execute()覆盖。如果Func为空,则CanExecute()返回 true 或者,如果不为空,它执行并返回true。Execute()执行Action参数。
public override bool CanExecute(object parameter)
=> _canExecute == null || _canExecute((T)parameter);
public override void Execute(object parameter)
{ _execute((T)parameter); }
更新 MainWindow.xaml.cs
当您使用RelayCommand s 时,在构造新命令时,需要指定委托的所有方法。这并不意味着代码需要存在于代码隐藏中(如此处所示);它只需要可以从代码隐藏中访问。它可以存在于另一个类(甚至另一个程序集)中,提供创建自定义命令类的代码封装优势。
添加一个类型为RelayCommand<Car>的新私有变量和一个名为DeleteCarCmd的公共属性,如下所示:
private RelayCommand<Car> _deleteCarCommand = null;
public RelayCommand<Car> DeleteCarCmd
=> _deleteCarCommand ??=
new RelayCommand<Car>(DeleteCar,CanDeleteCar));
还必须创建DeleteCar()和CanDeleteCar()方法,如下所示:
private bool CanDeleteCar(Car car) => car != null;
private void DeleteCar(Car car)
{
_cars.Remove(car);
}
注意方法中的强类型——这是使用RelayCommand<T>的好处之一。
添加和实现删除汽车按钮
最后一步是添加按钮,并分配Command和CommandParameter绑定。添加以下标记:
<Button x:Name="btnDeleteCar" Content="Delete Car" Margin="5,0,5,0" Padding="4, 2"
Command="{Binding Path=DeleteCarCmd,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}"/>
现在,当您运行应用时,您可以测试只有在下拉列表中选择了一辆汽车时,Delete Car 按钮才被启用,并且单击该按钮确实会从汽车列表中删除该汽车。
包装命令
你在 WPF 司令部的短暂旅程到此结束。通过将事件处理从代码隐藏文件中移出并放到单独的命令类中,您可以获得代码封装、重用和提高可维护性的好处。如果您不需要那么多的关注点分离,您可以使用轻量级的RelayCommand实现。目标是提高可维护性和代码质量,因此选择最适合您的方法。
将代码和数据迁移到视图模型
正如在“WPF 验证”一节中一样,您可以继续在同一个项目中工作,或者您可以创建一个新的项目并复制所有的代码。我将创建一个名为 WpfViewModel 的新项目。如果您正在使用同一个项目,请务必注意本节代码示例中的名称空间,并根据需要进行调整。
在您的项目中创建一个名为ViewModels的新文件夹,并将名为MainWindowViewModel.cs的新类添加到该文件夹中。添加以下命名空间并将该类设为公共类:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Input;
using WpfViewModel.Cmds;
using WpfViewModel.Models;
Note
一个流行的惯例是根据视图模型支持的窗口来命名视图模型。我通常遵循这一惯例,并将在本章中这样做。然而,像任何模式或惯例一样,这不是一条规则,你会发现关于这一点有各种各样的观点。
移动 MainWindow.xaml.cs 代码
代码隐藏文件中的几乎所有代码都将被移动到视图模型中。最后,只有几行代码,包括对InitializeComponent()的调用和将窗口的数据上下文设置为视图模型的代码。
创建一个名为Cars的IList<Car>类型的公共属性,如下所示:
public IList<Car> Cars { get; } =
new ObservableCollection<Car>();
创建一个默认的构造函数,从MainWindow.xaml.cs文件中移走所有的汽车创建代码,更新列表变量名。您也可以从MainWindow.xaml.cs中删除_cars变量。以下是视图模型构造函数:
public MainWindowViewModel()
{
Cars.Add(
new Car { Id = 1, Color = "Blue", Make = "Chevy", PetName = "Kit", IsChanged = false });
Cars.Add(
new Car { Id = 2, Color = "Red", Make = "Ford", PetName = "Red Rider", IsChanged = false });
}
接下来,将所有与命令相关的代码从窗口代码隐藏文件移动到视图模型,更新对Cars的变量引用_cars。下面是更改后的代码:
//rest omitted for brevity
private void DeleteCar(Car car)
{
Cars.Remove(car);
}
更新主窗口代码和标记
从MainWindow.xaml.cs文件中删除了大部分代码。删除为组合框分配ItemsSource的行,只留下对InitializeComponent的调用。它现在应该是这样的:
public MainWindow()
{
InitializeComponent();
}
将下面的using语句添加到文件的顶部:
using WpfViewModel.ViewModels;
接下来,创建一个强类型属性来保存视图模型的实例。
public MainWindowViewModel ViewModel { get; set; }
= new MainWindowViewModel();
最后,在 XAML 的窗口声明中添加一个DataContext属性。
DataContext="{Binding ViewModel, RelativeSource={RelativeSource Self}}"
更新控件标记
既然Window的DataContext被设置为视图模型,控件的 XAML 绑定需要更新。从组合框开始,通过添加一个ItemsSource来更新标记。
<ComboBox Name="cboCars" Grid.Column="1" DisplayMemberPath="PetName" ItemsSource="{Binding Cars}" />
这是因为Window的数据上下文是MainWindowViewModel,而Cars是视图模型的公共属性。回想一下,绑定调用沿着元素树向上走,直到找到数据上下文。接下来,您需要更新Button控件的绑定。这很简单;由于绑定已经设置到窗口级别,您只需要更新绑定语句,从DataContext属性开始,如下所示:
<Button x:Name="btnAddCar" Content="Add Car" Margin="5,0,5,0" Padding="4, 2"
Command="{Binding Path=DataContext.AddCarCmd,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=ItemsSource}"/>
<Button x:Name="btnDeleteCar" Content="Delete Car" Margin="5,0,5,0" Padding="4, 2"
Command="{Binding Path=DataContext.DeleteCarCmd,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}" />
<Button x:Name="btnChangeColor" Content="Change Color" Margin="5,0,5,0" Padding="4, 2"
Command="{Binding Path=DataContext.ChangeColorCmd,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}"/>
包装视图模型
信不信由你,你刚刚完成了你的第一份 MVVM WPF 申请。您可能会想,“这不是真正的应用。数据呢?本例中的数据是硬编码的。你是对的。不是真正的 app 是 demoware。然而,这就是 MVVM 模式的美妙之处。视图不知道数据来自哪里;它只是绑定到视图模型上的一个属性。您可以交换视图模型实现,也许使用一个硬编码数据的版本用于测试,一个命中数据库的版本用于生产。
还有很多可以讨论的地方,包括各种开源框架、视图模型定位器模式,以及关于如何最好地实现该模式的许多不同观点。这就是软件设计模式的美妙之处——通常有许多正确的方法来实现它,然后您只需要根据您的业务和技术需求找到最佳方式。
更新自动 Lot。MVVM 的 Dal
如果你想为 MVVM 更新AutoLot.Dal,你必须将我们为Car类所做的更改应用到自动 Lot 中的所有实体。Dal.Models 项目,包括BaseEntity。
摘要
本章研究了支持模型-视图-视图模型模式的 WPF 主题。您已经开始学习如何在绑定管理器中将模型类和集合绑定到通知系统中。您实现了INotifyPropertyChanged并使用了ObservableCollections类来保持 UI 与绑定数据的同步。
接下来,使用IDataErrorInfo和INotifyDataErrorInfo向模型添加验证代码,并检查数据注释错误。然后,您在 UI 中显示任何验证错误,以便用户知道问题是什么以及如何修复它,并且您创建了一个样式和自定义控件模板来以有意义的方式呈现错误。
最后,您通过添加一个视图模型将所有这些放在一起,并且您清理了 UI 标记和代码隐藏文件以增加关注点的分离。
二十九、ASP.NET 核心简介
本书的最后一节介绍了 ASP.NET 核心,这是使用 C# 和。NET 核心。这一章介绍了 ASP.NET 核心和对以前版本的 web 开发框架 ASP.NET 的一些改变。
在了解了 ASP.NET 核心中实现的 MVC 模式的基础知识之后,您将开始构建两个能够协同工作的应用。第一个应用,一个 ASP.NET 核心 RESTful 服务,将在第三十章完成。第二个是使用模型-视图-控制器模式的 ASP.NET 核心 web 应用,将在第三十一章中完成。自动手枪。达尔和奥托洛特。你在第二十三章中完成的模型项目将作为两个应用的数据访问层。
快速回顾
微软在 2007 年发布了 ASP.NET MVC,取得了巨大的成功。该框架基于模型-视图-控制器模式,为对 WebForms 感到沮丧的开发人员提供了一个答案,web forms 本质上是 HTTP 上一个有漏洞的抽象。WebForms 的创建是为了帮助客户机-服务器开发人员转移到 Web 上,在这方面它相当成功。然而,随着开发人员越来越习惯于 web 开发,许多人希望对呈现的输出进行更多的控制,消除视图状态,并遵循经过验证的 web 应用设计模式。有了这些目标,ASP.NET MVC 就诞生了。
介绍 MVC 模式
模型-视图-控制器(MVC)模式自 20 世纪 70 年代就已经存在,最初是作为 Smalltalk 中使用的模式而创建的。这种模式最近死灰复燃,用许多不同的语言实现,包括 Java (Spring Framework)、Ruby (Ruby on Rails)和。NET(ASP.NET MVC)。
模型
模型是你的应用的数据。数据通常由普通的旧 CLR 对象(POCOs)表示。视图模型由一个或多个模型组成,并且是专门为数据消费者设计的。考虑模型和视图模型的一种方式是将它们与数据库表和数据库视图相关联。
从学术上讲,模型应该非常干净,不包含验证或任何其他业务规则。实际上,模型是否包含验证逻辑或其他业务规则完全取决于所使用的语言和框架,以及特定的应用需求。例如,EF Core 包含许多数据注释,这些注释既是形成数据库表的机制,也是在 ASP.NET Core web 应用中进行验证的手段。在本书中(以及在我的专业工作中),示例集中在减少代码的重复,这将数据注释和验证放在它们最有意义的地方。
景色
视图是应用的用户界面。视图接受命令并将这些命令的结果呈现给用户。视图应该尽可能的轻量级,并且不实际处理任何工作,而是将所有工作交给控制器。
控制器
控制器是应用的大脑。控制器通过动作方法接受来自用户(通过视图)或客户机(通过 API 调用)的命令/请求,并适当地处理它们。然后,操作的结果被返回给用户或客户端。控制器应该是轻量级的,并利用其他组件或服务来处理请求的细节。这促进了关注点的分离,并增加了可测试性和可维护性。
ASP.NET 核心和 MVC 模式
ASP.NET 核心能够创建许多类型的网络应用和服务。其中两个选项是使用 MVC 模式的 web 应用和 RESTful 服务。如果你使用过 ASP.NET 的“经典”,它们分别类似于 ASP.NET 的 MVC 和 ASP.NET 的 Web API。MVC web 应用和 API 应用类型共享模式的“模型”和“控制器”部分,而 MVC web 应用也实现“视图”来完成 MVC 模式。
ASP.NET 核心和。净核心
正如实体框架核心是实体框架 6 的完全重写,ASP.NET 核心是流行的 ASP.NET 框架的重写。重写 ASP.NET 不是一件小事,但为了消除对System.Web的依赖,这是必要的。消除这种依赖性使 ASP.NET 应用能够在 Windows 之外的操作系统和 Internet 信息服务(IIS)之外的其他 web 服务器上运行,包括自托管。这为 ASP.NET 核心应用使用名为 Kestrel 的跨平台、轻量级、快速和开源的 web 服务器打开了大门。Kestrel 提供了跨所有平台的统一开发体验。
Note
Kestrel 最初基于 LibUV,但是从 ASP.NET 核心 2.1 开始,它现在基于托管套接字。
和 EF Core 一样,ASP.NET Core 也正在 GitHub 上开发,作为一个完全开源的项目(https:// github.com/aspnet )。它也被设计成一个 NuGet 包的模块化系统。开发人员只安装特定应用所需的功能,从而最小化应用的占用空间,减少开销,并降低安全风险。其他改进包括简化的启动、内置的依赖注入、更简洁的配置系统和可插拔的中间件。
一个框架,多种用途
ASP.NET 核心中有许多变化和改进,您将在本节的其余章节中看到。除了跨平台能力,另一个重要的变化是 web 应用框架的统一。ASP.NET 核心将 ASP.NET MVC、ASP.NET Web API 和 Razor Pages 包含在一个开发框架中。开发 web 应用和服务。NET 框架提供了几种选择,包括 WebForms、MVC、Web API、Windows Communication Foundation(WCF)和 WebMatrix。它们都有积极和消极的一面;有些是密切相关的,其他的则大不相同。所有可用的选择意味着开发人员必须了解每一个选择,以便为手头的任务选择合适的一个,或者只选择一个并希望最好的。
借助 ASP.NET 核心,您可以构建使用 Razor 页面、模型-视图-控制器模式、RESTful 服务的应用,以及使用 Angular 和 React 等 JavaScript 框架的 SPA 应用。虽然 UI 呈现会随着 MVC、Razor Pages 和 JavaScript 框架之间的选择而变化,但是底层开发框架在所有选择中都是相同的。之前的两个选择是 WebForms 和 WCF,它们没有被引入 ASP.NET 核心系统。
Note
由于所有的独立框架都在同一个屋檐下,ASP.NET MVC 和 ASP.NET Web API 的前身已经正式退役。在本书中,为了简单起见,我仍然将使用模型-视图-控制器模式的 ASP.NET 核心 web 应用称为 MVC,将 ASP.NET RESTful 服务称为 Web API。
来自 MVC/Web API 的 ASP.NET 核心特性
许多让开发人员使用 ASP.NET MVC 和 ASP.NET Web API 的设计目标和特性在 ASP.NET 核心中仍然受到支持(并得到了改进)。下面列出了其中的一些(但不是全部):
-
约定胜于配置
-
控制器和动作
-
模型绑定
-
模型验证
-
选择途径
-
过滤
-
布局和剃刀视图
这些将在接下来的章节中介绍,除了布局和 Razor 视图,它们将在第三十一章中介绍。
约定胜于配置
ASP.NET MVC 和 ASP.NET Web API 通过引入某些约定减少了必要的配置量。当遵循这些约定时,会减少手动(或模板化)配置的数量,但也要求开发人员了解这些约定以便利用它们。两个主要的约定包括命名约定和目录结构。
命名规格
对于 MVC 和 API 应用,ASP.NET 核心有多种命名约定。例如,除了从Controller(或ControllerBase)派生之外,控制器通常以Controller后缀命名(例如HomeController)。通过路由访问时,Controller后缀会被删除。当查找控制器的视图时,控制器名称减去后缀就是开始搜索的位置。这种删除后缀的惯例在 ASP.NET 核心中重复出现。在接下来的章节中会有很多例子。
另一个命名约定用于视图位置和选择。默认情况下,一个动作方法(在 MVC 应用中)将呈现与该方法同名的视图。编辑器和显示模板以它们在视图中呈现的类命名。如果您的应用需要,可以更改这些默认值。所有这些都将在 AutoLot 时进一步探讨。构建 Mvc 应用。
目录结构
要成功构建 ASP.NET 核心 web 应用和服务,您必须了解几个文件夹约定。
控制器文件夹
按照惯例,Controllers文件夹是 ASP.NET 核心 MVC 和 API 实现(以及路由引擎)期望放置应用控制器的地方。
视图文件夹
Views文件夹是存储应用视图的地方。每个控制器在以控制器名称命名的主Views文件夹下有自己的文件夹(减去Controller后缀)。默认情况下,操作方法将在其控制器的文件夹中呈现视图。例如,Views/Home文件夹保存了HomeController控制器类的所有视图。
共享文件夹
在Views下面有一个特殊的文件夹叫做Shared。所有控制器及其操作方法都可以访问该文件夹。搜索以控制器命名的文件夹后,如果找不到视图,则在Shared文件夹中搜索视图。
wwwroot 文件夹(ASP.NETCore)
对 ASP.NET MVC 的一个改进是为 ASP.NET 核心 web 应用创建了一个名为wwwroot的特殊文件夹。在 ASP.NET MVC 中,JavaScript 文件、图像、CSS 和其他客户端内容与所有其他文件夹混合在一起。在 ASP.NET 核心中,客户端都包含在wwwroot文件夹下。当使用 ASP.NET 核心时,编译文件与客户端文件的这种分离显著地清理了项目结构。
控制器和动作
就像 ASP.NET MVC 和 ASP.NET Web API 一样,控制器和动作方法是 ASP.NET 核心 MVC 或 API 应用的核心。
控制器类
如前所述,ASP.NET 核心统一了 ASP.NET mv C5 和 ASP.NET Web API。这种统一还将 MVC5 和 Web API 2.2 中的Controller、ApiController和AsyncController基类合并成一个新类Controller,它有自己的基类,名为ControllerBase。ASP.NET 核心 web 应用控制器继承自Controller类,而 ASP.NET 核心服务控制器继承自ControllerBase类(将在下一章介绍)。
Controller类为 web 应用提供了许多助手方法。表 29-1 列出了最常用的方法。
表 29-1
由Controller类提供的一些助手方法
助手方法
|
生命的意义
|
| --- | --- |
| ViewDataTempDataViewBag | 通过ViewDataDictionary、TempDataDictionary和动态ViewBag传输向视图提供数据。 |
| View | 返回一个ViewResult(从ActionResult派生而来)作为 HTTP 响应。默认为与操作方法同名的视图,可以选择指定特定的视图。所有选项都允许指定一个强类型的view model并发送给View。视图包含在第三十一章中。 |
| PartialView | 向响应管道返回一个PartialViewResult。部分视图包含在第三十一章中。 |
| ViewComponent | 向响应管道返回一个ViewComponentResult。在第三十一章中有涉及。 |
| Json | 返回一个包含一个序列化为 JSON 的对象的JsonResult作为响应。 |
| OnActionExecuting | 在操作方法执行之前执行。 |
| OnActionExecutionAsync | OnActionExecuting的异步版本。 |
| OnActionExecuted | 在操作方法执行后执行。 |
ControllerBase 类
除了返回 HTTP 状态代码的帮助方法之外,ControllerBase类还为 ASP.NET 核心 web 应用和服务提供了核心功能。表 29-2 列出了ControllerBase中的一些核心功能,表 29-3 涵盖了一些返回 HTTP 状态码的帮助器方法。
表 29-3
由ControllerBase类提供的一些 HTTP 状态代码帮助器方法
助手方法
|
HTTP 状态代码操作结果
|
状态代码
|
| --- | --- | --- |
| NoContent | NoContentResult | 204 |
| Ok | OkResult | 200 |
| NotFound | NotFoundResult | 404 |
| BadRequest | BadRequestResult | 400 |
| CreatedCreatedAtActionCreatedAtRoute | CreatedResultCreatedAtActionResultCreateAtRouteResult | 201 |
| AcceptedAcceptedAtActionAcceptedAtRoute | AcceptedResultAcceptedAtActionResultAcceptedAtRouteResult | 202 |
表 29-2
由ControllerBase类提供的一些助手方法
助手方法
|
生命的意义
|
| --- | --- |
| HttpContext | 返回当前正在执行的动作的HttpContext。 |
| Request | 返回当前正在执行的动作的HttpRequest。 |
| Response | 返回当前正在执行的动作的HttpResponse。 |
| RouteData | 返回当前执行动作的RouteData(路由将在本章后面介绍)。 |
| ModelState | 返回与模型绑定和验证相关的模型状态(这两个问题将在本章后面讨论)。 |
| Url | 返回IUrlHelper的实例,提供对 ASP.NET 核心 MVC 应用和服务的构建 URL 的访问。 |
| User | 返回ClaimsPrincipal用户。 |
| Content | 向响应返回一个ContentResult。重载允许添加内容类型和编码定义。 |
| File | 向响应返回一个FileContentResult。 |
| Redirect | 通过返回一个RedirectResult将用户重定向到另一个 URL 的一系列方法。 |
| LocalRedirect | 仅当 URL 是本地的时,将用户重定向到另一个 URL 的一系列方法。比一般的Redirect方法更安全。 |
| RedirectToActionRedirectToPageRedirectToRoute | 重定向到另一个动作方法、Razor 页面或命名路由的一系列方法。本章稍后将介绍路由。 |
| TryUpdateModel | 显式模型绑定(将在本章后面介绍)。 |
| TryValidateModel | 显式模型验证(将在本章后面介绍)。 |
行动
动作是控制器上返回IActionResult(或者异步操作的Task<IActionResult>)或者实现IActionResult的类的方法,比如ActionResult或者ViewResult。在接下来的章节中将会详细介绍这些操作。
模型绑定
模型绑定是 ASP.NET 核心使用在 HTTP Post 调用中提交的名称-值对为模型赋值的过程。要绑定到引用类型,名称-值对来自表单值或请求正文,引用类型必须有一个公共的默认构造函数,并且要绑定的属性必须是公共的和可写的。当赋值时,隐式类型转换(例如使用int设置string属性值)在适用的地方被使用。如果类型转换不成功,该属性将被标记为错误。在更详细地讨论绑定之前,理解ModelState字典及其在绑定(和验证)过程中的作用是很重要的。
模型状态字典
ModelState字典包含每个被绑定属性的一个条目和模型本身的一个条目。如果在模型绑定期间出现错误,绑定引擎会将错误添加到属性的字典条目中,并设置ModelState.IsValid = false。如果所有匹配的属性都被成功分配,绑定引擎将设置ModelState.IsValid = true。
Note
模型验证也设置了ModelState字典条目,发生在模型绑定之后。隐式和显式模型绑定都会自动调用模型验证。下一节将介绍验证。
将自定义错误添加到模型状态字典中
除了由绑定引擎添加的属性和错误之外,还可以将自定义错误添加到ModelState字典中。可以在属性级别或整个模型中添加错误。要添加属性的特定错误(例如,Car实体的PetName属性),请使用以下命令:
ModelState.AddModelError("PetName","Name is required");
要为整个模型添加一个错误,使用string.Empty作为属性名,如下所示:
ModelState.AddModelError(string.Empty, $"Unable to create record: {ex.Message}");
隐式模型绑定
当要绑定的模型是动作方法的参数时,会发生隐式模型绑定。它对复杂类型使用反射和递归,将模型的可写属性名与发布到 action 方法的名称-值对中包含的名称进行匹配。如果存在名称匹配,绑定器将使用名称-值对中的值来尝试设置属性值。如果名称-值对中有多个名称匹配,则使用第一个匹配名称的值。如果找不到名称,该属性将设置为默认值。名称-值对的搜索顺序如下:
-
来自 HTTP Post 方法的表单值(包括 JavaScript AJAX posts)
-
请求体(用于 API 控制器)
-
通过 ASP.NET 核心路由提供的路由值(对于简单类型)
-
查询字符串值(对于简单类型)
-
上传的文件(针对
IFormFile类型)
例如,下面的方法将尝试设置Car类型的所有属性。如果绑定过程没有错误地完成,ModelState.IsValid属性返回true。
[HttpPost]
public ActionResult Create(Car entity)
{
if (ModelState.IsValid)
{
//Save the data;
}
}
显式模型绑定
显式模型绑定是通过调用TryUpdateModelAsync()来执行的,传递被绑定类型的实例和要绑定的属性列表。如果模型绑定失败,该方法返回false并以与隐式模型绑定相同的方法设置ModelState错误。当使用显式模型绑定时,被绑定的类型不是 action 方法的参数。例如,您可以这样编写前面的Create()方法,并使用显式模型绑定:
[HttpPost]
public async Task<IActionResult> Create()
{
var vm = new Car();
if (await TryUpdateModelAsync(vm,"",
c=>c.Color,c=>c.PetName,c=>c.MakeId, c=>c.TimeStamp))
{
//do something important
}
}
绑定属性
HTTP Post 方法中的Bind属性允许您限制参与模型绑定的属性,或者为名称-值对中的名称设置前缀。限制可以绑定的属性有助于减少过度发布攻击的威胁。如果一个Bind属性被放置在一个引用参数上,那么在Include列表中列出的字段是唯一将通过模型绑定被分配的字段。如果没有使用Bind属性,所有字段都是可绑定的。
在下面的Create()动作方法示例中,Car实例上的所有字段都可以用于模型绑定,因为没有使用Bind属性:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(Car car)
{
if (ModelState.IsValid)
{
//Add the record
}
//Allow the user to retry
}
假设您的业务需求指定只允许更新Create()方法中的PetName和Color字段。添加Bind属性(如下例所示)会限制参与绑定的属性,并指示模型绑定器忽略其余的属性。
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(
[Bind(nameof(Car.PetName),nameof(Car.Color))]Car car)
{
if (ModelState.IsValid)
{
//Save the data;
}
//Allow the user to retry
}
Bind属性也可以用来指定属性名的前缀。如果名称-值对的名称在发送给 action 方法时添加了前缀,那么Bind属性用于通知ModelBinder如何将名称映射到类型的属性。以下示例为名称设置前缀,并允许绑定所有属性:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(
[Bind(Prefix="MakeList")]Car car)
{
if (ModelState.IsValid)
{
//Save the data;
}
}
控制 ASP.NET 核心中的模型绑定源
绑定源可以通过动作参数上的一组属性来控制。也可以创建定制的模型绑定;然而,这超出了本书的范围。表 29-4 列出了可用于控制模型绑定的属性。
表 29-4
控制模型绑定源
|属性
|
生命的意义
|
| --- | --- |
| BindingRequired | 如果无法发生绑定,将会添加模型状态错误,而不只是将属性设置为其默认值。 |
| BindNever | 告诉模型绑定器永远不要绑定到这个参数。 |
| FromHeaderFromQueryFromRouteFromForm | 用于指定要应用的确切绑定源(头、查询字符串、路由参数或表单值)。 |
| FromServices | 使用依赖注入绑定类型(将在本章后面介绍)。 |
| FromBody | 从请求体绑定数据。基于请求的内容(例如,JSON、XML 等)选择格式化程序。).最多只能有一个用FromBody属性修饰的参数。 |
| ModelBinder | 用于覆盖默认模型绑定器(用于自定义模型绑定)。 |
模型验证
模型验证在模型绑定(显式和隐式)之后立即发生。由于转换问题,模型绑定会向ModelState数据字典添加错误,而验证会基于业务规则向ModelState数据字典添加错误。业务规则的示例包括必填字段、具有最大允许长度的字符串或在特定允许范围内的日期。
验证规则是通过内置或自定义的验证属性设置的。表 29-5 列出了一些内置的验证属性。请注意,有几个还兼作数据注释,用于形成 EF 核心实体。
表 29-5
一些内置的验证属性
|属性
|
生命的意义
|
| --- | --- |
| CreditCard | 对信用卡号执行 Luhn-10 检查 |
| Compare | 验证模型匹配中的两个属性 |
| EmailAddress | 验证属性是否具有有效的电子邮件格式 |
| Phone | 验证属性是否具有有效的电话号码格式 |
| Range | 验证属性是否在指定的范围内 |
| RegularExpression | 验证属性是否与指定的正则表达式匹配 |
| Required | 验证属性是否有值 |
| StringLength | 验证属性没有超过最大长度 |
| Url | 验证属性是否具有有效的 URL 格式 |
| Remote | 通过调用服务器上的操作方法来验证客户端上的输入 |
还可以开发定制的验证属性,但不在本书中讨论。
选择途径
路由是 ASP.NET 核心如何将 HTTP 请求匹配到应用中的控制器和动作(可执行的端点),而不是将 URL 匹配到项目文件结构的旧 Web 表单过程。它还提供了一种基于这些端点从应用内部创建 URL 的机制。MVC 或 Web API 风格的应用中的端点由控制器、动作(仅限 MVC)、HTTP 动词和可选值(称为路由值)组成。
Note
路由也适用于 Razor pages、SignalR、gRPC 服务等。这本书涵盖了 MVC 和 Web API 风格的控制器。
ASP.NET 核心使用路由中间件来匹配传入请求的 URL,并生成响应中发出的 URL。中间件注册在Startup类中,端点添加在Startup类中,或者通过路由属性添加,这两个都将在本章后面介绍。
URL 模式和路由令牌
路由端点由 URL 模式和文字组成,URL 模式包含可变占位符(称为标记,文字放入一个有序集合中,称为路由表。每个条目定义一个不同的 URL 模式来匹配。占位符可以是自定义变量,也可以来自预定义列表。表 29-6 列出了保留的路由名称。
表 29-6
为 MVC 和 API 应用保留路由令牌
|代币
|
生命的意义
|
| --- | --- |
| Area | 定义路线的 MVC 区域 |
| Controller | 定义控制器(减去控制器后缀) |
| Action | 定义 MVC 应用中的动作名称 |
除了保留的令牌之外,路由还可以包含映射(模型绑定)到动作方法参数的自定义令牌。
路由和 ASP.NET 核心 RESTful 服务
为 ASP.NET 服务定义路由时,未指定操作方法。相反,一旦找到控制器,要执行的操作方法就基于请求的 HTTP 谓词和分配给操作方法的 HTTP 谓词。稍后会有更多内容。
传统路由
传统路由在Startup类的UseEndpoints()方法中构建路由表。MapControllerRoute()方法将端点添加到路由表中。方法为 URL 模式中的变量指定名称、URL 模式和任何默认值。在下面的代码示例中,预定义的{controller}和{action}占位符引用一个控制器和包含在该控制器中的动作方法。占位符{id}是自定义的,并被转换为动作方法的参数(名为id)。向路由令牌添加问号表示它是可选的。
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
当请求 URL 时,会对照路由表进行检查。如果匹配,则执行位于该应用端点的代码。由该路由提供服务的一个示例 URL 是Car/Delete/5。这将调用CarController上的Delete()动作方法,将 5 传递给id参数。
缺省值指定如何为不包含所有已定义组件的 URL 填充空格。在前面的代码中,如果 URL 中没有指定任何内容(例如http://localhost:5001,那么路由引擎将调用HomeController类的Index()操作方法,而不使用id参数。缺省值是渐进的,这意味着它们可以从右到左被排除。但是,路线部分不能跳过。输入一个像http://localhost:60466/Delete/5这样的网址将无法通过{controller}/{action}/{id}模式。
路由引擎将尝试根据控制器、操作、自定义令牌和 HTTP 谓词来查找第一条路由。如果路由引擎不能确定最佳路径,它将抛出一个AmbiguousMatchException。
请注意,路由模板不包含协议或主机名。路由引擎在创建路由时会自动预先考虑正确的信息,并使用 HTTP 谓词、路径和参数来确定正确的应用端点。例如,如果您的站点在 https://www.skimedic.com 上运行,协议(HTTPS)和主机名( www.skimedic.com )会在创建时自动添加到路由前面(例如 https://www.skimedic.com/Car/Delete/5 )。对于传入的请求,路由引擎使用 URL 的Car/Delete/5部分。
命名路线
路由名称可以用作在应用中生成 URL 的简写。在之前的常规回合中,端点被赋予名称default。
属性路由
在属性路由中,路由是使用控制器及其动作方法上的 C# 属性来定义的。这可以导致更精确的路由,但也会增加配置量,因为每个控制器和动作都需要指定路由信息。
例如,以下面的代码片段为例。Index()动作方法上的四个Route属性等同于前面定义的相同路线。Index()动作方法是应用端点为 mysite.com 、 mysite.com/Home 、 mysite.com/Home/Index 或 mysite.com/Home/Index/5 。
public class HomeController : Controller
{
[Route("/")]
[Route("/Home")]
[Route("/Home/Index")]
[Route("/Home/Index/{id?}")]
public IActionResult Index(int? id)
{
...
}
}
传统路由和属性路由的主要区别在于,传统路由覆盖应用,而属性路由覆盖带有Route属性的控制器。如果不使用常规路由,每个控制器都需要定义路由,否则将无法访问。例如,如果路由表中没有定义默认路由,则无法发现以下代码,因为控制器没有配置任何路由:
public class CarController : Controller
{
public IActionResult Delete(int id)
{
...
}
}
Note
常规和属性路由可以一起使用。如果默认控制器路由设置在UseEndpoints()(如在传统路由示例中),前面的控制器将通过路由表定位。
当在控制器级别添加路线时,动作方法源自该基本路线。例如,下面的控制器路线涵盖了 Delete() (以及任何其他)动作方法:
[Route("[controller]/[action]/{id?}")]
public class CarController : Controller
{
public IActionResult Delete(int id)
{
...
}
}
Note
在属性路由中使用方括号([])来区分内置令牌,而不是传统路由中使用的花括号({})。自定义标记仍然使用花括号。
如果一个动作方法需要重新启动路由模式,在路由前加上一个正斜杠(/)。例如,如果删除方法应该遵循 URL 模式 mysite.com/Delete/Car/5 ,则配置操作如下:
[Route("[controller]/[action]/{id?}")]
public class CarController : Controller
{
[Route("/[action]/[controller]/{id}")]
public IActionResult Delete(int id)
{
...
}
}
路由还可以对路由值进行硬编码,而不是使用令牌替换。下面的代码将产生与前面的代码示例相同的结果:
[Route("[controller]/[action]/{id?}")]
public class CarController : Controller
{
[Route("/Delete/Car/{id}")]
public IActionResult Delete(int id)
{
...
}
}
命名路线
也可以为路线指定名称。这就创建了一种简单的方法,只需使用名称就可以重定向到特定的路由。例如,以下路由属性的名称为GetOrderDetails:
[HttpGet("{orderId}", Name = "GetOrderDetails")]
路由和 HTTP 动词
您可能已经注意到,这两种路由模板定义方法都没有定义 HTTP 动词。这是因为路由引擎(在 MVC 和 API 风格的应用中)结合使用路由模板和 HTTP 动词来选择适当的应用端点。
Web 应用(MVC)路由中的 HTTP 谓词
当使用 MVC 模式构建 web 应用时,通常会有两个应用端点匹配特定的路由模板。这些实例中的鉴别器是 HTTP 动词。例如,如果CarController包含两个名为Delete()的动作方法,并且它们都匹配路由模板,那么选择执行哪个方法是基于请求中使用的动词。第一个Delete()方法是用HttpGet属性修饰的,将在传入请求是 get 请求时执行。第二个Delete()方法用HttpPost属性修饰,将在传入请求是 post 时执行。
[Route("[controller]/[action]/{id?}")]
public class CarController : Controller
{
[HttpGet]
public IActionResult Delete(int id)
{
...
}
[HttpPost]
public IActionResult Delete(int id, Car recordToDelete)
{
...
}
}
也可以使用 HTTP 动词属性而不是Route属性来修改路由。例如,下面显示了添加到两个Delete()方法的路由模板中的可选id路由令牌:
[Route("[controller]/[action] ")]
public class CarController : Controller
{
[HttpGet("{id?}")]
public IActionResult Delete(int? id)
{
...
}
[HttpPost("{id}")]
public IActionResult Delete(int id, Car recordToDelete)
{
...
}
}
也可以使用 HTTP 动词重新启动路由;只需在模板化的路线前加一个正斜杠(/),如下例所示:
[HttpGet("/[controller]/[action]/{makeId}/{makeName}")]
public IActionResult ByMake(int makeId, string makeName)
{
ViewBag.MakeName = makeName;
return View(_repo.GetAllBy(makeId));
}
Note
如果 action 方法没有用 HTTP verb 属性修饰,它默认为 get 方法。但是,在 MVC web 应用中,未标记的动作方法也可以响应 post 请求。因此,用正确的动词属性显式标记所有动作方法被认为是最佳实践。
API 服务路由
用于 MVC 风格应用的路由定义和用于 RESTful 服务的路由定义之间的一个显著区别是,服务路由定义不指定动作方法。操作方法是根据请求的 HTTP 动词(和可选的内容类型)而不是名称来选择的。下面的代码展示了一个 API 控制器,它有四个方法,都匹配同一个路由模板。请注意 HTTP 动词属性:
[Route("api/[controller]")]
[ApiController]
public class CarController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetCarsById(int id)
{
...
}
[HttpPost]
public IActionResult CreateANewCar(Car entity)
{
...
}
[HttpPut("{id}")]
public IActionResult UpdateAnExistingCar(int id, Car entity)
{
...
}
[HttpDelete("{id}")]
public IActionResult DeleteACar(int id, Car entity)
{
...
}
}
如果一个动作方法没有 HTTP 动词属性,它将被视为 get 请求的应用端点。如果路由请求匹配,但没有具有正确动词属性的操作方法,服务器将返回 404(未找到)。
Note
如果名称以 Get 、 Put 、 Delete 或 Post 开头,ASP.NET Web API 允许您省略方法的 HTTP 动词。这个惯例通常被认为是一个坏主意,并已在 ASP.NET 核心删除。如果一个动作方法没有指定 HTTP 谓词,它将被使用 HTTP Get 调用。
API 控制器的最后一个端点选择器是可选的Consumes属性,它指定端点接受的内容类型。该请求必须使用匹配的内容类型头,否则将返回 415 不支持的媒体类型错误。以下两个示例端点都在同一个控制器中,它们区分了 JSON 和 XML:
[HttpPost]
[Consumes("application/json")]
public IActionResult PostJson(IEnumerable<int> values) =>
Ok(new { Consumes = "application/json", Values = values });
[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
public IActionResult PostForm([FromForm] IEnumerable<int> values) =>
Ok(new { Consumes = "application/x-www-form-urlencoded", Values = values });
使用路由重定向
路由的另一个优点是你不再需要为你站点中的其他页面硬编码 URL。路由条目用于匹配传入的请求以及构建 URL。构建 URL 时,会根据当前请求的值添加方案、主机和端口。
过滤
ASP.NET 核心中的过滤器在请求处理管道的特定阶段之前或之后运行代码。有用于授权和缓存的内置过滤器,以及用于分配客户过滤器的选项。表 29-7 列出了可以添加到管道中的过滤器类型,按照它们的执行顺序列出。
表 29-7
ASP.NET 核心中可用的过滤器
|过滤器
|
生命的意义
| | --- | --- | | 授权过滤器 | 首先运行并确定用户是否有权处理当前请求。 | | 资源筛选器 | 在授权筛选器之后立即运行,并且可以在管道的其余部分完成之后运行。在模型绑定之前运行。 | | 动作过滤器 | 在执行操作之前和/或执行操作之后立即运行。可以改变传递给操作的值和从操作返回的结果。 | | 异常过滤器 | 用于将全局策略应用于写入响应正文之前发生的未处理异常。 | | 结果过滤器 | 成功执行操作结果后立即运行代码。对于围绕视图或格式化程序执行的逻辑很有用。 |
授权过滤器
授权过滤器与 ASP.NET 核心身份系统配合使用,以防止对用户无权使用的控制器或操作的访问。不建议构建自定义授权过滤器,因为内置的AuthorizeAttribute和AllowAnonymousAttribute通常在使用 ASP.NET 核心身份时提供足够的覆盖范围。
资源筛选器
before 代码在授权筛选器之后和任何其他筛选器之前执行,after 代码在所有其他筛选器之后执行。这使得资源筛选器能够缩短整个响应管道。资源过滤器的一个常见用户是用于缓存。如果响应在缓存中,过滤器可以跳过管道的其余部分。
动作过滤器
before 代码在 action 方法执行之前立即执行,after 代码在 action 方法执行之后立即执行。动作过滤器可以短路动作方法和被动作过滤器包装的任何过滤器(执行和包装的顺序将很快介绍)。
异常过滤器
异常过滤器在应用中实现横切错误处理。它们没有 before 或 after 事件,但是它们处理控制器创建、模型绑定、动作过滤器或动作方法中任何未处理的异常。
结果过滤器
结果过滤器包装动作方法的IActionResult的执行。一个常见的场景是使用结果过滤器将头信息添加到 HTTP 响应消息中。
ASP.NET 核心的新功能
除了支持 ASP.NET MVC 和 ASP.NET Web API 的基本功能之外,该团队还能够在以前的框架上添加许多新功能和改进。除了框架和控制器的统一之外,还有一些额外的改进和创新:
-
内置依赖注入。
-
云就绪、基于环境的配置系统。
-
轻量级、高性能和模块化的 HTTP 请求管道。
-
整个框架基于细粒度的 NuGet 包。
-
现代客户端框架和开发工作流的集成。
-
标签助手介绍。
-
视图组件介绍。
-
性能的巨大提高。
内置依赖注入
依赖注入(DI)是一种支持对象间松散耦合的机制。参数被定义为接口,而不是直接创建依赖对象或将特定的实现传递给类和/或方法。这样,接口的任何实现都可以传递到类或方法和类中,极大地增加了应用的灵活性。
DI 支持是重写 ASP.NET 核心的主要原则之一。不仅仅是Startup类(本章后面会讲到)通过依赖注入接受所有的配置和中间件服务,您的定制类也可以(并且应该)被添加到服务容器中,以注入到应用的其他部分。当一个项目被配置到 ASP.NET 核心 DI 容器中时,有三个生命周期选项,如表 29-8 所示。
表 29-8
服务的终身选项
|终身期权
|
提供的功能
|
| --- | --- |
| Transient | 在需要的时候创建*。* |
| Scoped | 为每个请求创建一次。推荐用于实体框架DbContext对象。 |
| Singleton | 在第一次请求时创建一次,然后在对象的生存期内重用。相对于将类作为单例实现,这是推荐的方法。 |
DI 容器中的条目可以被注入到类构造函数和方法以及 Razor 视图中。
Note
如果你想使用一个不同的依赖注入容器,ASP.NET 核心就是考虑到这种灵活性而设计的。查阅文档,了解如何插入不同的容器: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection 。
环境意识
ASP.NET 核心应用通过一个IWebHostEnvironment实例了解其执行环境,包括主机环境变量和文件位置。表 29-9 显示了通过该接口可用的属性。
表 29-9
IWebHostEnvironment 属性
|财产
|
提供的功能
|
| --- | --- |
| ApplicationName | 获取或设置应用的名称。默认为入口程序集的名称。 |
| ContentRootPath | 获取或设置包含应用内容文件的目录的绝对路径。 |
| ContentRootFileProvider | 获取或设置一个指向ContentRootPath的IFileProvider。 |
| EnvironmentName | 获取或设置环境的名称。设置为环境变量ASPNETCORE_ENVIRONMENT的值。 |
| WebRootFileProvider | 获取或设置一个指向WebRootPath的IFileProvider。 |
| WebRootPath | 获取或设置包含 web 可服务的应用内容文件的目录的绝对路径。 |
除了访问相关的文件路径,IWebHostEnvironment用于确定运行时环境。
确定运行时环境
ASP.NET 核心自动读取名为ASPNETCORE_ENVIRONMENT的环境变量的值来设置运行时环境。如果未设置变量ASPNETCORE_ENVIRONMENT,ASP.NET 核心将该值设置为Production。通过IWebHostEnvironment上的EnvironmentName属性可以访问该值集。
在开发 ASP.NET 核心应用时,通常使用设置文件或命令行来设置此变量。下游环境(试运行、生产等。)通常使用标准的操作系统环境变量。
您可以自由地使用环境的任何名称或者由静态类Environments提供的三个名称。
public static class Environments
{
public static readonly string Development = "Development";
public static readonly string Staging = "Staging";
public static readonly string Production = "Production";
}
HostEnvironmentEnvExtensions类在IHostEnvironment上提供了扩展方法,用于处理环境名称属性。表 29-10 列出了可用的便利方法。
表 29-10
HostEnvironmentEnvExtensions 方法
|方法
|
提供的功能
|
| --- | --- |
| IsProduction | 如果环境变量设置为Production(不区分大小写),则返回 true |
| IsStaging | 如果环境变量设置为Staging(不区分大小写),则返回 true |
| IsDevelopment | 如果环境变量设置为Development(不区分大小写),则返回 true |
| IsEnvironment | 如果环境变量与传递给方法的字符串匹配,则返回 true(不区分大小写) |
以下是使用环境设置的一些示例:
-
确定要加载哪些配置文件
-
设置调试、错误和日志记录选项
-
加载特定于环境的 JavaScript 和 CSS 文件
在构建自动 Lot 的过程中,您将会看到其中的每一项。Api 和 AutoLot。下两章将讨论 Mvc 应用。
应用配置
以前版本的 ASP.NET 使用web.config文件来配置服务和应用,开发者通过System.Configuration类访问配置设置。当然,网站的所有配置设置,而不仅仅是特定应用的设置,都被转储到web.config文件中,使得它(可能)变得复杂混乱。
ASP.NET 核心引入了一个大大简化的配置系统。默认情况下,它基于简单的 JSON 文件,这些文件将配置设置保存为名称-值对。配置的默认文件是appsettings.json文件。初始版本的appsettings.json文件(由 ASP.NET 核心 web 应用和 API 服务模板创建)仅包含日志记录的配置信息以及限制主机的设置,如下所示:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
该模板还创建了一个appsettings.Development.json文件。配置系统与运行时环境感知协同工作,以基于运行时环境加载附加的配置文件。这是通过指示配置系统在appSettings.json文件之后加载一个名为appsettings.{environmentname}.json的文件来实现的。在开发下运行时,appsettings.Development.json文件在初始设置文件之后加载。如果环境正在升级,则加载appsettings.Staging.json文件。值得注意的是,当加载多个文件时,两个文件中出现的任何设置都会被最后加载的文件覆盖;它们不是相加的。
所有配置值都可以通过一个IConfiguration实例来访问,该实例可以通过 ASP.NET 核心依赖注入系统获得。
正在检索设置
一旦构建好配置,就可以使用传统的Get系列方法来访问设置,比如GetSection()、GetValue()等等。
Configuration.GetSection("Logging")
还有一个获取应用连接字符串的快捷方式。
Configuration.GetConnectionString("AutoLot")
本书的其余部分将会用到更多的配置特性。
部署 ASP.NET 核心应用
以前版本的 ASP.NET 应用只能部署到使用 IIS 的 Windows 服务器上。ASP.NET 核心可以以多种方式部署到多个操作系统,包括在 web 服务器之外。高级选项如下:
-
在使用 IIS 的 Windows 服务器(包括 Azure)上
-
在 IIS 之外的 Windows 服务器(包括 Azure 应用服务)上
-
在使用 Apache 或 NGINX 的 Linux 服务器上
-
在 Docker 容器中的 Windows 或 Linux 上
这种灵活性允许组织决定对组织最有意义的部署平台,包括流行的基于容器的部署模型(如使用 Docker),而不是局限于 Windows 服务器。
轻量级和模块化的 HTTP 请求管道
遵循…的原则。净核心,你必须选择在 ASP.NET 核心的一切。默认情况下,应用中不会加载任何内容。这使得应用尽可能地轻量级,提高性能并最小化表面区域和潜在风险。
创建和配置解决方案
现在,您已经了解了 ASP.NET 核心的一些主要概念,是时候开始构建 ASP.NET 核心应用了。可以使用 Visual Studio 或命令行创建 ASP.NET 核心项目。这两个选项都将在接下来的两节中讨论。
使用 Visual Studio
Visual Studio 具有 GUI 的优势,可以引导您完成创建解决方案和项目、添加 NuGet 包以及创建项目间引用的过程。
创建解决方案和项目
首先在 Visual Studio 中创建新项目。从“创建新项目”对话框中选择 C# 模板 ASP.NET 核心 Web 应用。在“配置你的新项目”对话框中,输入 AutoLot。项目名称为 Api ,解决方案名称为 AutoLot ,如图 29-1 所示。
图 29-1
创建自动 Lot。Api 项目和自动 Lot 解决方案
在下一个屏幕上,选择 ASP.NET 核心 Web API 模板。NET 核心和 ASP.NET 核心 5.0。保留高级复选框的默认设置,如图 29-2 所示。
图 29-2
选择 ASP.NET 核心 Web API 模板
接下来,向解决方案添加另一个 ASP.NET 核心 web 应用。选择 ASP.NET 核心 Web 应用(模型-视图-控制器)模板。确保。在顶部的选择框中选择了 NET Core 和 ASP.NET Core 5.0;保留高级复选框的默认值。
最后,添加一个 C# 类库(。NET Core)添加到项目中,并将其命名为 AutoLot.Services,编辑项目文件,将TargetFramework设置为net5.0,如下所示:
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
加入自动 Lot。模型和自动 Lot。木豆
该解决方案需要第二十三章中完整的数据访问层。您可以将文件复制到当前的解决方案目录中,也可以将它们留在原处。无论哪种方式,您都需要在解决方案资源管理器中右键单击您的解决方案名称,选择添加➤现有项目,并导航到AutoLot.Models.csproj文件并选择它。对自动 Lot 重复上述步骤。Dal 项目。
Note
虽然项目添加到解决方案的顺序在技术上并不重要,但 Visual Studio 将保留 AutoLot 之间的引用。模型和自动 Lot。如果首先添加模型项目,则为 Dal。
添加项目引用
通过在解决方案资源管理器中右击项目名称并为每个项目选择“添加➤项目引用”,添加以下项目引用。
AutoLot。Api 和 AutoLot。Mvc 引用了以下内容:
-
AutoLot。模型
-
汽车旅馆
-
AutoLot。服务
AutoLot。服务参考了以下内容:
-
AutoLot。模型
-
汽车旅馆
添加 NuGet 包
需要附加的 NuGet 包来完成应用。
去自动售货机。Api 项目,添加以下包:
-
AutoMapper -
System.Text.Json -
Swashbuckle.AspNetCore.Annotations -
Swashbuckle.AspNetCore.Swagger -
Swashbuckle.AspNetCore.SwaggerGen -
Swashbuckle.AspNetCore.SwaggerUI -
Microsoft.VisualStudio.Web.CodeGeneration.Design -
Microsoft.EntityFrameworkCore.SqlServer
Note
在 ASP.NET 核心 5.0 API 模板中,Swashbuckle.AspNetCore已经被引用。列出的Swashbuckle包增加了基本实现之外的功能。
去自动售货机。Mvc 项目,添加以下包:
-
AutoMapper -
System.Text.Json -
LigerShark.WebOptimizer.Core -
Microsoft.Web.LibraryManager.Build -
Microsoft.VisualStudio.Web.CodeGeneration.Design -
Microsoft.EntityFrameworkCore.SqlServer
去自动售货机。服务项目,添加以下包:
-
Microsoft.Extensions.Hosting.Abstractions -
Microsoft.Extensions.Options -
Serilog.AspNetCore -
Serilog.Enrichers.Environment -
Serilog.Settings.Configuration -
Serlog.Sinks.Console -
Serilog.Sinks.File -
Serilog.Sinks.MSSqlServer -
System.Text.Json
使用命令行
如本书前面所示。NET 核心项目和解决方案可以使用命令行创建。打开提示符并导航到您希望解决方案所在的目录。
Note
列出的命令使用 Windows 目录分隔符。如果您使用的是非 Windows 操作系统,请根据需要调整分隔符。
以下命令创建自动放样解决方案并添加现有自动放样。模型和自动 Lot。Dal 项目融入解决方案:
rem create the solution
dotnet new sln -n AutoLot
rem add autolot dal to solution
dotnet sln AutoLot.sln add ..\Chapter_23\AutoLot.Models
dotnet sln AutoLot.sln add ..\Chapter_23\AutoLot.Dal
创建自动 Lot。服务项目,将其添加到解决方案中,添加 NuGet 包,并添加项目引用。
rem create the class library for the application services and add it to the solution
dotnet new classlib -lang c# -n AutoLot.Services -o .\AutoLot.Services -f net5.0
dotnet sln AutoLot.sln add AutoLot.Services
dotnet add AutoLot.Services package Microsoft.Extensions.Hosting.Abstractions
dotnet add AutoLot.Services package Microsoft.Extensions.Options
dotnet add AutoLot.Services package Serilog.AspNetCore
dotnet add AutoLot.Services package Serilog.Enrichers.Environment
dotnet add AutoLot.Services package Serilog.Settings.Configuration
dotnet add AutoLot.Services package Serilog.Sinks.Console
dotnet add AutoLot.Services package Serilog.Sinks.File
dotnet add AutoLot.Services package Serilog.Sinks.MSSqlServer
dotnet add AutoLot.Services package System.Text.Json
dotnet add AutoLot.Services reference ..\Chapter_23\AutoLot.Models
dotnet add AutoLot.Services reference ..\Chapter_23\AutoLot.Dal
创建自动 Lot。Api 项目,将其添加到解决方案中,添加 NuGet 包,并添加项目引用。
dotnet new webapi -lang c# -n AutoLot.Api -au none -o .\AutoLot.Api -f net5.0
dotnet sln AutoLot.sln add AutoLot.Api
dotnet add AutoLot.Api package AutoMapper
dotnet add AutoLot.Api package Swashbuckle.AspNetCore
dotnet add AutoLot.Api package Swashbuckle.AspNetCore.Annotations
dotnet add AutoLot.Api package Swashbuckle.AspNetCore.Swagger
dotnet add AutoLot.Api package Swashbuckle.AspNetCore.SwaggerGen
dotnet add AutoLot.Api package Swashbuckle.AspNetCore.SwaggerUI
dotnet add AutoLot.Api package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add AutoLot.Api package Microsoft.EntityFrameworkCore.SqlServer
dotnet add AutoLot.Api package System.Text.Json
dotnet add AutoLot.Api reference ..\Chapter_23\AutoLot.Dal
dotnet add AutoLot.Api reference ..\Chapter_23\AutoLot.Models
dotnet add AutoLot.Api reference AutoLot.Services
创建自动 Lot。Api 项目,将其添加到解决方案中,添加 NuGet 包,并添加项目引用。
dotnet new mvc -lang c# -n AutoLot.Mvc -au none -o .\AutoLot.Mvc -f net5.0
dotnet sln AutoLot.sln add AutoLot.Mvc
rem add project references
dotnet add AutoLot.Mvc reference ..\Chapter_23\AutoLot.Models
dotnet add AutoLot.Mvc reference ..\Chapter_23\AutoLot.Dal
dotnet add AutoLot.Mvc reference AutoLot.Services
rem add packages
dotnet add AutoLot.Mvc package AutoMapper
dotnet add AutoLot.Mvc package System.Text.Json
dotnet add AutoLot.Mvc package LigerShark.WebOptimizer.Core
dotnet add AutoLot.Mvc package Microsoft.Web.LibraryManager.Build
dotnet add AutoLot.Mvc package Microsoft.EntityFrameworkCore.SqlServer
dotnet add AutoLot.Mvc package Microsoft.VisualStudio.Web.CodeGeneration.Design
这就完成了使用命令行的设置。如果你不需要 Visual Studio GUI 来帮助你,它会更有效。
运行 ASP.NET 核心应用
以前版本的 ASP.NET web 应用总是使用 IIS(或 IIS Express)运行。借助 ASP.NET 核心,应用通常使用 Kestrel web 服务器运行,并可选择使用 IIS、Apache、Nginx 等。通过 Kestrel 和其他 web 服务器之间的反向代理。这不仅背离了严格使用 IIS 来改变部署模型,而且还改变了开发的可能性。在开发过程中,您现在可以通过以下方式运行您的应用:
-
从 Visual Studio,使用 IIS Express
-
在 Visual Studio 中,使用 Kestrel
-
在命令提示符下使用。NET CLI,使用 Kestrel
-
从 Visual Studio 代码,使用 Kestrel,从运行菜单
-
在 Visual Studio 代码的终端窗口中,使用。NET CLI 和 Kestrel
配置启动设置
launchsettings.json文件(位于解决方案资源管理器的 Properties 节点下)配置应用如何在开发中运行,包括在 Kestrel 和 IIS Express 下。此处列出了launchsettings.json文件以供参考(您的 IIS Express 端口会有所不同):
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:42788",
"sslPort": 44375
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"AutoLot.Api": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
使用 Visual Studio
iisSettings部分定义了使用 IIS Express 作为 web 服务器运行应用的设置。需要注意的最重要的设置是定义端口的applicationUrl和定义运行时环境的environmentVariables块。在调试模式下运行时,此设置将取代任何机器环境设置。第二个概要文件(AutoLot.Mvc或AutoLot.Api,取决于您正在使用的项目)定义了使用 Kestrel 作为 web 服务器运行应用时的设置。该配置文件定义了applicationUrl和端口,以及环境。
Visual Studio 中的 Run 命令允许选择 IIS Express 或 Kestrel,如图 29-3 所示。一旦选择了一个概要文件,您就可以通过按 F5(调试模式)、按 Ctrl+F5(与“调试”菜单中的“启动而不调试”相同)或单击绿色的运行箭头(与“调试”菜单中的“启动调试”相同)来运行项目。
图 29-3
可用的 Visual Studio 调试配置文件
Note
从 Visual Studio 运行应用时,不再支持“编辑并继续”。
使用命令行或 Visual Studio 代码终端窗口
要从命令行或 VSC 终端运行,导航到应用的csproj文件所在的目录。输入以下命令,使用 Kestrel 作为 web 服务器启动您的应用:
dotnet run
若要结束该过程,请按 Ctrl+C。
调试时更改代码
从命令行运行时,代码可以更改,但不会反映在运行的 app 中。要在运行的应用中反映更改,请输入以下命令:
dotnet watch run
该命令的更新在启动应用的同时运行文件监视器。当在任何项目(或引用的项目)文件中检测到更改时,应用将自动停止,然后重新启动。新的 ASP.NET 核心 5,任何连接的浏览器窗口也将重新加载。它不完全是“编辑并继续”,但它是一个很好的开发解决方案。
使用 Visual Studio 代码(VS 代码)
若要从 Visual Studio 代码运行项目,请打开解决方案所在的文件夹。当你按下 F5(或者点击 Run),VS 代码会提示你选择要运行的项目(AutoLot。Api 或 AutoLot。Mvc),然后创建一个运行配置并把它放在一个名为launch.json的文件中。VS 代码也使用launchsettings.json文件进行端口配置。
调试时更改代码
从 VS 代码运行时,代码可以更改,但不会在运行的 app 中体现出来。要在运行的应用中反映更改,请从终端运行dotnet watch run命令。
调试 ASP.NET 核心应用
从 Visual Studio 或 Visual Studio 代码运行应用时,调试按预期工作。当从命令行运行时,必须先连接到正在运行的进程,然后才能调试应用。在 Visual Studio 和 Visual Studio 代码中做到这一点很容易。
使用 Visual Studio 附加
启动您的应用(使用dotnet run或dotnet watch run)后,在 Visual Studio 中选择调试➤附加到进程。当“附加到进程”对话框出现时,根据您的应用名称过滤进程,如图 29-4 所示。
图 29-4
附加到正在运行的应用以便在 Visual Studio 中进行调试
一旦附加到正在运行的进程,就可以在 Visual Studio 中设置断点,调试就可以按预期进行了。您不能编辑并继续;您必须从进程中分离,更改才能反映在您正在运行的应用中。
用 Visual Studio 代码附加
启动应用后(使用dotnet run或dotnet watch run,选择。NET Core Attach 代替。点击 VS 代码中的绿色运行箭头,启动 NET Core(web),如图 29-5 所示。
图 29-5
附加到正在运行的应用以在 Visual Studio 代码中进行调试
当您单击“运行”按钮时,系统会提示您选择要附加的进程。选择您的应用。现在,您可以按预期设置断点。
使用 Visual Studio 代码的优势在于,一旦它被附加(并使用dotnet watch run)你就可以在运行时更新你的代码(无需分离),你的更改将会反映在你的应用中。
更新自动 Lot。Api 端口
你可能已经注意到了。Api 和 AutoLot。Mvc 为其 IIS Express 配置文件指定了不同的端口,但两者都将其 Kestrel 端口配置为 5000 (HTTP)和 5001 (HTTPS)。当你尝试一起运行应用时,这会导致问题。更新自动 Lot。Api 端口到 5020 (HTTP)和 5021 (HTTPS),就像这样:
"AutoLot.Api": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"applicationUrl": "https://localhost:5021;http://localhost:5020",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
创建和配置 WebHost
与经典的 ASP.NET MVC 或 ASP.NET Web API 应用不同,ASP.NET 核心应用非常简单。创建和配置一个WebHost的. NET 核心控制台应用。WebHost的创建和随后的配置将应用设置为监听(和响应)HTTP 请求。WebHost是在Program.cs文件的Main()方法中创建的。然后在Startup.cs文件中为您的应用配置WebHost。
Program.cs 文件
打开自动 Lot 中的Program.cs类。Api 应用,并检查这里显示的内容,以供参考:
namespace AutoLot.Api
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
CreateDefaultBuilder()方法将最典型的应用设置压缩到一个方法调用中。它配置应用(使用环境变量和appsettings JSON 文件),配置默认的日志记录提供者,并设置依赖注入容器。这种设置是由 API 和 MVC 风格的应用的 ASP.NET 核心模板提供的。
下一个方法(ConfigureWebHostDefaults())也是元方法,增加了对 Kestrel、IIS 和附加配置的支持。最后一步是设置特定于应用的配置类,在本例中(按照惯例)命名为Startup。最后一步是使用Run()方法来激活 web 主机。
除了WebHost之外,前面的代码还创建了IConfiguration实例,并将其添加到依赖注入容器中。
Startup.cs 文件
Startup类配置应用如何处理 HTTP 请求和响应,配置任何需要的服务,并向依赖注入容器添加服务。类名可以是任何东西,只要它匹配CreateHostBuilder()方法配置中的UseStartup<T>()行,但是约定是命名类Startup。
可用于启动的服务
启动过程需要访问框架和环境服务及值,这些由框架注入到类中。表 29-11 中列出了Startup类可用于配置应用的五种服务。
表 29-11
启动时可用的服务
|服务
|
提供的功能
|
| --- | --- |
| IApplicationBuilder | 定义一个类,该类提供配置应用请求管道的机制。 |
| IWebHostEnvironment | 提供有关运行应用的 web 宿主环境的信息。 |
| ILoggerFactory | 用于配置日志记录系统,并从已注册的日志记录提供程序创建日志程序实例。 |
| IServiceCollection | 指定服务描述符集合的协定。这是依赖注入框架的一部分。 |
| IConfiguration | 应用配置的实例,在Program类的Main方法中创建。 |
构造函数接受一个IConfiguration的实例和一个IWebHostEnvironment / IHostEnvironment的可选实例。ConfigureServices()方法在Configure()方法获取IServiceCollection实例之前运行。Configure()方法必须接受IApplicationBuilder的一个实例,但是也可以接受IWebHostEnvironment / IHostEnvironment、ILoggerFactory以及添加到ConfigureServices()中依赖注入容器的任何接口的实例。每个组件都将在接下来的章节中讨论。
构造函数
构造函数获取由Program.cs文件中的Host.CreateDefaultBuilder方法创建的IConfiguration接口的实例,并将其分配给Configuration属性,以便在类中的其他地方使用。构造函数也可以获取IWebHostEnvironment和/或ILoggerFactory的一个实例,尽管它们没有被添加到默认模板中。
将IWebHostEnvironment的参数添加到构造函数中,并将其赋给一个局部类级变量。这在ConfigureServices()方法中是需要的。对两个自动驾驶仪都这样做。Api 和 AutoLot。Mvc 应用。
private readonly IWebHostEnvironment _env;
public Startup(
IConfiguration configuration, IWebHostEnvironment env)
{
_env = env;
Configuration = configuration;
}
ConfigureServices 方法
ConfigureServices()方法用于配置应用所需的任何服务,并将它们插入依赖注入容器。这包括支持 MVC 应用和 API 服务所需的服务。
AutoLot。美国石油学会(American Petroleum Institute)
AutoLot API 的ConfigureServices()方法在默认情况下只配置了一个服务,该服务添加了对控制器的支持。在这个元方法的背后是一系列附加的服务,包括路由、授权、模型绑定以及本章已经讨论过的所有非 UI 项目。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
可以扩展AddControllers()方法。一个例子是配置 JSON 处理。ASP.NET 核心的缺省值是 camel case JSON(首字母小写,每个后续单词字符大写,如" c ar R epo ")。这与大多数用于 web 开发的非微软框架相匹配。然而,ASP.NET·帕斯卡的早期版本对所有东西都进行了封装。对于许多期待 Pascal 大小写的应用来说,camel 大小写的改变是一个突破性的改变。要将应用的 JSON 处理改回 Pascal 大小写(并更好地格式化 JSON),请将AddControllers()方法更新为:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.WriteIndented = true;
});
}
下一次更新需要将下面的using语句添加到Startup.cs类中:
using AutoLot.Dal.EfStructures;
using AutoLot.Dal.Initialization;
using AutoLot.Dal.Repos;
using AutoLot.Dal.Repos.Interfaces;
using Microsoft.EntityFrameworkCore;
API 服务需要访问数据访问层中的ApplicationDbContext和 repos。内置支持将 EF 核心添加到 ASP.NET 核心应用中。将以下代码添加到Startup类的ConfigureServices()方法中:
var connectionString = Configuration.GetConnectionString("AutoLot");
services.AddDbContextPool<ApplicationDbContext>(
options => options.UseSqlServer(connectionString,
sqlOptions => sqlOptions.EnableRetryOnFailure()));
第一行从设置文件中获取连接字符串(稍后将详细介绍)。下一行将一个ApplicationDbContext实例池添加到 DI 容器中。与连接池非常相似,ApplicationDbContexts的池可以通过让预初始化的实例等待使用来提高性能。当需要一个上下文时,就从池中加载它。当它被用完时,它被清理掉任何使用的残留物,并被放回水池。
下一个更新是将 repos 添加到 DI 容器中。将以下代码添加到ConfigureServices()方法中配置ApplicationDbContext的代码之后:
services.AddScoped<ICarRepo, CarRepo>();
services.AddScoped<ICreditRiskRepo, CreditRiskRepo>();
services.AddScoped<ICustomerRepo, CustomerRepo>();
services.AddScoped<IMakeRepo, MakeRepo>();
services.AddScoped<IOrderRepo, OrderRepo>();
将连接字符串添加到应用设置
将appsettings.development.json文件更新如下,这将连接字符串添加到数据库中。请确保包含分隔各部分的逗号,并更新连接字符串以匹配您的环境。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLotFinal;User ID=sa;Password=P@ssw0rd;"
}
}
如前所述,每个配置文件都以一个环境命名。这允许将特定于环境的值分离到不同的文件中。将名为appsettings.production.json的新文件添加到项目中,并将其更新为以下内容:
{
"ConnectionStrings": {
"AutoLot": "ITSASECRET"
}
}
这使得真正的连接字符串不受源代码控制,并允许在部署过程中替换标记(ITSASECRET)。
AutoLot。手动音量调节
MVC 风格的 web 应用的ConfigureServices()方法增加了 API 应用的基本服务以及对呈现视图的支持。MVC 风格的应用不调用AddControllers(),而是调用AddControllersWithViews(),如下所示:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
}
将以下using语句添加到Startup.cs类中:
using AutoLot.Dal.EfStructures;
using AutoLot.Dal.Initialization;
using AutoLot.Dal.Repos;
using AutoLot.Dal.Repos.Interfaces;
using Microsoft.EntityFrameworkCore;
web 应用也需要使用数据访问层。将以下代码添加到Startup类的ConfigureServices()方法中:
var connectionString = Configuration.GetConnectionString("AutoLot");
services.AddDbContextPool<ApplicationDbContext>(
options => options.UseSqlServer(connectionString,
sqlOptions => sqlOptions.EnableRetryOnFailure()));
services.AddScoped<ICarRepo, CarRepo>();
services.AddScoped<ICreditRiskRepo, CreditRiskRepo>();
services.AddScoped<ICustomerRepo, CustomerRepo>();
services.AddScoped<IMakeRepo, MakeRepo>();
services.AddScoped<IOrderRepo, OrderRepo>();
Note
MVC web 应用将使用数据访问层和 API 来与数据交互,以演示这两种机制。
将连接字符串添加到应用设置
将appsettings.development.json文件更新为以下内容:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLotFinal;User ID=sa;Password=P@ssw0rd;"
}
}
该配置方法
Configure()方法用于设置应用来响应 HTTP 请求。这个方法在和ConfigureServices()方法之后执行*,这意味着添加到 DI 容器中的任何东西也可以被注入到Configure()方法中。*
API 风格的应用和 MVC 风格的应用在处理 HTTP 管道请求和响应的配置上有所不同。
AutoLot。美国石油学会(American Petroleum Institute)
默认模板检查环境,如果它被设置为开发,那么UseDeveloperExceptionPage()中间件被添加到处理管道中。这提供了调试信息,您可能不希望在生产中公开这些信息。
然后调用UseHttpsRedirection()将所有流量重定向到 HTTPS(而不是 HTTP)。然后添加对app.UseRouting()、app.UseAuthorization()和app.UseEndpoints()的调用。下面列出了整个方法:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
//If in development environment, display debug info
app.UseDeveloperExceptionPage();
//Original code
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "AutoLot.Api v1"));
}
//redirect http traffic to https
app.UseHttpsRedirection();
//opt-in to routing
app.UseRouting();
//enable authorization checks
app.UseAuthorization();
//opt-in to using endpoint routing
//use attribute routing on controllers
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
我们将对此进行的更改是当系统在开发中运行时初始化数据库。将ApplicationDbContext作为参数添加到方法中,并从 AutoLot.Dal 调用InitializeData()。更新后的代码如下所示:
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
ApplicationDbContext context)
{
if (env.IsDevelopment())
{
//If in development environment, display debug info
app.UseDeveloperExceptionPage();
//Initialize the database
if (Configuration.GetValue<bool>(“RebuildDataBase”))
{
SampleDataInitializer.InitializeData(context);
}
}
...
}
现在,用RebuildDataBase属性更新appsettings.development.json(现在将节点设置为false)。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"RebuildDataBase": false,
"ConnectionStrings": {
"AutoLot": "Server=db;Database=AutoLotPresentation;User ID=sa;Password=P@ssw0rd;"
}
}
AutoLot。手动音量调节
web 应用的Configure()方法比 API 方法要复杂一些。此处列出了完整的方法,稍后将进行讨论:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
该方法还检查环境,如果设置为development,则添加中间件DeveloperExceptionPage。如果环境不是开发,那么通用的ExceptionHandler中间件以及 HTTP 严格传输安全协议(HSTS)将被添加到管道中。
回到主执行路径,像它的 API 对应物一样,添加了对app.UseHttpsRedirection()的调用。下一步是用app.UseStaticFiles()添加对静态文件的支持。作为一种安全措施,可以选择支持静态文件。如果你的 app 不需要它们(比如 API),那么就不要添加支持,它们不可能是安全隐患。添加了路由、授权和端点中间件。
将ApplicationDbContext作为参数添加到方法中,并从 AutoLot.Dal 调用InitializeData()。更新后的代码如下所示:
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
ApplicationDbContext context)
{
if (env.IsDevelopment())
{
//If in development environment, display debug info
app.UseDeveloperExceptionPage();
//Initialize the database
if (Configuration.GetValue<bool>(“RebuildDataBase”))
{
SampleDataInitializer.InitializeData(context);
}
}
...
}
现在,用RebuildDataBase属性更新appsettings.development.json(现在将节点设置为false)。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"RebuildDataBase": false,
"ConnectionStrings": {
"AutoLot": "Server=db;Database=AutoLotPresentation;User ID=sa;Password=P@ssw0rd;"
}
}
在UseEndpoints()方法中,默认模板设置常规路由。我们将关闭它,并在整个应用中使用属性路由。注释掉(或删除)对MapControllerRoute()的调用,并替换为MapControllers(),如下所示:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
下一个变化是将路线属性添加到自动 Lot 中的HomeController。Mvc 应用。首先,将控制器/动作模式添加到控制器本身:
[Route("[controller]/[action]")]
public class HomeController : Controller
{
...
}
接下来,将三条路线添加到Index()方法中,这样当没有指定动作或者没有指定控制器或动作时,它就是默认动作。此外,将HttpGet属性放在方法上,将它显式声明为“get”动作:
[Route("/")]
[Route("/[controller]")]
[Route("/[controller]/[action]")]
[HttpGet]
public IActionResult Index()
{
return View();
}
记录
作为启动和配置过程的一部分,基本日志被添加到依赖注入容器中。ILogger<T>是日志基础设施使用的日志接口,非常简单。日志记录的主力是LoggerExtensions类,其方法定义如下:
public static class LoggerExtensions
{
public static void LogDebug(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogDebug(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogDebug(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogDebug(this ILogger logger, string message, params object[] args)
public static void LogTrace(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogTrace(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogTrace(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogTrace(this ILogger logger, string message, params object[] args)
public static void LogInformation(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogInformation(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogInformation(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogInformation(this ILogger logger, string message, params object[] args)
public static void LogWarning(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogWarning(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogWarning(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogWarning(this ILogger logger, string message, params object[] args)
public static void LogError(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogError(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogError(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogError(this ILogger logger, string message, params object[] args)
public static void LogCritical(this ILogger logger, EventId eventId,
Exception exception, string message, params object[] args)
public static void LogCritical(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogCritical(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogCritical(this ILogger logger, string message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel, string message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, string message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel,
Exception exception, string message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId,
Exception exception, string message, params object[] args)
}
ASP.NET 核心的一个强大特性是管道整体的可扩展性,特别是日志记录。只要新的框架能够与日志模式集成,默认日志记录器就可以与另一个日志框架交换。Serilog 是一个与 ASP.NET 核心集成的框架。接下来的部分将介绍如何创建基于 Serilog 的日志记录基础设施,以及如何配置 ASP.NET 核心应用来使用新的日志记录代码。
IAppLogging 接口
首先在 AutoLot 中添加名为Logging的新目录。服务项目。在这个目录中,添加一个名为IAppLogging<T>的新接口。更新此接口中的代码以匹配以下内容:
using System;
using System.Runtime.CompilerServices;
namespace AutoLot.Services.Logging
{
public interface IAppLogging<T>
{
void LogAppError(Exception exception, string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppError(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppCritical(Exception exception, string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppCritical(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppDebug(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppTrace(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppInformation(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
void LogAppWarning(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0);
}
}
属性CallerMemberName、CallerFilePath和CallerLineNumber检查调用堆栈,从调用代码中获取它们的命名值。例如,如果调用LogAppWarning()的代码行在名为MyClassFile.cs的文件中的DoWork()函数中,并且位于第 36 行,那么调用:
_appLogger.LogAppWarning(“A warning”);
被转换成等价的:
_appLogger.LogAppWarning(“A warning”,”DoWork”,”c:/myfilepath/MyClassFile.cs”,36);
如果值被传入方法调用,则使用传入的值,而不是属性中的值。
应用类
AppLogging类实现了IAppLogging接口。添加一个名为AppLogging的新类,并将using语句更新如下:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Serilog.Context;
公开类并实现IAppLogging<T>。添加一个接受ILogger<T>(ASP.NET 内核直接支持的接口)实例和IConfiguration实例的构造函数。在构造函数中,访问配置以从设置文件中检索应用名称。所有这三项(ILogger<T>、IConfiguration和应用名称)都需要保存在类级变量中。
namespace AutoLot.Services.Logging
{
public class AppLogging<T> : IAppLogging<T>
{
private readonly ILogger<T> _logger;
private readonly IConfiguration _config;
private readonly string _applicationName;
public AppLogging(ILogger<T> logger, IConfiguration config)
{
_logger = logger;
_config = config;
_applicationName = config.GetValue<string>("ApplicationName");
}
}
}
Serilog 通过将属性推送到LogContext上,支持将属性添加到标准日志记录过程中。添加一个内部方法来推送MemberName、FilePath、LineNumber和ApplicationName属性。
internal List<IDisposable> PushProperties(
string memberName,
string sourceFilePath,
int sourceLineNumber)
{
List<IDisposable> list = new List<IDisposable>
{
LogContext.PushProperty("MemberName", memberName),
LogContext.PushProperty("FilePath", sourceFilePath),
LogContext.PushProperty("LineNumber", sourceLineNumber),
LogContext.PushProperty("ApplicationName", _applicationName)
};
return list;
}
每个方法实现都遵循相同的过程。第一步是调用PushProperties()方法来添加额外的属性,然后调用由ILogger<T>上的LoggerExtensions公开的适当的日志记录方法。这里列出了所有实现的接口方法:
public void LogAppError(Exception exception, string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogError(exception, message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppError(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogError(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppCritical(Exception exception, string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogCritical(exception, message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppCritical(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogCritical(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppDebug(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogDebug(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppTrace(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogTrace(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppInformation(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogInformation(message);
foreach (var item in list)
{
item.Dispose();
}
}
public void LogAppWarning(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var list = PushProperties(memberName, sourceFilePath, sourceLineNumber);
_logger.LogWarning(message);
foreach (var item in list)
{
item.Dispose();
}
}
日志记录配置
通过向 AutoLot 的Logging目录添加一个名为LoggingConfiguration的新类,开始用 Serilog 替换默认日志程序。服务项目。将using语句更新为以下内容,并创建public和static类,如下所示:
using System;
using System.Collections.Generic;
using System.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.MSSqlServer;
namespace AutoLot.Services.Logging
{
public static class LoggingConfiguration
{
}
}
Serilog 使用接收器写入不同的日志记录目标。我们将用于登录 ASP.NET 核心应用的目标是文本文件、数据库和控制台。文本文件和数据库接收器需要配置、文本文件接收器的输出模板和数据库接收器的字段列表。
要设置文件模板,创建以下静态readonly字符串:
private static readonly string OutputTemplate =
@"[{Timestamp:yy-MM-dd HH:mm:ss} {Level}]{ApplicationName}:{SourceContext}{NewLine}Message:{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
SQL Server 接收器需要一个使用SqlColumn类型标识的列列表。添加以下代码来配置数据库列:
private static readonly ColumnOptions ColumnOptions = new ColumnOptions
{
AdditionalColumns = new List<SqlColumn>
{
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "ApplicationName"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "MachineName"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "MemberName"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "FilePath"},
new SqlColumn {DataType = SqlDbType.Int, ColumnName = "LineNumber"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "SourceContext"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "RequestPath"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "ActionName"},
}
};
用 Serilog 替换默认日志程序是一个三步的过程。第一步是清除现有的提供者,第二步是将 Serilog 添加到HostBuilder中,第三步是完成 Serilog 的配置。添加一个名为ConfigureSerilog()的新方法,它是对IHostBuilder的扩展方法。
public static IHostBuilder ConfigureSerilog(this IHostBuilder builder)
{
builder
.ConfigureLogging((context, logging) => { logging.ClearProviders(); })
.UseSerilog((hostingContext, loggerConfiguration) =>
{
var config = hostingContext.Configuration;
var connectionString = config.GetConnectionString("AutoLot").ToString();
var tableName = config["Logging:MSSqlServer:tableName"].ToString();
var schema = config["Logging:MSSqlServer:schema"].ToString();
string restrictedToMinimumLevel =
config["Logging:MSSqlServer:restrictedToMinimumLevel"].ToString();
if (!Enum.TryParse<LogEventLevel>(restrictedToMinimumLevel, out var logLevel))
{
logLevel = LogEventLevel.Debug;
}
LogEventLevel level = (LogEventLevel)Enum.Parse(typeof(LogEventLevel), restrictedToMinimumLevel);
var sqlOptions = new MSSqlServerSinkOptions
{
AutoCreateSqlTable = false,
SchemaName = schema,
TableName = tableName,
};
if (hostingContext.HostingEnvironment.IsDevelopment())
{
sqlOptions.BatchPeriod = new TimeSpan(0, 0, 0, 1);
sqlOptions.BatchPostingLimit = 1;
}
loggerConfiguration
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.WriteTo.File(
path: "ErrorLog.txt",
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: logLevel,
outputTemplate: OutputTemplate)
.WriteTo.Console(restrictedToMinimumLevel: logLevel)
.WriteTo.MSSqlServer(
connectionString: connectionString,
sqlOptions,
restrictedToMinimumLevel: level,
columnOptions: ColumnOptions);
});
return builder;
}
一切就绪后,是时候用 Serilog 替换默认日志了。
应用设置更新
自动 Lot 的所有应用设置文件(appsettings.json、appsettings.development.json和appsettings.production)的Logging部分。Api 和 AutoLot。Dal 项目必须用新的日志信息更新,并添加应用名称。
打开appsettings.json文件,将 JSON 更新为以下内容,确保为ApplicationName节点使用正确的项目名称,并更新连接字符串以匹配您的配置:
//appsettings.json
{
"Logging": {
"MSSqlServer": {
"schema": "Logging",
"tableName": "SeriLogs",
"restrictedToMinimumLevel": "Warning"
}
},
"ApplicationName": "AutoLot.Api",
"AllowedHosts": "*"
}
//appsettings.development.json
{
"Logging": {
"MSSqlServer": {
"schema": "Logging",
"tableName": "SeriLogs",
"restrictedToMinimumLevel": "Warning"
}
},
"RebuildDataBase": false,
"ApplicationName": "AutoLot.Api - Dev",
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
}
}
//appsettings.production.json
{
"Logging": {
"MSSqlServer": {
"schema": "Logging",
"tableName": "SeriLogs",
"restrictedToMinimumLevel": "Warning"
}
},
"RebuildDataBase": false,
"ApplicationName": "AutoLot.Api - Prod",
"ConnectionStrings": {
"AutoLot": "It's a secret"
}
}
Program.cs 更新
将以下using语句添加到两个自动 Lot 中的Program.cs文件中。Api 和 AutoLot。Mvc 项目:
using AutoLot.Services.Logging;
接下来,将两个项目中的CreateHostBuilder()方法更新为:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
}).ConfigureSerilog();
Startup.cs 更新
将以下using语句添加到两个自动 Lot 中的Startup.cs文件中。Api 和 AutoLot。Mvc 项目:
using AutoLot.Services.Logging;
接下来,需要将新的日志记录接口添加到依赖注入容器中。将以下内容添加到两个项目的ConfigureServices()方法中:
services.AddScoped(typeof(IAppLogging<>), typeof(AppLogging<>));
控制器更新
下一个变化是将对ILogger的任何引用更新为IAppLogging。从自动手枪中的WeatherForecastController开始。Api 项目。将下面的using语句添加到类中:
using AutoLot.Services.Logging;
接下来,更新ILogger<T> IAppLogging<T>。
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
...
private readonly IAppLogging<WeatherForecastController> _logger;
public WeatherForecastController(IAppLogging<WeatherForecastController> logger)
{
_logger = logger;
}
...
}
现在更新自动 Lot 中的HomeController。Mvc 项目。将下面的using语句添加到类中:
using AutoLot.Services.Logging;
接下来,更新ILogger<T> IAppLogging<T>。
[Route("[controller]/[action]")]
public class HomeController : Controller
{
private readonly IAppLogging<HomeController> _logger;
public HomeController(IAppLogging<HomeController> logger)
{
_logger = logger;
}
...
}
然后,只需像这样简单地调用记录器,就可以在每个控制器中完成日志记录:
//WeatherForecastController.cs (AutoLot.Api)
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
_logger.LogAppWarning("This is a test");
...
}
//HomeController.cs (AutoLot.Mvc)
[Route("/")]
[Route("/[controller]")]
[Route("/[controller]/[action]")]
[HttpGet]
public IActionResult Index()
{
_logger.LogAppWarning("This is a test");
return View();
}
测试日志框架
有了 Serilog 之后,是时候测试应用的日志记录了。如果您使用的是 Visual Studio,请设置 AutoLot。Mvc 应用作为启动应用(在解决方案资源管理器中右击,选择“设为启动项目”,然后单击绿色的运行箭头,或按 F5)。如果使用的是 Visual Studio 代码,打开终端窗口(Ctrl+),导航到AutoLot.Mvc目录,输入dotnet run`。
使用 Visual Studio,浏览器将自动启动到Home/Index视图(您将看到“欢迎/了解使用 ASP.NET 核心构建应用”)。如果您正在使用 Visual Studio 代码,您将需要打开一个浏览器并导航到https://localhost:5001。一旦浏览器加载完毕,您就可以关闭它,因为登录调用是在主页加载时进行的。使用 VS 关闭浏览器将会停止调试。若要停止使用 VS 代码进行调试,请在终端窗口中按 Ctrl+C。
在项目目录中,您会看到一个名为ErrorLogYYYYMMDD.txt的文件。在该文件中,您将看到一个类似如下的条目:
[YY-MM-DD hh:mm:ss Warning]AutoLot.Mvc - Dev:AutoLot.Mvc.Controllers.HomeController
Message:This is a test
in method Index at D:\Projects\Books\csharp9-wf\Code\New\Chapter_29\AutoLot.Mvc\Controllers\HomeController.cs:30
测试自动测试中的记录代码。Api 项目,将该项目设置为启动应用(VS)或导航到 AutoLot。终端窗口中的 Api 目录(VCS)。按 F5 或输入dotnet run并导航至https://localhost:44375/swagger/index.html。这将加载 API 应用的 Swagger 页面,如图 29-6 所示。
图 29-6
AutoLot 的初始 Swagger 页面。美国石油学会(American Petroleum Institute)
点击WeatherForecast条目的获取按钮。这将打开一个屏幕,显示该操作方法的详细信息,包括一个“尝试”选项,如图 29-7 所示。
图 29-7
天气预报控制器的 Get 方法的详细信息
点击“尝试”按钮后,点击执行按钮(图 29-8 ),顾名思义,执行对端点的调用。
图 29-8
执行天气预报控制器的 Get 方法的详细信息
在自动售货机里。Api 项目目录下,你会再次看到一个名为ErrorLogYYYYMMDD.txt的文件。在该文件中,您会发现类似于以下内容的条目:
[YY-MM-DD hh:mm:ss Warning]AutoLot.Api - Dev:AutoLot.Api.Controllers.WeatherForecastController
Message:This is a test
in method Get at D:\Projects\Books\csharp9-wf\Code\New\Chapter_29\AutoLot.Api\Controllers\WeatherForecastController.cs:30
Note
ASP.NET 核心 5 中新增的 Swagger 在 API 模板中是默认启用的。斯瓦格将在下一章详细讨论。
摘要
本章介绍了 ASP.NET 核心,是涵盖 ASP.NET 核心的一系列章节中的第一章。本章首先简要回顾了 ASP.NET 的历史,然后介绍了 ASP.NET 核心中的经典 ASP.NET MVC 和 ASP.NET Web API 的特性。
接下来的部分将研究 ASP.NET 核心中的新特性以及它们是如何工作的。然后,在了解了运行和调试 ASP.NET 核心应用的不同方法后,您用两个 ASP.NET 核心项目、一个应用服务的公共库和 AutoLot 数据访问层(来自第二十三章)建立了解决方案。最后,在这两个项目中,您用 Serilog 替换了默认的 ASP.NET 岩心记录器。
在下一章中,您将完成自动 Lot。Api 应用。