• 循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(4) -- 实现DataGrid数据的导入和导出操作


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

    1、系统界面设计

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

    由于这些操作功能基本上在各个页面模块,可能都会用到,因此尽可能的抽象到基类,以及提供通用的处理操作,实在有差异的,也可以通过一些属性或者事件方法的覆盖方式来实现即可。

    因此我们在Xaml里面定义按钮的时候,基本上是调用视图模型的方法来通用化的处理,如下代码所示。

    复制代码
    <Button
        Margin="5"
        hc:IconElement.Geometry="{StaticResource t_import}"
        Command="{Binding ImportExcelCommand}"
        Content="导入Excel"
        Style="{StaticResource ButtonWarning}" />
    <Button
        Margin="5"
        hc:IconElement.Geometry="{StaticResource SaveGeometry}"
        Command="{Binding ViewModel.ExportPdfCommand}"
        CommandParameter="用户信息列表"
        Content="导出PDF"
        Style="{StaticResource ButtonSuccess}" />
    <Button
        Margin="5"
        hc:IconElement.Geometry="{StaticResource SaveGeometry}"
        Command="{Binding ViewModel.ExportExcelCommand}"
        CommandParameter="用户信息列表"
        Content="导出Excel"
        Style="{StaticResource ButtonSuccess}" />
    复制代码

    而导入的处理操作函数ImportExcelComand的定义如下所示(注意这里声明了RelayCommand)代码会自动生成Command的后缀Command方法的。

    复制代码
        /// 
        /// 导出内容到Excel
        /// 
        [RelayCommand]
        private void ImportExcel()
        {
            var page = App.GetService<ImportExcelData>();
            page!.ViewModel.Items?.Clear();
            page!.ViewModel.TemplateFile = $"系统用户信息-模板.xls";
            page!.OnDataSave -= ExcelData_OnDataSave;
            page!.OnDataSave += ExcelData_OnDataSave;
    
            //导航到指定页面
            ViewModel.Navigate(typeof(ImportExcelData));
        }
    复制代码

    而其中 ImportExcelData 是我们定义的通用导入页面窗体类,这里只需要实现一些属性的设置(根据子类的不同而调整,后期可以用代码生成工具生成),以及一些事件用于子类延后实现,从而可以实现自定义的数据处理的功能。

    我们在下面再细说批量导入的处理细节。

     

    2、数据导出到Excel

    数据导出到Excel,在我们的Winform端中很常见,而WPF这里也是一样的处理方式,通用利用Excel的操作组件的封装类来实现,可以基于NPOI,也可以基于Aspose.Cell实现,根据自己的需要实现简单的封装调用即可。

    导出到Excel,首先需要弹出选择目录的对话框进行选取目录,然后用于生成Excel的文件,如下界面所示。

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

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

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

    我们把通用的导出操作放到了这个视图基类BaseListViewModel 里面即可,如下代码所示。

    复制代码
    /// 
    /// 触发导出Excel处理命令
    /// 
    [RelayCommand]
    protected virtual async Task ExportExcel(string title = "列表数据")
    {
        var table = await this.ConvertItems(this.Items);
        BaseExportExcel(table, title);
    }
    复制代码

    而其中对于DataTable的处理Excel,提供一个通用的方法。

    复制代码
        /// 
        /// 可供重写的基类函数,导出Excel
        /// 
        public virtual void BaseExportExcel(DataTable table, string title = "列表数据")
        {
            string file = FileDialogHelper.SaveExcel(string.Format("{0}.xls", title));
            if (!string.IsNullOrEmpty(file))
            {
                try
                {
                    string error = "";
                    AsposeExcelTools.DataTableToExcel2(table, file, out error);
    
                    if (!string.IsNullOrEmpty(error))
                    {
                        MessageDxUtil.ShowError(string.Format("导出Excel出现错误:{0}", error));
                    }
                    else
                    {
                        if (MessageDxUtil.ShowYesNoAndTips("导出成功,是否打开文件?") == System.Windows.MessageBoxResult.Yes)
                        {
                            Process.Start("explorer.exe", file);
                        }
                    }
                }
                catch (Exception ex)
                {
                    LogTextHelper.Error(ex);
                    MessageDxUtil.ShowError(ex.Message);
                }
            }
        }
    复制代码

    其中FileDialogHelper.SaveExcel 的代码如下所示。

    复制代码
    /// 
    /// 保存Excel对话框,并返回保存全路径
    /// 
    /// 
    public static string SaveExcel(string filename, string initialDirectory)
    {
        return Save("保存Excel", ExcelFilter, filename, initialDirectory);
    } 
    
    /// 
    /// 以指定的标题弹出保存文件对话框
    /// 
    /// 对话框标题
    /// 后缀名过滤
    /// 默认文件名
    /// 初始化目录
    /// 
    public static string Save(string title, string filter, string filename, string initialDirectory)
    {
        //多语言支持
        title = JsonLanguage.Default.GetString(title);
    
        var dialog = new SaveFileDialog();
        dialog.Filter = filter;
        dialog.Title = title;
        dialog.FileName = filename;
        dialog.RestoreDirectory = true;
        if (!string.IsNullOrEmpty(initialDirectory))
        {
            dialog.InitialDirectory = initialDirectory;
        }
    
        if (dialog.ShowDialog() == DialogResult.OK)
        {
            return dialog.FileName;
        }
        return string.Empty;
    }
    复制代码

    而其中SaveFileDialog是属于.net 中System.Windows.Forms里面的内容,WPF可以直接调用。

    而DataTableToExcel2 方法,这是我们封装的一个使用Aspose.Cell的调用,主要用于快速处理DataTable到Excel的操作封装,我们也可可以利用其它操作Excel的封装,如NPOI等都可以实现。

    代码如下所示。

    复制代码
    /// 
    /// 把DataTabel转换成Excel文件
    /// 
    /// DataTable对象
    /// 目标文件路径,Excel文件的全路径
    /// 错误信息:返回错误信息,没有错误返回""
    /// 
    public static bool DataTableToExcel2(DataTable datatable, string filepath, out string error)
    {
        error = "";
        var wb = new Aspose.Cells.Workbook();
    
        try
        {
            if (datatable == null)
            {
                error = "DataTableToExcel:datatable 为空";
                return false;
            }
    
            //为单元格添加样式    
            var style = wb.CreateStyle();
            //设置居中
            style.HorizontalAlignment = Aspose.Cells.TextAlignmentType.Center;
            //设置背景颜色
            style.ForegroundColor = System.Drawing.Color.FromArgb(153, 204, 0);
            style.Pattern = BackgroundType.Solid;
            style.Font.IsBold = true;
    
            int rowIndex = 0;
            for (int i = 0; i < datatable.Columns.Count; i++)
            {
                DataColumn col = datatable.Columns[i];
                string columnName = col.Caption ?? col.ColumnName;
                wb.Worksheets[0].Cells[rowIndex, i].PutValue(columnName);
                wb.Worksheets[0].Cells[rowIndex, i].SetStyle(style);
            }
            rowIndex++;
    
            foreach (DataRow row in datatable.Rows)
            {
                for (int i = 0; i < datatable.Columns.Count; i++)
                {
                    wb.Worksheets[0].Cells[rowIndex, i].PutValue(row[i].ToString());
                }
                rowIndex++;
            }
    
            for (int k = 0; k < datatable.Columns.Count; k++)
            {
                wb.Worksheets[0].AutoFitColumn(k, 0, 150);
            }
            wb.Worksheets[0].FreezePanes(1, 0, 1, datatable.Columns.Count);
            wb.Save(filepath);
            return true;
        }
        catch (Exception e)
        {
            error = error + " DataTableToExcel: " + e.Message;
            return false;
        }
    
    }
    复制代码

    导出Excel的内容如下界面所示。另外导出文档的内容,我们可以用于导入的数据模板的。

    我们可以根据需要设置要导出的列即可。

     

    3、数据导出到PDF

    同样,数据导出到PDF的处理操作类似,也是通过视图基类的封装方法,实现快速的导出到PDF处理,如下是视图基类里面的实现方法。

    复制代码
    /// 
    /// 触发导出PDF处理命令
    /// 
    [RelayCommand]
    protected virtual async Task ExportPdf(string title = "列表数据")
    {
        var table = await this.ConvertItems(this.Items);
        BaseExportPdf(table, title);
    }
    
    /// 
    /// 可供重写的基类函数,导出PDF
    /// 
    public virtual void BaseExportPdf(DataTable table, string title = "列表数据")
    {
        var pdfFile = FileDialogHelper.SavePdf();
        if (!pdfFile.IsNullOrEmpty())
        {
            bool isLandscape = true;//是否为横向打印,默认为true
            bool includeHeader = true;//是否每页包含表头信息
            var headerAlignment = iText.Layout.Properties.HorizontalAlignment.CENTER;//头部的对其方式,默认为居中
            float headerFontSize = 9f;//头部字体大小
            float rowFontSize = 9f;//行记录字体大小
            float? headerFixHeight = null;//头部的固定高度,否则为自适应
    
            var success = TextSharpHelper.ExportTableToPdf(title, table, pdfFile, isLandscape, includeHeader, headerAlignment, headerFontSize, rowFontSize, headerFixHeight);
    
            //提示信息
            var message = success ? "导出操作成功" : "导出操作失败";
            if (success)
            {
                Growl.SuccessGlobal(message);
                Process.Start("explorer.exe", pdfFile);
            }
            else
            {
                Growl.ErrorGlobal(message);
            }
        }
    }
    复制代码

    通过把List的列表转换为常规的DataTable来处理,我们就可以利用之前我们随笔《在Winform分页控件中集成导出PDF文档的功能》介绍到的PDF导出函数来实现WPF数据导出到PDF的处理。

    上面的 TextSharpHelper 就是对于itext7进行的封装,实现PDF的导出处理。

     引入相关的Nugget类后,封装它的辅助类代码如下所示。

    复制代码
        /// 
        /// 基于iText7对PDF的导出处理
        /// 
        public static class TextSharpHelper
        {
            /// 
            /// datatable转PDF方法
            /// 
            /// 标题内容
            /// dataTable数据
            /// PDF文件保存的路径
            /// 是否为横向打印,默认为true
            /// 是否每页包含表头信息
            /// 头部的对其方式,默认为居中对其
            /// 头部字体大小
            /// 行记录字体大小
            /// 头部的固定高度,否则为自适应
            /// 
            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)
            {var writer = new PdfWriter(pdfFile);
                PdfDocument pdf = new PdfDocument(writer);
                pdf.SetDefaultPageSize(isLandscape ? PageSize.A4.Rotate() : PageSize.A4); //A4横向
                var doc = new Document(pdf);//设置标题
                if (!string.IsNullOrEmpty(title))
                {
                    var param = new Paragraph(title)
                             .SetFontColor(iText.Kernel.Colors.ColorConstants.BLACK)
                             .SetBold()  //粗体
                             .SetFontSize(headerFontSize + 5)
                             .SetTextAlignment(TextAlignment.CENTER); //居中
                    doc.Add(param);
                }
                var table = new Table(data.Columns.Count)
                    .SetTextAlignment(TextAlignment.CENTER)
                    .SetVerticalAlignment(VerticalAlignment.MIDDLE)
                    .SetWidth(new UnitValue(UnitValue.PERCENT, 100));//缩放比例
                table.UseAllAvailableWidth();
                //添加表头
                foreach (DataColumn dc in data.Columns)
                {
                    var caption = !string.IsNullOrEmpty(dc.Caption) ? dc.Caption : dc.ColumnName;
                    var cell = new Cell().Add(new Paragraph(caption))
                        .SetBold()
                        .SetVerticalAlignment(VerticalAlignment.MIDDLE)
                        .SetHorizontalAlignment(headerAlignment)
                        .SetPadding(1)
                        .SetFontSize(headerFontSize);
                    if (headerFixHeight.HasValue)
                    {
                        cell.SetHeight(new UnitValue(UnitValue.POINT, headerFixHeight.Value));
                    }
                    table.AddHeaderCell(cell);
                }
                //插入数据
                var colorWhite = Color.ConvertRgbToCmyk(iText.Kernel.Colors.WebColors.GetRGBColor("White"));// System.Drawing.Color.White;
                var colorEvent = iText.Kernel.Colors.WebColors.GetRGBColor("LightCyan");// System.Drawing.Color.LightCyan;
                var EventRowBackColor = Color.ConvertRgbToCmyk(colorEvent);
                for (int i = 0; i < data.Rows.Count; i++)
                {
                    table.StartNewRow();//第一列开启新行
                    var backgroudColor = ((i % 2 == 0) ? colorWhite : EventRowBackColor);
                    for (int j = 0; j < data.Columns.Count; j++)
                    {
                        var text = data.Rows[i][j].ToString();
                        var cell = new Cell()
                            .SetBackgroundColor(backgroudColor)
                            .SetFontSize(rowFontSize)
                            .SetVerticalAlignment(VerticalAlignment.MIDDLE)
                            .Add(new Paragraph(text));
                        table.AddCell(cell);
                    }
                }
                doc.Add(table);
                pdf.Close();
                writer.Close();
                return true;
            }
        }
    复制代码

    导出PDF的文档效果如下所示。

     

    4、导入Excel数据

     Excel数据的导入,可以降低批量处理数据的难度和繁琐的界面一个个录入,这种是一种常见的操作方式,我们主要提供固定的模板给客户下载录入数据,然后提交进行批量的导入即可。

    导入的界面处理,我们这里涉及一个通用的导入界面(和WInform端的界面类似),这样我们每个不同的业务导入处理都可以重用,只需要设置一些不同的属性,以及一些事件的处理即可,如下是通用的界面效果。

     我们这里主要针对性的介绍它的设计方式,前面我们介绍,在业务界面里面调用它的时候,如下代码所示。

    复制代码
    /// 
    /// 导出内容到Excel
    /// 
    [RelayCommand]
    private void ImportExcel()
    {
        var page = App.GetService();
        page!.ViewModel.Items?.Clear();
        page!.ViewModel.TemplateFile = $"系统用户信息-模板.xls";
        page!.OnDataSave -= ExcelData_OnDataSave;
        page!.OnDataSave += ExcelData_OnDataSave;
    
        //导航到指定页面
        ViewModel.Navigate(typeof(ImportExcelData));
    }
    复制代码

    这个通用的窗体里面的视图模型,定义了一个模板的文件名称,以及一个通用的数据DataTable的集合,以及一个事件用于子类的导入转换的实现,它的视图模型类代码如下所示。

    复制代码
        /// 
        /// 批量导入Excel数据的视图模型基类
        /// 
        public partial class ImportExcelDataViewModel : BaseViewModel
        {
            [ObservableProperty]
            private string templateFile;
    
            [ObservableProperty]
            private string importFilePath;
    
            [ObservableProperty]
            private DataTable items;
    复制代码

    我们为了给客户打开模板文件,方便用于录入Excel数据,因此我们在本地打开模板文件即可。

    复制代码
            /// 
            /// 打开模板文件
            /// 
            [RelayCommand]
            private void OpenFile()
            {
                if (!this.TemplateFile.IsNullOrEmpty())
                {
                    var realFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, this.TemplateFile);
                    if (File.Exists(realFilePath))
                    {
                        Process.Start("explorer.exe", realFilePath);
                    }
                    else
                    {
                        MessageDxUtil.ShowError($"没有找到该模板文件:{realFilePath}");
                    }
                }
            }
    复制代码

    在通用的导入页面的后台代码里面,我们需要实现一些如选择Excel后,显示数据到DataGrid的操作,以及批量保存数据的处理。

    复制代码
            /// 
            /// 选择Excel文件后,显示Excel里面的表格数据
            /// 
            [RelayCommand]
            private void BrowseExcel()
            {
                string file = FileDialogHelper.OpenExcel();
                if (!string.IsNullOrEmpty(file))
                {
                    this.ViewModel.ImportFilePath = file;
                    ViewData();
                }
            }    
    复制代码
    复制代码
            /// 
            /// 查看Excel文件并显示在界面上操作
            /// 
            private void ViewData()
            {
                if (this.txtFilePath.Text == "")
                {
                    MessageDxUtil.ShowTips("请选择指定的Excel文件");
                    return;
                }
    
                try
                {
                    var myDs = new DataSet();
    
                    string error = "";
                    AsposeExcelTools.ExcelFileToDataSet(this.txtFilePath.Text, out myDs, out error);
                    this.ViewModel.Items = myDs.Tables[0];
                }
                catch (Exception ex)
                {
                    LogTextHelper.Error(ex);
                    MessageDxUtil.ShowError(ex.Message);
                }
            }
    复制代码

    导入处理的操作代码如下所示。

    复制代码
    /// 
    /// 批量保存数据到数据库
    /// 
    /// 
    [RelayCommand]
    private async Task SaveData()
    {            
        if (ViewModel.Items == null || ViewModel.Items?.Rows?.Count == 0)
            return new CommonResult(false);
    
        if (MessageDxUtil.ShowYesNoAndWarning("该操作将把数据导入到系统数据库中,您确定是否继续?") ==  System.Windows.MessageBoxResult.Yes)
        {
            var dt = this.ViewModel.Items;
            foreach (DataRow dr in dt.Rows)
            {
                try
                {
                    await OnDataSave(dr);
                }
                catch (Exception ex)
                {
                    LogTextHelper.Error(ex);
                    MessageDxUtil.ShowError(ex.Message);
                }
            }
            return new CommonResult(true, "操作成功");
        }
        return new CommonResult(false);
    }
    复制代码

    注意,我们这里使用了事件的处理,把数据的转换逻辑留给子类去实现的。

            /// 
            /// 数据保存的事件
            /// 
            public event SaveDataHandler OnDataSave;

    这样我们在用户信息的导入页面UserListPage.xaml.cs里面的代码就可以根据实际的情况进行实现事件了。

    复制代码
        /// 
        /// 导出内容到Excel
        /// 
        [RelayCommand]
        private void ImportExcel()
        {
            var page = App.GetService();
            page!.ViewModel.Items?.Clear();
            page!.ViewModel.TemplateFile = $"系统用户信息-模板.xls";
            page!.OnDataSave -= ExcelData_OnDataSave;
            page!.OnDataSave += ExcelData_OnDataSave;
    
            //导航到指定页面
            ViewModel.Navigate(typeof(ImportExcelData));
        }
    复制代码

    这个事件的实现,主要就是把个性化的用户信息(用户信息模板里面定义的字段),转换为DataTable的行信息即可,如下代码所示。具体根据模板设计的情况进行修改即可。

    具体就不再一一赘述,主要就是基类逻辑和具体实现分离,实现不同的业务功能处理即可。

    以上即是我们一个列表通用页面里面,往往需要用到的通用性的导入、导出操作的介绍,希望对读者在开发WPF应用功能上有所启发,有所参考,善莫大焉。

  • 相关阅读:
    SQLite:TIMESTAMP类型使用
    linux系统向github传文件
    CSS 注释语法与应用
    coreldraw2022新版本新功能介绍cdr2022
    SpringBoot-34-shiro整合thymeleaf
    无人驾驶运用了什么技术,无人驾驶技术是
    进程间通信IPC-管道
    pyltp postagger 运行过程中报错Process finished with exit code -1073740791 (0xC0000409)
    统一身份认证登录详细介绍-identity4
    windows 服务器 应用自启动
  • 原文地址:https://www.cnblogs.com/wuhuacong/p/17711356.html