返回公众号历史文章目录

文章 / 公众号历史文章

Excel催化剂开源第10波-VSTO开发之用户配置数据与工作薄文件一同存储

在传统的VBA开发中,若是用的是普通加载项方法,是可以存储数据在xlam上的,若用的是Com加载项方法同时是Addins程序级别的项目开发的,配置文件没法保存到工作薄中,一般另外用配置文件来存放供调用。 但无论以上两种方式都会带来一点缺陷,若用户配置好自己的数据,这些配置数据只能保存到自己电脑上,无

在传统的VBA开发中,若是用的是普通加载项方法,是可以存储数据在xlam上的,若用的是Com加载项方法同时是Addins程序级别的项目开发的,配置文件没法保存到工作薄中,一般另外用配置文件来存放供调用。

但无论以上两种方式都会带来一点缺陷,若用户配置好自己的数据,这些配置数据只能保存到自己电脑上,无法在文件共享给其他人使用时,配置文件一并传递过去。

当然一个折衷的方式是,在Excel文件中新建一个工作表并隐藏它来实现配置数据跟着工作薄一起带走。

这种方式有一缺点是,在用户工作薄里进行数据操作,用户很容易破坏到这些数据,就算深度隐藏工作表,仍然会轻松地用VBA方法重新将其显示出来,特别对一些敏感配置信息的保存非常不利。

针对以上的问题,是否有一种完美的解决办法呢?既能开发xlam或AddIns程序级的插件,同时又可以让配置文件信息跟着用户的文档走。
(如果是xlsm或文档型VSTO项目可以做到数据在文件中,但代码也仅能对此文档有效,对其他文档无效,也不是此处需要的完美解决方案)

大部分人能想到的方案是在自定义属性里CustomDocumentProperties 添加配置信息,但自定义属性,它的容量有限,且内容存储仅为文本型且有长度限制。

公众号文章插图

自定义属性

当需要存储复杂的结构如图片二进制数据时,用自定义属性就无能为力了,像Excel催化剂在上一波中提及的PictureBox关闭后不能存储的问题,如果需要存储其图片信息,供下一次打开文件时重新以PictureBox写入并关联对应的事件,这种方法显然仍然不符合要求。

使用CustomXMLPart对象保存配置信息

在xlsx版Excel文件中,区别于传统的xls文件,其文件本质是xml文件集合,在xlsx版文件结构中,除去工作表外,有另外一个对象同样可以存储数据,其存储数据的要求只要是xml格式的数据即可。

且此对象不止于在VSTO内使用,在VBA上也同样有此对象可供计用。

公众号文章插图

CustomXMLPart对象模型

虽然有此对象,但其要求的数据类型是xml,在VBA环境里操作xml将是个非常痛苦的事情,但在VSTO上操作,简直不能再简单的事情。

使用方式大概是先Add一个CustomXMLPart,然后使用LoadXML方法即可把xml内容加载到此对象中,下次访问时,只需访问其XML属性,即可将xml内容取出(文本格式)。

在.net环境下,特别是Ado.Net中,只需一个方法即可将一个DataTable转换为XML或将一个XML文件还原为DataTable对象,数据配置文件,无论多复杂,都可以用DataTable很轻松地管理数据,包括图片格式的二进制字段数据。

有以上.Net天然地对XML友好使用体验,在Excel上使用CustomXMLPart对象就变得十分轻松自在。

具体场景分享

在Excel催化剂的功能中,有好几个经典功能用上了CustomXMLPart存储配置信息,跟随文档传输的安全。

  • 插入图片功能,将PictureBox容器及相关属性序列化为CustomXMLPart,如图片对象,工作表中插入的单元格位置信息等。
  • 数据有效性验证功能中,验证规则及验证的单元格范围信息
  • 数据辅助录入功能中,辅助数据源及对应的录入单元格范围信息
  • 多级层级联动功能,多级数据源及对应的录入单元格范围信息。

核心代码分享

