Click here to Skip to main content
Click here to Skip to main content

Excel Add-in Framework for Validating and Exporting Data

, 17 Aug 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
A framework for scanning a worksheet for headers, reading and validating data, providing feedback to the user, and displaying the data in a form with the results.

The Problem

It is often a requirement to allow users to load information from Excel: users love Excel. However the problem comes with allowing a certain amount of flexibility: where in the spreadsheet will the information be and what happens if the user rearranges the columns?

Also there is the problem of validating values, and providing feedback to the user on where there are errors/issues. You can display a dialog that shows where there are issues that causes the operation to be canceled, but then the user has to figure out where to fix the problem.

One can be a Nazi about it and require columns to be fixed, but that limits the ability to organize data in a way that is easier to use. The user may also be using the spreadsheet to hold information besides what will be uploaded, so it is nice not to require the header row to be in the first row of the first column. There could also be columns that not all users have a reason to use, and if the user deletes the column, maybe he should be allowed to.

Overview

The program is implemented as an Excel add-in project. I tried to create a flexible environment for the user. I also stressed flexibility in implementation so that it will be easy for somebody else to take the code. A header row is almost a necessity for usability since otherwise the user will not know where to put his data, so this is one limitation I did put on the user. In my case I am requiring:

  • A specific header text for each column.
  • The header row is a single row.
  • The data be a contiguous set of rows right below the header row.
  • The searched for header to determine the header row must be in a column/row in which column + row < 100.
  • All the headers must be within the first 100 columns.
  • There cannot be a cell with the contents equal to the searched for header (the first header text in the list) before the cell that contains the header text in the Excel sheet.
  • To end a scan, all cells be empty or invalid.

These requirements could be easily changed, but there is obviously a need for some sort of limitations.

The first thing that is required is to find a cell that contains the string that is the header text for one of the columns. For this I created a method that scans cells from the upper left corner, doing a diagonal scan:

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;
}

Notice that I have put a limit of the sum of the row and columns for the search must be less than 100, which seemed reasonable to me, but can be easily changed. Calling this method is fairly straightforward, and simply checking the return flag from this method determines the result of the search. If the header text is not found, then I just display a message box and return.

The Worksheet Reader

The Worksheet Reader (WorksheetReader) class is a generic class that is responsible for finding the header row, reading and validating the data, and then transferring to a collection of concrete class with the properties the same as the column headers. The constructor has an Excel Worksheet argument, which it saves, and also sets up the collection of Header objects (HeaderTextColumn class) that associate an Excel row with a header text in the list. The other argument is the information about the columns that are to be read, including the header title, validation, which consists of an enumeration of ValueValidator. This enumeration is used to generate the HeaderTextColumn collection, and, later provides the validators for the dictionary used by the RowValidator.

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

There is an instance of the HeaderTextColumn class for each field (“Header”). Since it uses the same collection as used to generate ValueValidator instances, there is only one place required for adding a field (of course, we still need a class that actually implements the properties for finally saving the data {the Model}). The HeaderTextColumn contains three properties:

  • A property for the header string.
  • A property for the column containing the header text.
  • A property for a flag that indicates if the header is required to continue processing.

After finding the row containing the headers, the next task is finding the column for each header text. Again I limit the search to 100, but in this case it is 100 columns. In this case I use a method associated with the WorksheetReader class to do the scan, passing the row to search. The method contains a simple search:

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);
}

You will notice that I check to see if all columns have been found, and stop the search once that occurs. Also the return checks if all required columns have been found.

The return value allows it to be determined if all required headers have been associated with a column. To help in displaying an error to the user, there is another method in WorksheetReader that formats an error message that contains a list of missing headers:

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));
}

The following shows the error message being displayed when a required header is missing:

Now that we have the row that contains the headers, and the columns for each header, it is possible to start reading each subsequent row. This is also accomplished by a static method:

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;
}

The return is either an instance of a RowValidator class or null. A null will be returned whenever not a single valid value is found in any of the columns associated with the headers. Thus a blank row will ensure that a null will be returned, which will halt the loading of rows.

This is called by the GetItems method, which reads all the data rows, and stopping the process once a row is found that does not seem to be a data row:

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);
}

It also is responsible for going through all the validators and calling the MarkErrors method after the reading is complete, and for returning a Boolean false if no data records were found.

If no valid records are found, processing is halted at this point with a message box:

There is one important method in the HeaderColumn class, UpdateFromCell. The UpdateFromCell method updates the instance of RowValidator (the validator associated with the column header) passed as an argument with the value from the cell.

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

