(十一)重构与美化
1. 什么是数据模型

所谓的建模,就是人们通过对一个事物的观察,给这个事物总结出规律,然后通过使用这个规律来定义这个事物。这是一个科学的过程,人们对事物认知的深浅、观察事物的角度,直接决定了模型的好坏。


2.Entity Framework
EF 是一个 ORM数据库映射框架
微软的数据管理框架Entity Framework用来来连接、创建、初始化数据库,并通过package manager console工具来对数据库进行变更、完成数据迁移。
EF介绍
2.1 EF的特性
- 跨平台:EF Core是一个跨平台的框架,可以在Windows,Linux和Mac上运行。
- 建模:EF可以创建具有不同数据类型get / set属性的EDM(Entity Data Model/实体数据模型)。它使用此模型查询或保存底层数据库的数据。
- 查询:EF允许我们使用LINQ从底层数据库中检索数据,同时也支持直接对数据库执行原始SQL查询。
- 更改跟踪:EF会跟踪需要提交到数据库的实体实例(属性值)发生的更改。
- 保存:EF调用SaveChanges()方法时,根据实体发生的更改,对数据库执行INSERT,UPDATE和DELETE命令。EF还提供了异步的SaveChangesAsync()方法。
- 并发:默认情况下,从数据是从数据库中提取开始,EF使用乐观并发来避免我们做的修改被其他用户覆盖。
- 事务:EF在查询或保存数据时自动执行事务管理。它还提供自定义事务管理的选项。
- 缓存:EF包括开箱即用的第一级缓存。因此,重复查询将从缓存中返回数据,而不是访问数据库。
- 配置:EF允许我们使用注释属性配置EF模型,也可以使用Fluent API来覆盖默认约定。
- 迁移:EF提供了一组迁移命令,我们可以在NuGet Package Manager控制台或命令行界面中执行这些命令来创建或管理底层数据库计划。
2.2 EF的组件

