65.9K
CodeProject 正在变化。 阅读更多。
Home

用于将 Excel 数据解析为平面集合的 Silverlight 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.31/5 (4投票s)

2014年4月7日

CPOL

7分钟阅读

viewsIcon

9541

downloadIcon

223

引言

很多时候需要将Excel文件中的数据导入数据库,或者仅仅是将Excel数据解析成内存中的一个类型化集合,以便后续使用。在这里,我将介绍一个我编写的Silverlight用户控件,它应该能简化开发者在类似场景下的工作。

这个控件的主要用途是帮助我快速构建Excel数据导入Web应用程序的功能,我将从这个角度来解释它的工作原理。

背景

在我的工作中,我经常需要在Web应用程序中提供将Excel文件中的数据上传到数据库(主要是SQL Server)的功能。其中一个常见痛点是Excel数据的标准化非常困难,特别是当Excel文件格式不受我们控制,并且数据是由其他人整理的时候。诸如列标题中的拼写错误、名字的随意缩写(例如“Option”缩写为“opt”)、列顺序不固定等问题,在解决这类问题时都非常普遍。

16世纪以来的一种做法是,在服务器上安装JET ODBC引擎和Excel软件包,将文件物理上传到服务器,然后使用JET读取文件并将其中的数据导入数据库。不用说,这是一种老旧的方法,并且不再受Microsoft支持,服务器管理员不喜欢在服务器上安装Excel这样的软件包,最重要的是它扩展性不强。

另一种方法是使用SSIS(SQL Server Integration Service)并设置导入包,这可以做一些ETL方面的事情,但这就要求用户(不一定是技术人员)知道如何设置和配置它们。他们想要的是简单易用的东西,让他们能够导入Excel文件而无需了解太多技术。

因此,我编写这个控件正是为了解决这个问题,如前所述,它能帮助我快速构建Excel数据导入功能。也可能存在其他用例,例如将Excel数据解析成类型化集合进行处理,而不是纠结于字符串。让我们看看...

工作流

假设我的Excel文件看起来像这样,我想将其导入数据库

我将上面的Excel数据复制并粘贴到如下所示的文本框中

点击“Next”,然后在右侧的下拉列表中选择一个值,它代表左侧预期的列名

最后,我点击“Import”,粘贴的数据就被解析成一个扁平化的列值集合,可以被客户端代码使用了。

注意:在这里需要注意的是,映射屏幕中的下拉列表包含了Excel列标题,其顺序与上面Excel文件中的顺序相同。因此,通过在下拉列表中选择一个值,并读取其 SelectedIndex 属性,我实际上是选择了Excel数据中列的索引。这是“复制、粘贴、映射和导入”整个概念的关键。

架构

该控件的结构如下所示

图例

TDTP:TabDelimitedTextParser(实际控件,包含两个面板,一个用于将Excel的原始数据接收到文本框中,另一个用于根据客户端代码发送的预期列生成列映射屏幕)

CCM:ComboboxToColumnMapping(一个辅助类,包含一个 Dictionary<int, string>,它将下拉列表中数字后缀(例如 cmb1, cmb2, cmb3)存储为键,并将预期的列名存储为值。这用于跟踪左侧的哪个列与右侧下拉列表中的选择相对应。这比存储整个组合框的名称要容易得多。而且由于这些下拉列表是在运行时由系统生成的,名称都在我的控制之下,所以我可以自由地修改它们,而不必担心其他人更改名称而破坏此跟踪功能)

public class ComboboxToColumnMapping
    {
        private Dictionary<int, string> comboBoxNumberToExpectedColumnMappings;
 
        public ComboboxToColumnMapping()
        {
            comboBoxNumberToExpectedColumnMappings = new Dictionary<int, string>();
        }
        
        public void MapComboboxNumberToExpectedColumn(int comboBoxNumberSuffix, string expectedColumn)
        {
            if (!comboBoxNumberToExpectedColumnMappings.ContainsKey(comboBoxNumberSuffix))
            {
                comboBoxNumberToExpectedColumnMappings.Add(comboBoxNumberSuffix, expectedColumn);
            }
        }
 
        public string GetExpectedColumnComboboxIsMappedTo(int comboBoxNumberSuffix)
        {
            string expectedColumn = String.Empty;
 
            if (comboBoxNumberToExpectedColumnMappings.ContainsKey(comboBoxNumberSuffix))
            {
                expectedColumn = comboBoxNumberToExpectedColumnMappings[comboBoxNumberSuffix];
            }
 
            return expectedColumn;
        }
    } 

CIM:ColumnToIndexMapping(一个辅助类,包含一个 Dictionary<string, int>,它将预期的列名存储为键,并将下拉列表中的 SelectedIndex 值存储为值。这实际上是将左侧的预期列映射到右侧下拉列表中可用的列)

