循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(4) ...

打印 上一主题 下一主题

主题 579|帖子 579|积分 1737

在我们设计软件的很多地方,都看到需要对表格数据进行导入和导出的操作,主要是方便客户进行快速的数据处理和分享的功能,本篇随笔介绍基于WPF实现DataGrid数据的导入和导出操作。
1、系统界面设计

在我们实现数据的导入导出功能之前,我们在主界面需要提供给客户相关的操作按钮,如下界面所示,在列表的顶端提供导入Excel、导出PDF、导出Excel。

由于这些操作功能基本上在各个页面模块,可能都会用到,因此尽可能的抽象到基类,以及提供通用的处理操作,实在有差异的,也可以通过一些属性或者事件方法的覆盖方式来实现即可。
因此我们在Xaml里面定义按钮的时候,基本上是调用视图模型的方法来通用化的处理,如下代码所示。
  1. [/code]而导入的处理操作函数ImportExcelComand的定义如下所示(注意这里声明了RelayCommand)代码会自动生成Command的后缀Command方法的。
  2. [code]    /// <summary>
  3.     /// 导出内容到Excel
  4.     /// </summary>
  5.     [<strong>RelayCommand</strong>]
  6.     private void ImportExcel()
  7.     {
  8.         var page = App.GetService<<strong>ImportExcelData</strong>>();
  9.         page!.ViewModel.Items?.Clear();
  10.         page!.ViewModel.TemplateFile = $"系统用户信息-模板.xls";
  11.         page!.OnDataSave -= ExcelData_OnDataSave;
  12.         page!.OnDataSave += ExcelData_OnDataSave;
  13.         //导航到指定页面
  14.         ViewModel.Navigate(typeof(ImportExcelData));
  15.     }
复制代码
而其中 ImportExcelData 是我们定义的通用导入页面窗体类,这里只需要实现一些属性的设置(根据子类的不同而调整,后期可以用代码生成工具生成),以及一些事件用于子类延后实现,从而可以实现自定义的数据处理的功能。
我们在下面再细说批量导入的处理细节。
 
2、数据导出到Excel

数据导出到Excel,在我们的Winform端中很常见,而WPF这里也是一样的处理方式,通用利用Excel的操作组件的封装类来实现,可以基于NPOI,也可以基于Aspose.Cell实现,根据自己的需要实现简单的封装调用即可。
导出到Excel,首先需要弹出选择目录的对话框进行选取目录,然后用于生成Excel的文件,如下界面所示。

这个处理,由于WPF可以调用.net里面的System.Windows.Forms,因此我们直接调用里面的对话框处理封装即可,这个类来自于我们的Winform的UI公用类库部分。

在前面随笔,我们介绍过为了WPF开发的方便,我们设计了几个视图基类,用于减少代码的处理。

对于不同的业务类,我们也只需要根据实际情况,生成对应的业务视图模型类即可。

我们把通用的导出操作放到了这个视图基类BaseListViewModel 里面即可,如下代码所示。
  1. /// <summary>
  2. /// 触发导出Excel处理命令
  3. /// </summary>
  4. [RelayCommand]
  5. protected virtual async Task ExportExcel(string title = "列表数据")
  6. {
  7.     var table = await this.ConvertItems(this.Items);
  8.     BaseExportExcel(table, title);
  9. }