绑定事件,根据需要,绑定打开、关闭文件,或激活、失去激活状态事件

            Common.ExcelApp.WorkbookOpen += ExcelApp_WorkbookOpenLoadXMLPart;  
  
            Common.ExcelApp.WorkbookBeforeClose += ExcelApp_WorkbookBeforeCloseSerialXML;  
            Common.ExcelApp.WorkbookActivate += ExcelApp_WorkbookActivateLoadXMLPart;  
            Common.ExcelApp.WorkbookDeactivate += ExcelApp_WorkbookDeactivateSerialXML;

使用上一波的图片功能示例
打开文件时,运行图片CustomXMLPart的反序列化

        private void ExcelApp_WorkbookOpenLoadXMLPart(Excel.Workbook wb)  
        {  
            LoadXMLPartOfPictureBoxInfo(wb);  
  
        }  
  
  private static void LoadXMLPartOfPictureBoxInfo(Excel.Workbook Wb)  
        {  
            try  
            {  
                Common.ExcelApp.ScreenUpdating = false;  
                var prop = Utilities.CustomXMLPartUtility.GetCustomPropertyByPropName(Wb, pictureBoxInfoXmlPartIdName);  
  
                if (prop != null)  
                {  
                    DataSet.MemoryDataset.PictureBoxInfoDataTable pictureBoxInfos = new DataSet.MemoryDataset.PictureBoxInfoDataTable();  
  
                    Utilities.CustomXMLPartUtility.FillTableFromXMLPart(Wb, pictureBoxInfos, prop);  
  
                    foreach (var grp in pictureBoxInfos.Rows.Cast<DataSet.MemoryDataset.PictureBoxInfoRow>().GroupBy(s => s.PicSheetName))  
                    {  
                        Excel.Worksheet sht = Common.ExcelApp.ActiveWorkbook.Worksheets[grp.Key];  
  
                        Worksheet vstoSht = Globals.Factory.GetVstoObject(sht);  
  
                        foreach (var row in grp.Select(s => s))  
                        {  
                            Excel.Range dstRange = sht.Range[row.PicTopLeftCellAddress, row.PicBottomRightCellAddress];  
                            Image image = Image.FromStream(new MemoryStream(row.PictureImageBytes));  
                            ShapeArrange.AddPictureBoxToVstoWorkSheet(  
                                dstRange: dstRange,  
                                img: image,  
                                fileExt: row.PicExt,  
                                hasBordersMargin: row.PicIsLookupFromRangValue,  
                                isLookupFromRangValue: row.PicIsLookupFromRangValue,  
                                offsetRow: row.PicOffsetRow,  
                                offsetCol: row.PicOffsetCol);//  
                        }  
  
  
                    }  
                }  
            }  
            catch  
            {  
  
            }  
            finally  
            {  
                Common.ExcelApp.ScreenUpdating = true;  
            }  
        }