public class ColumnToIndexMapping
    {
        private Dictionary<string, int> expectedColumnToIndexMappings;
 
        public ColumnToIndexMapping()
        {
            expectedColumnToIndexMappings = new Dictionary<string, int>();
        }
 
        public void SeedColumnToIndexMappingTable(List<string> expectedColumns)
        {
            for (int i = 0; i < expectedColumns.Count; i++)
            {
                //bug fix: check to prevent duplicate keys. 5-Apr-2014
                if (!expectedColumnToIndexMappings.ContainsKey(expectedColumns[i]))
                {
                    expectedColumnToIndexMappings.Add(expectedColumns[i], -1);
                }
            }
        } 
       
        public void MapColumnToIndex(string columnName, int selectedIndex)
        {
            if (expectedColumnToIndexMappings.ContainsKey(columnName))
            {
                expectedColumnToIndexMappings[columnName] = selectedIndex;
            }
        }
 
        public int GetMappedColumnIndex(string columnName)
        {
            int index = -1;
 
            if (expectedColumnToIndexMappings.ContainsKey(columnName))
            {
                index = expectedColumnToIndexMappings[columnName];
            }
 
            return index;
        }
    } 

FCB:FlatCollectionBuilder(输出构建器)

public class FlatCollectionBuilder
    {
        public static Dictionary<string, List<string>> PrepareFlatColumnValuesCollection(string rawPastedData, List<string> expectedColumns, ColumnToIndexMapping columnToIndexMapping)
        {
            ImportParser importDataParser = new ImportParser(rawPastedData);
            string[] allRows = importDataParser.GetRows();
            Dictionary<string, List<string>> flatColumnValues = new Dictionary<string, List<string>>();
 
            foreach (string expectedColumn in expectedColumns)
            {
                List<string> valuesForThisColumn = new List<string>();
                int expectedColumnIndex = columnToIndexMapping.GetMappedColumnIndex(expectedColumn);
 
                //don't include the ones that have been left on their default selections
                if (expectedColumnIndex >= 0)
                {
                    for (int i = 1; i < allRows.Length; i++)
                    {
                        string[] columnsInThisRow = importDataParser.GetColumns(allRows[i]);
                        valuesForThisColumn.Add(columnsInThisRow[expectedColumnIndex]);
                    }
 
                    flatColumnValues.Add(expectedColumn, valuesForThisColumn);
                }
            }
 
            return flatColumnValues;
        }
    } 

DDE(未显示):DelimitedDataExtractor(辅助工具,用于使用 String 类的 Split(...) 方法将制表符和换行符分隔的Excel数据解析为字符串数组。实际的分隔功能封装在另一个名为ToolsNUtils.AgLight的DLL中,并随源代码一起提供)

public class DelimitedDataExtractor
    {
        public static List<string> ExtractColumnsFromPastedData(string rawPastedData)
        {
            ImportParser importDataParser = new ImportParser(rawPastedData);
            string[] allRows = importDataParser.GetRows();
            string[] allColumns = importDataParser.GetColumns(allRows[0]);
            List<string> columnsInPastedData = allColumns.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x).ToList();
            columnsInPastedData.Insert(columnsInPastedData.Count, Resource.ResourceManager.GetString("PleaseSelect"));
 
            return columnsInPastedData;
        }
    } 

此控件的输出是一个扁平化的1维列值表,即 Dictionary<string, List<string>>,看起来像这样

每个键代表一个列名,关联的值即 List 代表该列的值列表。不同键下的值之间存在严格的一对一对应关系,并代表Excel数据的一行,例如在上面的图片中,John的姓是Gostick,出生日期是1982年10月11日;同样,Mike的姓是Bennett,出生日期是1983年11月12日。

然后,这些数据就可以转换为类型化集合,例如 ObservableCollection<Student>,并导入数据库。

使此控件真正灵活的最重要的一点是,下拉映射屏幕是完全基于发送给它的预期列列表在运行时生成的。这些预期列形成左侧的标签(列名),而粘贴到此控件中的Excel数据的标题则填充到右侧的下拉列表中。

使用代码

此控件可以托管在Silverlight子窗口、另一个用户控件或页面等地方。客户端代码需要传递一个 List<string>(它期望在扁平化集合中看到的预期列列表)和一个 Action<Dictionary<string, List<string>>>(在输出准备好后执行),如下图所示,该控件托管在Silverlight ChildWindow 中。

宿主 ChildWindow: 的XAML标记:

<controls:ChildWindow x:Class="SimpleTabDelimitedDataImporter.Popups.ImportPopup"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
           Width="Auto" Height="Auto"            
           xmlns:ImportControls="clr-namespace:SimpleTabDelimitedDataImporter.Controls" Loaded="ChildWindow_Loaded">
    <Grid x:Name="LayoutRoot" Margin="2">
        <Grid.RowDefinitions>            
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
 
        <ImportControls:TabDelimitedTextParser x:Name="copyPasteImporter" Grid.Row="0"></ImportControls:TabDelimitedTextParser>
    </Grid>