复制代码
而其中对于DataTable的处理Excel,提供一个通用的方法。
  1.     /// <summary>
  2.     /// 可供重写的基类函数,导出Excel
  3.     /// </summary>
  4.     public virtual void BaseExportExcel(DataTable table, string title = "列表数据")
  5.     {
  6.         string file = <strong>FileDialogHelper.SaveExcel</strong>(string.Format("{0}.xls", title));
  7.         if (!string.IsNullOrEmpty(file))
  8.         {
  9.             try
  10.             {
  11.                 string error = "";
  12.                 <strong>AsposeExcelTools.DataTableToExcel2</strong>(table, file, out error);
  13.                 if (!string.IsNullOrEmpty(error))
  14.                 {
  15.                     MessageDxUtil.ShowError(string.Format("导出Excel出现错误:{0}", error));
  16.                 }
  17.                 else
  18.                 {
  19.                     if (MessageDxUtil.ShowYesNoAndTips("导出成功,是否打开文件?") == System.Windows.MessageBoxResult.Yes)
  20.                     {
  21.                         Process.Start("explorer.exe", file);
  22.                     }
  23.                 }
  24.             }
  25.             catch (Exception ex)
  26.             {
  27.                 LogTextHelper.Error(ex);
  28.                 MessageDxUtil.ShowError(ex.Message);
  29.             }
  30.         }
  31.     }
复制代码
其中FileDialogHelper.SaveExcel 的代码如下所示。
  1. /// <summary>
  2. /// 保存Excel对话框,并返回保存全路径
  3. /// </summary>
  4. /// <returns></returns>
  5. public static string <strong>SaveExcel</strong>(string filename, string initialDirectory)
  6. {
  7.     return Save("保存Excel", ExcelFilter, filename, initialDirectory);
  8. }
  9. /// <summary>
  10. /// 以指定的标题弹出保存文件对话框
  11. /// </summary>
  12. /// <param name="title">对话框标题</param>
  13. /// <param name="filter">后缀名过滤</param>
  14. /// <param name="filename">默认文件名</param>
  15. /// <param name="initialDirectory">初始化目录</param>
  16. /// <returns></returns>
  17. public static string <strong>Save</strong>(string title, string filter, string filename, string initialDirectory)
  18. {
  19.     //多语言支持
  20.     title = JsonLanguage.Default.GetString(title);
  21.     var dialog = new<strong> SaveFileDialog</strong>();
  22.     dialog.Filter = filter;
  23.     dialog.Title = title;
  24.     dialog.FileName = filename;
  25.     dialog.RestoreDirectory = true;
  26.     if (!string.IsNullOrEmpty(initialDirectory))
  27.     {
  28.         dialog.InitialDirectory = initialDirectory;
  29.     }
  30.     if (dialog.ShowDialog() == DialogResult.OK)
  31.     {
  32.         return dialog.FileName;
  33.     }
  34.     return string.Empty;
  35. }
复制代码
而其中SaveFileDialog是属于.net 中System.Windows.Forms里面的内容,WPF可以直接调用。
而DataTableToExcel2 方法,这是我们封装的一个使用Aspose.Cell的调用,主要用于快速处理DataTable到Excel的操作封装,我们也可可以利用其它操作Excel的封装,如NPOI等都可以实现。
代码如下所示。
  1. /// <summary>
  2. /// 把DataTabel转换成Excel文件
  3. /// </summary>
  4. /// <param name="datatable">DataTable对象</param>
  5. /// <param name="filepath">目标文件路径,Excel文件的全路径</param>
  6. /// <param name="error">错误信息:返回错误信息,没有错误返回""</param>
  7. /// <returns></returns>
  8. public static bool DataTableToExcel2(DataTable datatable, string filepath, out string error)
  9. {
  10.     error = "";
  11.     var wb = new Aspose.Cells.Workbook();
  12.     try
  13.     {
  14.         if (datatable == null)
  15.         {
  16.             error = "DataTableToExcel:datatable 为空";
  17.             return false;
  18.         }
  19.         //为单元格添加样式   
  20.         var style = wb.CreateStyle();
  21.         //设置居中
  22.         style.HorizontalAlignment = Aspose.Cells.TextAlignmentType.Center;
  23.         //设置背景颜色
  24.         style.ForegroundColor = System.Drawing.Color.FromArgb(153, 204, 0);
  25.         style.Pattern = BackgroundType.Solid;
  26.         style.Font.IsBold = true;
  27.         int rowIndex = 0;
  28.         for (int i = 0; i < datatable.Columns.Count; i++)
  29.         {
  30.             DataColumn col = datatable.Columns[i];
  31.             string columnName = col.Caption ?? col.ColumnName;
  32.             wb.Worksheets[0].Cells[rowIndex, i].PutValue(columnName);
  33.             wb.Worksheets[0].Cells[rowIndex, i].SetStyle(style);
  34.         }
  35.         rowIndex++;
  36.         foreach (DataRow row in datatable.Rows)
  37.         {
  38.             for (int i = 0; i < datatable.Columns.Count; i++)
  39.             {
  40.                 wb.Worksheets[0].Cells[rowIndex, i].PutValue(row[i].ToString());
  41.             }
  42.             rowIndex++;
  43.         }
  44.         for (int k = 0; k < datatable.Columns.Count; k++)
  45.         {
  46.             wb.Worksheets[0].AutoFitColumn(k, 0, 150);
  47.         }
  48.         wb.Worksheets[0].FreezePanes(1, 0, 1, datatable.Columns.Count);
  49.         wb.Save(filepath);
  50.         return true;
  51.     }
  52.     catch (Exception e)
  53.     {
  54.         error = error + " DataTableToExcel: " + e.Message;
  55.         return false;
  56.     }
  57. }