关闭Excel文件时,将配置信息序列化到XML中存储

   private static void SerialXMLOfPictureBox(Excel.Workbook Wb)  
        {  
            try  
            {  
                Common.ExcelApp.ScreenUpdating = false;  
                bool isExistsPictureBox = false;  
                if (Globals.Factory.HasVstoObject(Wb))//是否是vsto工作薄  
                {  
                    isExistsPictureBox = CheckExistingPictrueBox(Wb, isExistsPictureBox);  
  
                    if (isExistsPictureBox == true)  
                    {  
                        //是否保存工作薄  
                        if (Wb.Saved == false)  
                        {  
                            if (MessageBox.Show(text: "检测到有未保存的内容,请确认是否保存修改内容。\n" +  
                                "点击【是】保存修改内容并退出此工作薄,下次打开此工作薄时,工具插入的图片的双击、右键鼠标的操作仍然有效。\n" +  
                                "单击【否】不保存修改内容退出此工作薄,且在下次打开此工作薄时,工具插入的图片的双击、右键鼠标的操作会失效。",  
                                caption: "未保存内容确认", buttons: MessageBoxButtons.YesNo) == DialogResult.Yes)  
                            {  
                                Wb.Save();  
                            }  
                            else  
                            {  
                                return;  
                            }  
                        }  
                        //这里和用户交互,确定是否需要删除图片  
                        bool isReserveOlePictures = false;  
                        if (MessageBox.Show(text: "文档是否需要共享给其他没有安装【Excel催化剂】插件的人查阅?\n" +  
                            "点击【是】将保留工作薄中的图片,共享给其他人时,就算没安装【Excel催化剂】插件,插件插入的图片仍可查阅,但双击、右键鼠标的操作会失效。\n" +  
                            "单击【否】将删除工作薄中的图片,在安装【Excel催化剂】插件的电脑重新打开此工作薄时,插件插入的图片重新生成,减少存储一份图片副本,对Excel文件大小有要求时选择此项。",  
                                caption: "未保存内容确认", buttons: MessageBoxButtons.YesNo) == DialogResult.Yes)  
                        {  
                            isReserveOlePictures = true;  
                        }  
  
                        DataSet.MemoryDataset.PictureBoxInfoDataTable pictureBoxTable = new DataSet.MemoryDataset.PictureBoxInfoDataTable();  
                        //开始遍历工作表  
                        foreach (Excel.Worksheet sht in Wb.Worksheets)  
                        {  
                            if (Globals.Factory.HasVstoObject(sht))  
                            {  
                                ShapeArrange.AddSerializePictureBoxsToDataTable(sht, pictureBoxTable, isReserveOlePictures);//添加图片信息至dataTable里  
                            }  
                        }  
                        //序列化到xml文件中  
                        string xmlString = Utilities.CustomXMLPartUtility.SerialDataTableToXMLString(pictureBoxTable);  
  
                        Utilities.CustomXMLPartUtility.AddOrModifyCustomXMLPart(Wb, xmlString, pictureBoxInfoXmlPartIdName);  
  
                    }  
                    else  
                    {  
                        try  
                        {  
                            DocumentProperty prop = Common.ExcelApp.ActiveWorkbook.CustomDocumentProperties[pictureBoxInfoXmlPartIdName];  
                            CustomXMLPart xmlPart = Wb.CustomXMLParts.SelectByID(prop.Value);  
                            xmlPart.Delete();  
                        }  
                        catch  
                        {  
  
                        }  
                    }  
  
                    Wb.Save();  
                }  
            }  
            catch (Exception)  
            {  
  
            }  
            finally  
            {  
                Common.ExcelApp.ScreenUpdating = true;  
            }  
        }

在ADO.Net中进行DataTable的序列化和反序列化真的容易得很,直接贴源代码

        public static string SerialDataTableToXMLString(DataTable dt)  
        {  
            MemoryStream stream = new MemoryStream();  
            dt.WriteXml(stream, XmlWriteMode.WriteSchema);  
            stream.Position = 0;  
            StreamReader reader = new StreamReader(stream);  
            string xmlString = reader.ReadToEnd();  
            return xmlString;  
        }  
  
        public static string SerialDataSetToXMLString(System.Data.DataSet ds)  
        {  
            MemoryStream stream = new MemoryStream();  
            ds.WriteXml(stream, XmlWriteMode.WriteSchema);  
            stream.Position = 0;  
            StreamReader reader = new StreamReader(stream);  
            string xmlString = reader.ReadToEnd();  
            return xmlString;  
        }

将XML加入到CustomXMLPart也是很容易的事情

        public static void AddOrModifyCustomXMLPart(Excel.Workbook Wb, string xmlString, string xmlPartIdName)  
        {  
            //先查找有没有自定义属性PictureBoxInfoXmlPartId,有的话就是上次已经保存过xmlpart,这次只需改写内容  
            DocumentProperty prop = GetCustomPropertyByPropName(Wb, xmlPartIdName);  
            CustomXMLPart customXMLPart;  
            if (prop != null)  
            {  
                CustomXMLPart oldCustomXMLPart = Wb.CustomXMLParts.SelectByID(prop.Value);  
                if (oldCustomXMLPart != null)  
                {  
                    oldCustomXMLPart.Delete();  
                }  
  
                customXMLPart = Wb.CustomXMLParts.Add(xmlString);  
                Wb.CustomDocumentProperties[xmlPartIdName].Value = customXMLPart.Id;  
            }  
            else  
            {  
                customXMLPart = Wb.CustomXMLParts.Add(xmlString);  
                Wb.CustomDocumentProperties.Add(  
                    Name: xmlPartIdName,  
                    LinkToContent: false,  
                    Type: Microsoft.Office.Core.MsoDocProperties.msoPropertyTypeString,  
                    Value: customXMLPart.Id);  
            }  
  
        }