- EDM(实体数据模型): EDM包含三个主要部分——概念模型,映射和存储模型。
- 概念模型: 相当于我们之前课程所介绍的model(数据模型)。
- 存储模型: 存储模型是数据库设计模型,包括表、视图、存储过程、以及它们之间的关系和各种主键、外建。
- 映射(mapping): 映射由概念模型如何映射到存储模型的信息组成。
- LINQ:是一种的查询实体对象的语言, 它返回在概念模型中定义的实体对象。
- Entity SQL:Entity SQL是一个基于linq的、类似于sql语句的查询语言。
- Object Services(对象服务):对象服务是访问数据库中的数据并返回数据的主要入口点。它负责数据实例化,把Entity Client Data Provider(下一层)的数据转换成实体对象。
- Entity Client Data Provider:主要职责是将linq或Entity Sql语句转换成真正的Sql查询语句,它通过ADO.Net Data Provider向数据库发送或者索取数据。
- ADO.Net Data Provider:使用标准的Ado.net与数据库通信。
2.3 ORM
3. 构建数据模型
- 首先,我们会通过数据库逆向工具来获得数据模型;
- 然后,使用Entitiy Framework访问数据库,并通过对象的链式结构取代使用sql语句。
- 最后,通过数据模型来向各种UI组件绑定和传递数据。
那么我们将会使用到三个新的工具:
- 数据库映射工具,ORM框架,EntitiyFramework
- Sql server 链接工具,EntitiyFramework.SqlServer
- 以及数据库设计工具,EntitiyFramework.tools,我们将会使用这个工具来进行数据库逆向开发、获得数据模型
以上的这三个工具基本上是每个.net程序都必须掌握的。
3.1 安装项目依赖
在Nuget的浏览页面,首先,搜索关键词SqlServer,在搜索结果中,可以看到有两个与EntitiyFramework相关的sqlserver工具,因为我们项目的运行平台是.netcore ,所以应该选择的这个EntitiyFrameworkCore.sqlServer,版本选择与自己.net平台对应的版本就可以了,课程项目使用的是.net 5,可以搭配EntitiyFrameworkCore 5.0以上的版本,我就选择5.0.14了。
因为EntitiyFramework是这个工具的依赖项,所以,当我们安装了EntitiyFramework sqlServer以后,就不需要单独安装EntitiyFramework了。
接下来,安装数据库设计工具,搜索EntitiyFrameworkCore.tools,同样选择5.0以上的版本。
3.2 数据库逆向工程获得模型,DbFrist
- 点击tools -> nuget management -> package manageer console,这个时候,会打开一个类似命令行的窗口,我们在将会利用这个窗口来调用刚刚安装的数据库设计工具,完成数据库的逆向操作。
- 使用数据库上下文脚手架工具命令 Scaffold-DbContext ;然后找到自己数据库连接字符串,复制粘贴过去,注意使用双引号;然后,调用数据库设计工具Microsoft.EntityFrameworkCore.SqlServer; 接下来,填入两个参数,第一个是模型数据代码的输出文件夹,-OutputDir,我们希望输出的位置是Models文件夹;第二个参数是数据库连接器的名称,-Context ,一般来说,数据库连接器都会使用DbContext作为结尾,我们可以把它叫做 AppDbContext。
![image]()
Scaffold-DbContext "Data Source=localhost\SQLEXPRESS;Initial Catalog=master;Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Context AppDbContext
3.3 dbcontext
在models文件夹中,我们还会看到一个新文件,叫做AppDbContext。这个文件就是我们的数据库连接器,他是entity framework的关键环节,这个文件负责沟通数据库,并且把数据库中的数据转化为模型对象Customer和Appointment。
如果仔细观察,我们会发现AppDbContext继承于DbContex,而基类DbContex则来自entity framework。不管是增删改查、还是数据迁移,不管是数据库连接开启还是关闭,所有数据库的相关操作都已经被这个DbContex封装好了,我们可以更专注于业务的实现,而把所有底层数据库的操作全部托付出来,让DbContex接管所有数据库的生命周期。
OnModelCreating方法,顾名思义,这就定义的就是数据库表和数据模型的映射关系了。比如说,我们的数据模型,Appointment,在定义过程中被称作实体entity. 而entity framework的命名就来自这里。EntityFramework会按照命名规则自动去数据库中查找名称为Appointments的数据表。一旦找到,就会建立从数据库表Appointments table到实体模型 Appointment的映射关系。
modelBuilder.Entity<Appointment>(entity =>
{
entity.Property(e => e.Time).HasColumnType("datetime");
entity.HasOne(d => d.Customer)
.WithMany(p => p.Appointments)
.HasForeignKey(d => d.CustomerId)
.HasConstraintName("FK_Appointments_Customers");
});
4.ORM数据管理上
创建的上下文对象AppDbContext来处理整个数据库的生命周期。为了更有效的处理声明周期,c#有一个特殊的语法结构就是using,using 一个数据库变量db,等于实例化AppDbContext。
为什么我们要用对象的链式结构来处理数据呢?这是因为对于程序员来说,阅读和管理sql其实是一件非常痛苦的事情,尤其是当一个sql语句包含了各种各样复杂业务场景的时候。比如说,对于大型项目来说,每个岗位都需要有自己的分工,程序员应该专注于业务实现而不是业务规则的制订,至于业务规则的制定则应该交给PM(产品经理)或者BA(business analisyst 业务分析师)来处理。比如说,我现在所在的部门,BA的数量比程序员还多,这是因为我们的业务极为复杂,BA会使用sql脚本把业务先封装起来,然后程序员直接在程序中通过储存过程store prosudure来调用他们的脚本。于是,业务和代码就能彻底分离了,这样不仅能提高开发效率,还有利于业务的维护和管理。否则当我们把代码和业务混在一起的时候,就很有可能造成BA看不懂代码、程序员不理解业务的情况。
所以,这就是使用ORM框架的好处。对于业务量较小的项目来说,我们使用ORM来自动生成sql语句,通过数据库映射框架来获得数据模型,并且通过模型对象的链式结构来处理数据。业务将会被实现在代码中而不是sql语句中,所以,对于程序员来说,使用对象的链式结构更加符合面向对象的理念,而且代码阅读起来也会比阅读sql字符串更加轻松。
4.1 查询
var appointments = db.Appointments.Where(a => a.CustomerId == (int)customerId).ToList();
附加条件使用where语句,其实这里的where就和sql语句的的where关键词类似,可以进行数据的筛选。不过,linq语法中的where使用的是对象的链式结构、并且配合一个lambda表达式作为参数。从原理上来说,lambda表达式其实就是一个匿名方法或者叫做箭头方法,箭头的左边是参数,参数a表示数据库中每一个预约appointment;而箭头方法的右边则表示数据的筛选规则,每一个预约记录的CustomerId都必须等于当前选定的customerId才能从数据库中被捞出来。
4.2删除
FirstOrDefault来进行数据的提取。这个first代表的就是列表中的第一项、也是唯一的一项,而这个default则代表如果列表中一个数据都没有的时候,返回值为null。
因为进行了数据库的修改工作,目前对数据库的修改依然停留在内存中。所以,一定要在完成所有数据库操作以后调用db.SaveChanges来向数据库提交修改。
4.3 删除(连表)
在调用db.Customers的时候加上include这个方法,让entity framework把客户表和预约表连接在一起就可以同时处理两张表的信息了。
同时,我们还得打开AppDbContext文件,找到OnModelCreating方法, 把.OnDelete(DeleteBehavior.ClientSetNull) 这个删除条件删除掉、或者注释掉。
//.OnDelete(DeleteBehavior.ClientSetNull)
-----------------------------------------
var customerId = customerList.SelectedValue;
using (var db = new AddDbContext())
{
var customerToRmove = db.Customers
.Include(c => c.Appointments)
.Where(c => c.Id == (int)customerId)
.FirstOrDefault();
db.Customers.Remove(customerToRmove);
db.SaveChanges();
}
4.4 更新
最后直接保存数据库,SaveChanges就可以了。是不是很简单?这是因为凡是从数据库中隐射出来的数据,entity framework都会自动帮我们追踪他的数据状态,一旦发现改变就是随着我们调用SaveChanges的同时保存进入数据库。
5. 组件化布局
新建一个用户控件HeaderContol,可以添加到主窗体中
xmlns:controls="clr-namespace:CMS_Wpf.Controls"
<!--header-->
<controls:HeaderControl Grid.ColumnSpan="3"/>
6.MVVM