复制代码
导出Excel的内容如下界面所示。另外导出文档的内容,我们可以用于导入的数据模板的。

我们可以根据需要设置要导出的列即可。
 
3、数据导出到PDF

同样,数据导出到PDF的处理操作类似,也是通过视图基类的封装方法,实现快速的导出到PDF处理,如下是视图基类里面的实现方法。
  1. /// <summary>
  2. /// 触发导出PDF处理命令
  3. /// </summary>
  4. [RelayCommand]
  5. protected virtual async Task ExportPdf(string title = "列表数据")
  6. {
  7.     var table = await this.<strong>ConvertItems</strong>(this.Items);
  8.     <strong>BaseExportPdf</strong>(table, title);
  9. }
  10. /// <summary>
  11. /// 可供重写的基类函数,导出PDF
  12. /// </summary>
  13. public virtual void <strong>BaseExportPdf</strong>(DataTable table, string title = "列表数据")
  14. {
  15.     var pdfFile =<strong> FileDialogHelper.SavePdf</strong>();
  16.     if (!pdfFile.IsNullOrEmpty())
  17.     {
  18.         bool isLandscape = true;//是否为横向打印,默认为true
  19.         bool includeHeader = true;//是否每页包含表头信息
  20.         var headerAlignment = iText.Layout.Properties.HorizontalAlignment.CENTER;//头部的对其方式,默认为居中
  21.         float headerFontSize = 9f;//头部字体大小
  22.         float rowFontSize = 9f;//行记录字体大小
  23.         float? headerFixHeight = null;//头部的固定高度,否则为自适应
  24.         var success =<strong> TextSharpHelper.ExportTableToPdf</strong>(title, table, pdfFile, isLandscape, includeHeader, headerAlignment, headerFontSize, rowFontSize, headerFixHeight);
  25.         //提示信息
  26.         var message = success ? "导出操作成功" : "导出操作失败";
  27.         if (success)
  28.         {
  29.             Growl.SuccessGlobal(message);
  30.             Process.Start("explorer.exe", pdfFile);
  31.         }
  32.         else
  33.         {
  34.             Growl.ErrorGlobal(message);
  35.         }
  36.     }
  37. }
