一篇文章,带你玩转MVVM,Dapper,AutoMapper
一、背景
由于现在做的项目都是采用WPF来进行UI设计,开发过程中都是基于MVVM来进行开发,但是项目中的MVVM并不是真正的把实体和视图进行解耦,而是将实体和视图完全融合起来,ViewModel只是用来实现View和Model的数据同步,违背了MVVM设计的数据双向绑定的初衷,完全没有发挥出MVVM的优势。
二、MVVM基本概念
1.M:表示Model,也就是一个实体对象。
2.V:表示VIew,也就是UI界面展示,即人机交互界面。
3.ViewModel:可以理解为搭建View和Model的一个业务逻辑桥梁。
三、Demo来说明
首先建立解决方案,方案框架如下:
在Models中创建一个Model实体对象Contacts
public class Contacts { public int ID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public string Company { get; set; } public string Title { get; set; } }
接下来我们在ViewModel中实现V和M的完全解耦,在ViewModels中添加ContractsViewModels
public class ContractsViewModels : BaseViewModels { public Contacts contracts = null; public ContractsViewModels(Contacts contracts) { this.contracts = contracts; } public ContractsViewModels() { contracts = new Contacts(); } public int ID { get { return contracts.ID; } set { contracts.ID = value; OnPropertyChanged(this, nameof(ID)); } } public string FirstName { get { return contracts.FirstName; } set { contracts.FirstName = value; OnPropertyChanged(this, nameof(FirstName)); } } public string LastName { get { return contracts.LastName; } set { contracts.LastName = value; OnPropertyChanged(this, nameof(LastName)); } } public string Email { get { return contracts.Email; } set { contracts.Email = value; OnPropertyChanged(this, nameof(Email)); } } public string Company { get { return contracts.Company; } set { contracts.Company = value; OnPropertyChanged(this, nameof(Company)); } } public string Title { get { return contracts.Title; } set { contracts.Title = value; OnPropertyChanged(this, nameof(Title)); } } }
当数据双向绑定时,视图知道自己绑定了那个实体,为了让实体的属性改变中,能够通知绑定的View,我们需要实现INotifyPropertyChanged这个接口,具体实现如下
public class BaseViewModels : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(object sender, string name) { if (PropertyChanged != null) { PropertyChanged(sender ?? this, new PropertyChangedEventArgs(name)); } } }
以上实现,在ViewModel中改变属性,便可以轻松的通知到绑定时View控件,真正意义上实现了双向数据绑定。
接下来在ViewModel中添加ContractsVMServices来方便View的数据操作
代码如下:
/// <summary> /// ContractsViewModels服务类 /// </summary> public class ContractsVMServices { /// <summary> /// 加载所有数据的事件 /// </summary> public event EventHandler OnLoadAllContracts; /// <summary> /// 数据集合对象 /// </summary> public ObservableCollection<ContractsViewModels> ContractsViewModels { get; private set; } /// <summary> /// 单个实体的事件 /// </summary> public event EventHandler OnLoadContracts; /// <summary> /// 单个实体 /// </summary> public ContractsViewModels ContractsViewModel { get; private set; } private ContractsVMServices() { } public static ContractsVMServices Instances = new ContractsVMServices(); /// <summary> /// 初始化 /// </summary> public void Init() { ContractsViewModels = new ObservableCollection<ContractsViewModels>(); ContractsViewModel = new ContractsViewModels(new Contacts()); OnLoadAllContracts?.Invoke(this, EventArgs.Empty); OnLoadContracts?.Invoke(this, EventArgs.Empty); AutoMapperWrapper.Start(); LoadAllContracts(); } /// <summary> /// 定时模拟数据改变 /// </summary> /// <param name="afterSecond"></param> /// <param name="freSecond"></param> public void ChangeByTime(int afterSecond = 5, int freSecond = 2) { Task.Run(async () => { await Task.Delay(afterSecond * 1000); while (true) { try { foreach (var item in ContractsViewModels) { item.Title = "Change" + DateTime.Now.ToString(); string update = "Update contacts set Title=@Title where ID=@ID"; List<KeyValuePair<string, object>> ls = new List<KeyValuePair<string, object>>(); ls.Add(new KeyValuePair<string, object>(nameof(item.Title), item.Title)); ls.Add(new KeyValuePair<string, object>(nameof(item.ID), item.ID)); DapperHelper.Update(update, ls); } //string insert = @"Update into contacts (Id,FirstName,LastName,Email,Company,Title) values(@Id,@FirstName,@LastName,@Email,@Company,@Title)"; } catch (Exception ex) { } finally { await Task.Delay(freSecond * 1000); } } }); } /// <summary> /// 从数据库中加载所有数据 /// </summary> private void LoadAllContracts() { try { //Task.Run(() => //{ ContractsViewModels.Clear(); List<Contacts> contracts = DBDapper.DapperHelper.Query<Contacts>("Contacts"); // ContractsViewModels = AutoMapperWrapper.Map<List<Contacts>, ObservableCollection<ContractsViewModels>>(contracts); foreach (var item in contracts) { //ContractsViewModels models= AutoMapperWrapper.Map<Contacts, ContractsViewModels>(item); ContractsViewModels.Add(new ViewModels.ContractsViewModels(item)); } //}); } catch (Exception ex) { Console.WriteLine( ex.ToString()); } } /// <summary> /// 根据ID来加载指定数据 /// </summary> /// <param name="id"></param> public void LoadOnContracts(int id) { ContractsViewModel = ContractsViewModels.Where(obj => obj.ID == id).FirstOrDefault(); } /// <summary> /// 创建一个新的对象 /// </summary> public void Add() { try { int id = ContractsViewModels.Count > 0 ? ContractsViewModels[ContractsViewModels.Count - 1].ID + 1 : 0; string insert = @"insert into contacts (Id,FirstName,LastName,Email,Company,Title) values(@Id,@FirstName,@LastName,@Email,@Company,@Title)"; int res = DapperHelper.Insert<Contacts>(insert, new List<Contacts>() { new Contacts() { ID =id, FirstName = "123", LastName = "456", Email = "123", Company = "1324", Title = "444" } }); if (res > 0) { LoadAllContracts(); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } /// <summary> /// 删除对象 /// 为了方便演示我只删除数据集合中的第一个对象 /// </summary> public void Delete() { try { if (ContractsViewModels.Count > 0) { string delete = "delete from Contacts where ID= @ID"; int res = DapperHelper.Delete(delete, new { ID = ContractsViewModels[0].ID }); if (res > 0) { LoadAllContracts(); } } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } }
实现数据库的CRUD操作,我这里主要用到半自动化的ORM----Dapper。
老生常谈,首先需要从Nuget中安装Dapper,安装到DBDapper项目中。
或者在控制台中输入: Install -Package Dapper 完成Dapper下载安装。
创建一个DapperHelper帮助类
public class DapperHelper { //三部曲 //第一步:使用连接字符串创建一个IDBConnection对象; static IDbConnection conn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["SqlServerConnString"].ToString()); //第二步:编写一个查询并将其存储在一个普通的字符串变量中; public static void test() { string query = "SELECT * from contacts;"; List<Contacts> contracts=(List<Contacts>)conn.Query<Contacts>(query); Console.WriteLine( contracts.Count); } public static List<T> Query<T>(string typeofName) { string query = $"SELECT * from {typeofName};"; List<T> contracts = (List<T>)conn.Query<T>(query); return contracts; } public static int Insert<T>(string sql,IEnumerable<T> ls) { try { return conn.Execute(sql, ls); } catch (Exception ex) { throw new Exception(ex.ToString()); } } public static int Update(string sql, IEnumerable<KeyValuePair<string,object>> ls) { try { return conn.Execute(sql, ls); } catch (Exception ex) { throw new Exception(ex.ToString()); } } public static int Delete(string sql, object obj) { try { return conn.Execute(sql, obj); } catch (Exception ex) { throw new Exception(ex.ToString()); } } //public static int Update<T>(string sql, List<T> ls) //{ //} //第三步:调用db.execute()并传递查询,完成。 }
看上面的三步曲,和ADO.net大同小异,不过我们再也不用将数据库表的数据转换成实体了,采用Dapper便傻瓜式的转换了。
看我们的代码,我们会发现,Model跟View完全独立,不过这个时候,ViewModel会多出跟Model一样的一个实体出来,为了实现Model到ViewModel的一个完美映射,我这里采用了AutoMapper来实现映射。
老套路,我们从Nuget中下载AutoMapper,安装到ViewModels。
或者在控制台中输入: Install -Package AutoMapper完成AutoMapper下载安装。
在ViewModels中建立AutoMapper的一个包装类AutoMapperWrapper,AutoMapper需要初始化应映射规则,因此我们需要先创建一个映射
public class SourceProfile : MapperConfigurationExpression { public SourceProfile() { base.CreateMap<ContractsViewModels, Contacts>(); base.CreateMap<Contacts, ContractsViewModels>(); //base.CreateMap<ContractsViewModels,Models.Contracts>(); } }
AutoMapperWrapper如下:
public class AutoMapperWrapper { //protected DTOObject Result { get; set; } //protected IEnumerable<DTOObject> Results { get; set; } static Mapper mapper = null; public static void Start() { MapperConfiguration configuration = new MapperConfiguration(new SourceProfile()); mapper = new Mapper(configuration); // mapper.Map<,> // Mapper //new SourceProfile(); } public static T Map<S, T>(S soure) { T to = mapper.Map<S, T>(soure); return to; } }
以上我们便实现了一个ViewModel,DB的后台管理。接下来我们来看一个View怎么实现数据的绑定
先看一下代码
<Window x:Class="View.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:View" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800" Loaded="Window_Loaded"> <Grid> <DataGrid x:Name="datagrid" ItemsSource="{Binding ContractsViewModels}" AutoGenerateColumns="False" HorizontalAlignment="Left" Height="273" Margin="72,70,0,0" VerticalAlignment="Top" Width="415"> <DataGrid.Columns> <DataGridTextColumn Header="ID" Binding="{Binding ID}"></DataGridTextColumn> <DataGridTextColumn Header="FirstName" Binding="{Binding FirstName}"></DataGridTextColumn> <DataGridTextColumn Header="LastName" Binding="{Binding LastName}"></DataGridTextColumn> <DataGridTextColumn Header="Email" Binding="{Binding Email}"></DataGridTextColumn> <DataGridTextColumn Header="Company" Binding="{Binding Company}"></DataGridTextColumn> <DataGridTextColumn Header="Title" Binding="{Binding Title}"></DataGridTextColumn> </DataGrid.Columns> </DataGrid> <Button x:Name="btn_LoadAll" Content="加载所有Contract数据" HorizontalAlignment="Left" Margin="97,372,0,0" VerticalAlignment="Top" Click="btn_LoadAll_Click"/> <Button x:Name="btn_ChangeByTime" Content="定时改变" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Margin="284,372,0,0" Click="btn_ChangeByTime_Click"/> <Button x:Name="btn_Delete" Content="删除" HorizontalAlignment="Left" Margin="417,372,0,0" VerticalAlignment="Top" Width="75" Click="btn_Delete_Click"/> <TextBox x:Name="textBox" HorizontalAlignment="Left" Height="23" Margin="591,144,0,0" TextWrapping="Wrap" Text="{Binding ContractsViewModel.FirstName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/> <Label Content="FirstName" HorizontalAlignment="Left" Margin="506,141,0,0" VerticalAlignment="Top"/> </Grid> </Window>
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace View { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { try { ViewModels.ContractsVMServices.Instances.OnLoadAllContracts += (s, es) => { this.Dispatcher.InvokeAsync(() => datagrid.DataContext = s); }; ViewModels.ContractsVMServices.Instances.OnLoadContracts += (s, es) => { this.Dispatcher.InvokeAsync(() => textBox.DataContext = s); }; ViewModels.ContractsVMServices.Instances.Init(); ViewModels.ContractsVMServices.Instances.LoadOnContracts(0); } catch (Exception ex) { } } private void btn_LoadAll_Click(object sender, RoutedEventArgs e) { ViewModels.ContractsVMServices.Instances.Add(); } private void btn_ChangeByTime_Click(object sender, RoutedEventArgs e) { ViewModels.ContractsVMServices.Instances.ChangeByTime(); } private void btn_Delete_Click(object sender, RoutedEventArgs e) { ViewModels.ContractsVMServices.Instances.Delete(); } } }
以上我们看到DataGrid的ItemsSource必须要先绑定一个源,这个源的名称跟我们ViewModel中的数据集合是一致的。修改TextBox中的数据的时候我们发现DataGrid的数据也跟着改变。以上就是实现V与M的一个解耦。