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重構后項目運行示例圖
?