在很多系统,我们都知道,Excel数据的导入导出操作是必不可少的一个功能,这种功能能够给使用者和外部进行数据交换,也能批量迅速的录入数据到系统中;但在一些系统中,为了方便,可能把很多个基础表或者相关的数据综合到一个Excel表格文件里面,然后希望通过接口进行导入,这种需求处理就显得比较复杂一点了。本文探讨在我的客户关系管理系统中,对于单个Excel表格中,集合了客户基础数据及相关数据的导入和导出操作的处理。
1、导入导出的需求分析
本随笔主要介绍如何在系统中,导入单一文件中的数据到系统中,这个文件包含了基础数据和相关数据的导入和导出操作,一般来说这样的操作对于导入数据已经足够简便了,但是,有时候数据很多的情况下,我们可能需要每次选定文件也是一个麻烦的事情。因此指定目录进行批量数据的导入操作也是一个好的需求,可以进一步简化用户的数据导入操作。
下面我们就来介绍,导入、批量导入和导出的三个重要的操作,如图所示。
导入的数据,是一个Excel,它要求包含几个不同表的数据,导入操作一次性完成数据的导入,Excel文件的格式如下所示。
2、数据导入操作的界面设计及处理
我们知道,要一次性导入几个表的数据,需要先读取Excel获取各个Sheet(工作表)的数据,然后把它转换为DataTable的数据对象,这样我们就可以根据它的字段赋值给对应的实体类,然后调用业务逻辑处理将数据写入数据库即可。
为了直观的给使用者查看将要导入的数据,我们把需要导入到数据库的数据,展现在界面上,供客户确认,如果没有问题,就可以进行导入操作。由于我们需要操作多个数据表,因此有效读取Excel里面的Sheet就是第一步工作。
查看Excel数据的操作代码如下所示,主要的逻辑就是调用Apose.Cell的封装类进行处理
AsposeExcelTools.ExcelFileToDataSet(this.txtFilePath.Text, out myDs, out error);
把Excel文件里面多个Sheet的数据转换为DataSet,然后每个进行依次的处理,展示代码如下所示。
private void ViewData() { if (this.txtFilePath.Text == "") { MessageDxUtil.ShowTips("请选择指定的Excel文件"); return; } try { myDs.Tables.Clear(); myDs.Clear(); this.gridCustomer.DataSource = null; string error = ""; AsposeExcelTools.ExcelFileToDataSet(this.txtFilePath.Text, out myDs, out error); this.gridCustomer.DataSource = myDs.Tables[0]; this.gridView1.PopulateColumns(); this.gridFollow.DataSource = myDs.Tables[1]; this.gridView2.PopulateColumns(); this.gridContact.DataSource = myDs.Tables[2]; this.gridView3.PopulateColumns(); this.gridSupplier.DataSource = myDs.Tables[3]; this.gridView4.PopulateColumns(); } catch (Exception ex) { LogTextHelper.Error(ex); MessageDxUtil.ShowError(ex.Message); } }
由于导入过程中需要耗费一定的时间,因此我们可以通过后台线程结合进度条的方式提示用户,界面设计效果如下效果所示。
刚才说到,保存数据,我们把它放到后台线程BackgroudWorker进行处理即可,处理代码如下所示。
private void btnSaveData_Click(object sender, EventArgs e) { if (worker.IsBusy) return; if (this.txtFilePath.Text == "") { MessageDxUtil.ShowTips("请选择指定的Excel文件"); return; } if (MessageDxUtil.ShowYesNoAndWarning("该操作将把数据导入到系统数据库中,您确定是否继续?") == DialogResult.Yes) { if (myDs != null && myDs.Tables[0].Rows.Count > 0) { this.progressBar1.Visible = true; worker.RunWorkerAsync(); } } }
后台线程操作的主要业务逻辑代码如下所示,就是依次把不同的数据进行解析,并保存即可。
void worker_DoWork(object sender, DoWorkEventArgs e) { if (myDs != null && myDs.Tables.Count >= 4 && myDs.Tables[0].Rows.Count > 0) { try { ImportCustomerDataHelper helper = new ImportCustomerDataHelper(); helper.LoginUserInfo = LoginUserInfo; //写入或更新客户信息 string customerID = helper.UpdateCustomer(myDs.Tables[0]); if (!string.IsNullOrEmpty(customerID)) { helper.AddFollow(customerID, myDs.Tables[1], worker); helper.AddContact(customerID, myDs.Tables[2], worker); helper.AddSupplier(customerID, myDs.Tables[3], worker); e.Result = "操作完成"; } else { e.Result = "操作失败"; } } catch (Exception ex) { e.Result = ex.Message; LogTextHelper.Error(ex); MessageDxUtil.ShowError(ex.ToString()); } } else { e.Result = "请检查数据记录是否存在"; } }
3、数据批量导入操作
虽然上面可以一次性导入客户和其相关数据,但是还是一次性导入一个Excel,如果对于客户数据比较多的情况下,一次次导入操作也是很繁琐的事情,因此客户提出,需要按照目录把所有相关的Excel数据一次性导入,这种导入有个问题就是我们不能再中途干预导入操作,因此为了数据的安全性,我提供一个界面让客户选择目录,然后把目录里面的Excel文件列出来,然后在让客户确认是否进一步导入。
上面操作的实现代码我逐一介绍,首先第一步是需要递归列出目录下面的Excel文件,然后显示出来供用户确认导入的清单。
private void btnSelectPath_Click(object sender, EventArgs e) { string mydocDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); string selectPath = FileDialogHelper.OpenDir(mydocDir); if (!string.IsNullOrEmpty(selectPath)) { //清空就记录 this.lstPath.Items.Clear(); string[] fileArray = Directory.GetFiles(selectPath, "*.xls", SearchOption.AllDirectories); if (fileArray != null && fileArray.Length > 0) { foreach (string file in fileArray) { string fileName = Path.GetFileName(file); this.lstPath.Items.Add(new CListItem(fileName, file)); } } } }
当用户确认操作的时候,提示客户确认是否进行,确认后将统一批量导入列表里面的文件,这个地方也是为了方便,使用后台线程进行数据的导出操作,并在过程中提供进度条的指示。
private void btnConfirm_Click(object sender, EventArgs e) { if (worker.IsBusy) return; if (this.lstPath.Items.Count > 0) { if (MessageDxUtil.ShowYesNoAndTips("您确认导入列表的Excel文件吗?") == System.Windows.Forms.DialogResult.Yes) { List<string> fileList = new List<string>(); foreach (object item in this.lstPath.Items) { CListItem fileItem = item as CListItem; if (fileItem != null) { fileList.Add(fileItem.Value); } } this.progressBar1.Visible = true; worker.RunWorkerAsync(fileList); } } }
这个后台线程的处理逻辑和单个文件导入的操作差不多,只不过这里需要增加一个文件列表的遍历处理而已,具体代码如下所示。
void worker_DoWork(object sender, DoWorkEventArgs e) { List<string> fileList = e.Argument as List<string>; if (fileList == null || fileList.Count == 0) return; bool hasError = false; ImportCustomerDataHelper helper = new ImportCustomerDataHelper(); helper.LoginUserInfo = LoginUserInfo; foreach (string file in fileList) { DataSet myDs = new DataSet(); string error = ""; AsposeExcelTools.ExcelFileToDataSet(file, out myDs, out error); if (myDs != null && myDs.Tables.Count >= 4 && myDs.Tables[0].Rows.Count > 0) { try { //写入或更新客户信息 string customerID = helper.UpdateCustomer(myDs.Tables[0]); if (!string.IsNullOrEmpty(customerID)) { helper.AddFollow(customerID, myDs.Tables[1], worker); helper.AddContact(customerID, myDs.Tables[2], worker); helper.AddSupplier(customerID, myDs.Tables[3], worker); } } catch (Exception ex) { hasError = true; LogTextHelper.Error(ex); } } } string msg = "操作完成"; if (hasError) { msg += ",导入出现错误。具体可以查看log.txt日志记录。"; } e.Result = msg; }
和上面的单个文件导入一样,我们这里使用了一个封装类ImportCustomerDataHelper,用来对数据进行转换实体类,然后保存到数据库的操作过程,下面我们来简单看看里面的处理代码
/// <summary> /// 客户数据的批量导入和普通导入的操作逻辑代码 /// </summary> public class ImportCustomerDataHelper { /// <summary> /// 登陆用户信息 /// </summary> public LoginUserInfo LoginUserInfo { get; set; } /// <summary> /// 写入或更新客户数据,如果成功更新返回ID值 /// </summary> /// <param name="dataTable">客户数据表</param> /// <returns></returns> public string UpdateCustomer(DataTable dataTable) { bool success = false; bool converted = false; DateTime dtDefault = Convert.ToDateTime("1900-01-01"); DateTime dt; string result = ""; DataRow dr = dataTable.Rows[0]; if (dr != null) { string customerName = dr["客户名称"].ToString(); CustomerInfo info = CallerFactory<ICustomerService>.Instance.FindByName(customerName); bool isNew = false; if (info == null) { info = new CustomerInfo(); isNew = true; } info.Name = customerName; info.HandNo = dr["客户编号"].ToString(); info.SimpleName = dr["客户简称"].ToString(); .......................... info.IsPublic = dr["公开与否"].ToString().ToBoolean(); info.Satisfaction = dr["客户满意度"].ToString().ToInt32(); info.TransactionCount = dr["交易次数"].ToString().ToInt32(); info.TransactionTotal = dr["交易金额"].ToString().ToDecimal(); info.Creator = dr["客户所属人员"].ToString(); converted = DateTime.TryParse(dr["创建时间"].ToString(), out dt); if (converted && dt > dtDefault) { info.CreateTime = dt; } info.Editor = LoginUserInfo.ID.ToString(); info.EditTime = DateTime.Now; if (isNew) { info.Dept_ID = LoginUserInfo.DeptId; info.Company_ID = LoginUserInfo.CompanyId; success = CallerFactory<ICustomerService>.Instance.Insert(info); } else { success = CallerFactory<ICustomerService>.Instance.Update(info, info.ID); } if (success) { result = info.ID; } } return result; } ...........................
4、数据的导出操作
导出操作,我们根据用户的选择,可以一次性导出多个Excel文件,每个Excel文件包含客户的基础信息,也包含相关数据,它们的格式和导入的格式保持一致即可,这样方便数据的交换处理。
导出操作,我们需要把客户的选择信息转换为需要导出的对象列表数据,然后绑定到Excel里面即可,因此我们的Excel里面,可以通过自定义模板,指定列的数据属性就可以绑定好数据了。
获取选择的客户信息的代码如下所示。
List<CustomerInfo> list = new List<CustomerInfo>(); foreach (int iRow in rowSelected) { string ID = this.winGridViewPager1.GridView1.GetRowCellDisplayText(iRow, "ID"); CustomerInfo info = CallerFactory<ICustomerService>.Instance.FindByID(ID); if (info != null) { list.Add(info); } }
前面介绍了,我们将使用自定义模板,在模板文件里面的对应字段下面,绑定一个参数属性就可以了,通过Aspose.Cell的操作处理,我们就很方便把数据导出到Excel里面了,而里面的字段还可以很方便实现自由的裁剪操作。
自定义模板文件效果如下所示。
导出客户以及相关信息的主要核心代码如下所示。
#region 导出操作 //依次每个客户数据导出一个文件 string ownerUserName = CallerFactory<IUserService>.Instance.GetFullNameByID(customerInfo.Creator.ToInt32()); string filePath = Path.Combine(selectPath, ownerUserName); DirectoryUtil.AssertDirExist(filePath); Dictionary<string, object> dict = new Dictionary<string, object>(); dict.Add("Customer", new List<CustomerInfo>() { customerInfo });//需要构造一个列表绑定 List<FollowInfo> followList = CallerFactory<IFollowService>.Instance.Find(string.Format("Customer_ID =‘{0}‘ ", customerInfo.ID)); dict.Add("Follow", followList); List<ContactInfo> contactList = CallerFactory<IContactService>.Instance.FindByCustomer(customerInfo.ID); dict.Add("Contact", contactList); PagerInfo pagerInfo = null; List<SupplierInfo> supplierList = CallerFactory<ISupplierService>.Instance.FindByCustomer(customerInfo.ID, "", ref pagerInfo); dict.Add("Supplier", supplierList); string templateFile = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "客户综合资料-导出模板.xls"); if (!File.Exists(templateFile)) { throw new ArgumentException(templateFile, string.Format("{0} 文件不存在,", Path.GetFileName(templateFile))); } string saveFileName = string.Format("{0}.xls", customerInfo.Name); string saveFilePath = Path.Combine(filePath, saveFileName); WorkbookDesigner designer = new WorkbookDesigner(); designer.Workbook = new Workbook(templateFile); foreach (string key in dict.Keys) { designer.SetDataSource(key, dict[key]); } designer.Process(); designer.Workbook.Save(saveFilePath, SaveFormat.Excel97To2003); #endregion
这样利用Aspose.Cell的处理操作,通过绑定相关的数据对象,我们就很容易实现数据导出到符合我们预期格式的Excel里面去了,这样操作高效、代码干净,Excel格式也非常符合我们的要求。
以上就是在客户关系管理系统里面碰到特殊的数据导入导出需求的介绍和实现,希望大家相互交流,共同把软件开发过程中,数据导入导出操作的使用体验做到最好,更符合我们客户使用的习惯和需求。