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

用于验证和导出数据的 Excel 加载项框架

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2012 年 8 月 17 日

CPOL

19分钟阅读

viewsIcon

60984

downloadIcon

1871

一个用于扫描工作表查找标题、读取和验证数据、向用户提供反馈并以表单形式显示结果数据的框架。

问题

用户通常需要从 Excel 加载信息:用户喜欢 Excel。然而,问题在于允许一定的灵活性:信息会在电子表格的哪个位置?如果用户重新排列列会怎样?

此外,还存在验证值并向用户反馈错误/问题的麻烦。您可以显示一个对话框,显示存在问题导致操作被取消,但那样用户就必须弄清楚在哪里修复问题。

可以像纳粹一样严格要求固定列,但这限制了以更易于使用的方式组织数据的能力。用户可能还使用电子表格来存储除要上传的信息之外的其他信息,因此最好不要要求标题行位于第一列的第一个单元格。也可能有一些列并非所有用户都有理由使用,如果用户删除了该列,也许应该允许他这样做。

概述

该程序被实现为一个 Excel 加载项项目。我试图为用户创建一个灵活的环境。我还强调了实现的灵活性,以便其他人能够轻松地获取代码。标题行几乎是可用性的必需品,否则用户将不知道将数据放在哪里,因此这是我确实对用户施加的一个限制。在我的情况下,我要求

  • 每列具有特定的标题文本。
  • 标题行是单行。
  • 数据是标题行正下方的一组连续行。
  • 用于确定标题行的搜索标题必须位于列/行中,其中列 + 行 < 100。
  • 所有标题都必须在前 100 列内。
  • 在包含标题文本的单元格之前,不能存在内容等于所搜索标题(列表中的第一个标题文本)的单元格。
  • 要结束扫描,所有单元格都必须为空或无效。

这些要求可以轻松更改,但显然需要某种限制。

首先要求的是找到一个包含某列标题文本的单元格。为此,我创建了一个从左上角扫描单元格的方法,进行对角扫描。

public static bool ScanForText(Worksheet worksheet, object search,
                               out int columnIndex, out int rowIndex)
{
  string searchText = search.ToString().ToLower();
  for (int i = 1; i <= 100; i++)
  {
    for (rowIndex = 1, columnIndex = i; rowIndex <= i; rowIndex++, columnIndex--)
      if (worksheet.Cells[rowIndex, columnIndex].Value != null &&
          worksheet.Cells[rowIndex, columnIndex].Value.ToString().
            ToLower() == searchText) return true;
  }
  columnIndex = -1; rowIndex = -1;
  return false;
}

请注意,我为搜索设置了行和列的总和限制为小于 100,这在我看来是合理的,但可以轻松更改。调用此方法非常直接,只需检查此方法的返回标志即可确定搜索结果。如果找不到标题文本,我只会显示一个消息框然后返回。

工作表读取器

WorksheetReader(WorksheetReader)类是一个通用类,负责查找标题行、读取和验证数据,然后将其传输到具体类的集合中,该类的属性与列标题相同。构造函数有一个 Excel 工作表的参数,它会保存该工作表,并设置标题对象(HeaderTextColumn类)的集合,该集合将 Excel 行与列表中的标题文本关联起来。另一个参数是关于要读取的列的信息,包括标题、验证,其中包含ValueValidator的枚举。此枚举用于生成HeaderTextColumn集合,并且稍后为RowValidator使用的字典提供验证器。

public WorksheetReader(Excel.Worksheet worksheet)
{
  _worksheet = worksheet;
  _headerColumns = valueValidators.Select
        (i => new HeaderTextColumn(i.HeaderText, i.Required)).ToArray();
  _valueValidators = valueValidators;
}

每个字段(“标题”)都有一个HeaderTextColumn类的实例。由于它使用了生成ValueValidator实例的相同集合,因此只需要在一个地方添加一个字段(当然,我们仍然需要一个类来实现实际保存数据的属性 {模型})。HeaderTextColumn包含三个属性:

  • 一个用于标题字符串的属性。
  • 一个用于包含标题文本的列的属性。
  • 一个用于指示是否需要标题继续处理的标志的属性。

