Working with Delimited Text Files in C#


Introduction

This article shall describe an approach to working with delimited text files. In general it will discuss the construction of the connection strings and schema.ini files needed to bind to a delimited text file and it will define an approach for editing and saving delimited text files.

DlmtTxtFl1.gif

Figure 1: CSV Editor File displaying a delimited text file

DlmtTxtFl2.gif

Figure 2: Creating a schema file definition

Working with delimited text files can be a barrel of laughs but it is certainly something one expects to do from time to time. The article will focus on creating connections for binding to and reading/editing comma delimited text files, tab delimited text files, custom character delimited text files, and fixed column width text files. Different types of delimited files exist because not all delimiter types work for all files as based upon the contents of those files. For example, if one of the columns of the file contains a business name and the file is comma delimited, it is going to cause a bit of trouble if the business name is something like "Acme Tool Company, Inc." In such cases, if the fields may contain commas, using a comma as a delimiter is a bad idea.

In order to establish an OLE DB connection to a delimited file, you need to have a schema.ini file residing in the same location as the delimited file you are trying to open for viewing or edit (or some other processing). To that end, the approach used in this application is to provide a dialog used to capture the content needed to construct a schema.ini file. This information is also used to build a connection string used to connect to the file as a viable data source.

DlmtTxtFl3.gif

Figure 3: A Schema.ini file opened in Notepad

The schema.ini file shown in Figure 3 is used to open a comma delimited file that contains column headers in the first row and that contains four columns. The first column is called "ID" and the data type is a short, the next three columns are "First", "Last", and "City" and those columns are all text. This schema.ini file was build using the application and it may be used to open the file shown in brackets on the first line of the file.

Getting Started

The solution contains two projects; the first is a Windows form application entitled, "CsvEditor" and the second is a class library project entitled, "SchemaSpec". The windows form application contains three forms, the editor form is used to open, edit, save, and close a delimited text file. The schema form is used to build a viable schema definition that may be then used to build a schema.ini file. The last form is just an about box.

The schema definition file in the second project contains two classes; one is used to store the schema definition, and the other is used to hold the column specifications for each of the columns present in the delimited text file.

DlmtTxtFl4.gif

Figure 4: Solution Explorer with the Projects Visible

DlmtTxtFl5.gif

Figure 5: Application Settings used to hold the schema definition and connection string

First Project - SchemaSpec:

Code: Schema Definition Class File (SchemaDef.cs)

The schema specification file contains two classes; the details for the two classes are contained in the notation included with the file.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

// Used to contain information about the delimited file to be opened
// using the CSV editor application

namespace SchemaSpec
{
    
/// <summary>
    /// The item specification class is used to 
    /// define the columns contained within the 
    /// delimited file to be opened.  For each column
    /// we need to know the data type (and I am 
    /// using the jet data types here), the column 
    /// number, the column name, data type, and 
    /// column width (if the file is delimited 
    /// using fixed widths alone) - a list of 
    /// item specification is added to the 
    /// schema definition trailing this class
    /// </summary>
    [Serializable]
    
public class ItemSpecification
    {
        
// this enumeration is used to 
        // limit the type data property to 
        // a matching jet data type
        public enum JetDataType
        {
            Bit,
            Byte,
            Short,
            Long,
            Currency,
            Single,
            Double,
            DateTime,
            Text,
            Memo
        };

        // the position of the column beginning with 1 to n
        public int ColumnNumber { getset; }

        // the column name
        public string Name { getset; }

        // the data type
        public JetDataType TypeData { getset; }

        // optional column width for fixed width files
        public int ColumnWidth { getset; }
    }

    /// <summary>
    /// The schema definition class is used to hold the 
    /// contents of the schema.ini file used by the 
    /// connection to open a delimited file (using 
    /// an oledb connection).  The schema dialog is used 
    /// to define a schema definition which is stored as a 
    /// application property
    /// </summary>
    [Serializable]
    
public class SchemeDef
    {
        
/// <summary>
        /// the constructor will create a default comma delimited 
        /// file definition with an empty list of items specifications 
        /// and will default to set the first row is a header row 
        /// option to false
        /// </summary>
        public SchemeDef()
        {
            DelimiterType = 
DelimType.CsvDelimited;
            ColumnDefinition = 
new List<ItemSpecification>();
            UsesHeader = 
FirstRowHeader.No;
        }

        // this enumeration is used to limit the delimiter types 
        // to one of the four we are interested in which are 
        // comma delimited, tab delimited, custom delimited 
        // (such as a pipe or an underscore), or fixed column 
        // widths
        public enum DelimType
        {
            CsvDelimited,
            TabDelimited,
            CustomDelimited,
            FixedWidth
        };

