Archive for category C# Development

Compare SubSonic Objects (Columns) for Differences

Awhile back I needed a way to quickly compare a set of SubSonic v2.2 objects ( data properties ) for differences. Since the two source rows of data would be created at different times and have different primary keys, I needed a way to selectively compare columns/properties. My solution, was a function called “ColumnsAreEqual” that I added to “RecordBase.cs” that compares each property value of two SubSonic Objects, while allowing you to set a list of columns/properties to exclude.

        /// <summary>
        /// Compare values of two SubSonic objects for differences.  You can also pass an exclusion list
        /// of columns/properties to exclude,  like a primary key or CreatedOn date.
        /// </summary>
        /// <param name="comparableObject">Object to Compare</param>
        /// <param name="ignoreColumnList">Comma separated list of ColumnNames to ignore</param>
        /// <returns></returns>
        public bool ColumnsAreEqual(T comparableObject, string ignoreColumnList = null)
        {
            if (comparableObject == null)
                return false;

            // If ignore list is set, build array of column names to skip
            string[] ignoreColumnNames = new string[] {};
            if (ignoreColumnList != null && ignoreColumnList.IndexOf(',') > 0)
            {
                ignoreColumnNames = ignoreColumnList.Split(',');
            }

            foreach (TableSchema.TableColumnSetting setting in columnSettings)
            {
                // If there are columns to ignore, check the current ColumnName against ignore list and skip.
                if (ignoreColumnNames.Length > 0)
                {
                    bool ignored = false;

                    foreach (string ignoreColumnName in ignoreColumnNames)
                    {
                        if (ignoreColumnName.Trim().ToLower() == setting.ColumnName.ToLower())
                        {
                            ignored = true;
                            break;
                        }
                    }

                    // If this is a ignore column, skip.
                    if (ignored)
                    {
                        continue;
                    }
                }

                var before = setting.CurrentValue;
                var after = comparableObject.GetColumnValue<object>(setting.ColumnName);

                // If both are null, then they are equal
                if (before == null && after == null)
                    continue;

                // If one is null then they are not equal
                if (before == null || after == null)
                    return false;

                // Compare two non-null objects
                if (!before.Equals(after))
                    return false;
            }

            return true;
        }

// Example Usage
bool areEqual = before.Packaging.ColumnsAreEqual(after.Packaging,"PackagingId,CreatedOn,ModifiedOn");

Fishbowl Inventory C# .NET Intergration

We’re using a program at work called Fishbowl Inventory for managing our sample inventory.  The program is a full blown ISRP (Inventory, Shipping, Receiving, and Packing) solution that does everything from sales order entry to warehouse logistics.  It’s been pretty rock-solid (tho I do regular full backups/restores and database re-indexes).  As of today, we are still using version 4.7 (very old) because the product road map changed after version 5.0 ( I’m not sure who drove their road map, but I feel the went the wrong way on a bunch of changes and every change was followed with huge bug lists).  In a nutshell, Fishbowl is a Java application that uses a Firebird database to store it’s data.  Upon the release of version 4.x, Fishbowl released a new “integration” feature that allowed developers to write applications that could communicate with Fishbowl via XML.  This was a big “improvement”, since previously I had written a few applications directly against the DB that generates labels (another missing feature) with a Dymo thermal printer and ran into lots of problems because Fishbowl does not like DB SCHEMA changes (even very simple benign changes gave me and support many issues).

Fast forward 4 years and here I am, I need to get some data out of Fishbowl but I find the ODBC drivers very unstable and I get errors > 50% of the time when I try to pull data out of Fishbowl.  I also tried their C# SDK and also received a bunch of random errors (probably because it’s for version 2011.7 vs. 4.7).  Since I needed to get some data out, I did a quick google search and found somebody made a PHP wrapper called  fishconnect based on the SDK for version 2010.4.  It says it required Fishbowl Inventory 2010.4+ to work, but after reviewing the code (don’t have PHP server, so I didn’t test anything), I’m convinced that it will probably work for all previous versions back to v4.7.  After a few minutes reading the code, I figured I could make myself a nice C# wrapper to use on our ASP.NET intranet site.

Here is a quick summary of what I used to put this together….

 

1. Have XSD file, now generate me some objects!