The RowValidator instance has a SetValue (and GetValue) method that will update the value associated with the specified header text. This SetValue method is used in a try/catch block because the SetValue method is responsible for validating the value. If the value does not validate, the RowValidator will throw a ValidatorException. This allows the UpdateFromCell to return a value indicating if the value is good.

The SetValue in the Validator calls the Validate method of the ValueValidator class to save the value, and validate; if there is a problem with the validation, it will throw a ValidatorException to indicate that the row has an invalid value.

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

The Start method of the WorksheetReader is called to initiate the reading, calling each of the methods above in order, checking that the operation was successful, displaying a message on errors and aborting, or continuing the processing if the operation was successful:

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();
}

The Validator

The Validator is used to validate and store the values of a single record or row, which can have numerous values, each associated with a header. The heart of the RowValidator is the ValueValidator collection. This list is provided as an argument in the constructor, and is used to generate the Dictionary used for saving and validating the data.

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

The importance of this collection can be easily seen in that it is the only variable associated with the RowValidator. What is critical to understand is that this collection is not a simple collection because a new ValueValidator instance must be generated for each row for each RowValidator. If a simple collection was passed, then each RowValidator would be using the same instance of each ValueValidator associated with a particular column. What is passed is a generator that generates new instances of a ValueValidator each time the iterator is executed using yield return. To add a property to the collection, a line of code in the iterator would look something like the following:

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

Using yield return I think yields a cleaner solution than using the ValueValidator constructor in the RowValidator class, or someplace else. Very seldom have I used yield return, but this is definitely an instance that I think the value of the yield return shines in a new way.

The constructor for the ValueValidator is:

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

Notice that the validator argument is of type Func<StringWapper, string>. The StringWapper just wraps a string in a class. This is because string is immutable, and I will want to be able to update the string. Later I will cover samples of validators that update the 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; }
}

You will note that I added a little bit of extra functionality so that I would not always have to specify the Value property when using the StringWrapper class. Unfortunately, whenever there is an assignment to the Value property, we need to specify the property.

The validation methods are contained in a separate static class named StaticValidators. The Concatenate method in the command above is one of the critical methods in this class. It is used to concatenate a series of tests on a value when it is set:

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

This function allows any number of tests to be run, going through its arguments, running the test specified in each argument, and going on to the next test if the test returns a null. A sample of a test method is as follows:

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)); 
}

You will notice “{{0}}” in the String.Format call. Currently the ValueValidator Validate method will insert the header title into the returned error message.

A number of tests have already been created in this application, and it is quite easy to add more. These methods are currently contained as static methods within the StaticValidators class.

To show the flexibility of this approach, it is also possible to include a substitute method:

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;
}

This method will replace the value with another value if there is a match in the parameters. I also use a method extensively that will force the value to be upper case:

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

The only other important methods in the Validator are GetValue and SetValue. The implementation is straightforward. I just added a Contract.Assert to both to catch any issues with keys not being in the dictionary. A third method just goes through the collection of ValueValidator instances, and checks that all are valid using LINQ:

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

There are also a couple of methods for formatting the cells to indicate to the user the issues with the row values using the ValueValidator instances to actually change to cell formatting. The second method clears the formatting changes.

To keep things together, I have put the code to generate the collection of ValueValidator instances in the same class that is used to ultimately save the values from the Validator instances in a form that has concrete properties.

The Value Validator

The value validator is a class that is used to contain the detailed information about a cell and its value. It also contains the code that actually initiates the validation, and throws the exception if there is an error that will contain the error message:

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);
  }
}

As can be seen, this method has a lot of responsibilities: it has to save the value, validate the value, save the cell, and the error message for marking the cell so the user can see that the value has a problem, and throw the exception required by the argument throwException.

As with the RowValidator, there are also a couple of methods for formatting the cells to indicate to the user the issues with the row values using the ValueValidator and to clear the formatting.

Formatting Excel UI

As currently implemented, the cell containing a header is given a yellow background to indicate the headers that were found. If there is an error in the cell of a data row, then the cell is given a red border, and a comment with the error message is added to the cell.

The cells are not formatted to show issues until the reading of the worksheet is completed. ValueValidator is ultimately responsible for initiating the formatting:

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

The MarkErrors method in the ValueValidator class is called by MarkErrors in the AbstractVailidator class:

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

This is called by the GetItems method of the WorksheetReader class after completion of the reading of records (this will also make it easy to use a BackgroundWorker).