        // This enum allows the first row is a header 
        // row option to be set to yes or no; that text 
        // is used in the connection string (rather than 
        // true or false)
        public enum FirstRowHeader
        {
            Yes,
            No
        };
        
// The properties used to build the schema.ini file include the 
        // delimiter type, a custom delimiter (if used), a list of 
        // column definitions, and a determination as to whether 
        // the first row of the file contains header information rather 
        // than data
        public DelimType DelimiterType { getset; }
        
public string CustomDelimiter { getset; }
        
public List<ItemSpecification> ColumnDefinition { getset; }
        
public FirstRowHeader UsesHeader { getset; }
    }

}

Second Project - CSV Editor

Code: Editor Form (frmEditor.cs)

The editor form is used to open, edit, and save a delimited text file. The code will create a connection string and a schema.ini file (placed in the path of the file you are trying to open) when a file is opened. The schema definition is stored in the application settings when the user creates a schema definition using the schema definition dialog; this form will recover that definition and use it to construct the schema.ini file and the connection string. Files are opened using an OLE DB connection and files saves are accomplished by overwriting the contents of the file using the contents of the data grid (which allows saving the edits made in the grid; it also allows cancelling the edits since nothing is committed until the file is overwritten). A binding source and navigator are used to traverse, view, and edit the data.

You may not need to do all of that, you might just want to connect to a delimited text file in some fixed format; if so, just look at the connection string and schema.ini file related code relevant to that particular task. If you just want to save grid based edits to a delimited text file, you might just want to look at the code used to save the file.

The code for this form class is annotated and should be easy enough to follow. The code contained in the LoadDataFile and SaveFileChanges methods will cover the more important parts of the form's code and the purpose of each is self-explanatory.

Lastly, there is a "Process File" button on the form; it does nothing but the intent was, if you wanted to open a delimited text file and do something with it, the code present will get the file open, you can use the button's click event handler to do something with the file contents. Once the connection to the file is open, you can loop through the contents as you would with any other OLE DB connection.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Data.OleDb;
using System.IO;
using System.Net;

namespace CsvEditor
{
    
public partial class frmEditor : Form
    { 

        #region Form Variable Declarations

        private string CsvFilePath;
        
private BindingSource bsrc;
        
private OleDbDataAdapter adp;
        
private DataTable dt;

        #endregion

        /// <summary>
        /// Create a new, empty binding source in construction
        /// </summary>
        public frmEditor()
        {
            InitializeComponent();
            bsrc = 
new BindingSource();
        }
 

        /// <summary>
        /// Open the editor used to define the current schema; this tool will allow the user
         /// define the schema that will be used to create a connection string and a schema.ini file
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void createToolStripMenuItem_Click(object sender, EventArgs e)
        {
            
frmSchema f = new frmSchema();
            f.ShowDialog();
        }

        /// <summary>
        /// Display the current connection string; this applies to the last 
              
/// open file as the connection string is 
        /// not really defined and stored until a file is actually opened with the defined schema
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void viewCurrentSchemaToolStripMenuItem_Click(object sender, 
        
EventArgs e)
        {
            
MessageBox.Show(Properties.Settings.Default.ConnString
                
"Current Connection");
        }

        /// <summary>
        /// Use the current schema to build a connection string for the 
              
/// delimited text file and use that connection
        /// string to open a delimited text file
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void openToolStripMenuItem_Click(object sender, EventArgs e)
        {
            
// define the open file dialog particulars; you could add txt files if you were
            // so inclined
            openFileDialog1.Title = "Source CSV File";
            openFileDialog1.Filter = 
"csv files (*.csv)|*.csv";
            openFileDialog1.FileName = 
string.Empty;

            // return if the user cancels the open file dialog
            if (openFileDialog1.ShowDialog() == DialogResult.Cancel)
                
return;
 
            
// set the string variable used to hold the path to the delimited text file
            // to the open file dialog's file name
            CsvFilePath = openFileDialog1.FileName;

            // if the string is empty, give warning and return
            if (String.IsNullOrEmpty(CsvFilePath))
            {
                
MessageBox.Show("File path does not exist""File");
                
return;
            }

            // make certain the file actually exists before proceeding
            if (System.IO.File.Exists(CsvFilePath) == false)
            {
                
MessageBox.Show(String.Format("File {0} does not exist"
                 CsvFilePath), 
"Missing File");
                
return;
            }

            // Load the delimited file into the form's data grid
            LoadFileData(CsvFilePath);

            // if the file actually has content, enable saving
            if (bsrc.Count > 0)
                bindingNavigatorSave.Enabled = 
true;
        }