I grabbed the “fbimsg.xsd” from fishbowlconnect PHP project, which is also provided with the official Fishbowl Java SDK.

Microsoft has had the “XSD.EXE” for a long time, it will generate your objects based on a XSD file.  The command was moved into the Visual Studio Command Prompt with VS2010.  To use the XSD tool, go to your Microsoft Visual Studio 2010 folder and launch “Visual Studio Command Prompt (2010)”.  Once your at a command prompt, type the following.

xsd <location of XSD>.xsd /s

Presto, you got a nice DAL that is strongly typed!!!

 

2. Communications with Fishbowl Requires a Big Endian Stream

Why MS only supports Little Endian streams out of the box is a mystery to me, maybe a push to use the MS stack?  I don’t know, but regardless you have to solve this problem to talk with Fishbowl.  I did a quick and dirty sample to get it working, but it was a terrible “permanent” solution and required “Allow unsafe code” to be enabled.  I ran to StackOverflow to find somebody smarter with better code and ended up grabbing what I needed from Jon Skeet’s MiscUtil Library.  With this, I now had full support for BigEndian data streams.

 

3. Approving Fishbowl Integrated Applications

Fishbowl Inventory requires you to approve all 3rd party applications in the Fishbowl Server GUI, before the become active.  If you are running Fishbowl as a service you’ll have to stop it and then run it interactively. Once started, go to “Integrated Applications” and you should see Fishbowl Connect C# listed, select and check the green check mark to approve.

Fishbowl Inventroy Integration - Approve

Fishbowl Server Integrated Applications - Approval Screen

 

4. Putting it all together and a working example.

The final product consists of a single class called “FishbowlSession” that implements IDisposable.  This class handles all the communications with fishbowl. In addition to this class we have a DAL that was created from our XSD file and a Utilities library to provide a few helpers.  I wrote the main class by writing NUnit tests, if you want to test against your Fishbowl Server then change values in [Setup] test to reference valid data for your server (e.g. Server IP, Login, Password, Part Numbers).

If you find bugs please let me know, I only tested sending/receiving 4 message types so I’m not sure if all message types will work.

 

Example showing how to get the inventory for a part in Fishbowl.

        private static string GetFishbowlPartInventory(string part)
        {
            string inventoryResults = "Fishbowl Inventory Offline!";

            using (FishbowlSession fishbowlInventory = new FishbowlSession("192.168.168.168"))
            {
                // Connect to fishbowl using a valid login/password
                fishbowlInventory.Connect("app","app");

                // Make sure we are authenticated before we try to send/receive messages
                if (fishbowlInventory.IsAuthenticated)
                {
                    // Build PartTagQueryRqType Request
                    PartTagQueryRqType partTagQueryRqType = new PartTagQueryRqType { LocationGroup = "Product", PartNum = part };

                    // Submit Request and get Response
                    PartTagQueryRsType partTagQueryRsType = fishbowlInventory.IssueRequest<PartTagQueryRsType>(partTagQueryRqType);

                    // Unregister Part, show custom message
                    if (partTagQueryRsType.statusCode != "1000")
                    {
                        inventoryResults = Utilities.StatusCodeMessage(partTagQueryRsType.statusCode);
                    }

                    // Tag object is optional, if there is no tag element you can not get the quantity
                    if (partTagQueryRsType.Tag != null && partTagQueryRsType.Tag.Length > 0)
                    {
                        Tag tag = partTagQueryRsType.Tag[0];

                        inventoryResults = int.Parse(tag.Quantity) > 0
                                               ? tag.Quantity + " Available in Fishbowl Inventory."
                                               : "No Inventory Available in Fishbowl Inventory.";
                    }
                }
            }

            return inventoryResults;
        }

IMPORTANT NOTE: There is an important caveat to this solution… Communication with the server will consume one of your seats that you have available based on your license key. This means, if your key only allows for 5 concurrent users and your app tries to connect when all the seats are full… it will fail! I have put a lot of other “things” in place to prevent this in my setup; short session timeouts, terminal server user caps, etc… but it’s still possible…

Download Fishbowl Inventory C# Wrapper

NPOI – Set Cell Helper