复制代码
通过把List的列表转换为常规的DataTable来处理,我们就可以利用之前我们随笔《在Winform分页控件中集成导出PDF文档的功能》介绍到的PDF导出函数来实现WPF数据导出到PDF的处理。
上面的 TextSharpHelper 就是对于itext7进行的封装,实现PDF的导出处理。

 引入相关的Nugget类后,封装它的辅助类代码如下所示。
  1.     /// <summary>
  2.     /// 基于iText7对PDF的导出处理
  3.     /// </summary>
  4.     public static class TextSharpHelper
  5.     {
  6.         /// <summary>
  7.         /// datatable转PDF方法
  8.         /// </summary>
  9.         /// <param name="title">标题内容</param>
  10.         /// <param name="data">dataTable数据</param>
  11.         /// <param name="pdfFile">PDF文件保存的路径</param>
  12.         /// <param name="isLandscape">是否为横向打印,默认为true</param>
  13.         /// <param name="includeHeader">是否每页包含表头信息</param>
  14.         /// <param name="headerAlignment">头部的对其方式,默认为居中对其</param>
  15.         /// <param name="headerFontSize">头部字体大小</param>
  16.         /// <param name="rowFontSize">行记录字体大小</param>
  17.         /// <param name="headerFixHeight">头部的固定高度,否则为自适应</param>
  18.         /// <returns></returns>
  19.         public static bool ExportTableToPdf(string title, DataTable data, string pdfFile, bool isLandscape = true, bool includeHeader = true, iText.Layout.Properties.HorizontalAlignment headerAlignment = iText.Layout.Properties.HorizontalAlignment.CENTER, float headerFontSize = 9f, float rowFontSize = 9f, float? headerFixHeight = null)
  20.         {var writer = new PdfWriter(pdfFile);
  21.             PdfDocument pdf = new PdfDocument(writer);
  22.             pdf.SetDefaultPageSize(isLandscape ? PageSize.A4.Rotate() : PageSize.A4); //A4横向
  23.             var doc = new Document(pdf);//设置标题
  24.             if (!string.IsNullOrEmpty(title))
  25.             {
  26.                 var param = new Paragraph(title)
  27.                          .SetFontColor(iText.Kernel.Colors.ColorConstants.BLACK)
  28.                          .SetBold()  //粗体
  29.                          .SetFontSize(headerFontSize + 5)
  30.                          .SetTextAlignment(TextAlignment.CENTER); //居中
  31.                 doc.Add(param);
  32.             }
  33.             var table = new Table(data.Columns.Count)
  34.                 .SetTextAlignment(TextAlignment.CENTER)
  35.                 .SetVerticalAlignment(VerticalAlignment.MIDDLE)
  36.                 .SetWidth(new UnitValue(UnitValue.PERCENT, 100));//缩放比例
  37.             table.UseAllAvailableWidth();
  38.             //添加表头
  39.             foreach (DataColumn dc in data.Columns)
  40.             {
  41.                 var caption = !string.IsNullOrEmpty(dc.Caption) ? dc.Caption : dc.ColumnName;
  42.                 var cell = new Cell().Add(new Paragraph(caption))
  43.                     .SetBold()
  44.                     .SetVerticalAlignment(VerticalAlignment.MIDDLE)
  45.                     .SetHorizontalAlignment(headerAlignment)
  46.                     .SetPadding(1)
  47.                     .SetFontSize(headerFontSize);
  48.                 if (headerFixHeight.HasValue)
  49.                 {
  50.                     cell.SetHeight(new UnitValue(UnitValue.POINT, headerFixHeight.Value));
  51.                 }
  52.                 table.AddHeaderCell(cell);
  53.             }
  54.             //插入数据
  55.             var colorWhite = Color.ConvertRgbToCmyk(iText.Kernel.Colors.WebColors.GetRGBColor("White"));// System.Drawing.Color.White;
  56.             var colorEvent = iText.Kernel.Colors.WebColors.GetRGBColor("LightCyan");// System.Drawing.Color.LightCyan;
  57.             var EventRowBackColor = Color.ConvertRgbToCmyk(colorEvent);
  58.             for (int i = 0; i < data.Rows.Count; i++)
  59.             {
  60.                 table.StartNewRow();//第一列开启新行
  61.                 var backgroudColor = ((i % 2 == 0) ? colorWhite : EventRowBackColor);
  62.                 for (int j = 0; j < data.Columns.Count; j++)
  63.                 {
  64.                     var text = data.Rows[i][j].ToString();
  65.                     var cell = new Cell()
  66.                         .SetBackgroundColor(backgroudColor)
  67.                         .SetFontSize(rowFontSize)
  68.                         .SetVerticalAlignment(VerticalAlignment.MIDDLE)
  69.                         .Add(new Paragraph(text));
  70.                     table.AddCell(cell);
  71.                 }
  72.             }
  73.             doc.Add(table);
  74.             pdf.Close();
  75.             writer.Close();
  76.             return true;
  77.         }
  78.     }