        /// <summary>
        /// Load the CSV file into the data grid view for 
        /// user inspection and edit
        /// </summary>
        /// <param name="inputFile">Path to the source CSV file</param>
        private void LoadFileData(string inputFile)
        {
            
// verify the file path string has content
            if (String.IsNullOrEmpty(inputFile))
            {
                
MessageBox.Show("You must provide a source file in order to proceed",
                  
"Missing File");
                
return;
            }

            // get the path to use in defining the connection string
            string path = Path.GetDirectoryName(inputFile);

            // create the schema file using the requested file path and the existing schema definition;
            // the schema definition is stored in app settings as an instance of the SchemaDef class
            CreateSchemaIni(inputFile);

            // create a new schema definition, make sure the user has created a schema file
            // before proceeding - the schema definition is stored in the application properties
            SchemaSpec.SchemeDef sdef = new SchemaSpec.SchemeDef();
            
if (Properties.Settings.Default.SchemaSpec == null)
            {
                
MessageBox.Show("No schema has been defined; prior to opening 
                 a CSV file, use the Schema tool to construct a schema definition"

                 "Missing Schema");
                
return;
            }
            
else
            {
                sdef = Properties.
Settings.Default.SchemaSpec;
            }

            // create a variable to hold the connection string
            string connbit = string.Empty;

            // based upon the use of a header line and the delimiter type, create the correct
            // type of connection string
            switch (sdef.DelimiterType)
            {
                
case SchemaSpec.SchemeDef.DelimType.CsvDelimited:
                    
if(sdef.UsesHeader == SchemaSpec.SchemeDef.FirstRowHeader.Yes)
                        connbit = 
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" 
                          + path + 
@";Extended Properties=
                          ""Text;HDR=Yes;FMT=CsvDelimited"""
;
                    
else
                        connbit = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" 
                          + path + 
@";Extended Properties=
                          ""Text;HDR=No;FMT=CsvDelimited"""
;
                    
break;
                
case SchemaSpec.SchemeDef.DelimType.CustomDelimited:
                    
if (sdef.UsesHeader == SchemaSpec.SchemeDef.FirstRowHeader.Yes)
                        connbit = 
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" 
                          + path + 
@";Extended Properties=
                          ""Text;HDR=Yes;FMT=Delimited("
 
                          + sdef.CustomDelimiter + 
")" + "\"";
                    
else
                        connbit = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" 
                          + path + 
@";Extended Properties=
                          ""Text;HDR=No;FMT=Delimited("
 
                          + sdef.CustomDelimiter + 
")" + "\"";
                    
break;
                
case SchemaSpec.SchemeDef.DelimType.FixedWidth:
                    
if (sdef.UsesHeader == SchemaSpec.SchemeDef.FirstRowHeader.Yes)
                        connbit = 
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" 
                          + path + 
@";Extended Properties=
                          ""Text;HDR=Yes;FMT=FixedLength"""
;
                    
else
                        connbit = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" 
                          + path + 
@";Extended Properties=
                          ""Text;HDR=No;FMT=FixedLength"""
;
                    
break;
                
case SchemaSpec.SchemeDef.DelimType.TabDelimited:
                    
if (sdef.UsesHeader == SchemaSpec.SchemeDef.FirstRowHeader.Yes)
                        connbit = 
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" 
                          + path + 
@";Extended Properties=
                          ""Text;HDR=Yes;FMT=TabDelimited"""
;
                    
else
                        connbit = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" 
                          + path + 
@";Extended Properties=
                          ""Text;HDR=No;FMT=TabDelimited"""
;
                    
break;
                
default:
                    
break;
            }

            // put the connection string into the properties and save the properties
            Properties.Settings.Default.ConnString = connbit;
            Properties.
Settings.Default.Save();

            // make sure we have a connection string before proceeding
            if (String.IsNullOrEmpty(connbit))
            {
                
MessageBox.Show("Invalid Schema; use the scheme utility to 
                 define a schema for the file you are attempting to open"

                 "Invalid Schema");
            }

            // use the connection string to open an ole db connection to the delimited file
            using (OleDbConnection conn = new OleDbConnection(connbit))
            {
                
using (OleDbCommand cmd =
                    
new OleDbCommand("SELECT * FROM " + 
                    
Path.GetFileName(inputFile), conn))
                {
                    conn.Open();

                    adp = new OleDbDataAdapter(cmd);
                    dt = 
new DataTable();
                    adp.Fill(dt);

                    // set the binding source's data source to the data table
                    // and set the grid's data source to the binding source
                    bsrc.DataSource = dt;
                    dgvMaster.DataSource = bsrc;

                    // create a variable to keep track of the column position
                    int i = 0;

                    foreach (SchemaSpec.ItemSpecification s in sdef.ColumnDefinition)
                    {
                        
// set the column header text to match the column name from the 
                        // item specification
                        dgvMaster.Columns[i].HeaderText = s.Name;
                        i++;
                    }

                    // widen out the last row to fill the available space to tidy up 
                    // the grid a bit
                    dgvMaster.Columns[dgvMaster.Columns.Count - 1]
                    .AutoSizeMode = 
DataGridViewAutoSizeColumnMode.Fill;

                    // set the binding source navigator to the binding source
                    bnav.BindingSource = bsrc;

                    // display the file name to the user
                    groupBox1.Text = "Input File" + " -  Reading " + inputFile;
                }
            }
        }

        /// <summary>
        /// Overwrite the existing input file will all changes made 
        /// thus far. 
        /// </summary>
        private void SaveFileChanges()
        {
            
// if the data adapter is not null, end the edit session
            if (adp != null)
            {
                dgvMaster.EndEdit();

                // make the the schema definition exists and set the current schema 
                // definition to the stored schema definition
                SchemaSpec.SchemeDef sdef = new SchemaSpec.SchemeDef();
                
if (Properties.Settings.Default.SchemaSpec == null)
                {
                    
MessageBox.Show("No schema has been defined; prior to 
                     working with a CSV file, use the Schema tool to construct 
                    a schema definition"
"Missing Schema");
                    
return;
                }
                
else
                {
                    sdef = Properties.
Settings.Default.SchemaSpec;
                }

                // create a string builder instance to hold our text from the
                // data grid
                StringBuilder sb = new StringBuilder();
                
int intCols = dt.Columns.Count;

                switch (sdef.DelimiterType)
                {
                    
case SchemaSpec.SchemeDef.DelimType.CsvDelimited:
                        
// if the file uses a header, add the column header text 
                                             // back in as the column name source
                        // we will just get it from the grid though the grid may not 
                                             // initially match the text file as the 
                        // user may define the columns using any name they want to apply to it
                        if (Properties.Settings.Default.UsesHeader == true)
                        {
                            
StringBuilder sbTitleRow = new StringBuilder();
                            
for(int i=0; i<dgvMaster.ColumnCount; i++)
                            {
                                sbTitleRow.Append(dgvMaster.Columns[i].HeaderText);
                                
if ((i + 1) != intCols)
                                    sbTitleRow.Append(
",");
                                
else
                                    sbTitleRow.Append("\n");
                            }
                            sb.Append(sbTitleRow.ToString());
                        }
                        
// populate the rows with the data (including any edits); that version 
                                             // will overwrite the 
                        // existing content in the file so as to capture those edits â€“ 
                                             // of course in your version you
                        // may wish to disable editing altogether.
                        foreach (DataRowView dr in dt.DefaultView)
                        {
                            
for (int x = 0; x < intCols; x++)
                            {
                                sb.Append(dr[x].ToString());
                                
if ((x + 1) != intCols)
                                {
                                    sb.Append(
","); // comma delimited
                                }
                            }
                            sb.Append(
"\n"); // terminate the row
                        }
                        
break;
                    
case SchemaSpec.SchemeDef.DelimType.CustomDelimited:
                        
// if the file uses a header, add the column header 
                                             // text back in as the column name source
                        if (Properties.Settings.Default.UsesHeader == true)
                        {
                            
StringBuilder sbTitleRow = new StringBuilder();
                            
for (int i = 0; i < dgvMaster.ColumnCount; i++)
                            {
                                sbTitleRow.Append(dgvMaster.Columns[i].HeaderText);
                                
if ((i + 1) != intCols)
                                    sbTitleRow.Append(sdef.CustomDelimiter);
                                           
// user defined delimiter such as a pipe
                                else
                                    sbTitleRow.Append("\n"); // terminate the row
                            }
                            sb.Append(sbTitleRow.ToString());
                        }
                        
// populate the rows
                        foreach (DataRowView dr in dt.DefaultView)
                        {
                            
for (int x = 0; x < intCols; x++)
                            {
                                sb.Append(dr[x].ToString());
                                
if ((x + 1) != intCols)
                                {
                                    sb.Append(sdef.CustomDelimiter);
                                           
// user defined delimiter
                                }
                            }
                            sb.Append(
"\n"); // terminate the line
                        }
                        
break;
                    
case SchemaSpec.SchemeDef.DelimType.FixedWidth:
                        
// if the file uses a header, add the column header 
                                             //text back in as the column name source
                        if (Properties.Settings.Default.UsesHeader == true)
                        {
                            
StringBuilder sbTitleRow = new StringBuilder();
                            
for (int i = 0; i < dgvMaster.ColumnCount; i++)
                            {
                                
// being fixed width, we need to right pad the 
                                                           // values to attain the proper
                                // column widths-the column widths are stored with the 
                                                            //schema definition,
                                // we don't use any delimiters on this type so just the 
                                                           // padding goes in
                                sbTitleRow.Append(dgvMaster.Columns[i].HeaderText.
                                   ToString().Trim().PadRight(
                                   sdef.ColumnDefinition[i].ColumnWidth));

                                if ((i + 1) == intCols)
                                    sbTitleRow.Append(
"\n");
                            }
                            sb.Append(sbTitleRow.ToString());
                        }
                        
// populate the rows
                        foreach (DataRowView dr in dt.DefaultView)
                        {
                            
for (int x = 0; x < intCols; x++)
                            {
                                sb.Append(dr[x].ToString().Trim().
                                   PadRight(sdef.ColumnDefinition[x].ColumnWidth));
                            }
                            sb.Append(
"\n");
                        }
                        
break;
                    
case SchemaSpec.SchemeDef.DelimType.TabDelimited:
                        
// if the file uses a header, add the column header text back 
                                             //in as the column name source
                        if (Properties.Settings.Default.UsesHeader == true)
                        {
                            
StringBuilder sbTitleRow = new StringBuilder();
                            
for (int i = 0; i < dgvMaster.ColumnCount; i++)
                            {
                                sbTitleRow.Append(dgvMaster.Columns[i].HeaderText);
                                
if ((i + 1) != intCols)
                                    sbTitleRow.Append(
"\t"); // tab delimited
                                else
                                    sbTitleRow.Append("\n"); // terminate the row
                            }
                            sb.Append(sbTitleRow.ToString());
                        }
                        
// populate the rows
                        foreach (DataRowView dr in dt.DefaultView)
                        {
                            
for (int x = 0; x < intCols; x++)
                            {
                                sb.Append(dr[x].ToString());
                                
if ((x + 1) != intCols)
                                {
                                    sb.Append(
"\t"); // tab delimited
                                }
                            }
                            sb.Append(
"\n"); // terminate the row
                        }
                        
break;
                    
default:
                        
break;
                }

                // use a stream writer to overwrite the existing file with the new content 
                // including any new rows and edits to existing rows
                using (StreamWriter sw = new StreamWriter(CsvFilePath))
                {
                    
try
                    {
                        sw.Write(sb.ToString());
                        
MessageBox.Show("File " + CsvFilePath + " saved."
                         
"File Saved");
                    }
                    
catch (Exception ex)
                    {
                        
MessageBox.Show("Unable to save: " + CsvFilePath + 
                         
", Error " + ex.Message, "Save Error");
                    }
                }
            }
        }

        /// <summary>
        /// Create a schema.ini file to control the format and data types used 
        /// within the applications - this must be saved in the path of the input file.
        /// It will overwrite any existing schema.ini file there but the whole process
        /// is transparent to the end user.  The specification of the actual input file
        /// received from Intuit/Medfusion must match here exactly.
        /// 
        /// If you wish to conceal any information from the end user just hide the column 
        /// in LoadFileData()
        /// </summary>
        /// <param name="filePath"></param>
        private void CreateSchemaIni(string filePath)
        {
            
try
            {
                
// define a new schema definition and populate it from the 
                // application properties
                SchemaSpec.SchemeDef sdef = new SchemaSpec.SchemeDef();
                
if (Properties.Settings.Default.SchemaSpec == null)
                {
                    
MessageBox.Show("No schema has been defined; prior to opening a 
                    CSV file, use the Schema tool to construct a schema definition"

                         
"Missing Schema");
                    
return;
                }
                
else
                {
                    sdef = Properties.
Settings.Default.SchemaSpec;
                }

                // start a string builder to hold the contents of the schema file as it is construction
                StringBuilder sb = new StringBuilder();

                // the first line of the schema file is the file name in brackets
                sb.Append("[" + Path.GetFileName(filePath) + "]" 
                 + 
Environment.NewLine);

                // the next line of the schema file will be used to determine whether or not
                // the first line of the file contains column headers or not
                string colHeader = sdef.UsesHeader == 
                 SchemaSpec.
SchemeDef.FirstRowHeader.No ? 
                
"ColNameHeader=False" : "ColNameHeader=True";
                sb.Append(colHeader + 
Environment.NewLine);

                //  next we need to add the format to the schema file
                switch (sdef.DelimiterType)
                {
                    
case SchemaSpec.SchemeDef.DelimType.CsvDelimited:
                        
// a comma delimited file
                        sb.Append("Format=CsvDelimited" + Environment.NewLine);
                        
break;
                    
case SchemaSpec.SchemeDef.DelimType.CustomDelimited:
                        
// a custom delimiter is used here; need to check and make sure the user
                        // provided a character to serve as a delimiter
                        if (String.IsNullOrEmpty(sdef.CustomDelimiter))
                        {
                            
MessageBox.Show("A custom delimiter was not identified 
                             for this schema."
"Invalid Schema");
                            
return;
                        }
                        sb.Append(
"Format=Delimited(" + sdef.CustomDelimiter + ")" + 
                         
Environment.NewLine);
                        
break;
                    
case SchemaSpec.SchemeDef.DelimType.FixedWidth:
                        
// the file columns here have a fixed width; no other delimiter is supplied
                        sb.Append("Format=FixedLength" + Environment.NewLine);
                        
break;
                    
case SchemaSpec.SchemeDef.DelimType.TabDelimited:
                        
// the columns here are tab delimited
                        sb.Append("Format=TabDelimited" + Environment.NewLine);
                        
break;
                    
default:
                        
break;
                }

                // next each column number, name and data type is added to the schema file
                foreach (SchemaSpec.ItemSpecification s in sdef.ColumnDefinition)
                {
                    
string tmp = "Col" + s.ColumnNumber.ToString() + "=" + s.Name 
                    + 
" " + s.TypeData;

                    if (s.ColumnWidth > 0)
                        tmp += 
" Width " + s.ColumnWidth.ToString();

                    sb.Append(tmp + Environment.NewLine);
                }

                // the schema.ini file has to live in the same folder as the file we 
                             // are going to open; it has to carry the name
                // schema.ini.  When we connect to the file, the connection will 
                             // find and use this schema.ini file to 
                // determine how to treat the file contents; only the correct 
                              // schema.ini file for a particular file type can 
                // be used you cannot, for example, open a comma delimited 
                             // file with a schema.ini file defined for a 
                // pipe delimited file.
                using (StreamWriter outfile = new StreamWriter(Path.GetDirectoryName(
                 filePath) + 
@"\schema.ini"))
                {
                    outfile.Write(sb.ToString());
                }
            }
            
catch (Exception ex)
            {
                
MessageBox.Show(ex.Message"Error");
            }
        }

        /// <summary>
        /// Exit the application
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            
Application.Exit();
        }

        /// <summary>
        /// We will just exit, you might want to do something else with cancel
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnCancel_Click(object sender, EventArgs e)
        {
            
Application.Exit();
        }

        /// <summary>
        /// Assuming that you want to open the delimited text file for some purpose, you could 
        /// add that processing routine here
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSubmit_Click(object sender, EventArgs e)
        {
            
// Process the file to do whatever it is you want to do with the 
            // delimited text file import 
            MessageBox.Show("Add your code here to process the delimited file as 
             needed."
"Feature Not Implemented");
        }

        /// <summary>
        /// the click event handler for the navigator's save button
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void bindingNavigatorSave_Click(object sender, EventArgs e)
        {
            SaveFileChanges();
        }

        /// <summary>
        /// Show the about box
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void aboutCSVEditorToolStripMenuItem_Click(object sender, EventArgs e)
        {
            
frmAbout f = new frmAbout();
            f.ShowDialog(
this);
        }

    }
}


Code: Schema Definition Form (frmSchema.cs)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using SchemaSpec; 

namespace CsvEditor
{
    public partial class frmSchema : Form
    { 
        // contains the schema definition
        // defined for use in opening the
        // delimited file within the application
        public SchemeDef sdef;
 