I saw a discussion posting on the NPOI discussion forum on CodePlex today, asking if there was a function like SetCell(X,Y,Value) in NPOI. Unfortunately there isn’t… At least I was never able to find one, so I created my own. Since I only needed to set values I added three basic ones into my helper class to make available to everybody using NPOI in my project. I was really tempted to add these to the HSSFWorksheet, but to keep my code save I figured a helper class for all my extras would be enough. Here is my version of a SetCellValue() helper set of functions.

        private static void SetCellValue(HSSFSheet worksheet, int columnPosition, int rowPosition, DateTime value)
        {
            // Get row
            using (HSSFRow row = worksheet.GetRow(rowPosition))
            {
                // Get or Create Cell
                using (HSSFCell cell = row.GetCell(columnPosition) ?? row.CreateCell(columnPosition))
                {
                    cell.SetCellValue(value);
                    cell.CellStyle.DataFormat = 14;
                }
            }
        }

        private static void SetCellValue(HSSFSheet worksheet, int columnPosition, int rowPosition, double value)
        {
            // Get row
            using (HSSFRow row = worksheet.GetRow(rowPosition))
            {
                // Get or Create Cell
                using (HSSFCell cell = row.GetCell(columnPosition) ?? row.CreateCell(columnPosition))
                {
                    cell.SetCellValue(value);
                }
            }
        }

        private static void SetCellValue(HSSFSheet worksheet, int columnPosition, int rowPosition, string value)
        {
            // Get row
            using (HSSFRow row = worksheet.GetRow(rowPosition))
            {
                // Get or Create Cell
                using (HSSFCell cell = row.GetCell(columnPosition) ?? row.CreateCell(columnPosition))
                {
                    cell.SetCellValue(value);
                }
            }
        }

        // Set Date
        SetCellValue(sheet, 9, 3, DateTime.Now);
        // Set Number
        SetCellValue(sheet, 9, 4, 100.01);
        // Set Text
        SetCellValue(sheet, 9, 5, "Zach Roxs!");

As you can see it’s pretty easy to create a SetCellValue() helper. I plan to create another version of these that uses Excel coordinates (e.g. A5, Z10, etc…), so my die hard Excel teammates can use their native Excel mapping syntax!

NPOI – Copy Row Helper

Another day and another little tidbit on using NPOI. I was doing tool mock-up at work today when I ran across a need for a copy row function. After searching high and low, I realized NPOI does not currently offer this capability. After looking around (Google, NPOI and POI threads) I decided to create my own helper function. I’m sure there might be a few things I missed in my routine since the library is a bit new to me, but after testing this against a bunch of different scenarios I’m pretty confident this will work for 99% of my needs and maybe a high percent of yours as well.