There is also a RestoreFormatting method in the ValueValidator that uses information saved in a class during the MarkErrors execution to restore cell properties to formats before the MarkErrors methods are executed. The methods are called on each Validator when the WorksheetReader class is disposed of:

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  ();
}

The calling of the RestoreFormatting has to be done in the reverse order of the MarkErrors to ensure that formatting changes done during MarkErrors are undone in the same order since there could be shared borders. That is why the validators are saved in an array and a for loop is used instead of a foreach loop. The destructor is responsible for reversing the rows, and the Validator code is responsible for reversing the columns. The ValueValidator actually calls the helper method that does the restore. Obviously we have to ensure that the WorksheetReader is disposed of to ensure that the restore is done. In the code the dispose is called before a new instance of the WorksheetReader is initialized. Since a new instance of the WorksheetReader is created each time, in the implementation of the Dispose method to clean up the formatting of the Excel worksheet, this cleanup works perfectly. If an attempt was made to use the same instance, and not reset all formatting, there would be a great deal of increased complexity to deal with errors in contiguous cells which are identified by the border changing color.

Transfer of Data from Validator

The final step is to transfer to an object, if that is desired. This object must be designed to have all properties that match the header text if the static ValidatorAdapter class’ Convert method will work effectively. Reflection is used to transfer data from the Validator to the object.

The heart of this adapter is in the constructor:

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;
}

The FixValue method uses the TypeDescriptor.GetConverter method to convert to the proper type, if a converter is available, otherwise the code could have trouble if a conversion is required, and there will probably be an exception thrown in the constructor. The nice thing is that the Reflection SetValue will cast the object to the correct type:

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;
  }
}

Note on TypeConverter: TypeConverter is used in WPF for converting during binding. This means that it is really good at converting to/from a string value, being able to convert colors, and many other types.

Configuration Class

In this example there is a single class named Order used to configure the reader. It has a method that generates the validators required to ensure that values read are correct, and a simple property associated with each item in the collection and whose name matches the header text value in the collection items. Because of the design of the ValidatorClassAdapter adapter, the properties can be most types as long as there is a way to convert the string value using the TypeConverter associated with the type. Beware that there may be an exception if it is not possible to convert the value, so it is important that the validation for the property in the ValueValidator checks to ensure that the string is the correct one for the type.

It is not necessary to have the ValueValidator generator in the same class as the properties, but I like this solution because everything is in one class. You may prefer to just have a class for the properties, and create the ValueValidator generator someplace else.

Still this design is really nice because it is easy to create a project that will handle many different types of transfers in a project, and have no duplication. All the core code could easily be put into a separate project, and added to a library.

Excel Add-in Code

The Excel part of the code starts with creating a new project of type Excel 2010 Add-In under the Office group. This automatically adds an Excel Host Item which contains a file called ThisAddIn.cs. This can be renamed, but seems more trouble than it is worth (I think it should be much easier). I really do not need much in this ThisAddIn.cs file, and simplify it from what is provided, and include information so that the ribbon will be loaded:

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();
    }
}

Next I add a new Ribbon (XML) item to the project, calling it FoxExcelAddInRibbon (don’t like leaving the name as Ribbon1). This adds an XML file and a cs file to the project. If you open up the FoxExcelAddInRibbon.cs file you will see the code I added to the ThisAddIn.cs file. In the ribbon code you will see a lot of code, most of it generated for me, but I did a little cleanup. The only code I added were event handlers to handle the ribbon button, and some code to get a custom image defined as a resource. I kept the code in this fairly simple, probably should have made it simpler. In the ribbon XML, I have the three buttons defined:

<?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>

Make sure that there is no space between the first “<” and the start of the file because the software is fussy about any space here. I used one custom icon button (the first) where I have the name of the procedure in the code-behind in the getImage attribute, and pass the name of the resource in the tag attribute. The other two buttons use predefined images that can be found in the Office2010IconsGallery Word file which can be downloaded as http://www.microsoft.com/en-us/download/details.aspx?id=21103.

Display of Results

If nothing else, it is good to have a way to display results in a test example like this. However, in a real application it gives the user additional confidence that his data is going to be uploaded. Since I could use Excel to display these results, this from was not absolutely required, but the project engineer wanted the capability.

I used a Windows Form for this functionality. It is possible to use WPF (I generally program in WPF), but awkward, so Windows Forms is the way to go. The form I created centers around display of data in a DataGridView control. I use the docking capability to allow the user to resize the view. The form also has a panel at the bottom with buttons to Cancel and Submit, a Label to present an overview of the conversion result, and a simple icon that is green if all rows pass validation, and yellow otherwise.