复制代码
导出PDF的文档效果如下所示。

 
4、导入Excel数据

 Excel数据的导入,可以降低批量处理数据的难度和繁琐的界面一个个录入,这种是一种常见的操作方式,我们主要提供固定的模板给客户下载录入数据,然后提交进行批量的导入即可。
导入的界面处理,我们这里涉及一个通用的导入界面(和WInform端的界面类似),这样我们每个不同的业务导入处理都可以重用,只需要设置一些不同的属性,以及一些事件的处理即可,如下是通用的界面效果。

 我们这里主要针对性的介绍它的设计方式,前面我们介绍,在业务界面里面调用它的时候,如下代码所示。
  1. /// <summary>
  2. /// 导出内容到Excel
  3. /// </summary>
  4. [RelayCommand]
  5. private void<strong> ImportExcel</strong>()
  6. {
  7.     var page = App.GetService<ImportExcelData>();
  8.     page!.<strong>ViewModel.Items</strong>?.Clear();
  9.     page!.<strong>ViewModel.TemplateFile</strong> = $"系统用户信息-模板.xls";
  10.     page!.<strong>OnDataSave</strong> -= ExcelData_OnDataSave;
  11.     page!.OnDataSave += ExcelData_OnDataSave;
  12.     //导航到指定页面
  13.    <strong> ViewModel.Navigate</strong>(typeof(<strong>ImportExcelData</strong>));
  14. }
复制代码
这个通用的窗体里面的视图模型,定义了一个模板的文件名称,以及一个通用的数据DataTable的集合,以及一个事件用于子类的导入转换的实现,它的视图模型类代码如下所示。
  1.     /// <summary>
  2.     /// 批量导入Excel数据的视图模型基类
  3.     /// </summary>
  4.     public partial class ImportExcelDataViewModel : BaseViewModel
  5.     {
  6.         [<strong>ObservableProperty</strong>]
  7.         private string templateFile;
  8.         [<strong>ObservableProperty</strong>]
  9.         private string importFilePath;
  10.         [<strong>ObservableProperty</strong>]
  11.         private DataTable items;
复制代码
我们为了给客户打开模板文件,方便用于录入Excel数据,因此我们在本地打开模板文件即可。
  1.         /// <summary>
  2.         /// 打开模板文件
  3.         /// </summary>
  4.         [RelayCommand]
  5.         private void OpenFile()
  6.         {
  7.             if (!this.TemplateFile.IsNullOrEmpty())
  8.             {
  9.                 var realFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, this.TemplateFile);
  10.                 if (File.Exists(realFilePath))
  11.                 {
  12.                     Process.Start("<strong>explorer.exe</strong>", realFilePath);
  13.                 }
  14.                 else
  15.                 {
  16.                     MessageDxUtil.ShowError($"没有找到该模板文件:{realFilePath}");
  17.                 }
  18.             }
  19.         }
复制代码
在通用的导入页面的后台代码里面,我们需要实现一些如选择Excel后,显示数据到DataGrid的操作,以及批量保存数据的处理。
  1.         /// <summary>
  2.         /// 选择Excel文件后,显示Excel里面的表格数据
  3.         /// </summary>
  4.         [RelayCommand]
  5.         private void BrowseExcel()
  6.         {
  7.             string file = FileDialogHelper.OpenExcel();
  8.             if (!string.IsNullOrEmpty(file))
  9.             {
  10.                 this.ViewModel.ImportFilePath = file;
  11.                 <strong>ViewData</strong>();
  12.             }
  13.         }   