找到包含标题的行后,下一个任务是为每个标题文本找到列。同样,我将搜索限制为 100,但在这种情况下是 100 列。在这种情况下,我使用WorksheetReader类的一个关联方法来执行扫描,传递要搜索的行。该方法包含一个简单的搜索。

private bool ScanForHeaders(int rowIndex)
{
  int counter = 0;
  for (var columnIndex = 1; columnIndex < 100; columnIndex++)
  {
    if (_worksheet.Cells[rowIndex, columnIndex].Value != null)
    {
      Excel.Range cell = _worksheet.Cells[rowIndex, columnIndex];
      string cellText = cell.Value.ToString().ToLower();
      var headerColumn = _headerColumns.FirstOrDefault
                (i => i.CellText.ToLower() == cellText);
 
      if (headerColumn != null)
      {
        headerColumn.SetColumn(cell);
        //No need to proceed further if found all titles
        if (_headerColumns.Count() == ++counter) break;
      }
    }
  }
  return _headerColumns.All(i => !i.Required || i.ColumnIndex > 0);
}

您会注意到我检查了所有列是否都已找到,并在找到后停止搜索。此外,return检查是否已找到所有必需的列。

返回值允许确定是否已将所有必需的标题与列相关联。为了帮助向用户显示错误,WorksheetReader中还有另一个方法,该方法会格式化一个包含丢失标题列表的错误消息。

private string HeadersErrorMessage()
{
  IEnumerable<string> names = _headerColumns.Where(
        i => i.Required && i.ColumnIndex == 0).Select(i => i.CellText);
  return string.Format(
    "The following required headers are missing from the header row:{0} {1}.",
    Environment.NewLine, string.Join(", ", names));
}

以下显示了在缺少必需标题时显示的错误消息。

现在我们有了包含标题的行以及每个标题的列,就可以开始读取每一行了。这也通过一个静态方法来完成。

private bool GetSingleItem(int rowIndex, out RowValidator validator)
{
  validator = new RowValidator();
  bool isNotEndingRow = false;
  foreach (HeaderTextColumn headerColumn in _headerColumns)
  {
    int columnIndex = headerColumn.ColumnIndex;
    if (columnIndex > 0)
      isNotEndingRow |= headerColumn.UpdateFromCell(validator,
               _worksheet.Cells[rowIndex, columnIndex]);
  }
  return isNotEndingRow;
}

返回值是RowValidator类的一个实例或 null。当在与标题关联的任何列中未找到单个有效值时,将返回 null。因此,空白行将确保返回 null,这将停止行的加载。

这由GetItems方法调用,该方法读取所有数据行,并在找到似乎不是数据行的行时停止进程。

private bool GetItems(int rowTitleIndex)
{
  _validators = new List<RowValidator>();
  do
  {
    RowValidator validator;
    if (GetSingleItem(++rowTitleIndex, out validator))
      _validators.Add(validator);
    else
      break;
  } while (true);
  // Mark cells with comments and highlight, or remove comments
  foreach (var validator in Validators)
    validator.MarkErrors();
  return _validators.Any(i => i.IsValid);
}

它还负责遍历所有验证器,并在读取完成后调用MarkErrors方法,如果未找到数据记录,则返回布尔值 false。

如果没有找到有效记录,则在此处停止处理并显示消息框。

HeaderColumn类中有一个重要的方法,即UpdateFromCellUpdateFromCell方法使用单元格的值更新作为参数传递的RowValidator实例(与列标题关联的验证器)。

public bool UpdateFromCell(RowValidator record, Excel.Range cell)
{
  try
  {
    record.SetValue(CellText, cell);
    return null != cell.Value;
  }
  catch (ValidatorException)
  {
    return false;
  }
}

RowValidator实例具有SetValue(和GetValue)方法,该方法将更新与指定标题文本关联的值。此SetValue方法在try/catch块中使用,因为SetValue方法负责验证值。如果值无效,RowValidator将抛出ValidatorException。这允许UpdateFromCell返回一个指示值是否良好的值。

Validator 中的SetValue调用ValueValidator类的Validate方法来保存值并进行验证;如果验证有问题,它将抛出ValidatorException以指示该行具有无效值。

public void SetValue(string headerText, Excel.Range cell)
{
  ValueValidator foundValidator = _validators[headerText];
  foundValidator.Validate(cell);
}

WorksheetReaderStart方法被调用以启动读取,按顺序调用上述每个方法,检查操作是否成功,在出错时显示消息并中止,或者在操作成功时继续处理。

public void Start()
{
  int rowTitleIndex;
  int columnIndex;
  string searchText = _headerColumns.Select(i => i.CellText).First();
  if (!ExcelHelper.ScanForText(_worksheet, searchText, out columnIndex, 
        out rowTitleIndex))
    MessageBox.Show(string.Format(
      "Did not find a the header row.{0}(Searching for cell  " + 
               "containing '{1}'){0}Aborting...",
      Environment.NewLine, searchText), "Abort Reason", 
           MessageBoxButtons.OK, MessageBoxIcon.Error);
  else if (!ScanForHeaders(rowTitleIndex))
    MessageBox.Show(HeadersErrorMessage() + Environment.NewLine + "Aborting...",
              "Abort Reason", MessageBoxButtons.OK, MessageBoxIcon.Error);
  else if (!GetItems(rowTitleIndex))
    MessageBox.Show(string.Format(
      "Did not find any valid records during scan.{0}Aborting...",
      Environment.NewLine), "Abort Reason", MessageBoxButtons.OK, 
      MessageBoxIcon.Error);
  else if (typeof(T) == typeof(object))    //type of "object" will stop transfer.
    return;
  else if (!ReportResults())
    return;
  else
    Transfer();
}

验证器

验证器用于验证和存储单个记录或行的值,该记录或行可以有许多值,每个值都与一个标题相关联。RowValidator的核心是ValueValidator集合。此列表作为参数在构造函数中提供,并用于生成用于保存和验证数据的Dictionary