上述方法中,同样用到了自定义文档属性,用于存放customXMLPart的ID信息

 Wb.CustomDocumentProperties.Add(  
                    Name: xmlPartIdName,  
                    LinkToContent: false,  
                    Type: Microsoft.Office.Core.MsoDocProperties.msoPropertyTypeString,  
                    Value: customXMLPart.Id);

结语

本篇对Excel开发进行一个大胆的尝试和创新,使Addins程序级的插件项目,在维护用户配置文件信息时,有了更优的解决方案,特别是对于一些复杂的配置信息如图片等二进制数据,通过customXMLPart对象的方式存放,带来了极大的维护便利性。

技术交流QQ群

QQ群名:Excel催化剂开源讨论群, QQ群号:788145319

公众号文章插图

Excel催化剂开源讨论群二维码

关于Excel催化剂

Excel催化剂先是一微信公众号的名称,后来顺其名称,正式推出了Excel插件,插件将持续性地更新,更新的周期视本人的时间而定争取一周能够上线一个大功能模块。Excel催化剂插件承诺个人用户永久性免费使用!

Excel催化剂插件使用最新的布署技术,实现一次安装,日后所有更新自动更新完成,无需重复关注更新动态,手动下载安装包重新安装,只需一次安装即可随时保持最新版本!

Excel催化剂插件下载链接:https://pan.baidu.com/s/1Iz2\_NZJ8v7C9eqhNjdnP3Q

公众号文章插图

联系作者

公众号文章插图

公众号

取名催化剂,因Excel本身的强大,并非所有人能够立马享受到,大部分人还是在被Excel软件所虐的阶段,就是头脑里很清晰想达到的效果,而且高手们也已经实现出来,就是自己怎么弄都弄不出来,或者更糟的是还不知道Excel能够做什么而停留在不断地重复、机械、手工地在做着数据,耗费着无数的青春年华岁月。所以催生了是否可以作为一种媒介,让广大的Excel用户们可以瞬间点燃Excel的爆点,无需苦苦地挣扎地没日没夜的技巧学习、高级复杂函数的烧脑,最终走向了从入门到放弃的道路。

最后Excel功能强大,其实还需树立一个观点,不是所有事情都要交给Excel去完成,也不是所有事情Excel都是十分胜任的,外面的世界仍然是一个广阔的世界,Excel只是其中一枚耀眼的明星,还有其他更多同样精彩强大的技术、工具等。*Excel催化剂也将借力这些其他技术,让Excel能够发挥更强大的爆发!

关于Excel催化剂作者

姓名:李伟坚,从事数据分析工作多年(BI方向),一名同样在路上的学习者。
服务过行业:零售特别是鞋服类的零售行业,电商(淘宝、天猫、京东、唯品会)

技术路线从一名普通用户,通过Excel软件的学习,从此走向数据世界,非科班IT专业人士。
历经重重难关,终于在数据的道路上达到技术平原期,学习众多的知识不再太吃力,同时也形成了自己的一套数据解决方案(数据采集、数据加工清洗、数据多维建模、数据报表展示等)。

擅长技术领域:Excel等Office家族软件、VBA&VSTO的二次开发、Sqlserver数据库技术、Sqlserver的商业智能BI技术、Powerbi技术、云服务器布署技术等等。