        // a binding source used for navigating and editing the 
        // file contents
        private BindingSource bsrc;
 
        public frmSchema()
        {
            InitializeComponent();
            bsrc = new BindingSource();
 
            // if an existing schema file has been included, use it to 
            // set the schema definition and to populate the form for edit
            if (Properties.Settings.Default.SchemaSpec != null)
            {
                sdef = Properties.Settings.Default.SchemaSpec;
                LoadFromSdef();
            }
        }
 
        /// <summary>
        /// Close the form without saving anything
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {
            this.Dispose();
        }
        /// <summary>
        /// Populate the form fields using the current 
        /// schema definition
        /// </summary>
        private void LoadFromSdef()
        {
            if (sdef != null)
            {
               switch(sdef.DelimiterType.ToString())
                {
                    case "CsvDelimited":
                        cboDelimType.Text = "Csv Delimited";
                        txtCustomDelimiter.Enabled = false;
                        txtCustomDelimiter.Text = string.Empty;
                        break;
                    case "TabDelimited":
                        cboDelimType.Text = "Tab Delimited";
                        txtCustomDelimiter.Enabled = false;
                        txtCustomDelimiter.Text = string.Empty;
                        break;
                    case "CustomDelimited":
                        cboDelimType.Text = "Custom Delimited";
                        
                        if (!string.IsNullOrEmpty(sdef.CustomDelimiter))
                            txtCustomDelimiter.Text = sdef.CustomDelimiter;
 
                        txtCustomDelimiter.Enabled = true;
                        break;
                    case "FixedWidth":
                        cboDelimType.Text = "Fixed Width";
                        txtCustomDelimiter.Enabled = false;
                        txtCustomDelimiter.Text = string.Empty;
                        break;
                    default:
                        break;
                }
 
                if(sdef.UsesHeader == SchemeDef.FirstRowHeader.Yes)
                    chkHeaders.Checked = true;
                else
                    chkHeaders.Checked = false;
 
                // show the defined schema columns and data in the grid
                dgvRowView.DataSource = null;
                bsrc.DataSource = sdef.ColumnDefinition;
                dgvRowView.DataSource = bsrc;
                bnav.BindingSource = bsrc;
                dgvRowView.Columns[dgvRowView.Columns.Count - 1].AutoSizeMode = 
                  DataGridViewAutoSizeColumnMode.Fill;
 
                // clear input fields; leave the data type the same, probably be text most of the time
                txtColumnName.Text = string.Empty;
                txtColWidth.Text = string.Empty;
            }
        }
 