</controls:ChildWindow> 

弹出窗口的代码隐藏

public partial class ImportPopup : ChildWindow
    {
        private List<string> expectedColumns;
        private Action<Dictionary<string, List<string>>> columnValuesReadyCallback;
 
        public ImportPopup()
        {
            InitializeComponent();
            expectedColumns = new List<string>();
        }
 
        public ImportPopup(List<string> expectedColumns, Action<Dictionary<string, List<string>>> columnValuesReadyCallback)
        {
            InitializeComponent();
 
            if (expectedColumns != null)
            {
                this.expectedColumns = expectedColumns;
            }
            else
            {
                this.expectedColumns = new List<string>();
            }
 
            this.columnValuesReadyCallback = columnValuesReadyCallback;
        }        
 
        private void ChildWindow_Loaded(object sender, RoutedEventArgs e)
        {
            SetLocalisedWindowTitle();
            copyPasteImporter.ExpectedColumns = this.expectedColumns;
            copyPasteImporter.ColumnValuesReadyCallback = this.columnValuesReadyCallback;
        }
 
        private void SetLocalisedWindowTitle()
        {
            this.Title = Resource.ResourceManager.GetString("ImportPopup");
        }
    } 

正如所见,我准备了一个我希望将Excel列映射到的列列表(即期望看到的列),定义了一个我希望在输出准备好后执行的 Action,最后将这些输入参数加载到弹出窗口中。

public void ImportData()
        {            
            List<string> expectedcolumns = new List<string>();
            expectedcolumns.Add(Helpers.Constants.FIRSTNAME);
            expectedcolumns.Add(Helpers.Constants.LASTNAME);
            expectedcolumns.Add(Helpers.Constants.DATEOFBIRTH);
            importPopup = new Popups.ImportPopup(expectedcolumns, new Action<Dictionary<string, List<string>>>(ColumnValuesReady));
            importPopup.Show();
        }
 
        private void ColumnValuesReady(Dictionary<string, List<string>> columnValues)
        {
            if (columnValues != null && columnValues.Count > 0)
            {
                importPopup.DialogResult = true;
                ImportStudentData(columnValues);
            }            
        } 

当结果准备就绪时,会执行 ColumnValuesReady 回调函数。正如你所见,我选择了将此数据导入数据库。但是,如何将这种扁平化的1维数据导入数据库表呢?嗯,我首先将其转换为一个代表表结构的类型化集合,如下所示。

private ObservableCollection<Services.StudentDTO> ConvertToDTOs(Dictionary<string, List<string>> flatColumnValues)
        {
            ObservableCollection<Services.StudentDTO> studentsToBeImported = new ObservableCollection<Services.StudentDTO>();
            //parsing logic. Since the number of values under each key would always be the same (Excel data would be x rows and y columns
            //but the number of cells under each column would always be same, you cannot select jagged data. With notepad its possible
            //so would need to handle that case)
            //get the iteration count from under any key here i have used the first key
            int numberOfStudents = flatColumnValues[Helpers.Constants.FIRSTNAME].Count;
 
            for (int i = 0; i < numberOfStudents; i++)
            {
                Services.StudentDTO student = new Services.StudentDTO();
                student.Id = Guid.NewGuid();
                student.FirstName = flatColumnValues[Helpers.Constants.FIRSTNAME][i];
                student.LastName = flatColumnValues[Helpers.Constants.LASTNAME][i];
                student.DateOfBirth = DateTime.Parse(flatColumnValues[Helpers.Constants.DATEOFBIRTH][i]);
                studentsToBeImported.Add(student);
            }                                   
            
            return studentsToBeImported;
        } 

这里的绝妙之处在于,由于不同列下的值是一对一对应的,我可以轻松地找出需要导入多少个学生记录,并将其作为迭代限制 numberOfStudents

因此,通过执行

int numberOfStudents = flatColumnValues[Helpers.Constants.FIRSTNAME].Count; 

我可以找出学生名字的数量,这表明了学生的总数(一个学生只会有一个名字)。我也可以选择 LASTNAMEDATEOFBIRTH,结果会一样,都是3(看看Excel数据的截图,有3个学生我要导入数据库)。

关注点

虽然这基本上完成了我想要它做的工作,但我确信它远非完美,这就是为什么在这样的网站上分享很有帮助,因为有人可能会发现bug和/或修复它,或者告诉我。我想要进一步研究的一个案例是如何处理关联对象,例如客户-订单数据是一个二维数据结构,但这个控件只返回一维结构,并且所有都是字符串类型,所以也许客户端代码需要自己进行数据重塑,这听起来并不过分。控件只是做了它被设计来做的事情,不多也不少。

请随时在下面的评论区提问或发表评论、批评,并随意使用这些代码。希望如果您发现自己不时地遇到类似问题,您会觉得它有用。

© . All rights reserved.