2018年开始职业生涯作了重大调整,从原来的正职工作,转为自由职业者,暂无固定收入,暂对前面道路不太明朗,苦重新回到正职工作,对Excel催化剂的运营和开发必定受到很大的影响(正职工作时间内不可能维护也不可能随便把工作时间内的成果公布于外,工作外的时间也十分有限,因已而立之年,家庭责任重大)。

和广大拥护者一同期盼:Excel催化剂一直能运行下去,我所惠及的群体们能够给予支持(多留言鼓励下、转发下朋友圈推荐、小额打赏下和最重点的可以和所在公司及同行推荐推荐,让我的技术可以在贵司发挥价值,实现双赢(初步设想可以数据顾问的方式或一些小型项目开发的方式合作)。

技术交流QQ群

QQ群名:Excel催化剂开源讨论群, QQ群号:788145319

公众号文章插图

Excel催化剂开源讨论群二维码

关于Excel催化剂

Excel催化剂先是一微信公众号的名称,后来顺其名称,正式推出了Excel插件,插件将持续性地更新,更新的周期视本人的时间而定争取一周能够上线一个大功能模块。Excel催化剂插件承诺个人用户永久性免费使用!

Excel催化剂插件使用最新的布署技术,实现一次安装,日后所有更新自动更新完成,无需重复关注更新动态,手动下载安装包重新安装,只需一次安装即可随时保持最新版本!

Excel催化剂插件下载链接:https://pan.baidu.com/s/1Iz2\_NZJ8v7C9eqhNjdnP3Q

公众号文章插图

联系作者

公众号文章插图

公众号

取名催化剂,因Excel本身的强大,并非所有人能够立马享受到,大部分人还是在被Excel软件所虐的阶段,就是头脑里很清晰想达到的效果,而且高手们也已经实现出来,就是自己怎么弄都弄不出来,或者更糟的是还不知道Excel能够做什么而停留在不断地重复、机械、手工地在做着数据,耗费着无数的青春年华岁月。所以催生了是否可以作为一种媒介,让广大的Excel用户们可以瞬间点燃Excel的爆点,无需苦苦地挣扎地没日没夜的技巧学习、高级复杂函数的烧脑,最终走向了从入门到放弃的道路。

最后Excel功能强大,其实还需树立一个观点,不是所有事情都要交给Excel去完成,也不是所有事情Excel都是十分胜任的,外面的世界仍然是一个广阔的世界,Excel只是其中一枚耀眼的明星,还有其他更多同样精彩强大的技术、工具等。*Excel催化剂也将借力这些其他技术,让Excel能够发挥更强大的爆发!

关于Excel催化剂作者

姓名:李伟坚,从事数据分析工作多年(BI方向),一名同样在路上的学习者。
服务过行业:零售特别是鞋服类的零售行业,电商(淘宝、天猫、京东、唯品会)

技术路线从一名普通用户,通过Excel软件的学习,从此走向数据世界,非科班IT专业人士。
历经重重难关,终于在数据的道路上达到技术平原期,学习众多的知识不再太吃力,同时也形成了自己的一套数据解决方案(数据采集、数据加工清洗、数据多维建模、数据报表展示等)。

擅长技术领域:Excel等Office家族软件、VBA&VSTO的二次开发、Sqlserver数据库技术、Sqlserver的商业智能BI技术、Powerbi技术、云服务器布署技术等等。

2018年开始职业生涯作了重大调整,从原来的正职工作,转为自由职业者,暂无固定收入,暂对前面道路不太明朗,苦重新回到正职工作,对Excel催化剂的运营和开发必定受到很大的影响(正职工作时间内不可能维护也不可能随便把工作时间内的成果公布于外,工作外的时间也十分有限,因已而立之年,家庭责任重大)。

和广大拥护者一同期盼:Excel催化剂一直能运行下去,我所惠及的群体们能够给予支持(多留言鼓励下、转发下朋友圈推荐、小额打赏下和最重点的可以和所在公司及同行推荐推荐,让我的技术可以在贵司发挥价值,实现双赢(初步设想可以数据顾问的方式或一些小型项目开发的方式合作)。