Using closedXML C# library, How can I figure out what data is causing this error when trying to save
Asked Answered
R

3

6

I have a C# app that exports to Excel using ClosedXML. It works fine but just ran into an issue where when i hit the :

  var ms = new MemoryStream();
  workbook.SaveAs(ms);

I get an exception:

' ', hexadecimal value 0x0B, is an invalid character

Its definitely data related because it I look at certain data it works fine but other data it causes this issue.

how can i figure out which character is causing the issue? Also, once I figure that out, what is the best way of finding where this character is within my data?

Roaster answered 20/11, 2013 at 17:44 Comment(6)
according to : web.cs.mun.ca/~michael/c/ascii-table.html you are searching for "+" which could be problematic in excelSupplant
@Supplant - i checked and i don't have any + in my dataRoaster
I believe 0x0B is a vertical tab.Uncut
@VincentTan - any suggestions for how i can find this in my data ?Roaster
Can you please post full stack trace?Siouan
@Supplant 0x0B or \v is a vertical tab. That being said, 0x0B is not valid in XML 1.0.Chemesh
P
6

Since you have invalid characters in the data / strings you put into the ClosedXML sheet, you have to find them and get them out.

The simplest solution is to add

.Replace((0x0B).ToString(), " ")

to all your strings to get rid of the vertical tabs and replace them with spaces.

Perception answered 26/11, 2013 at 10:20 Comment(7)
Wouldn't newline be more appropriate replacement for vertical tab? I believe MS Word uses vertical tabs as line separators (and \n as paragraph separators).Goldwin
That is of course possible too. The code would then be .Replace((0x0B).ToString(), Environment.Newline).Perception
Is there another way to detect and replace all invalid characters (instead of just this one )Roaster
Look at this question especially this answer.Perception
@Roaster The removal (or at the very least, the detection) should be done inside the library, not outside of it. Having to strip invalid XML characters from your data is merely a workaround for the problems of the code inside the library.Cchaddie
And in what library would you do this? ClosedXML or Open XML SDK? And with what would you replace the invalid characters? I think this should be done in the code which gets and prepares the data before giving it over to ClosedXML.Perception
@Perception ClosedXML, of course: at the minimum, the ClosedXML library would have to notify the caller that the data is invalid - right at the time the caller is trying to add the data, not at the time when the user is trying to save it. Replacing would be my other choice, in which case I would simply remove the invalid characters.Cchaddie
C
6

Since ClosedXML is an open source project, the simplest way of tracking the error down would be building it from the source *, and then running your code against the library in debug mode.

Once you see the full stack trace, you should be able to identify the spot from which the error is coming. Good chances are that it is a bug in the way the ClosedXML project uses Microsoft XML libraries, because the error that you mentioned is reported by a library outside the ClosedXML project.


* I downloaded the project, and tried building it. Everything in the closedxml-79843.zip package builds correctly.
Cchaddie answered 26/11, 2013 at 19:34 Comment(0)
L
3

Since ClosedXML doesn't prevent you from using the 0x0B character in values, you'll either have to scrub your data of it yourself (as suggested by @Raidri), or you could force and exception, or do a string replace when the value is set. I've created a sample program below which uses Castle's Dynamic Proxy to wrap the IXLWorksheet and IXLCell interfaces. Firstly, we proxy the IXLWorksheet values (which returned from adding a new worksheet as in the example below, or by indexing an existing worksheet). This needs to be done manually via a method call; everything else from then on is set up. When accessing cells (via the Cell methods, or the ActiveCell property) a proxied IXLCell value is returned which checks the data being set via the Value property and the SetValue method. The check is done in the ValidateMethodInterceptor as per the comments. This whole mechanism can be left in your codebase and turned on/off via a switch in the Program.Proxy method if you so desire.

As a further alternative, the package EPPlus (which has similar functionality to ClosedXML) doesn't crash when confronted with the VT character. Instead it replaces it with the value _x00B_. Perhaps a switch would be more beneficial?

internal class Program
{
    private static void Main(string[] args)
    {
        var stream = new MemoryStream();

        using (stream)
        {
            using (var workbook = new XLWorkbook())
            {
                using (var worksheet = Proxy(workbook.Worksheets.Add("Sheet 1")))
                {
                    worksheet.Cell("A1").Value = "This  is a test";
                    worksheet.Cell("A2").Value = "This \v is a test";
                    workbook.SaveAs(stream);
                }
            }
        }
    }

    public static IXLWorksheet Proxy(IXLWorksheet target)
    {
        var generator = new ProxyGenerator();
        var options = new ProxyGenerationOptions { Selector = new WorksheetInterceptorSelector() };

        return generator.CreateInterfaceProxyWithTarget<IXLWorksheet>(target, options);
    }
}

public class WorksheetInterceptorSelector : IInterceptorSelector
{
    private static readonly MethodInfo[] methodsToAdjust;
    private readonly ProxyCellInterceptor proxyCellInterceptor = new ProxyCellInterceptor();

    static WorksheetInterceptorSelector()
    {
        methodsToAdjust = typeof(IXLWorksheet).GetMethods()
            .Where(x => x.Name == "Cell")
            .Union(new[] { typeof(IXLWorksheet).GetProperty("ActiveCell").GetGetMethod() })
            .ToArray();
    }

    #region IInterceptorSelector Members

    public IInterceptor[] SelectInterceptors(System.Type type, System.Reflection.MethodInfo method, IInterceptor[] interceptors)
    {
        if (!methodsToAdjust.Contains(method))
            return interceptors;

        return new IInterceptor[] { proxyCellInterceptor }.Union(interceptors).ToArray();
    }

    #endregion
}

public class CellInterceptorSelector : IInterceptorSelector
{
    private static readonly MethodInfo[] methodsToAdjust = new[] { typeof(IXLCell).GetMethod("SetValue"), typeof(IXLCell).GetProperty("Value").GetSetMethod() };

    private ValidateMethodInterceptor proxyCellInterceptor = new ValidateMethodInterceptor();

    #region IInterceptorSelector Members

    public IInterceptor[] SelectInterceptors(System.Type type, MethodInfo method, IInterceptor[] interceptors)
    {
        if (method.IsGenericMethod && method.Name == "SetValue" || methodsToAdjust.Contains(method))
            return new IInterceptor[] { proxyCellInterceptor }.Union(interceptors).ToArray();
        return interceptors;
    }

    #endregion
}

public class ProxyCellInterceptor : IInterceptor
{
    #region IInterceptor Members

    public void Intercept(IInvocation invocation)
    {
        invocation.Proceed();

        //Wrap the return value
        invocation.ReturnValue = Proxy((IXLCell)invocation.ReturnValue);
    }

    #endregion

    public IXLCell Proxy(IXLCell target)
    {
        var generator = new ProxyGenerator();
        var options = new ProxyGenerationOptions { Selector = new CellInterceptorSelector() };

        return generator.CreateInterfaceProxyWithTarget<IXLCell>(target, options);
    }
}

public class ValidateMethodInterceptor : IInterceptor
{
    #region IInterceptor Members

    public void Intercept(IInvocation invocation)
    {
        var value = invocation.Arguments[0];

        //Validate the data as it is being set
        if (value != null && value.ToString().Contains('\v'))
        {
            throw new ArgumentException("Value cannot contain vertical tabs!");
        }

        //Alternatively, you could do a string replace:
        //if (value != null && value.ToString().Contains('\v'))
        //{
        //    invocation.Arguments[0] = value.ToString().Replace("\v", Environment.NewLine);
        //}

        invocation.Proceed();
    }

    #endregion
}
Leah answered 2/12, 2013 at 10:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.