public RowValidator(IEnumerable<ValueValidator> validators)
{
  foreach (var validator in validators)
  {
    _validators.Add(validator.HeaderText, validator);
  }

此集合的重要性不言而喻,因为它是与RowValidator相关联的唯一变量。关键是要理解,这个集合不是一个简单的集合,因为每个RowValidator必须为每行生成一个新的ValueValidator实例。如果传递了一个简单的集合,那么每个RowValidator将使用与特定列关联的每个ValueValidator的同一个实例。传递的是一个生成器,每次使用yield return执行迭代器时,它都会生成新的ValueValidator实例。要向集合添加属性,迭代器中的一行代码可能如下所示:

yield return new ValueValidator("Desk", value =>
        StaticValidators.Concatenate(
          () => StaticValidators.IsNotNullOrEmpty(value),
          () => StaticValidators.ToUpper(ref value),
          () => StaticValidators.IsOneOf(value, "NBEM")),
                                      true);

我认为使用yield return比使用RowValidator类中的ValueValidator构造函数或其他地方更简洁。我很少使用yield return,但这绝对是一个我认为yield return的价值以新的方式闪耀的实例。

ValueValidator的构造函数是:

public ValueValidator(string headerText, Func<  apper, string> validator, 
           bool required)
{
  _validator = validator;
  HeaderText = headerText;
  Required = required;
  Validate(null, false);
}

请注意,validator 参数的类型是Func<StringWapper, string>StringWapper只是将一个字符串包装在一个类中。这是因为字符串是不可变的,而我希望能够更新字符串。稍后我将介绍更新Value的验证器示例。

public class StringWrapper
{
    public string Value { get; set; }
    public StringWrapper(string value) { Value = value; }
 
    public static implicit operator string(StringWrapper value)
    {
      return value.Value;
    }
 
    public int Length { get { return Value.Length; } }
 
    public override string ToString() { return Value; }
}

您会注意到我添加了一些额外的功能,这样在使用StringWrapper类时就不必总是指定Value属性。不幸的是,每当对Value属性进行赋值时,都需要指定该属性。

验证方法包含在一个名为StaticValidators的单独静态类中。上面命令中的Concatenate方法是该类中关键方法之一。它用于在设置值时连接一系列测试。

public static string Concatenate(params Func<string>[] tests)
{
  return tests.Select(test => test.Invoke()).
    FirstOrDefault(result => result != null);
}

此函数允许运行任意数量的测试,遍历其参数,运行每个参数中指定的测试,如果测试返回 null,则继续下一个测试。测试方法的示例是:

public static string IsOneOf(  apper value, params string[] values)
{
  Contract.Assert(values != null && values.All(i => i != null),
                  "One of the values is null");
  if (values.Any(i => i == value)) return null;
  return String.Format("{{0}} must be one of the following values: {0}",
                       String.Join(", ", values)); 
}

您会注意到String.Format调用中的“{{0}}”。目前,ValueValidatorValidate方法会将标题插入返回的错误消息中。

该应用程序中已创建了许多测试,并且添加更多测试非常容易。这些方法当前作为静态方法包含在StaticValidators类中。

为了展示这种方法的灵活性,还可以包含一个替换方法。

public static string Substitute(  apper value, string returnValue, 
          params string[] possibleValues)
{
  var savedValue = value;
  if (possibleValues.Any(i => i == savedValue))
  {
    value.Value = returnValue;
  }
  return null;
}

如果参数中存在匹配项,此方法将用另一个值替换该值。我还广泛使用一个强制将值转换为大写的方法。

public static string ToUpper(ref string value)
{
  value.Value = value.Value == null ? null : value.Value.ToUpper();
  return null;
}

Validator 中仅有的其他重要方法是GetValueSetValue。实现很简单。我只是在两者中都添加了一个Contract.Assert来捕获任何键不在字典中的问题。第三种方法只是遍历ValueValidator实例的集合,并使用 LINQ 检查所有实例是否都有效。

public bool IsValid
{
  get { return _validators.All(i => i.Value.IsValid); }
}

还有几个用于格式化单元格的方法,以向用户指示行值的问题,使用ValueValidator实例来实际更改单元格格式。第二个方法清除格式更改。

为了将代码放在一起,我将生成ValueValidator实例集合的代码放在了用于最终将数据从 Validator 实例保存到具有具体属性的表单的同一个类中。

值验证器

值验证器是一个类,用于包含有关单元格及其值的详细信息。它还包含实际启动验证的代码,并在发生错误时抛出包含错误消息的异常。

public void Validate(Excel.Range cell, bool throwException = true)
{
  _errorMessage = null;
  _cell = cell;
  Value = cell == null ? string.Empty : cell.Value == null ? 
    string.Empty : cell.Value.ToString();
  IsValid = true;
  var validateReturn = new   apper(Value);
  _errorMessage = _validator(validateReturn);
  Value = validateReturn.Value;
  if (!string.IsNullOrEmpty(_errorMessage))
  {
    _errorMessage = string.Format(_errorMessage, HeaderText);
    IsValid = false;
    if (throwException)
      throw new ValidatorException(_errorMessage);
  }
}

正如您所看到的,此方法有很多职责:它必须保存值、验证值、保存单元格以及用于标记单元格的错误消息,以便用户可以看到值有问题,并抛出参数throwException所需的异常。

RowValidator一样,还有几个用于格式化单元格的方法,以向用户指示行值的问题,使用ValueValidator并清除格式。

格式化 Excel UI

目前实现的单元格包含一个标题,并给出黄色背景以指示找到的标题。如果数据行的单元格中存在错误,则该单元格将获得红色边框,并在单元格中添加一个带有错误消息的注释。

在完成工作表的读取之前,单元格不会被格式化以显示问题。ValueValidator最终负责启动格式化。

public void MarkErrors()
{
  if (!IsValid)
    _savedCellFormating = ExcelHelper.AddErrorComment(_cell, _errorMessage);
}

AbstractVailidator类中的MarkErrors方法被ValueValidator类中的MarkErrors调用。

public void MarkErrors()
{
  foreach (var validator in _validators)
    validator.Value.MarkErrors();
}

这由WorksheetReader类在读取记录完成后调用(这也使得使用BackgroundWorker变得容易)。

ValueValidator中还有一个RestoreFormatting方法,它使用在MarkErrors执行期间保存在类中的信息来将单元格属性恢复到MarkErrors方法执行之前的格式。在WorksheetReader类被销毁时,这些方法会在每个验证器上调用。

public void Dispose()
{
  if (_validators != null)
  {
    var validators = _validators.ToArray();
    for (int i = _validators.Count - 1; i > -1; i--)
      validators[i].RestoreFormatting();
  }
  if (_headerColumns != null)
    foreach (var headerColumn in _headerColumns)
      headerColumn.RestoreFormatting  ();
}

调用RestoreFormatting必须按与MarkErrors相反的顺序进行,以确保格式更改以相同的顺序撤销,因为可能存在共享边框。这就是为什么验证器保存在数组中并使用for循环而不是foreach循环的原因。析构函数负责反转行,验证器代码负责反转列。ValueValidator实际调用执行恢复的辅助方法。显然,我们必须确保WorksheetReader被销毁以确保恢复完成。在代码中,销毁会在初始化新WorksheetReader实例之前调用。由于每次都创建一个新WorksheetReader实例,在Dispose方法中清理 Excel 工作表的格式,此清理效果很好。如果尝试使用同一个实例而不重置所有格式,将会大大增加处理连续单元格中错误的复杂性,这些单元格通过边框变色来识别。

从验证器传输数据

最后一步是根据需要将其传输到对象。该对象必须设计为具有与标题文本匹配的所有属性,以便静态ValidatorAdapter类的Convert方法能够有效工作。反射用于将数据从验证器传输到对象。

该适配器的核心在于构造函数。

public static List<T> Convert(IEnumerable<RowValidator> validators)
{
  var results = new List<T>();
  var properties = typeof(T).GetProperties();
  foreach (var validator in validators.Where(i => i.IsValid))
  {
    var instance = new T();

        foreach (var property in properties)
    {
      try
      {
        var value = validator.GetValue(property.Name);
        object convertedValue = FixValue(property.PropertyType, value);
        property.SetValue(instance, convertedValue, null);
      }
      catch (ArgumentException e)
      {
        // Will let issue pass since if important would 
        // have been found during development
        DisplayValidatorIssue(e.Message);
      }
    }
    results.Add(instance);
  }
  return results;
}

FixValue方法使用TypeDescriptor.GetConverter方法将值转换为正确的类型,如果存在转换器,否则代码可能会遇到问题,如果需要转换,可能会在构造函数中抛出异常。好处是反射SetValue会将对象转换为正确的类型。

private static object FixValue(Type targetType, object value)
{
  if (value.GetType() == targetType)
    return value;
  try
  {
    TypeConverter converter = TypeDescriptor.GetConverter(targetType);
    return converter.ConvertFrom(value);
  }
  catch
  {
    DisplayConverterIssue(targetType, value);
    return value;
  }
}

关于TypeConverter的说明:TypeConverter在 WPF 中用于绑定过程中的转换。这意味着它非常擅长将字符串值转换为/自字符串值,能够转换颜色以及许多其他类型。

配置类

在此示例中,有一个名为Order的单个类用于配置读取器。它有一个方法,该方法生成确保读取的值正确的验证器,以及与集合中每个项关联的一个简单属性,其名称与集合项中的标题文本值匹配。由于ValidatorClassAdapter适配器的设计,属性可以是大多数类型,只要可以通过与类型关联的TypeConverter将字符串值转换为其他类型。请注意,如果无法转换值,可能会发生异常,因此ValueValidator中该属性的验证很重要,可以检查以确保字符串是正确的。

不一定需要将ValueValidator生成器放在与属性相同的类中,但我喜欢这个解决方案,因为它将所有内容都放在一个类中。您可能更喜欢只创建一个用于属性的类,并在其他地方创建ValueValidator生成器。

尽管如此,这种设计真的很不错,因为它很容易创建一个项目,该项目可以处理项目中的许多不同类型的传输,并且没有重复。所有核心代码都可以轻松地放在一个单独的项目中,并添加到库中。

Excel 加载项代码

代码的 Excel 部分从创建一个新的 Office 组下的 Excel 2010 加载项类型的项目开始。这会自动添加一个 Excel Host Item,其中包含一个名为ThisAddIn.cs的文件。可以重命名它,但似乎不值得(我认为它应该更容易)。我实际上不需要在ThisAddIn.cs文件中太多内容,并对其进行了简化,并包含了加载功能区的信息。

public partial class ThisAddIn
{
    private void InternalStartup()
    {
      //Startup += ThisAddIn_Startup;
      //Shutdown += ThisAddIn_Shutdown;
    }
 
    //private void ThisAddIn_Startup(object sender, System.EventArgs e)
    //{
    //}
 
    //private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
    //{
    //}
 
    protected override Microsoft.Office.Core.IRibbonExtensibility 
           CreateRibbonExtensibilityObject()
    {
      return new FoxExcelAddInRibbon();
    }
}

接下来,我向项目添加了一个新的Ribbon(XML)项,将其命名为FoxExcelAddInRibbon(我不喜欢将名称保留为Ribbon1)。这会在项目中添加一个 XML 文件和一个cs文件。如果打开FoxExcelAddInRibbon.cs文件,您将看到我添加到ThisAddIn.cs文件中的代码。在功能区代码中,您会看到很多代码,大部分是为我生成的,但我进行了一些清理。我唯一添加的代码是用于处理功能区按钮的事件处理程序,以及用于获取定义为资源的自定义图像的一些代码。我将此处的代码保持相当简单,可能应该使其更简单。在功能区 XML 中,我定义了三个按钮。

<?xml version="1.0" encoding="UTF-8"?>
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui"
          onLoad="Ribbon_Load">
  <ribbon>
    <tabs>
      <tab idMso="TabAddIns">
        <group id="FxOrders"
               label="FX Orders">
          <button id="ButtonExecuteOrders"
                  size="large"
                  label="Read Orders"
                  onAction="OnExecuteOrders"
                  getImage="GetCustomImage"
                  tag="Search"/>
          <button id="ButtonInsertTitleRow"
                  size="large"
                  label="Insert Titles"
                  onAction="OnInsertTitleRow"
                  imageMso="CreateTableTemplatesGallery"/>
          <button id="ButtonShowResults"
                  size="large"
                  label="Show Results"
                  onAction="OnShowResults"
                  imageMso="GroupListToolQuickView" 
                  screentip=" Show results in new window"/>
        </group>
      </tab>
    </tabs>
  </ribbon>
</customUI>

确保在第一个“<”和文件开头之间没有空格,因为软件对这里的任何空格都很挑剔。我使用了一个自定义图标按钮(第一个),其中getImage属性中是我在代码隐藏中的过程名称,并且tag属性中传递了资源的名称。另外两个按钮使用 Office2010IconsGallery Word 文件中预定义的图像,该文件可下载为http://www.microsoft.com/en-us/download/details.aspx?id=21103

显示结果

如果别的没有,在这种测试示例中有一个显示结果的方法是很好的。然而,在实际应用程序中,它会给用户更多信心,让他们相信他们的数据将被上传。由于我可以使用 Excel 显示这些结果,因此这个窗体并非绝对必需,但项目工程师想要这个功能。

我为此功能使用了一个 Windows 窗体。可以使用 WPF(我通常用 WPF 编程),但这很麻烦,所以 Windows 窗体是最佳选择。我创建的窗体围绕在DataGridView控件中显示数据。我使用停靠功能允许用户调整视图大小。窗体底部还有一个面板,其中包含取消和提交按钮、一个Label用于显示转换结果的概述,以及一个简单的图标,如果所有行都通过验证,则显示绿色,否则显示黄色。

最终结果显示不成问题,因为DataGridView可以轻松绑定到具有要显示数据属性的对象集合。然而,设计是这样的:只有有效行才会被转换为对象;否则,只有RowValidation对象集合中的数据,其中数据实际上存储在字典中。我最初尝试使用DynamicObject适配器来显示数据,但它无法识别DynamicObject中的属性。我使用DataTable进行了编码,但这可行,但必须将字典条目转换为行中的字段。然后有人指出ICustomTypeDescriptor接口和PropertyDescriptor类。这很容易定制,以便与Dictionary<string, ValueValidator>数据源接口。

所有ICustomTypeDescriptor所做的就是提供一个存储字典的地方,并返回一个PropertyDescriptorCollection,其中包含每个属性的PropertyDescriptor实例,使用GetProperties方法。

我创建了一个继承自PropertyDescriptorRowValidatorDescriptor,为ValueValidator实例提供适配器。在此实现中有两个重要方法:PropertyType(返回类型)和GetValue。在这种情况下,类型始终是stringGetValue必须搜索正确的ValueValidator并返回其Value属性。

我对这个类做了一个小小的定制,以便在每行旁边显示一个图标。ICustomTypeDescriptor类包含一个额外的PropertyDescriptor实例用于此图标字段,并且该字段的PropertyDescriptor返回一个图像,该图像取决于所有ValueValidator实例的IsValid属性是否为 true。唯一棘手的部分是ICustomTypeDescriptorGetProperties方法中的 LINQ 语句。

public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
    return new PropertyDescriptorCollection(
        (new[] { new RowIsValidValidatorDescriptor() }).
        Cast<PropertyDescriptor>().Union(
        _valueValidators.Keys.Select(
        key => new RowValudValidatorDescriptor(key))).ToArray());
}

LINQ 需要Cast来确保这两种不同类型的PropertyDescriptor实例能够正常工作。

自定义示例

支持自定义的所有文件都在ExporterCustom文件夹中。您会注意到该文件夹中有几个额外的文件。OrderAdapter.cs是用于传输信息并包含验证信息的类。CustomValidators.cs包含一个特殊验证器,用于检查经纪人列表是否是三个字符的单词列表。StringList.csOrderAdapter中用作经纪人的类型。它派生自IEnumerable<string>。我将此类用于经纪人的原因是,我可以定义另一个类StringListTypeConverter.cs,它将在ValidatorAdapter中将字符串转换为StringList类型。TypeConverter是必需的,因为ValidatorAdapter使用类的TypeConverter将值转换为正确的类型。这两个类允许我自动将字符串转换为列表,而无需额外的代码。

Excel 问题

我在恢复格式方面有一个重大问题。如果用户更改了格式,它会工作得很好,但当我尝试恢复边框时,我会得到一个黑色边框。我不知道如何解决这个问题。

Excel 加载项说明

我经常发现加载项在 Excel 中未运行。这几乎肯定是因为它已被禁用。在 Excel 2010 中,您需要转到“文件”选项卡,单击“选项”按钮,选择“加载项”选项卡/按钮,然后在“管理”组合框中选择“已禁用项”。然后您必须单击“转到...”按钮。在对话框中,单击列表框中加载项的名称,然后单击“启用”按钮。不幸的是,Visual Studio 和 Excel 都不会通知您加载项已被禁用。这很方便。如果我知道这个问题,它本来可以为我节省一些时间。

结论

如果您经常与喜欢使用 Excel 输入数据的客户打交道,这是一个可以添加到您的工具包中的绝佳工具,它能让您快速创建一个新的 Excel 导出,该导出将提供验证并向用户反馈他们数据中的问题。现在可以通过提供更多关于传输的信息来进一步改进它,我相信这段代码将在我的团队中不断发展。

© . All rights reserved.