         /// <summary>

        /// Set the delimiter type upon selection from an option in the 
        /// delimiter type combo box - starts a new configuration
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void cboDelimType_SelectedIndexChanged(object sender, EventArgs e)
        {
            // if no schema exists, create on to set the delimiter type to the default
            if(sdef == null)
                sdef = new SchemeDef();

           // if clear is called, clear the headers checkbox and return
            if (cboDelimType.SelectedItem == null)
            {
                chkHeaders.Checked = false;
                return;
            }

            // set the delimiter type from the drop down
            switch (cboDelimType.SelectedItem.ToString())
            {
                case "Csv Delimited":
                    txtCustomDelimiter.Enabled = false;
                    txtCustomDelimiter.Text = string.Empty; 
                    sdef.DelimiterType = SchemeDef.DelimType.CsvDelimited;
                    break;
                case "Tab Delimited":
                    txtCustomDelimiter.Enabled = false;
                    txtCustomDelimiter.Text = string.Empty; 
                    sdef.DelimiterType = SchemeDef.DelimType.TabDelimited;
                    break;
                case "Custom Delimited":
                    sdef.DelimiterType = SchemeDef.DelimType.CustomDelimited;
                    txtCustomDelimiter.Enabled = true;
                    break;
                case "Fixed Width":
                    txtCustomDelimiter.Enabled = false;
                    txtCustomDelimiter.Text = string.Empty; 
                    sdef.DelimiterType = SchemeDef.DelimType.FixedWidth;
                    break;
                default:
                    break;
            }
        } 