复制代码
  1.         /// <summary>
  2.         /// 查看Excel文件并显示在界面上操作
  3.         /// </summary>
  4.         private void<strong> ViewData</strong>()
  5.         {
  6.             if (this.txtFilePath.Text == "")
  7.             {
  8.                 MessageDxUtil.ShowTips("请选择指定的Excel文件");
  9.                 return;
  10.             }
  11.             try
  12.             {
  13.                 var myDs = new DataSet();
  14.                 string error = "";
  15.                 <strong>AsposeExcelTools</strong>.<strong>ExcelFileToDataSet</strong>(this.txtFilePath.Text, out myDs, out error);
  16.                 this.ViewModel.Items = myDs.Tables[0];
  17.             }
  18.             catch (Exception ex)
  19.             {
  20.                 LogTextHelper.Error(ex);
  21.                 MessageDxUtil.ShowError(ex.Message);
  22.             }
  23.         }
复制代码
导入处理的操作代码如下所示。
  1. /// <summary>
  2. /// 批量保存数据到数据库
  3. /// </summary>
  4. /// <returns></returns>
  5. [RelayCommand]
  6. private async Task<CommonResult> SaveData()
  7. {            
  8.     if (ViewModel.Items == null || ViewModel.Items?.Rows?.Count == 0)
  9.         return new CommonResult(false);
  10.     if (MessageDxUtil.ShowYesNoAndWarning("该操作将把数据导入到系统数据库中,您确定是否继续?") ==  System.Windows.MessageBoxResult.Yes)
  11.     {
  12.         var dt = this.ViewModel.Items;
  13.         foreach (DataRow dr in dt.Rows)
  14.         {
  15.             try
  16.             {
  17.                 <strong>await</strong><strong> OnDataSave(dr);</strong>
  18.             }
  19.             catch (Exception ex)
  20.             {
  21.                 LogTextHelper.Error(ex);
  22.                 MessageDxUtil.ShowError(ex.Message);
  23.             }
  24.         }
  25.         return new CommonResult(true, "操作成功");
  26.     }
  27.     return new CommonResult(false);
  28. }
复制代码
注意,我们这里使用了事件的处理,把数据的转换逻辑留给子类去实现的。
  1.         /// <summary>
  2.         /// 数据保存的事件
  3.         /// </summary>
  4.         public event <strong>SaveDataHandler</strong> OnDataSave;
复制代码
这样我们在用户信息的导入页面UserListPage.xaml.cs里面的代码就可以根据实际的情况进行实现事件了。
  1.     /// <summary>
  2.     /// 导出内容到Excel
  3.     /// </summary>
  4.     [RelayCommand]
  5.     private void ImportExcel()
  6.     {
  7.         var page = App.GetService<ImportExcelData>();
  8.         page!.ViewModel.Items?.Clear();
  9.         page!.ViewModel.TemplateFile = $"系统用户信息-模板.xls";
  10.         page!.OnDataSave -=<strong> ExcelData_OnDataSave</strong>;
  11.         page!.OnDataSave +=<strong> ExcelData_OnDataSave</strong>;
  12.         //导航到指定页面
  13.         ViewModel.Navigate(typeof(ImportExcelData));
  14.     }
复制代码
这个事件的实现,主要就是把个性化的用户信息(用户信息模板里面定义的字段),转换为DataTable的行信息即可,如下代码所示。具体根据模板设计的情况进行修改即可。

具体就不再一一赘述,主要就是基类逻辑和具体实现分离,实现不同的业务功能处理即可。
以上即是我们一个列表通用页面里面,往往需要用到的通用性的导入、导出操作的介绍,希望对读者在开发WPF应用功能上有所启发,有所参考,善莫大焉。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

数据人与超自然意识

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表