7.
目前市面上有三种比较主流的UI设计规范,分别是苹果的扁平化ui设计,android 或者说google的Material Design,以及微软的Metro 风格。

7.1 安装Material Design
Nuget
打开项目,右键点击项目名称,选择nuget,搜索关键词 Material Design。第一个搜索结果Material Design themes就是我们要的。
更新 App.xmal 文件
<Application x:Class="CMS_Wpf.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CMS_Wpf" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="DeepPurple" SecondaryColor="Lime" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
7.2 更新主页Window元素
首先修改主页的名称,title修改为CMS;高度600,宽度900;背景变透明,Background=“Transparent”,AllowsTransparency=“True”;app启动位置WindowStartupLocation设置为屏幕中央,CenterScreen;最后,字体FontFamily也统一一下,我就设置为Cambria了。
- Title=“CMS”
- Height=“600” Width=“900”
- Background=“Transparent”
- AllowsTransparency=“True”
- WindowStyle=“None”
- WindowStartupLocation=“CenterScreen”
- FontFamily=“Cambria”
7.3 添加card效果
为了让内容显示更加立体,我们可以选择使用materialDesign的卡片组件card元素。
<!--客户信息-->
<materialDesign:Card Grid.Row="1" Grid.Column="1" Width="250" Height="440" Margin="10">
</materialDesign:Card>
- 可以添加贴图背景美化
![image]()
7.4 更新TextBox
删掉label,我们等会会利用materialDesign来把这个文字标签和输入框整合到一起去。调用materialDesign风格,让属性style等于花括号,StaticResource MaterialDesignOutlinedTextBox;最后,加上文字标签效果,materialDesign:HintAssist.Hint=“姓名”
<TextBox
Name="NameTextbox"
Margin="10"
materialDesign:HintAssist.Hint="姓名"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
Text="{Binding SelectedCustomer.Name,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
8.自定义依赖属性
8.1 使用 Calendar 代替 ListView
<Calendar
Name="AppointmentCalendar"
Height="320"
Width="300"
beh:CalendarAttachedProperties.RegisterBlackoutDates="{Binding Appointments, Mode=OneWay}"/>
我们希望能在日历上显示客户的预约日期,也就是如果日历上的某一天客户已经有预约了,那么这一天需要显示在日历上并且变为灰色,这一天就不能再添加新的预约了。
WPF的这个默认的BlackoutDates是不支持MVVM数据绑定的
官方给出的解释是BlackoutDates是一个只读属性,所以无法更改。