        /// <summary>
        /// Add a row to the schema definition, each row added defines
        /// a schema column – we need a column number, name, data type, and optional column width
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnAddRow_Click(object sender, EventArgs e)
        {
            // create a new row item specifiation
            ItemSpecification itm = new ItemSpecification();
            int rows = dgvRowView.Rows.Count;
 
            // set row number
            itm.ColumnNumber = rows + 1;
 
            // set the column name
            if (!String.IsNullOrEmpty(txtColumnName.Text))
                itm.Name = txtColumnName.Text;
            else
            {
                MessageBox.Show("Specify a column name to continue"
                  "Invalid Column Name");
                return;
            }
 
            // set the column data type
            if (cboDataType.SelectedItem != null)
                itm.TypeData = 
                 (ItemSpecification.JetDataType)Enum.Parse(typeof(
                  ItemSpecification.JetDataType), 
                 cboDataType.SelectedItem.ToString());
            else
            {
                MessageBox.Show("Specify a data type to continue"
                  "Invalid Data Type");
                return;
            }
 
            // set the column width (ignored unless the delimiter type is fixed width)
            if (!String.IsNullOrEmpty(txtColWidth.Text))
            {
                itm.ColumnWidth = Convert.ToInt32(txtColWidth.Text);
            }
            else
            {
                if (sdef.DelimiterType == SchemeDef.DelimType.FixedWidth)
                {
                    MessageBox.Show("A fixed width delimited file requires a 
                          column width""Missing Column Width");
                    itm = null;
                    return;
                }
            }
 
            // update rows and grid display - each row defines a column in the schema
            sdef.ColumnDefinition.Add(itm);
            dgvRowView.DataSource = null;
 
            bsrc.DataSource = sdef.ColumnDefinition;
            bnav.BindingSource = bsrc;
 
            dgvRowView.DataSource = sdef.ColumnDefinition;
            dgvRowView.Columns[dgvRowView.Columns.Count - 1]
                 .AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
 
            // clear input fields; leave the data type the same, probably be text most of the time
            txtColumnName.Text = string.Empty;
            txtColWidth.Text = string.Empty;
        }
 
         /// <summary>
        /// Save the schema definition to the application settings 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSave_Click(object sender, EventArgs e)
        {
            if (dgvRowView.Rows.Count > 0)
            {
                Properties.Settings.Default.SchemaSpec = sdef;
                Properties.Settings.Default.Save();
 
                MessageBox.Show("The schema model has been saved and is 
                 now available to the application.""Schema Saved");
                this.Dispose();
            }
            else
            {
                MessageBox.Show("The specification contains no column specifications"
                  "Invalid Specification");
            }
        }
 
        /// <summary>
        /// The user may clear out any existing schema and start defining one from scratch;
        /// by default the user will be put into edit of the existing schema
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnClearAll_Click(object sender, EventArgs e)
        {
            if (MessageBox.Show("If you continue, the existing schema 
                 will be overwritten""Continue?"
                 MessageBoxButtons.YesNo, 
                 MessageBoxIcon.Question) == DialogResult.No)
                return;
 
            sdef = new SchemeDef();
            dgvRowView.DataSource = sdef;
            cboDataType.SelectedIndex = -1;
            cboDelimType.SelectedIndex = -1;
            txtColumnName.Text = string.Empty;
            txtColWidth.Text = string.Empty

            txtCustomDelimiter.Text = 
string.Empty;
            txtCustomDelimiter.Enabled = false;
        }
 
         /// <summary>
        /// Set the custom delimiter character
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void txtCustomDelimiter_TextChanged(object sender, EventArgs e)
        {
            if (sdef != null && !String.IsNullOrEmpty(txtCustomDelimiter.Text))
                sdef.CustomDelimiter = txtCustomDelimiter.Text.Substring(0, 1);
        }
 

        /// <summary>
        /// Set whether or not the first row contains column headers
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void chkHeaders_CheckedChanged(object sender, EventArgs e)
        {
            if (chkHeaders.Checked)
            {
                sdef.UsesHeader = SchemeDef.FirstRowHeader.Yes;
                Properties.Settings.Default.UsesHeader = true;
                Properties.Settings.Default.Save();
            }
            else
            { 
                sdef.UsesHeader = SchemeDef.FirstRowHeader.No;
                Properties.Settings.Default.UsesHeader = false;
                Properties.Settings.Default.Save();
            }
        }
    }
}

Code:  About Box (frmAbout.cs)

The about box code is not important and is not described in this document.

That wraps up the sum of the code needed to drive the CSV Editor application.  With the code provided, it is possible construct a viable CSV editor and to build a schema on the fly that may be used to connect to and interact with a delimited text file, regardless of format.

Summary.

While this article was written to demonstrate an approach to working with delimited text files, you might need to connect to only one particular type of delimited text file; the information contained in this document could be used to build a canned scheme.ini file and a connection string that might be then used to open a delimited text file of a matching schema as much and as often as is necessary.

The download includes examples of each delimited file type (comma delimited, tab delimited, fixed width, and custom character (in this case pipe delimited) with and without headers.  The files are named such that you can figure out the delimiter type and whether or not it includes a header row.  To open any delimited text file, create a schema definition that works with that file type and save it using the schema definition dialog prior to opening it.

Looking at the Schema dialog (Schema Generator) shown in the following figure, start by setting the delimiter type.  If the delimiter is a custom character, the delimiter text box will enable and you can enter the delimiter there.  If the file you are trying to open uses the first row to define the column header, check the box below the delimiter type selection.

You can then begin to add rows to the definition.  For each row, enter a column name and a JET data type (you can use text for all of the fields if you want to or you can set them to the correct type).  If the delimiter type is fixed width; you will also need to set the column width value before adding the column definition to the schema.

Once all of the columns have been added, you can save the schema definition using the Save button at the bottom of the dialog.  After saving, when you return to the editor form, you can open any CSV file that matches the schema definition.  When a file is opened, the editor code will look at the schema definition and create a schema.ini file in the same path as the file to be opened, it will also construct a connection string to enable opening the delimited text file using and OLE DB connection.

DlmtTxtFl

Figure 6: Defining a Schema


Similar Articles