Here is the function in all it’s glory, I thought about modify the NPOI source but since I’m not sure where it’s going I figured I’d just add this in my own little NPOI.CustomHelpers class that I can use with my NPOI project.

        /// <summary>
        /// HSSFRow Copy Command
        ///
        /// Description:  Inserts a existing row into a new row, will automatically push down
        ///               any existing rows.  Copy is done cell by cell and supports, and the
        ///               command tries to copy all properties available (style, merged cells, values, etc...)
        /// </summary>
        /// <param name="workbook">Workbook containing the worksheet that will be changed</param>
        /// <param name="worksheet">WorkSheet containing rows to be copied</param>
        /// <param name="sourceRowNum">Source Row Number</param>
        /// <param name="destinationRowNum">Destination Row Number</param>
        private void CopyRow(HSSFWorkbook workbook, HSSFSheet worksheet, int sourceRowNum, int destinationRowNum)
        {
            // Get the source / new row
            HSSFRow newRow = worksheet.GetRow(destinationRowNum);
            HSSFRow sourceRow = worksheet.GetRow(sourceRowNum);

            // If the row exist in destination, push down all rows by 1 else create a new row
            if (newRow != null)
            {
                worksheet.ShiftRows(destinationRowNum, worksheet.LastRowNum, 1);
            }
            else
            {
                newRow = worksheet.CreateRow(destinationRowNum);
            }

            // Loop through source columns to add to new row
            for (int i = 0; i < sourceRow.LastCellNum; i++)
            {
                // Grab a copy of the old/new cell
                HSSFCell oldCell = sourceRow.GetCell(i);
                HSSFCell newCell = newRow.CreateCell(i);

                // If the old cell is null jump to next cell
                if (oldCell == null)
                {
                    newCell = null;
                    continue;
                }

                // Copy style from old cell and apply to new cell
                HSSFCellStyle newCellStyle = workbook.CreateCellStyle();
                newCellStyle.CloneStyleFrom(oldCell.CellStyle); ;
                newCell.CellStyle = newCellStyle;

                // If there is a cell comment, copy
                if (newCell.CellComment != null) newCell.CellComment = oldCell.CellComment;

                // If there is a cell hyperlink, copy
                if (oldCell.Hyperlink != null) newCell.Hyperlink = oldCell.Hyperlink;

                // Set the cell data type
                newCell.SetCellType(oldCell.CellType);

                // Set the cell data value
                switch (oldCell.CellType)
                {
                    case HSSFCellType.BLANK:
                        newCell.SetCellValue(oldCell.StringCellValue);
                        break;
                    case HSSFCellType.BOOLEAN:
                        newCell.SetCellValue(oldCell.BooleanCellValue);
                        break;
                    case HSSFCellType.ERROR:
                        newCell.SetCellErrorValue(oldCell.ErrorCellValue);
                        break;
                    case HSSFCellType.FORMULA:
                        newCell.SetCellFormula(oldCell.CellFormula);
                        break;
                    case HSSFCellType.NUMERIC:
                        newCell.SetCellValue(oldCell.NumericCellValue);
                        break;
                    case HSSFCellType.STRING:
                        newCell.SetCellValue(oldCell.RichStringCellValue);
                        break;
                    case HSSFCellType.Unknown:
                        newCell.SetCellValue(oldCell.StringCellValue);
                        break;
                }
            }

            // If there are are any merged regions in the source row, copy to new row
            for (int i = 0; i < worksheet.NumMergedRegions; i++)
            {
                CellRangeAddress cellRangeAddress = worksheet.GetMergedRegion(i);
                if (cellRangeAddress.FirstRow == sourceRow.RowNum)
                {
                    CellRangeAddress newCellRangeAddress = new CellRangeAddress(newRow.RowNum,
                                                                                (newRow.RowNum +
                                                                                 (cellRangeAddress.FirstRow -
                                                                                  cellRangeAddress.LastRow)),
                                                                                cellRangeAddress.FirstColumn,
                                                                                cellRangeAddress.LastColumn);
                    worksheet.AddMergedRegion(newCellRangeAddress);
                }
            }

        }

The code comments above should give you a good idea of what I’m doing, if something doesn’t make sense just ask. The key things I wanted to make sure got copied were; Cell Style, Cell Value, Cell Type, Merged Cell Settings. In the end I noticed a few other things that I thought I might use in the future, so I included them as well. Here is an example of how to call to CopyRow along with a snapshot of the end result.

            // Grab my NPOI workbook memorystream
            HSSFWorkbook workbook = new HSSFWorkbook(memoryStream);

            // Grab my test worksheet
            HSSFSheet sheet = workbook.GetSheet("Sheet1");

            // Copy Excel Row 1 to Excel Row 3
            CopyRow(workbook, sheet, 0, 2);

            // Copy Excel Row 2 to Excel Row 4
            CopyRow(workbook, sheet, 1, 3);

NPOI - Copy Row Helper Function Test Results

How to read in XLSX data for editing with NPOI

NPOI is a great solution for working with XLS documents, but what happens when you need to read in XLSX documents as part of your solution? The easiest solution I could think of was to use OLEDB to extract out all the data (the data, the whole data and nothing but the data) from a worksheet into a new NPOI document.

If you need more support than just reading in data from Excel 2007+ (XLSX), you should look at using the ExcelPackage project that is hosted at CodePlex.

The benefit to my approach, is that your able to use your existing NPOI solutions when data is provided in XLSX format. This is far from full Excel 2007+ support, but if your applicaitons only requirement is “reading in data” then your problem is solved. NPOI v1.6 is supposed to fully support the XLSX format, until then this function will provide basic XLSX support.