8.2 添加自定义依赖属性
一般来说,我在实际工作中所遇到的问题,99%以上都能在这个叫做stackoverflow的网站上找到解决思路。
c# - How to Bind BlackoutDates in WPF Toolkit Calendar control? - Stack Overflow
// Adds a collection of command bindings to a date picker's existing BlackoutDates collection, since the collections are immutable and can't be bound to otherwise.
// Usage: <DatePicker hacks:AttachedProperties.RegisterBlackoutDates="{Binding BlackoutDates}" >
public class CalendarAttachedProperties : DependencyObject
{
#region Attributes
private static readonly List<Calendar> _calendars = new List<Calendar>();
private static readonly List<DatePicker> _datePickers = new List<DatePicker>();
#endregion
#region Dependency Properties
public static DependencyProperty RegisterBlackoutDatesProperty = DependencyProperty.RegisterAttached("RegisterBlackoutDates", typeof(ObservableCollection<DateTime>), typeof(CalendarAttachedProperties), new PropertyMetadata(null, OnRegisterCommandBindingChanged));
public static void SetRegisterBlackoutDates(DependencyObject d, ObservableCollection<DateTime> value)
{
d.SetValue(RegisterBlackoutDatesProperty, value);
}
public static ObservableCollection<DateTime> GetRegisterBlackoutDates(DependencyObject d)
{
return (ObservableCollection<DateTime>)d.GetValue(RegisterBlackoutDatesProperty);
}
#endregion
#region Event Handlers
private static void CalendarBindings_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
ObservableCollection<DateTime> blackoutDates = sender as ObservableCollection<DateTime>;
Calendar calendar = _calendars.First(c => c.Tag == blackoutDates);
if (e.Action == NotifyCollectionChangedAction.Reset)
{
calendar.BlackoutDates.Clear();
}
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (DateTime date in e.NewItems)
{
var range = new CalendarDateRange(date);
calendar.BlackoutDates.Add(range);
}
}
}
private static void DatePickerBindings_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
ObservableCollection<DateTime> blackoutDates = sender as ObservableCollection<DateTime>;
DatePicker datePicker = _datePickers.First(c => c.Tag == blackoutDates);
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (DateTime date in e.NewItems)
{
datePicker.BlackoutDates.Add(new CalendarDateRange(date));
}
}
}
#endregion
#region Private Methods
private static void OnRegisterCommandBindingChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Calendar calendar = sender as Calendar;
if (calendar != null)
{
ObservableCollection<DateTime> bindings = e.NewValue as ObservableCollection<DateTime>;
if (bindings != null)
{
if (!_calendars.Contains(calendar))
{
calendar.Tag = bindings;
_calendars.Add(calendar);
}
calendar.BlackoutDates.Clear();
foreach (DateTime date in bindings)
{
calendar.BlackoutDates.Add(new CalendarDateRange(date));
}
bindings.CollectionChanged += CalendarBindings_CollectionChanged;
}
}
else
{
DatePicker datePicker = sender as DatePicker;
if (datePicker != null)
{
ObservableCollection<DateTime> bindings = e.NewValue as ObservableCollection<DateTime>;
if (bindings != null)
{
if (!_datePickers.Contains(datePicker))
{
datePicker.Tag = bindings;
_datePickers.Add(datePicker);
}
datePicker.BlackoutDates.Clear();
foreach (DateTime date in bindings)
{
datePicker.BlackoutDates.Add(new CalendarDateRange(date));
}
bindings.CollectionChanged += DatePickerBindings_CollectionChanged;
}
}
}
}
#endregion
}
不过,有一个小问题还是得修改一下,在CalendarBindings_CollectionChanged方法中添加 “calendar.BlackoutDates.Clear();。
if(e.Action == NotifyCollectionChangedAction.Reset)
{
calendar.BlackoutDates.Clear();
}
使用 DateTime 代替 AppointmentViewModel
前面,我们说到,填充Appointments列表需要使用时间而不再是ViewModel,所以,我们把ObservableCollection的泛型改为时间DateTime。
//public ObservableCollection<AppointmentViewModel> Appointments { get; set; } = new();
public ObservableCollection<DateTime> Appointments { get; set; } = new();
相应的,我们在LoadAppointments方法中提取预约的时候,也同样需要对数据塑性,提取有用信息。在for循环中向Appointments列表添加的应该是a.Time,而不再是添加一个新的预约视图模型了。
public void LoadAppointments(int customerId=1)
{
Appointments.Clear();
using (var db = new AddDbContext())
{
var appointments = db.Appointments
.Where(a => a.CustomerId == customerId)
.ToList();
foreach (var a in appointments)
{
//Appointments.Add(new AppointmentViewModel(a));
Appointments.Add(a.Time);
}
}
}
绑定日历数据
引用自定义命名空间的语法结构如下,xmlns加上冒号,然后使用一个自己喜欢的名字来表示这个命名空间,
接下来,找到日历控件,现在,我们就可以像使用普通属性一样,来使用刚刚创建的自定义依赖属性绑定视图模型的数据了。

然后创建一个当前选中的日期时间,在预约方法中使用





浙公网安备 33010602011771号