The final results are no problem displaying since the DataGridView easily binds to a collection of objects that have properties for the data to be displayed. However, the design is such that only valid rows are converters to the objects; otherwise, there is only the data in the collection of RowValidation objects where the data is actually in a dictionary. I originally tried to display the data using a DynamicObject adapter but that does not recognize properties in a DynamicObject. I coded it using DataTable and that worked, but had to convert the dictionary entries to fields in a row. Then somebody pointed out the ICustomTypeDescriptor interface with the PropertyDescriptor class. This was fairly easy to customize to interface to a data source of Dictionary<string, ValueValidator>.

All the ICustomTypeDescriptor does is provide a place to save the dictionary, and return a PropertyDescriptorCollection that contains the PropertyDescriptor instances for each property using the method GetProperties.

I created a RowValidatorDescriptor that inherits from PropertyDescriptor to provide the adapter to the ValueValidator instances. There are two important methods in this class for this implementation: PropertyType (which returns the type) and GetValue. The type in this case is always string, the GetValue has to search for the right ValueValidator and return its Value property.

I did one little customization to this class so that an icon is displayed with each row. The ICustomTypeDescriptor class includes an extra PropertyDescriptor instance for this icon field, and the PropertyDescriptor for this field returns an image that depends on whether all the ValueValidator instances IsValid property is true. The only tricky part was the LINQ statement in the GetProperties method of the ICustomTypeDescriptor:

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

LINQ requires the Cast to ensure that the two different types of PropertyDescriptor instances would work.

Examples of Customizing

All the files required to support the customization are in the folder ExporterCustom. You will notice a couple of extra files in this folder. OrderAdapter.cs is the class that is used to transfer the information and has the validation information. CustomValidators.cs contains a special validator that is used to check if the Brokers list is a list of three character words. StringList.cs is used in OrderAdapter as the type for the Brokers. It is derived from IEnumerable<string>. The reason I use this class for the Brokers is that I can define another class, StringListTypeConverter.cs, which will convert a string to the StringList type in the ValidatorAdapter. The TypeConverter is required because the ValidatorAdapter uses a class’ TypeConverter to convert to the proper type. These two classes allow me to automatically convert a string to a list without any extra code.

Excel Issue

I have one significant issue with the restoring of the formatting. It works great if the user has made changes in the formatting, but when I try to restore the borders, and then I will get a black border. I do not know how to fix this.

Excel Add-In Note

Quite often I would find that the add-in was not running in Excel. This is almost certainly because it has been disabled. In Excel 2010 you will need to go to the File tab, click the Options button, select the Add-Ins tab/button, then select Disabled Items in the Manage combo box. Then you have to click the Go… button. In the dialog box, click on the name of the add-in in the list box and click the Enable button. Unfortunately, neither Visual Studio nor Excel informs you that the add-in has been disabled. It would be nice. It would have saved me some time if I had known about this problem.

Conclusion

If you work a lot with customers that like to input data using Excel, this is a great tool to add to your toolkit that will allow you to quickly create a new Excel export that will provide validation and feedback to the user on issues in their data. There are a number of ways that this can now be further improved by providing more information on the transfer, and I am sure that this code will evolve within my group.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Clifford Nelson
Software Developer (Senior) ETeam/Delloitte
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last three years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with NBC Universal in Universal City, CA

Comments and Discussions

 
QuestionCannot debug the solution Pinmembershuaibur Rahman2-Dec-14 16:09 
AnswerRe: Cannot debug the solution Pinmembershuaibur Rahman2-Dec-14 16:22 
Questionwhy use this over vba macro? PinmemberWilliam Evans16-Sep-14 16:22 
QuestionNeed web App Pinprofessionalrakeshswain28-Aug-14 2:30 
GeneralWell done PinmvpEspen Harlinn29-Aug-13 21:09 
AnswerRe: Well done PinmemberClifford Nelson30-Aug-13 7:40 
AnswerSee second part: PinmemberClifford Nelson3-Oct-12 11:58 
Using Attributes in Excel Add-in Framework for Validating and Exporting Data[^]
QuestionMy vote of 5 PinmemberMaciej Los3-Oct-12 11:37 
AnswerRe: My vote of 5 PinmemberClifford Nelson3-Oct-12 11:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 17 Aug 2012
Article Copyright 2012 by Clifford Nelson
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid