11-13 【重构】INotifyPropertyChanged 与 ObservableCollection
现在我们来完成新建客户的功能。
当用户点击"客户添加"按钮以后系统会清空当前所选定的客户,客户的详细信息以及客户的预约记录会从 UI 中被清除。然后我们就可以在输入框中输入新的客户信息了,最后按下保存按钮这个时候新客户就被保存进数据库并且显示在客户列表中了。
--\MainWindow.xaml
<StackPanel Grid.Row="1" Grid.Column="0">
<Button Content="添加客户" Click="ClearSelectedCustomer_Click"/>
<ListView ItemsSource="{Binding Customers, Mode=OneWay}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}" />
</StackPanel>
--\MainWindow.xaml.cs
private MainViewModel _viewModel;
............
private void ClearSelectedCustomer_Click(object sender, RoutedEventArgs e)
{
_viewModel.ClearSelectedCustomer();
}
如何清空客户呢我们还是得从视图模型入手打开 MainViewModel 创建一个清空当前客户的方法 并直接把 _selectedCustomer 设置为空就好了。
--\ViewModels\MainViewModel.cs
public void ClearSelectedCustomer()
{
_selectedCustomer = null;
}
运行一下代码试试看,程序跑起来选择一个客户,点击"添加客户"翻车了,即使我们在代码中把视图模型中的 _selectedCustomer 清空了,但是 UI 并没有发生改变!这是为什么呢?
虽然我们在客户信息的 UI 绑定过程中使用了双向绑定,但是在 ViewModel 中改变 _selectedCustomer 的数据以后,我们依然需要通知 UI 数据的变化过程,也就是要一个 ViewModel 与 UI 的联动过程。
那么在 WPF 中处理 UI 与视图模型的联动过程,我们可以通过实现 INotifyPropertyChanged 这个接口来实现。打开 MainViewModel 让这个类实现接口 INotifyPropertyChanged 这个接口。使用 Visual Studio 来自动实现这个接口的代码,可以看到这个 INotifyPropertyChanged 实现,实际上就是一个委托或者说是一个事件,这个事件将会发送给视图。
视图在接收到事件以后会根据事件的内容做出 UI 的调整,事件则是通知 UI 视图模型属性发生了变化。所以我们创一个私有的方法(RaisePropertyChanged)来处理这个事件。这个方法将会告诉 UI 到底是哪个属性发生了变化。
方法中我们调用 PropertyChanged ,尤其仅当它不为 null 的时候我们调用 Invoke 方法。通过 Invoke 向 UI 发送事件。 Invoke 方法的第一个参数就是视图模型本身 this ,而第二个参数则是实例化一个 PropertyChangedEventArgs 传入参数的属性名称 propertyName 。
--\ViewModels\MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
............
public void ClearSelectedCustomer()
{
_selectedCustomer = null;
RaisePropertyChanged(nameof(SelectedCustomer));
}
}
接下来我们就可以在清空当前选择客户以后调用这个方法通知 UI 了。参数传入 nameof(SelectedCustomer) 。
在客户选择的过程中同样也要调用这个 UI 的事件。
在 SelectedCustomer 的 set 中更新了当前客户以后,向 UI 发送客户更新通知。
--\ViewModels\MainViewModel.cs
private CustomerViewModel _selectedCustomer;
public CustomerViewModel SelectedCustomer
{
get => _selectedCustomer;
set
{
if (value != _selectedCustomer)
{
_selectedCustomer = value;
RaisePropertyChanged(nameof(SelectedCustomer));
LoadAppointments(SelectedCustomer.Id);
}
}
}
接下来我们来完成客户的添加功能。
它具体的业务逻辑是什么呢?这一次我们需要把两个业务混合在同一个方法中,如果当前选定的客户 _selectedCustomer 不为空,那么我们就执行数据的更新工作。否则我们就添加一个新的客户。
--\ViewModels\MainViewModel.cs
public void SaveCustomer(string name, string idNumber, string address)
{
if(SelectedCustomer != null)
{
// 更新客户数据
using (var db = new AppDbContext())
{
var customer = db.Customers.Where(c => c.Id == SelectedCustomer.Id).FirstOrDefault();
customer.Name = name;
customer.IdNnumber = idNumber;
customer.Address = address;
db.SaveChanges();
}
}
else
{
// 添加新客户
using (var db = new AppDbContext())
{
var newCustomer = new Customer()
{
Name = name,
IdNnumber = idNumber,
Address = address
};
db.Customers.Add(newCustomer);
db.SaveChanges();
}
LoadCustomers();
}
}
其实客户数据的更新和添加在之前的课程中我们就实现了,代码非常简单。
添加客户同样也是使用 using 来托管数据库先创建一个 newCustomer ,...... 完成新客户添加以后我们还要刷新这个客户列表 LoadCustomers 。不过在 LoadCustomers 中还有一个 bug 需要更新,因为我们需要的是刷新 customer 列表,按照目前的逻辑客户列表只会增加不会减少,因此每次加载客户数据的时候我们都应该先重置 customer 列表,然后再添加新数据。
public void LoadCustomers()
{
Customers.Clear();
............
}
接下来处理页面逻辑,打开主页 xaml 文件双击客户"保存"按钮创建点击事件重命名一下这个点击事件 SaveCustomer_Click ,记得在 XML 空间中也需要改一下名字。
因为要访问数据库所以我们需要使用 try catch 来处理一下异常,客户的名称、身份证、住址 均来自文本框 TextBox ,所以我们也需要给这三个文本框加上名字 <TextBox Name="NameTextBox" ...... />。
--\MainWindow.xaml
<StackPanel Grid.Row="1" Grid.Column="1">
<TextBlock Text="姓名" Margin="10 10 10 0"/>
<TextBox Name="NameTextBox" Margin="10" Text="{Binding SelectedCustomer.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="身份证" Margin="10 10 10 0"/>
<TextBox Name="IdTextBox" Margin="10" Text="{Binding SelectedCustomer.IdNnumber, Mode=TwoWay}" />
<TextBlock Text="地址" Margin="10 10 10 0"/>
<TextBox Name="AddressTextBox" Margin="10" Text="{Binding SelectedCustomer.Address, Mode=TwoWay}" />
<Button Content="保存" Margin="10 10 10 30" VerticalAlignment="Bottom" HorizontalAlignment="Left" Click="SaveCustomer_Click" />
</StackPanel>
--\MainWindow.xaml.cs
private void SaveCustomer_Click(object sender, RoutedEventArgs e)
{
try
{
string name = NameTextBox.Text.Trim();
string idNumber = IdTextBox.Text.Trim();
string address = AddressTextBox.Text.Trim();
_viewModel.SaveCustomer(name, idNumber, address);
}
catch (Exception error)
{
MessageBox.Show(error.ToString());
}
}
运行一下试试看,代码跑起来了选择一个客户更改名称点击保存数据保存没有问题。
接着试着添加一个新客户点击保存现在问题出现了!我们明明点击了保存但是客户列表并没有更新,而且也没有报错!这是怎么回事呢?那么我们的数据到底添加成功了吗?
关闭当前这个窗口重新再运行一次,这一次我们就可以看到客户列表中多了一条数据,证明数据已经成功被添加了,那么为什么客户列表并没有成功更新呢?
回到 MainViewModel 还记得我们刚刚使用过的 INotifyPropertyChanged 这个接口吗?
这个接口可以帮我们向 UI 发送视图模型更新的指令,那么是不是我们可以采用类似的方法来继续处理 UI 更新、继续处理用户列表的更新呢?
可以的,不过这个 INotifyPropertyChanged 只能处理"非列表型的数据",对于列表 WPF 有另一种处理方式,这种处理方式就是 Observable 观察者模式,虽然观察者模式听起来好像很高大上,不过 WPF 已经帮我们做了最顶层的封装了,我们直接使用就可以了,甚至感觉不到观察者模式的存在。
代码修正非常简单找到 List<CustomerViewModel> Customers 这个客户列表,我们使用 ObservableCollection 来代替这个 List 。这个 ObservableCollection 来自 System.Collections.ObjectModel 命名空间。
public ObservableCollection<CustomerViewModel> Customers { get; set; } = new();
11-14 【重构】显示预约列表
打开 AppointmentViewModel ,与客户视图类似这个预约视图的基本数据来自预约模型 Appointment 。创建一个私有预约对象并且在构造方法中传递数据接着声明两个属性 ID 与预约时间。
处理方式跟 CustomerViewModel 类似。
11-15 【重构】添加新预约
AddAppointment

MVVM重构后项目运行示例图