Functions used to convert XLSX document to a NPOI document
    /// <summary>
    /// Render a Excel 2007 (xlsx) Worksheet to NPOI Excel 2003 Worksheet, all excel formatting
    /// from XLSX will be lost when converted.  NPOI roadmap shows v1.6 will support Excel 2007 (xlsx).
    /// NPOI Roadmap  : http://npoi.codeplex.com/wikipage?title=NPOI%20Road%20Map&referringTitle=Documentation
    /// NPOI Homepage : http://npoi.codeplex.com/
    /// </summary>
    /// <param name="excelFileStream">XLSX FileStream</param>
    /// <param name="sheetName">Excel worksheet to convert</param>
    /// <returns>MemoryStream containing NPOI Excel workbook</returns>
    public static Stream ConvertXLSXWorksheetToXLSWorksheet(Stream excelFileStream, string sheetName)
    {
        // Temp file name
        string tempFile = HttpContext.Current.Server.MapPath("~/uploads/" + HttpContext.Current.Session.LCID + ".tmp");

        // Temp data container (using DataTable to leverage existing RenderDataTableToExcel function)
        DataTable dt = new DataTable();

        try
        {
            // Create temp XLSX file
            FileStream fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write);

            const int length = 256;
            Byte[] buffer = new Byte[length];
            int bytesRead = excelFileStream.Read(buffer, 0, length);

            while (bytesRead > 0)
            {
                fileStream.Write(buffer, 0, bytesRead);
                bytesRead = excelFileStream.Read(buffer, 0, length);
            }

            excelFileStream.Close();
            fileStream.Close();

            // Read temp XLSX file using OLEDB
            // Tested on Vista & Windows 2008 R2
            using (OleDbConnection con = new OleDbConnection(@"Provider=Microsoft.ACE.OLEDB.12.0; Extended Properties=Excel 12.0;Data Source=" + tempFile + @";Extended Properties=Excel 12.0;"))
            {
                con.Open();
                string sql = String.Format("SELECT * FROM [{0}$]", sheetName);
                OleDbDataAdapter da = new OleDbDataAdapter(sql, con);

                da.Fill(dt);
            }
        }
        finally
        {
            // Make sure temp file is deleted
            File.Delete(tempFile);
        }

        // Return a new POI Excel 2003 Workbook
        return RenderDataTableToExcel(dt);
    }

    /// <summary>
    /// Render DataTable to NPOI Excel 2003 MemoryStream
    /// NOTE:  Limitation of 65,536 rows suppored by XLS
    /// </summary>
    /// <param name="sourceTable">Source DataTable</param>
    /// <returns>MemoryStream containing NPOI Excel workbook</returns>
    public static Stream RenderDataTableToExcel(DataTable sourceTable)
    {
        HSSFWorkbook workbook = new HSSFWorkbook();
        MemoryStream memoryStream = new MemoryStream();
        // By default NPOI creates "Sheet0" which is inconsistent with Excel using "Sheet1"
        HSSFSheet sheet = workbook.CreateSheet("Sheet1");
        HSSFRow headerRow = sheet.CreateRow(0);

        // Header Row
        foreach (DataColumn column in sourceTable.Columns)
            headerRow.CreateCell(column.Ordinal).SetCellValue(column.ColumnName);

        // Detail Rows
        int rowIndex = 1;

        foreach (DataRow row in sourceTable.Rows)
        {
            HSSFRow dataRow = sheet.CreateRow(rowIndex);

            foreach (DataColumn column in sourceTable.Columns)
            {
                dataRow.CreateCell(column.Ordinal).SetCellValue(row[column].ToString());
            }

            rowIndex++;
        }

        workbook.Write(memoryStream);
        memoryStream.Flush();
        memoryStream.Position = 0;

        return memoryStream;
    }

NOTE: You could convert an entire workbook (multiple work sheets) into a new NPOI workbook if you looped over each work sheet in the XLSX document. Since all my tools are based on using a single work sheet, I’ll leave the workbook conversion up to you.

Example using XLSX function
            if (FileUpload.PostedFile.ContentLength > 0)
            {
                Stream uploadFileStream = FileUpload.PostedFile.InputStream;

                // If the file uploaded is "XLSX", convert it's Sheet1 to a NPOI document
                if (FileUpload.PostedFile.FileName.EndsWith("xlsx"))
                {
                    uploadFileStream = ExcelHelper.ConvertXLSXWorksheetToXLSWorksheet(uploadFileStream, "Sheet1");
                }
            }

** All your NPOI logic that adds columns, changes cell/row formatting, etc… will now work with the data extracted from the XLSX document.