Programming in Visual Studio 2017 C# - Combined PDF

Download as pdf or txt
Download as pdf or txt
You are on page 1of 1392

An Absolute Beginners Guide to C# - Volume 1

Visual Studio C# 2017


Intro -Through Forms
by Tim R. Wolf
2017.06 Version 1.04
Introduction

Audience:

This book is volume one of a three volume set and is intended for beginning Microsoft
Visual Studio C# (C Sharp) programmers, versions 2015 through VS 2017. This is a
hands-on book, with actual programs, starting in Chapter 1 and it talks about the
practical day-to-day, nuts-and-bolts programming that real people need to know.

It assumes no prior programming experience.


Little time is spent on theory.

Each topic has step-by-step instructions with numerous code examples and over 900
cropped and annotated illustrations. It explains the "why" of a program.

Prior Experience

If you already have moderate programming experience, especially in Visual Basic, this
book will expand your skills, giving confidence in the new language.

The goal is to have as much time on the keyboard, working with common business
problems. After studying this book and working through the examples, you will be a
proficient programmer – able to write real programs that do real work. You will be able
to read files, parse data, write to database, and build data input screens.

Why this book?

This book is different than most publications. Little time is spent on theories and
technical side-trips are rare. Starting in Chapter 1, you will immediately begin working
with loops, if-statements and string-manipulation. This means some topics, such as
conversions, numeric types, and other such concepts, are glossed over until they are more
germane to the concepts being taught.

Where other publications might spend a page or two on a topic, this book dives into the
most common and most useful ways to solve a problem. For example, over 80 pages are
devoted to opening multiple forms and how to pass data between them. The parsing
chapter devotes 70 pages to this subject, covering delimiters, CSV, Tab, Excel, and other
techniques. This is not over-kill. You will find these address real-world data-processing
problems. I cover the tips and tricks you will need to know.

Chapters often show different techniques for the same problem and the benefits and
drawbacks of each are explained. If there is a chance of making a mistake in
punctuation, style, or logic, the examples show how to resolve them. Compiler errors are
scattered throughout the book and there is a comprehensive alphabetic error reference in
the appendix, showing likely causes and recommendations.

A side-effect of this first volume will be a library of utility modules that can be used in
all of your programs. These utilities can automate mundane tasks, such as parsing
delimited files, punctuating phone numbers, street-addresses, and capitalizing proper
names. These libraries will save boat-loads of time and will be literally useful in all your
programs.

You will also notice none of the examples use Console-applications (DOS-like
programs) – all are Windows forms. Besides being more visually interesting, they allow
greater feedback while developing and the programs more accurately reflect what
happens in the business world.

What this book does not cover:

I do not cover some of the more theoretical underpinnings of modern programming.


Things such as polymorphism and object-inheritance are not discussed directly, but the
techniques are used throughout the book. Advanced programmers may take exception,
but I favor a more procedural background and I always approach problems from a
business-oriented perspective. You have a job to do, files to process, data-entry-screens
to write. These are the things this book is concerned about.

These volumes do not cover web-development but the programming skills taught are
100% transferrable. SQL databases are introduced, but four lengthy chapters only
scratch the surface. However, if you fear treading in this area, these four chapters will
get you started and will show relatively advanced techniques.

Why C#?:

Microsoft has built a wonderful programming environment which verges on magical.


The language is mature and consistent and can be applied in any conceivable situation.
It is a powerful tool that can solve complex problems.

The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.

How to Use This Book:

These three volumes are teaching books and because of that, it makes a poor reference
guide. To get the most utility, start at page one and work your way through chapters.
Each chapter builds on the previous. It takes time and effort to learn programming. As
you work through the chapters you must sit in front of the compiler and write the code.

This series was divided into three volumes, partly to aid in printing, and partly to make
the chapters more accessible.

Volume 1:
1 Introduction to the Editor
2 Introduction to Loops
3 Conditional Branching
4 Strings
5 Numbers and Dates
6 Utility Functions
7 Advanced Utility Functions
8 Class Libraries
9 Variable Scope
10 Form Controls and Events
11 Calling Multiple Forms
A Compiler Error Messages
B Compile and Distribute

Volume 2:
12 ASCII Files
13 Parsing Tab and CSV Files
14 INI Files
15 XML and App.config Files
16 Windows Registry
17 Reading Excel and Access
18 External Programs (Shell)
19 Wait, Delays, Pauses
20 Printing
21 Formatting

Volume 3:
22 Arrays
23 File Manipulation
24 Console Applications
25 SQL Databases
26 SQL Record Edits
27 SQL Data Grids
28 Data Grid Cell Editing
C Installing SQL Server Express
D Routines (of Interest)

Thank you

Thank you for purchasing this book. I hope you enjoy it as much as I have had writing it.
Comments and suggestions are welcome.

traywolf at keyliner com


www.keyliner.com

This book was written with Visual Studio 2013 through 2017's Community Edition, with
sections referencing Microsoft Office and Microsoft SQL Server 2016 Express.
© 2017 by Tim R.Wolf. All rights reserved.

Original text written with WordPerfect version X7. Illustrations created with Corel's
PaintShop Pro, version X8.
The Compiler and Other Tools:

As of mid 2017, Microsoft allows you to freely download a fully-capable version of


Visual Studio. The software is free and this book was written with these versions. See
this link:

http://www.visualstudio.com/en-us/products/visual-studio-community-vs

This book is applicable to VS 2010 and newer, with an emphasis in version 2017.

For the database chapters you will need a copy of Microsoft MS-SQL database or a copy
of Microsoft's free downloadable "MS-SQL Express". Details can be found in the
Appendixes.

Everything in this book was designed to run on a stand-alone (home) workstation,


including the SQL chapters. You will not need a server, external SQL database or
Active Directory to complete the chapters. However, when useful, references to these
other resources are made.
Conventions

Printing conventions are designed to help you find pertinent code quickly.

Code examples are show in plain text, white text boxes or shaded text boxes. The
boxing and shading gives you a clue about how "solid" or complete the code is. If you
are skimming through the chapters looking for something, the formatting style tells you
how far along the text is.

The formatting styles move from discussions to preliminary to final code.

At the beginning of each chapter is a summary or outline of the most common commands
or coding techniques. These act as a reference for the details that follow.

Discussion Code Blocks:

Code that is in-line with the text, and without a text-box border, is a discussion or
theoretical block and you are not expected to type or test. For example, this is a
discussion code block:

//Consider showing which number was selected


MessageBox.Show("Selected value: " + Convert.ToString(inumericValue));

Code is always typed in a fixed-width, non-proportional Courier font.


Author Comments:

If I have an opinion on a programming technique, it is typed as an indented, italicized


block, such as this:

In real life, no programmer would define a "part-number" as a


floating-point number, even if it has a decimal point. This
should really be declared as a string. Only declare numeric
values if you intend to perform mathematical operations.

Paragraph Numbering:

There may be numerous steps to solve a programming goal and you will find I am a fan
of numbering them. Within each section, you will find numbered steps, 1, 2, 3 or
A, B, C.

Setup or pre-requisite steps, as well as testing, are numbered "A, B, C". Actual
programming steps (devoted to the section you are reading), are numbered "1, 2, 3".

Terminology:

Although I assume no previous programming knowledge, it is helpful to understand a


few terms.

"Variable"
A variable is a named place-holder for a value. For example, a field may be named
"FirstName" – the variable's name holds anybody's name, hence its variableness. Other
famous variable names include "i", which typically is nothing more than a counting
variable, where "i" might equal 10.

While programming, make-up or invent your own variable names. The names should be
descriptive, which helps identify its purpose later in the program and I favor long,
descriptive names: frontSideDelimiter and RecordCategoryCode.

"Strings"
Textual information (words, phrases, sentences, letters, and characters) are all called
"strings". Strings can be concatenated (appended to each other), or parsed (taken apart
into smaller, constituent pieces).

Examples of strings:
"Dog"
"M"
"A sentence, such as this one."
"Product ID: 3003-22"
"123 Elm Street"
"65,002"
"10/06/2007"
Strings, by their very definition are nothing but blobs of text. This is where most of your
computing time will be spent. Numbers and dates can be represented as strings, but
mathematical calculations can not be performed against them.

"literal"
A literal is an explicit text string, manually coded into the program. Some call this a
"hard-coded" value. Literals are enclosed in quotes. For example,

string FirstName = "John Smith";

where FirstName is a string variable (declared as a string) and "John Smith" is a literal,
typed in quotes. Literals can live inside of other statements, such as:

MessageBox.Show("Display this literal now");

"Numbers"
Numbers are more interesting. The general intention is to use them for calculations, such
as a summation, count, or other mathematical operations. There are amazingly a dozen
different numeric types, where the most common are "integers" (whole numbers, such as
1, 2, 3, 0, -5, 200, etc) and real numbers (such as 3.24, 7/8ths), which are also called
Floating Point numbers.

"boolean"
Booleans are yes-no, true-false variables and they are neither a string or a number.
Booleans are often expressed in the terms of an if-statement, such as:

if (FirstName == "Smith")

or sometimes booleans can be defined as a variable, as in:

bool EndofFileSwitch = false;

if (EndofFileSwitch)
if (EndofFileSwitch == true)

Note the double-equal signs.

"Declare"
The text will say "declare" (or define) a variable. This means you are creating a new
variable by telling the compiler what type of variable it is, and its name. Variables are
created by typing a statement, for example:

string FirstName = "John Smith";

where "string" declares a new variable "of type 'string'" with a declared name of
"FirstName". Variables do not have to be initialized with a value when they are
declared. For example, it can be declared on one line and populated on another:
string FirstName; //Defined or declared...

(then later...)
FirstName = "John Smith"; //Initialized or populated

Integer variables are declared in a similar fashion:

int loopCounter;
loopCounter = 15;

int totalCount = 120;

and with all variables, you can assign other similarly-typed values to another variable:

int loopCounter = 0;
int someOtherCounter = 200;
loopCounter = someOtherCounter; //where loopCounter now equals 200

After the first couple of chapters, I tend to prefix my variables with an indicator showing
what type of variable these are, for example, "str" for string, "i" for integer. This is a
documentation aid, and I use this style in my own programming:

strFirstName a string (text) variable for the person's first name.


strpassedPartNumber a string variable holding a part number

iCurrentRow the current row number being processed in a list


iCounter an integer (counting number, 1, 2, 3...) called "counter" because
a more descriptive name wasn't needed for a short-term routine.

cbxAddressType a checkbox field


lblSequenceNumber a "label" called "SequenceNumber" (on a form)
boolEndofFile a "boolean" (True/False) variable

Not all developers use prefixes and some are violently opposed to them.

"Visual Studio Project"


A Visual Studio "Project" is the program you are writing and it may consist of multiple
forms, libraries and code, all combined into one "solution file" (.sln). The technicalities
are minor. How to create, open and edit solutions and projects are described in the next
chapter.

Legal Stuff:
Visual Studio, Excel, Access, Visual Basic, Microsoft SQL, SQL Express, are Microsoft
Products with all of their copy-rights and trademarks

Let's get to work...


- TRW, 2017.01
Chapter 1 - Introduction to the Editor

This chapter acquaints you with Visual Studio's editing environment and basic syntax.
This is an introductory chapter and is hands-on; you will begin writing a program starting
on the next page.

Topics Covered:

• Your First Program


• Toolbox flyout
• +Common Controls Flyout sub-menu
• Dragging objects to the form
• Form Design tab and the Code Tab (Form1.cs)
• Labels
• Strings
• int (Integer)
• Convert.ToString
• Case Sensitivity
• Introduction to variable Scope
• Standard Text Boxes
• Assigning Strings to TextBox
• Properties Panel

Chapter 1 - The Editing Environment and First Program Page: 3


Your First Program

1. Begin by launching Microsoft Visual Studio or Microsoft Visual Studio Express, from
either the Start Menu or from an icon on your desktop.

2. Once the program opens, select File, New, Project from the top menu.
Or alternately, click "More project templates..." from the Start Page:

3. Visual C# displays a New Project dialog box.

• On the Installed Templates tree, left-side, expand "Visual C#" by clicking the
triangle (it may be already expanded).

• Within the tree, select the "Windows Classic Desktop", then choose "Windows
Forms App" (in older versions of Visual Studio, select "Windows"):

Chapter 1 - The Editing Environment and First Program Page: 4


• For now, accept the default/suggested filenames at the bottom of the screen (not
illustrated)

• Click OK.

4. The editor opens to a screen divided into several sections and some of the window panes
may not be displayed on first launch. Here are the main panes you need to know and
each are described in more detail later in this chapter:

• Solution Explorer – A tree-view of the project's components, including the main


form and any libraries you may use.

• Properties - Individual object properties, such as name, color, size, etc. In this same
pane are Events, such as "on click", "minimize", and others. The properties and
event panels are context sensitive.

• ToolBox - "Flyout" menus for design tools (for adding textboxes, buttons).

• Error List - A list of compiler and syntax errors while coding. This is likely hidden
and the steps to expose are described below.

Chapter 1 - The Editing Environment and First Program Page: 5


Exposing Window Panes:

When you open your first editing session, some panes, such as Error List, may not be
visible on the design screens. Open with these steps:

From the top menu, choose "View".


Expose the following, as needed:

View, "Solution Explorer" (probably already displayed)


View, "Properties Window"
View, "ToolBox" (click outside the box to have it dock back to the side)
View, "Error List" (see bottom of editing window, near the "Output" tab)

At the bottom of the editing screen is the Error List, but often, it is obscured by the
Output tab. Click the Output Tab's "X" to close that window.

Chapter 1 - The Editing Environment and First Program Page: 6


By default, the View panes "dock" at the sides of the editor, and it is recommended they
stay in these locations. By choice or accident, they can be ripped off the edge and can
float above the working surface. If a pane becomes un-docked, re-dock by dragging the
title-bar to a docking indicator, which appears as you start dragging. Good luck figuring
this out.

Chapter 1 - The Editing Environment and First Program Page: 7


The Toolbox flyout behaves differently. Clicking the left-margin tab, "Toolbox", opens
the flyout. Note the thumbtack, which can anchor the window open. Most developers
allow the toolbox to auto-hide when not needed:

Finally, near the top, just above the Form1 form, is a tab-bar, where "Form1.cs [Design]"
is highlighted. In a moment, the "Code View" will appear as a second tabbed item on
this top-row. Use these tabs to switch between the form's design and code editing views.

First Program:

Begin the first program by adding a "text box" (data-entry field) and a button to the
default form, Form1. The things you are adding are called "objects."

1. In design view, in the center of the screen, confirm "Form1" is visible.

Chapter 1 - The Editing Environment and First Program Page: 8


• Click anywhere inside the form to activate the object, noting the black "handles"
(knobs) along the perimeter, indicating the form is active and re-sizeable.

• Click the Toolbox tab, found on the left-edge of the screen. The menu will fly-out.
(If the Toolbox flyout is not shown, select View, "Toolbox" from the menu-bar at the
top of the screen.)

• From the Toolbox flyout, scroll to "Common Controls"; open the list by clicking the
triangle (older versions of Visual Studio use "+").

Chapter 1 - The Editing Environment and First Program Page: 9


2. Click-and-drag the "Button" menu-item from the menu to the form, as illustrated.
Drop the button anywhere on the form; it doesn't matter where. The flyout may obscure
part or most of the form; drag past the form and the flyout will slink out of the way. The
button is automatically named "button1".

3. Drag a second Button, "button 2" to your form. This will be used in a later example.

4. From the same flyout-menu, drag a "Label" object to the form.


A "label" is simply visible text on your screen and it can say anything. For now, leave it
at it's default value, "label1". Later, this program will programmatically change its
value.

Click and drag the new objects, maneuvering them until they appear similar to the
illustration below:

Chapter 1 - The Editing Environment and First Program Page: 10


5. Still in design view, double-click button1.

This opens the screen into Code View - e.g. programming view and stubs-in a new
method called "button1_Click". Notice the new tab on the top row: "Form1.cs", where
".cs" stands for "C-Sharp". Alternately, in Solution Explorer, right-mouse-click
"Form1.cs" and choose "View Code / F7" and type the following code by hand.

Chapter 1 - The Editing Environment and First Program Page: 11


6. In the lower pane of the editor, notice the Error List. You may have this message:

"IDE1006 Naming rule violation". This is new to Visual Studio 2017 and indicates a
method or procedure should begin with a capital letter (e.g. "Button1_Click"). For now,
this can be ignored.

(If the Error List is not visible on the bottom-left of the screen, select top-menu View,
"Error List". Or, the tab may be obscured by the "Output" tab.)

Additional notes: This book was written using Visual Studio 2017 Release Candidate 1
and may change once the program is released.

7. In the displayed code, locate the line

"private void button1_Click (object sender, EventArgs e)"

Type the following statements between the opening and closing {braces}, starting with
the line "string myFirstWordA;" Each word is upper/lowercase sensitive and each
statement ends in a semi-colon ( ; ). Indent with spaces or tabs, but you can be sloppy
and the editor will align and indent after pressing Enter.

Program 1.1, completed


private void button1_Click(object sender, EventArgs e)
{
string myFirstWordA;

myFirstWordA = "Cat";
label1.Text = myFirstWordA;
}

Important Notes:

In the code, pay close attention to the opening and closing {braces}. The new code must
live between the braces directly below the "private void button1_Click" statement.
Notice the two closing braces at the bottom of the program – these belong to the
solution's namespace and are not part of the button1 block.

Chapter 1 - The Editing Environment and First Program Page: 12


The number-one problem beginning programmers have is mis-placing the braces or
typing outside of the correct bracing. The editor can help you find a paired-brace; click
the mouse directly behind the closing brace; notice how the top (matching) brace
highlights in a subtle gray.

Everything in C# is case-sensitive (upper and lower-cased letters are important).


Keywords, commands and invented variables, such as "myFirstWordA", are case-
sensitive. It is common to have mixed-cases, as in, "label1.Text" and "button1_Click".

Almost every statement, but not all, ends with a semi-colon, " ; ". These mark the end of
the statement. Longer, more complicated statements may take multiple lines in the
editing screen, but it is the closing semi-colon that marks the end of the command. As
you progress through this and the next chapter, the rules for semi-colons become evident.

Indenting with spaces or tabs are optional but considered good programming style and
the editor will help enforce indents.

Run and test Program 1:1:

The program is ready to test and run.

A. Run the code by pressing the keyboard's F5 key (or pressing the green Start arrow on the
top tool bar).

Chapter 1 - The Editing Environment and First Program Page: 13


A new program window opens and displays on the Windows task bar - this is your first
C-sharp program. It is a fully-functional Windows program, complete with title-bars,
minimize and close buttons and the window can be resized and moved.

Button1 is functional, while button2 is not. Try the following:

Click button1;
label1 should change to "Cat".
button2 will do nothing as there is no code written for it.

B. Close the newly-run program by clicking the "X" in the upper corner; this closes the
program and returns to the editor, probably in code view.

For all of the examples in this book, it is assumed the test program is closed before
returning to design or code view. If you forget to do this, a lovely error message greets
you as you try to edit your program: "Changes are not allowed while code is running."

Build Errors:

Chapter 1 - The Editing Environment and First Program Page: 14


As you run and test, you may see this message if there is a bug in the program:
"There were build errors. Would you like to continue and run the last successful
build?"

1. For example, remove a semi-colon (after the variable assignment myFirstWordA =


Cat;). Note the error "(semi-colon) ; expected".

2. Press F5 (or click green Start button) to run the program.

Note "There were build errors. Would you like to continue and run the last
successful build?"

Click "Do not show this dialog again" and then "No". You never want to run the
"last successful" program – you always want to see the bugs so you can correct them.

Chapter 1 - The Editing Environment and First Program Page: 15


This option can and should be changed in this menu:

Tools, Options, "Projects and Solutions"


Choose "Build and Run"
Set to On Run "Do not Launch"

Comments on Program 1.1:

When "button1" was clicked in the running program (not in the editor), you initiated an
"event" (button1_click). The code within the opening and closing "squirrelly braces"
defined what happens. This block of code ran for the duration of the routine, up to the
closing brace.

The statement "string myFirstStringA;" creates a holding area (a variable) that stores
text-data – also called a "string". The name, myFirstStringA is an invented name.

Chapter 1 - The Editing Environment and First Program Page: 16


When declaring a string, integer or other type of variable, you are marking what kind of
data can live within that space – and C# requires all variables to be declared before they
can be used, making C# a 'tightly cast' language. Later chapters discuss the nuances of
variables and declarations.

Modify the Program to Work with Numbers:

For the next example, modify the program, changing all strings to numeric "integers".
The program will add the numbers together and display the results. Follow these steps:

1. Confirm the previously-running program was closed. If needed, click the "X" in the
upper corner or by ending the task on the Windows taskbar. This should return you to
the code-editor. Jump into Code View, when you return to the editor.

2. Scroll down the program code and locate the button1 event ("private void
button1_Click").

Make the following changes to the code (replacing all of the code originally typed). As
before, watch upper and lower cased letters, semicolons and braces:

• Between the opening and closing braces in button1's click event, delete all
statements previously written, but do not delete the braces. If the braces are
accidentally erased, re-type them.

• Create two "int" (integer) declarations (see code example, below), including
semicolons:

int valueA;
int valueB;

• On the next two statements, initialize (assign) each variable with a default value, in
the example, the numbers 3 and 2. (Integers can contain whole numbers, such as 0,
15, 99, -4, etc, but no fractions).

valueA = 3;
valueB = 2;

• Write a statement that changes 'label1.Text'. This command adds values A+B,
converts them to a string, then assigns the results to label1. Note in particular the
parenthesis, the period, and a closing semicolon.

label1.Text = Convert.ToString (valueA + valueB);

Chapter 1 - The Editing Environment and First Program Page: 17


Program 1.2; Working with Numbers, revised and completed
private void button1_Click(object sender, EventArgs e)
{
int valueA;
int valueB;

valueA = 3;
valueB = 2;

label1.Text = Convert.ToString(valueA + valueB);


}

By definition, a label (label1) is a string field.

3. Run the program by pressing F5. Click button1 to run the event.

Results: Label1 changes from it's default "label1" to "5" (3 + 2).

Comments:

The keyword "int" declares an integer (positive and negative whole numbers: 1, 2, 3, -55,
etc.). Be aware there are a half-dozen or so other numeric-type variables including
floating point, and these will be covered in later chapters.

The next line is more interesting:


label1.Text = Convert.ToString(valueA + valueB);

When reading a line of code like this, read it from the inside-out – starting at the inner-
most parenthesis:

• "(valueA + valueB)" are added together.

• "Convert.ToString (5)"
– the numeric values, once added, are converted to a string (think text).

• "label1.Text = "
Move (assign) the converted-to-string text to Label1.Text via an equal sign.

C# is a stickler about moving numbers and text around. If a variable is declared as an


integer (or the results of a calculation are integer), then the answer can only live in a
place where numbers are expected. Generally, you cannot move it to a Text field
without first converting.

If you've worked with Visual Basic before, you may know that VB implicitly converted
numerics to strings and it had little concern about moving numbers into text fields. C# is
not as lenient (especially versions before VS 2008). If you run afoul with this rule, you

Chapter 1 - The Editing Environment and First Program Page: 18


will get a compiler error (a compiler error is an error in the editor which shows before
you are allowed to run the program). This scenario is demonstrated next.

Modified with errors (implicit convert):

It is easy to forget the "Convert.ToString" when working with numbers. Simulate this
type of compiler error by doing the following:

1. If needed, stop the prior running program by clicking the "X in the upper right, returning
to the editing environment.

2. In code view (if needed, click on the top tab "Form1.cs" or by double-clicking button1).
Change the line:

"label1.Text...", removing the Convert.ToString( ) phrase, leaving this statement:

label1.Text = Convert.ToString(valueA + valueB);


label1.Text = valueA + valueB;

4. Press F5 and attempt to run the program. The compiler should immediately error with

"Cannot implicitly convert type 'int' to "string"

and the program will not run (see illustration, below, noting the Error List). If the Error
List does not appear at the bottom of the screen, choose top-menu, "View, Error List".

5. In the Error List window, double-click the error message text. This jumps the cursor to
the failed line in your program. Note the squiggly line.

Chapter 1 - The Editing Environment and First Program Page: 19


6. Correct the mis-typed statement by returning the "Convert.ToString( ...)" command, as
before.

Other ".text" Errors:

It is also easy to mis-type the 'dot-Text' after the label's name. The editor, which
normally helps while typing, can also automate the problem. Make this minor change in
your program:

1. Modify this line by replacing the .T (capital T) with a lowercase "t"

label1.text = Convert.ToString(valueA + valueB);

2. Run the program by pressing F5; note the compiler error:

'System.Windows.Forms.Label' does not contain a definition for 'text'...

Chapter 1 - The Editing Environment and First Program Page: 20


3. Again, double-click on the compiler error message and the editor jumps to the offending
line.

4. Finally, Visual Basic programmers often forget to type the ".Text" phrase all-together,
which was perfectly acceptable in that language, but not in C-Sharp. When you do this,
the compiler errors with:

"Cannot implicitly convert type 'string' to 'System.Windows.Forms.Label'

Which basically means the new string (Convert.ToString) does not have a string-variable
to arrive at – without the .Text property, C# refuses to play. Correct the error by
returning the word .Text to its proper case and position.

Comments on the "definition" error:

"label1" is an object added to the form, and like all objects in Visual C#, they have
properties. Properties can include things such as the font, where the label is on the
screen, and a myriad of others controls. In this case, the ".Text" (dot-Text) is a property
that describes what the label "says" on the form.

Since everything in C# is case-sensitive, including property names, mis-typing ".Text"


with a lower-cased "t" is tantamount to mis-spelling the keyword. Admittedly, the error
"System.Window.forms.Label does not contain a definition for 'text' " is not particularly
helpful.

As you were typing ".text", the editor offered help by suggesting a correct spelling, via a
feature called "Intellisense." In your program, try this by deleting and re-typing the .Text
phrase. As you partially type the keyword (".te" or ".tex"), arrow key to the correct entry
in the list and press the space-bar. The editor auto-completes the entry.

Chapter 1 - The Editing Environment and First Program Page: 21


However, there is some caution with this feature. A touch-typist who types a lower-
cased ".text", completing the word without using the menu pull-down, is stuck with the
lower-cased version, even if not intended. This will cause the error seen earlier and
sometimes these types of errors are troublesome to spot.

Chapter 1 - The Editing Environment and First Program Page: 22


Error Corrections

Program errors can be hard to locate. Here are some suggestions:

If the error makes no sense and line you were previously working on is flagged, check
the following:

• Is the current line, or the line directly above, missing a closing semicolon or closing
parenthesis?

• Do you have a closing semicolon on a line that is not supposed to have one?
In particular, these types of statements do not have semicolons at the end of their
lines:

if-statements,
loop-statements and
"private void..." declarations

For these statements, are opening and closing {braces} present? (These are covered
in detail in future chapters.)

• As you are typing "keywords," such as "dot-Text", does a pop-up intellisense


window appear? If not, the preceding variable name is likely misspelled or was not
declared or defined. Upper and lowercase is important.

• And probably the most common problem, are closing braces in the right position?
Especially near the end of the program. Be sure your code lives within an opening
anc closing-brace-pair – avoiding the last two in the program. Remember to click
behind a brace to see where its partner lives. They must match and must be lined up
properly.

The two braces at the very bottom of the program should never have anything typed
past them. They belong to these statements:

Chapter 1 - The Editing Environment and First Program Page: 23


• Although this is partially cosmetic (but important for easy-to-read code), confirm the
braces are indented properly.

"There were build Errors..." (Revisited)

Compiler errors may prompt with "There were build errors. Would you like to
continue with the last successful build?"

As a reminder, always select checkbox "Do not show again" and click No.

In other words, you would never want to run the previous version of your code (before
the new bugs were introduced – you really want to see the current bugs). I often wonder
why this is even offered as an option. If you accidentally click "[x] Do not show and
then Yes, you can fix this problem with Tools, Options, "Projects and Solutions", "Build
and Run". Set "On Run, when build or deployment errors occur" to "Do not Launch".

Chapter 1 - The Editing Environment and First Program Page: 24


"Form1 does not contain a definition for <button1_Click>..."

If you delete an object, such as button1 (or if you delete the underlying code and leave
the object on the Form), and try to run the program again, you may see this compiler
error: "Form1 does not contain a definition for "button1_Click..."

In Visual Studio, versions 2015 and older, this would cause a colossal crash. Double-
clicking the error will take you to a particularly-nasty-looking panel with a lot of code
you did not write. The highlighted line will be the culprit. Without fear, delete the line
and run the program again.

Starting Visual Studio 2017, the editor recovers more gracefully from these types of
deletes and it unobtrusively orphans the code and does not show an error.

Appendix A: Common Errors

Appendix A is a list of common errors and their solutions. The list is sorted
alphabetically by the error message's text. Most of the errors students will encounter
while using this book are listed. Starting with Visual Studio 2017, error messages and
warnings get a numeric code. Not all codes are listed; search by text.

Chapter 1 - The Editing Environment and First Program Page: 25


Variables and Scope

C# is always concerned about where a variable is defined and when it can be used. This
section attaches code to the unused button2 and demonstrates how variables are cleaned
up and discarded. The routine will attempt (and fail) to use the integers, valueA and
valueB, defined above in button1_Click. Basically, if a variable is declared in one
routine, it is not available or visible to other parts of the program. The correct lingo is
the variables "fell out of scope." This is best described with a short demonstration.

1. Return to the form's design view – see the top-row of tabs, clicking "Form1.cs [Design]".

If you see a message "Changes are not allowed while code is


running" or "Cannot currently modify...." then you have left
your previous example running. See the Windows task bar and
close the running program before returning to the editing
environment.

2 Double-click button2 (not button1). This creates a default event (button2_Click) for
that object.

Add this new code between button2's opening and closing brace. In this code
illustration, button1's previously-typed code is also shown. Button2's code may appear
above or below button1's statements; the order of the block (block of code) does not
matter.

As you type this new code, expect compiler errors in the editor's bottom Error List and
red-squiggly-lines and expect the editor to change what you have typed. Force these
editing changes into the module:

Program 1.4; Button 2, attempting to use valueA and B


private void button1_Click(object sender, EventArgs e)
{
int valueA;
int valueB;

valueA = 3;
valueB = 2;
label1.Text = ConvertToString (valueA = valueB);
}

//New Code for Button2 - this will fail

private void button2_Click(object sender, EventArgs e)


{
valueA = 3;
valueB = 4;

label1.Text = Convert.ToString(valueA + valueB);


}

Chapter 1 - The Editing Environment and First Program Page: 26


Important While typing "ValueA", the editor will use Intellisense and will
attempt to replace what you are typing with other values or
statements (this is a clear indication that something is wrong
with the variable name); force the editor to discard the
suggestions by pressing ESCape on the popup list, and keep
typing the text as illustrated:

The editor will be angry and a number of errors will show in the error list.

Run with Button 2:

3. Attempt to run the new code by pressing F5. Notice the compiler errors:

"The name 'valueA' does not exist in the current context."


"The name 'valueB' does not exist in the current context."

Chapter 1 - The Editing Environment and First Program Page: 27


4. Double-click on the first descriptive-text in the error message and notice it takes you to
button2's "valueA" statement.

Comments:

Although "int valueA;" was declared in the same program, it was declared within
button1. Because of that, the variables are not visible to other routines. A more
professional explanation is the "scope" of the variable was limited to button1. At
button1's closing brace, when button1 finished running, the variables are discarded and
are no longer available.

Variables with limited scope are double-edged. It is good that they are destroyed - this
leaves the program with a smaller memory footprint. It also keeps you from making
mistakes - especially in large programs where the same variable name ("myCounter")
might be used in different routines. Often, it is best when values die before they are
mistakenly used somewhere else. (Other chapters discuss variable scope and how to
make longer-lived variables)

Chapter 1 - The Editing Environment and First Program Page: 28


Continuing with button2:

5. Return to the form's design view (see "Form1.cs [Design]", along the top row of tabs).

6. From the form's design view, highlight button2, and press the keyboard's Delete key to
remove the object.

7. Double-click button1 to return to the code view (or use the Form1.cs tab). Scroll down
the program and note that button2's logic ("private void button2_Click") survived
the delete. This is normal. Deleting the object does not remove the associated code.
Outside of cluttering the program, there is no harm – provided the orphaned code does
not have errors.

Leaving button2's code in place, Press F5 again to re-run the program. You will get the
same compiler errors ("the name ValueA does not exist").

8. Return to code view and delete all statements dealing with button2; this includes the
everything from the "private void button2..." statement, through it's closing brace.
Press F5 to run. The program should run without errors.

Chapter 1 - The Editing Environment and First Program Page: 29


Working with Text Boxes

"textBoxes" are the data-entry fields seen in a Windows program. The boxes hold data
such as a name, phone number or address and the end-user tabs into the field and types.
The following examples introduce basic methods for building and using text boxes and
later chapters will cover this topic in greater detail.

Begin by starting a new C# program.

A. Discard any previous programs you have been working with.

From the editor, select File, "Close Solution".


(For this example, it is important to close the solution and start fresh.)

Visual Studio, version 2015 and older, will prompt to Save. If prompted, don't bother.

j Starting with Visual Studio 2017, it automatically saves, without prompting. If you
accidentally edited, and want to discard the changes, press Shift, Close Solution.

B. Select File, New Project,


As before, select "Windows Form Application."
Accept the default filenames.

C. On the newly-displayed blank form, create a similar program as before by clicking and
dragging the following objects from the ToolBox flyout menu onto the default Form1:

textBox1 (do not use Rich Text Boxes)


textBox2 (a second text box)
label1
button1

Double-click (or drag) each object from the ToolBox flyout and arrange them to look
something like the illustration below.

Chapter 1 - The Editing Environment and First Program Page: 30


As a reminder, TextBoxes are found in the ToolBox flyout, in "Common Controls."
Scroll down the list to find the control:

Chapter 1 - The Editing Environment and First Program Page: 31


If the Toolbox flyout menu does not have any options, showing "there are no
useable controls in this group...", you are probably in the code view tab.
Click the top-tab "Form1.cs[Design]".

Naming Fields in the Properties Window:

Each object has a variety of properties, including a "Name." To view an object's


properties, select the object by single-clicking, then "other-mouse-click" (right-click).
From the pop-up menu, select "Properties." Alternatively, select the top menu, View,
"Properties Window." A panel opens on the lower-right side of the editing screen. Once
opened, the pane remains open. A various objects are clicked, the details within the
properties window change. The object's Name is listed at the top of the Properties
window.

The object's name (e.g. textBox1) can be changed to any name of your choosing, but the
name must be a single word or phrase, without embedded spaces. For example, textBox1
could be renamed to "MyFirstDataEntryField", where most developers prefer to use
"CamelCasing," with initial caps on each word.

Most objects have an extensive list of properties and includes


such things as the size, location, visibility, and others. Most
developers leave the properties panel always open.

Chapter 1 - The Editing Environment and First Program Page: 32


I like to sort the properties alphabetically, as illustrated above.

Single-click each of the text boxes until you have identified "textBox1", again looking at
the property's name. Arrange the fields on your screen so it is the topmost box in your
design window.

Continuing work on Text Boxes: Concatenation

The goal of this example is simple: take two text strings and combine them into one new
string, displayed at label1. The program will start with two hard-coded values, "Cat" and
"Dog".

1. From the form's design view, double-click "button1" to jump into this object's code
view.

2. Add this statement inside of button1's _Click event. As before, the code lives between
the opening and closing braces and, as always, watch upper and lowercase. End the
statement with a required semicolon.

Program 1.5, Concatenation


private void button1_Click(object sender, EventArgs e)
{
label1.Text = "Cat" + "Dog";
}

In the code-editor, it will look like this:

Chapter 1 - The Editing Environment and First Program Page: 33


3. Run the program by pressing F5.
Click Button1.
Results: "CatDog" appears at label1.

Because the string literals "Cat" and "Dog" were hard-coded, the two textBox fields
were not used in this part of the example.

Comments on "Adding two text strings":

The line
label1.Text = "Cat" + "Dog";

instructs the compiler to concatenate two literal strings and place the result into
label1's .Text. Except for the fact that you "added" two strings together using a plus-
symbol, this shouldn't be too big of a surprise. (VB and Excel programmers always used
"&" for string concatenation, C# uses a plus.)

Chapter 1 - The Editing Environment and First Program Page: 34


4. Next, replace the hard-coded string literals with a reference to the text boxes. Replace
"dog" and "cat" with this statement – but note this is a flawed line of code:

label1.Text = textBox1 + textBox2; //Flawed

5. Press F5 to run and you'll get this error:


"Operator '+' cannot be applied to operands of type 'TextBox' and 'TextBox' "

The code seems reasonable and on the surface it should work. The problem is this: C#
never works on an object's name by itself – it always needs to manipulate what I call a
'dot-property.' For VB programmers this is a constant surprise because VB assumed
what was not explicitly stated.

6. Re-write the line to look like this:

label1.Text = textBox1.Text + textBox2.Text; //Corrected

Before any value in a textBox can be used, you must explicitly state the .Text property,
as in "textBox1.Text". C# is persnickety in this regard.

Testing:

Run the program (F5)


Type any short text string in textBox1 and in textBox2.
Click button1 to see the results. The two text box values will concatenate at Label1.
Close the program and return to design view.

Chapter 1 - The Editing Environment and First Program Page: 35


Default Text Values:

In design view (where the Form's Design is visible; see top-tab-bar "Form1.cs[Design]"),
single-click textBox1 to highlight the object. In the Properties window, lower-right
corner of the editing screen, scroll down the list. Note properties such as Background
Color, Border Styles. Near the bottom of the list, find the ".Text" property.

The ".Text" is the same ".Text" used earlier and is where the value of this object lives.
From design view, this property's default value can be pre-populated and the end user
can change at run time. Both the design and the user's view change the same value.

1. If you haven't already done so, close the previously running program by clicking the "X".
Return to the Form's design view (see the top-row tab: "Form1.cs [Design]")

2. Click textBox1 to select.


In the Properties window, scroll down the list, locating the "Text" property.

Type the word "Angry".


Similarly, in textBox2's Text Property, type "Beavers".
F5 to run the program.

Chapter 1 - The Editing Environment and First Program Page: 36


Results: TextBox1 and 2 are pre-populated. Clicking button1 combines them into one
value, "AngyBeavers" at Label1. Change the text in the textBoxes and click button1
again; note how it adjusts to the new values.

Locking a field with "Enable / Disable":

Using the same properties list, lock a field so users cannot edit - forcing them to accept
the defaults.

3. Return to design view


Highlight textBox2.
Locate the "Enabled" property.
Double-click the property's value to toggle between True/False, choosing False.

4. Run the program again.


Results: The second textBox is disabled and you are unable to edit the field.

Combining textBox Values and Literals:

As a last example, combine the two textBoxes with a third literal, adding the word
" and " between "AngryBeavers".

5. Modify button1's click event by adding the literal " and " between the two textBox
values. Double-click button1 to open Code View:

Chapter 1 - The Editing Environment and First Program Page: 37


label1.Text = textBox1.Text + " and " + textBox2.Text;

To make this look proper the " and " needs leading and trailing spaces between the
quotes. Without the spaces, the result would be "AngryandBeavers".

This concludes this introduction to C#'s editing environment and a basic textBoxes. The
next chapters go into greater detail, including how to audit and limit what types of data
users can type.

By now you should have a general feeling for how the editor works and how to read and
interpret various compiler errors that we all accidentally type while programming. The
next chapter continues with variables and loops.

Chapter 1 - The Editing Environment and First Program Page: 38


Exercises
Most chapters in this book contain exercises and they are meant to be challenging.
However, this chapter will guide you with step-by-step hints. You are encouraged to do
all exercises to re-enforce the topics.

Exercise A

Create a program that prompts the user for their first name, mid-name, and their last
name. Use button1 to combine the three names into one string and display the results in
a label. For example, the user may type "John", "Q." and "Smith", with the final results,
"John Q. Smith". Label the fields so they look pretty.

Attempt this now, before following the steps.

A. Start a new Project, with a Form1.

B. Using the Toolbox flyout, drop three textBox fields, textBox1, textBox2, and textBox3
onto the form.

C. Change each textBox's Name property, from textBox1 to a more appropriate variable
name:

FirstName
MidName
LastName

(Variable names cannot/should not have spaces in them)

The "Name" property is not visible to end-users

Chapter 1 - The Editing Environment and First Program Page: 39


D. Drop three cosmetic labels on the form, label1, label2, label3 and change their text
property to read "My First Name", etc., as illustrated above.

Note: Labels also have a NAME property, which is different


than their .Text property. With the Name, think "variable
name."

If the label is strictly cosmetic, leaving them named as label1,


label2, is acceptable. If you had logic attached to them, then
they should be re-named – but you could not use "FirstName"
because that is already taken by the textBox. A name, such as
"lblFirstName" would be more appropriate.

E. Add another label, "label4", leaving it with its default name and default text value. This
will hold the answer, as generated by button1.

F. Using the toolBox flyout, add Button1.

Double-click the newly-dropped button and create an on-click event that assembles the
three text fields and places the results in Label4. For example, "John Q. Smith". When
doing this, how can you avoid this result: "JohnQ.Smith"?

Exercise B

Select File, "Close Solution" (and if prompted, save all).

From the Visual Studio Getting Started page, note the Recent Section.
Ignoring the Recent section, confirm you can find and re-open the project/Solution
manually by clicking File, "Open Project/Solution"

Confirm you can find and re-open the project.

A. Select File, "Close Solution" (and if prompted, Save all).

B. From the Visual Studio "Getting Started" landing page, note the Recent section.

Open the Project/Solution Manually:

C. Ignoring the Recent section, Select File, "Open Project/Solution"

Browse to (this default location)


"This PC \ Documents \ Visual Studio 2017 \ Projects"
Double-click folder (WindowsFormApp2) (or most recent)
Double-click "WindowsFormsApp2.sln" (solution file)

Chapter 1 - The Editing Environment and First Program Page: 40


In general, always open the Solution file. Do not open .cs, .resx,
or other type files. The "solution" contains everything about
your project, including all forms and code, all in one package.

D. Once the solution opens, look in the Solution Explorer pane (top-right side of editing
screen). "Other-mouse-click" Form1.cs, choosing "View Designer" or "View Code".

Later chapters discuss better places to save your projects than the defaults
Microsoft chooses.

Chapter 1 - The Editing Environment and First Program Page: 41


Chapter 2 - Introduction to Loops

Loops tell the computer to repeat a group of instructions multiple times. For example, a
program may need to step through each record in a file or it may need to loop through a
string, looking for a particular set of characters. There are several different types of
loops and while they may seem interchangeable, each is designed for slightly different
needs.

Probably the two most useful loops are "while-loops," looping while a condition is true
and for-next loops, which loops a pre-determined number of times. There are also for-
each loops (processing each element in an array) and nested loops. This chapter
demonstrates each and a solid understanding of the topic is important.

Topics:

• "while" loops
• incrementing with <loopCounter>++
• MessageBox.Show
• Infinite Loops
• ctrl-alt-Break debugging
• scrollbars
• "do" loops
• for-next loops
• Controlling loops with textBoxes
• Interrupting loops with "continue" and "break"
• Nested loops

Calling a loop recursively - advanced loop: See Chapter 20 - Printing

The loops demonstrated here are simple: print your name ten
times, build geometric shapes and other nonsensical things, but
don't loose track of the goal – learning how loops operate. For
this chapter, there was no need to make the loops any more
complicated. Later chapters make extensive use of these
constructs.

Chapter 2 - Loops Page: 45


while loop, Overview
int iloopCounter = 0;

while (iloopCounter <= 10)


{
//May not execute, depending on loopCounter value...

iloopCounter++; //You must increment the loop counter!


}

All loops can use these Early Exits:


continue; //re-loop (but watch increments in while and do-loops)
break; //end loop immediately

do-loop, Overview
int iloopCounter = 0;

do
{
//Always executes at least one time;
//note semicolon on while-statement

loopCounter++; //You must increment the loopCounter!

} while (loopCounter <= 10);

for-loop (for-next), Overview


//Counting Forward from 1 to 10

for (int iloopCounter = 1; iloopCounter <= 10; iloopCounter++)


{
//Do stuff here
//No loopCounter statement needed inside the loop
}

//Looping backupwards, 10 to 1:

for (int iloopCounter = 10; iloopCounter >= 1; iloopCounter–)


{

Chapter 2 - Loops Page: 46


foreach (array) loop, Overview
//String array must be pre-defined

foreach(string strtempString in astringArray)


{

//Useful with arrays only; see Chapter 22

//Can only use the temp variable within the loop


//No loopCounter or conditional is needed
}

Building the Example Program:

Begin the first loop exercise by building a new project in the same manner as the
previous chapter and this same project can be used for all examples in the chapter.

A. Begin by discarding any previous programs.


Select File, "Close Solution". Do not bother saving previous examples.

B. From the Visual Studio top menu, select File, New Project,
Select "Windows Form Application"
Accept default filenames, etc.
(See the previous chapter for illustrations)

C. From the ToolBox flyout (Common Controls), drag or double-click these objects to the
new Form window.

textBox1
textBox2
button1

D. Locate textBox1 (by clicking each textBox and looking in the Properties pane for the
object's name).

Convert textBox1 to a Multi-line box:

• Click the textBox object one time to highlight, then scrolling through the Properties
window (on the lower-right of the screen), set the "Multiline" property to True. (If
you don't see the Properties panel in the lower-right corner of your screen, see menu
choice View, "Properties Window")

• Alternately, set the Multi-line property graphically: Click the textBox, then click the
flyout menu-arrow, checking [x] MultiLine:

Chapter 2 - Loops Page: 47


E. Still in design-view (confirm top-tab "Form1.cs [Design]"), highlight the form's
background and resize the form, and textBox1 by dragging its selection handles until it
looks similar to the illustration below: (Before you can stretch a field vertically, it must
be set to Multi-line).

Re-arrange/move the fields and buttons until the screen looks similar to this illustration.
You may need to resize the form and move fields to make room. Resize the form by
dragging the form's handles (illustrated). To move fields and buttons, click and drag.

The program is now ready to accept code.

Chapter 2 - Loops Page: 48


"while" Loops

"while" loops are probably the most common and most useful looping command. They
have the following characteristics:

• Loop "while a condition" is true.


• The number of iterations do not need to be known before starting the loop.
• Checks for the "condition" before doing the work.
• Requires initial variable setup and house-keeping.
• Can easily and accidentally become an "infinite" loop.

Imagine this loop: "while money in wallet go shopping; if there is no money, don't go
shopping." This is the key feature of the loop - check before running. There is a
possibility the loop does not execute.

"while" loops require housekeeping. You, as the developer,


provides and nurtures a variable that controls the loop. The
examples will use a newly-created variable called
"loopCounter". There is nothing special about the name – any
made-up name will do. How variables are declared are covered
in more detail in the next chapters but for now, just follow along
in the examples.

The first example will loop 10 times, printing some text each time. Later examples
expand this idea by introducing counters and line-breaks.

while Loop Example with loopCounter:

Write an admittedly pointless loop that prints the word "hi" 10 times.

Program 2.1

1. From design view (built on the previous page), double-click button1, opening button1's
code-view.

2. Starting after "button1_Click"'s opening brace, declare an integer variable


"loopCounter", and assign an initial value. This is the variable that tracks where you are
in the "while" loop. Statements that begin with "//" are comments and are ignored by
the program.

private void button1_Click(object sender, EventArgs e)


{
int loopCounter;
loopCounter = 1;

//The while-loop will go here...


}

When writing these statements, be sure the statements are typed between button1's
opening and closing braces. And notice how the two statements end in semi-colons.

Chapter 2 - Loops Page: 49


Below this paragraph's closing brace are two additional closing braces that don't belong
to this routine; ignore them for now.

3. The actual loop is written at the //comment's location.

Begin the loop with the keyword "while", typed with a lower-cased "w", followed by a
parenthesis and a conditional-statement that in English reads, "while loopCounter is less
than or equal to 10."

Notice after the closing parenthesis, there is *not* a semi-colon.

Beneath the while loop is a new set of opening and closing braces, which you must
manually type. This is where the loop's interior statements will live. The blank line
between the variable declarations and the loop is cosmetic.

private void button1_Click(object sender, EventArgs e)


{
int loopCounter;
loopCounter = 1;

while (loopCounter <= 10)


{

}
}

Complete the routine, by typing the two statements inside the loop and a MessageBox
statement after the loop's closing brace, but within the button1_Click's closing brace.
The position of these statements is important:

Program 2.1 - Basic "while" loop, complete


private void button1_Click(object sender, EventArgs e)
{
int loopCounter;
loopCounter = 1;

while (loopCounter <= 10)


{
textBox1.Text = textBox1.Text + "hi";
loopCounter = loopCounter + 1;
}

MessageBox.Show("Done");
}

where:

• A detailed explanation of the statements follows in the next several pages.

Chapter 2 - Loops Page: 50


• Pay attention to semicolons and upper and lowercased names. Almost all statements
in C# end with a semi-colon, but the while-loop line does not.

• Stylistically, indent your code with either spaces or tabs; The editor automatically
helps with indenting.

• As you type, count the opening and closing braces to make sure that each brace has a
matched pair.

When typing braces, type the opening and closing braces, one after the other,
before typing the code between. Put a couple of blank lines between, giving
yourself room for the interior statements. Typing the braces in this fashion
helps you keep track of their position. Starting in VS2013, typing the
opening brace, the editor automatically types the closing brace.

Many beginning programmers get the opening and closing braces confused with the
other braces in the routine. It is important they are paired properly. Although the
compiler does not particularly care about indentation, you can get confused about where
the closing braces belong when the formatting is poorly maintained. When typing the
while-statement, I recommend typing it in this fashion in order to keep the braces paired:

Type the "while (loopCounter <= 10)" (no semi-colon); Press Enter,
Type the opening brace, {
Press Enter, Enter (twice)
Type the closing brace, }

with results that look like this:

while (loopCounter <= 10)


{

Starting with Visual Studio 2013, the editor automatically types


the closing brace when an opening brace is typed.

Notice the while-clause does not have an ending semicolon


[while (loopCounter <= 10)] this is required by the syntax. Technically,
the next pair of opening and closing braces act as a semicolon - or if you will,
a end-of-statement delimiter. This is similar to the procedure's name
[private void button1....], which also does not have a closing semicolon
because of its braces.

Finding a brace's matching pair:

After typing the opening and closing brace, place the cursor after either brace and the
editor will highlight the other matching brace. If indented properly, the braces will be
easy to interpret.

Chapter 2 - Loops Page: 51


Make sure the loop and its braces stays above button1_Click's closing brace. Other
braces exist below this routine; leave them unmolested.

Some developers code their braces in this fashion, which is also acceptable:

while (loopCounter <= 10) {


textBox1.Text = textBox1.Text + "hi";
loopCounter = loopCounter + 1;
}

Testing the Program:

A. Once the loop is written, run the program by pressing F5.

B. Click button1 to execute the loop.

Results: textBox1 shows "hihihihihihihihihi" along with a simple dialogue "Done". Note
10 concatenated strings.

C. Click OK on the MessageBox "done".


Close the program ("X") and return to the editor.

Chapter 2 - Loops Page: 52


Comments on the while-loop:

There are several new concepts in this program. Starting at the top, a variable,
"loopCounter" is declared as an integer (a whole-number, counting variable), followed
by a statement that initializes a default value 1.

Of interest, the computer allocates memory for the variable


when initialized - not when declared. Also, if a variable is
declared but not initialized, the compiler complains. There is an
example of this in a few pages.

Many C# programmers prefer to declare and initialize on the same line, using a
condensed syntax. Either style is acceptable.

int loopCounter = 1;

vs
int loopCounter;
loopCounter = 1;

There is nothing special about the name "loopCounter" – this is an invented name. But
no matter what name is used, remember C# is case-sensitive. If you spell "loopCounter"
with a lowercased-el and a capital-C, you must use that same scheme throughout the
module.

j Variable names must be single-words or phrases with no embedded spaces. By


convention, most programmers use CamelCase phrases, where words, begins with a
capital letter.

Chapter 2 - Loops Page: 53


Using punctuation such as "Loop_Counter" or "Loop-Counter" (underscores,
hyphens) is considered poor technique and you will be looked-down-upon – mainly
because programmers are to lazy to type the dashes and underscores. Different
shops have different standards in this area.

By convention, I like to type a variable's first letter as lower-


cased, indicating the variable is a "local" variable, defined in
this function (as opposed Form-level or Global variable). These
ideas are described in future chapters.

Definitions

How a variable is cased is known by two names.

PascalCasing where the first letter of each word is capitalized. This is


used for function (Method) names and for variables
outside the scope of the current routine.

camelCasing Where the first letter of the first word is lower-cased and
all other words are uppercased. This is usually used for
variables within the current routine.

Additionally, you will see variables named in "Hungarian Notation",


where the variable is prefixed with what type of data is stored in the
variable. For example:

strPersonsName A string variable


iloopCounter Integer

Hungarian Notation is now frowned up, but is used in this book to help
illustrate and clarify the variable's purpose.

The main "while" statement:

Examine the while-statement for a moment. "while" <a condition is true> do all the
"stuff" between the next pair of braces. If the condition were initially false, none of the
code within the braces executes and program passes control to the line after the closing
brace, which in this case, is the MessageBox.

In this example, "loop while the counter is less-than or equal-to 10" is guaranteed to run
because the variable was initialized to one.

Chapter 2 - Loops Page: 54


In order for the loop to work properly, something inside the loop must change
the condition – in other words, loopCounter must increment, approach, then
pass 10 or the loop never ends.

There are other ways of making this loop run ten times. You could, for instance, do any
of the following, all with the same results:

initialize loopCounter =1 and run until <= 10 (as illustrated)


initialize loopCounter =1 and run until < 11 (less than)
initialize loopCounter = 0 and run until <= 9 (less than or equal to)

As the while-loop executes, it processes the statements within the braces.


When it reaches the closing brace, it loops back to the top and examines the
condition to see if it should continue.

Appending a String:

The example prints the word "hi" ten times in textBox1, one after the other. To
accomplish this, the code relied on a somewhat oddly-worded programming trick:

textBox1.Text = textBox1.Text + "hi";

The statement says:

• "Take what ever is currently in textBox1 and add the word 'hi'". With strings,
'add' means append or concatenate.

• When the loop first starts, textBox1 is empty so the first iteration concatenates
"hi" to an empty box.

• On the loop's second iteration, the box contain the first loop's "hi", then another
copy is concatenated. Remember, "take what ever is in textBox1 and add a new
"hi" – putting the results back into textBox1.

• On the loop's second run, textBox1 will contain "hihi".

After ten iterations, the loop will have appended ten "hi"'s, giving: "hihihihihihihihihihi".

Chapter 2 - Loops Page: 55


You can make this nonsensical output easier to read by inserting a space at the end of the
literal "hi", resulting in "hi hi hi hi...".

Incrementing a Counter:

The loopCounter statement uses the same trick, except with a numeric value:

loopCounter = loopCounter + 1;

The counter was initialized at 1 and then, on the first iteration, a "+1" is added the
current value (one), giving a total 2. On the next loop, the current value equals two plus
one, for three. The statement says, "take what ever the current value is, add one, and put
the results back into the current value." Mentally you can follow the variable's progress
through the loop.

loopCounter equals 1 as you enter the loop


"hi " prints for the first time
then the loopCounter, which still equals 1, is incremented: 1 + 1 = 2
Loop the next iteration

At Loop #2,
"hi " + "hi " is printed
loopCounter = 2; add + 1, for a total of 3
Print "hi hi " + "hi "

At Loop #3,
loopCounter = 3; add 1 (loopCounter = loopCounter + 1) = 4
"hi hi hi " + "hi " ... etc.

The syntax is admittedly strange.

What Would Happen if...:

= textBox1.text + "hi " may seem like unnecessary gibberish. Why not use a more
straight-forward statement, such as: textBox1.Text = "hi"; Try this now.

With this change, the computer still prints the word "hi" 10 times, but each iteration
replaces the previous – it did not append or concatenate. The computer takes the
instruction literally and the revised command says to 'Put "hi" in textBox1.' This runs
ten times with each "hi" replacing the earlier ones. There would be no consideration on
the previous contents and all ten copies happen in a millisecond – too fast to see. The
end result would be a single "hi" in the box, the tenth iteration.

Chapter 2 - Loops Page: 56


More importantly, if the loopCounter were written in the same way (e.g.
"loopCounter = +1"), it would have the same problem. Each iteration would put a "+1"
into the counter and it would never grow beyond 1 – no matter how many times the loop
ran. Since it would never reach 10, it would be an infinite loop (don't do this yet; you
can practice an infinite loop in a few pages).

Ending the Loop:

Here is the key to the loop: Ultimately, the loop enters its 10th iteration - where it prints
"hi" a 10th time. The counter increments to 11 and wraps around to the top, where the
"while" condition again checks the current value. Since it is now at 11, the loop ends
and program control falls to the statement after the closing brace.

Incrementing with "++":

The traditional way add +1 to a variable is:


loopCounter = loopCounter + 1;

however, most C# programmers use a syntactically-shorter version that performs the


same task and mentally reads the same way as the longer version.

++loopCounter; or
loopCounter++;

Using either a "++" in front or behind the variable accomplishes the same goal.
(Technically, if the ++ is in front, it adds one to the variable before using the variable in
other inline calculations. If placed behind++, it adds one after using the value in the
other calculations. This is subtle and in practice, there is little difference between the

Chapter 2 - Loops Page: 57


two, especially if the statement is a stand-alone statement on a single line of code. Most
programmers use the trailing++ version.)

This new syntax will cause problems if both styles are mixed on the same line. For
example, this statement is an infinite loop:

loopCounter = loopCounter++; //Do not do this

Instead:
loopcounter++; or
loopCounter = loopCounter + 1;

As an unimportant, trivial fact, in the bad example, the existing loopCounter++ (value
equals "1") is assigned to loopCounter before the +1 is added. Flipping the order of the
"++" to front of the statement would work properly, although this is non-standard and not
recommended.

Counting by two's:

"++" is the recommended method because this is the style most commonly used and is
slightly more efficient under the hood. But the older style is still needed. Consider a
loop that counts by twos: 2, 4, 6, 8, 10. The "++" method does not work and you must
use the longer style. This grows the counter by two, printing "hi" five times:

private void button1_Click(object sender, EventArgs e)


{
int loopCounter;
loopCounter = 2;

while (loopCounter <= 10)


{
textBox1.Text = textBox1.Text + "hi";
loopCounter = loopCounter + 2;
}

MessageBox.Show("Done");
}

Concatenating to Self with "+=":

The statement that appended "hi " to the existing textBox can also be abbreviated. Since
adding something to itself is an amazingly common operation, C# has a more concise
syntax that is favored by most experienced programmers.

Instead of
textBox1.Text = textBox1.Text + "hi ";

use this syntax, with a plus-equals:


textBox1.Text += "hi ";

Chapter 2 - Loops Page: 58


Which basically says "append the quoted string back over the top of the previous value."
Both statements accomplish the same goal.

The Counter's Position:

The position of the loopCounter-increment is important. In a "while" loop, it is always


placed near the end of the loop, usually directly above the loop's closing brace. If the
loop were incremented at the top of the loop, "loopCounter" jumps to 2 before it had a
chance to do any work with the previous value.

while (loopCounter <= 10)


{
<do stuff here>
<do more stuff>

loopCounter++; //increment at the end


}

Loop Increment Position

Common Mistakes when Typing while-loops:

As you study the program above, it probably looks simple, but I've found most beginning
programmers manage to get the brace-positions confused. If a closing brace is missing
or mis-placed, the compiler displays all kinds of strange and hard-to-figure-out error
messages.

You may have typed a semi-colon after the while statement, resulting in a compiler error,
"Possible mistaken empty statement".

Chapter 2 - Loops Page: 59


while (loopCounter <= 10) ; <Bad semicolon
{
:

Consider this loop problem, where the intent is to loop until strfoundString contains
some value other than an empty or null string (e.g. loop until string is empty). What is
wrong with this statement?

while (strfoundString = "")


{

with a compiler error: "Cannot implicitly convert type 'string' to 'bool'?

Answer: A single equal-sign is an assignment; use double-equals (==) in if-


statements: while (strfoundString == "").

Other Bad Code Examples:

Examine this code for a moment (or try it yourself).


Note how the MessageBox statement is outside of the button1_Click routine:

If code lives outside of the boundaries of it's routine, the compiler panics with a bizarre
"invalid token '(' in class, struct, or interface member" error. The highlighted line, which
has perfectly good syntax, will continue to complain as long as it is mis-positioned.

Chapter 2 - Loops Page: 60


Try this: What happens if the MessageBox statement is placed at the bottom of the
while-loop, just above the while-loop's closing brace, but still within the
module's closing brace?

Horrible Indenting Example:

In classes I have taught, beginning students don't always keep their braces typed in a
logical order and they don't always indent properly. Examine this code and decide if the
routine will work. The last two closing braces correctly belong to class and namespace
definitions higher-up in the program.

This routine works but it is difficult for humans to interpret. Indentation matters.

Chapter 2 - Loops Page: 61


Diagnostic MessageBoxes:

The computer can help you understand how a loop increments. Looking below at
Program 2.1b, line #10, add the new MessageBox statement. By inserting a second
"MessageBox.Show", you get what I like to call a "diagnostic MessageBox" (do not type
the line numbers).

Program 2.1b - Looping 1-10, modified


1 private void button1_Click(object sender, EventArgs e)
2 {
3 int loopCounter;
4 loopCounter=1;
5
6 while (loopCounter <= 10)
7 {
8 textBox1.Text = textBox1.Text + "hi ";
9
10 MessageBox.Show("Loop Counter = " +
Convert.ToString(loopCounter));
11
12 loopCounter = loopCounter + 1;
13 }
14 MessageBox.Show("Done");
15 }

You can type the statement as one line of code:

MessageBox.Show("Loop Counter = " + Convert.ToString(loopCounter));

or as one statement, typed on two physical lines, for cosmetic or style reasons:

MessageBox.Show
("Loop Counter = " + Convert.ToString(loopCounter));

or as:

MessageBox.Show
("Loop Counter =
" + Convert.ToString(loopCounter));

C# does not care about "white space." The closing semi-colon marks the real end of the
statement. However, you cannot break a quoted (literal) string in this fashion.

VB programmers will recall the _underscore was the line-


continuation character; C# does not need a line-continuation –
but does need a line-ending character (";")

Depending where you put the MessageBox, either above the increment or below, you'll
get differing results on loopCounter's status. For example, the MessageBox at line 10
shows the loopCounter at 10 just as the loop ends, but if the MessageBox were moved to
line 12a it would show 11. Press F5 to try it out.

Chapter 2 - Loops Page: 62


More on the MessageBox:

As you have probably surmised, MessageBox displays a simple dialogue box. The
example contains two MessageBoxes: One shows what the loopCounter equals inside
the loop and the second announces "Done."

The syntax of the MessageBox command should be evident. You might consider
"Messagebox" as a 'keyword' but that isn't strictly true. Amazingly, C# has only a few
dozen reserved keywords and MessageBox isn't one of them. In reality,"MessageBox" is
a "class" (of instructions) and ".Show" is one of the "methods" available to the class. As
you learn more about C#, these distinctions will become clear.

While testing and debugging code, I often use MessageBoxes to


peek inside of variables. It is a quick way to pop something up
on the screen and it doesn't require fields or labels. Later, more
powerful debugging tools are discussed.

Editing Concerns:

C# does not allow editing while the program is running and you will see problems,
especially if a MessageBox is waiting for you to click "OK". Run the example program
from above, but leave the "Done" Messagebox on the screen. Return to the editor and
attempt to change the program. The editor displays a clear message, "Changes are not
allowed while code is running" (Visual Studio 2010 and older: "Cannot currently modify
this text in the editor. It is read-only")

In the Form Designer's view, the indicators are more subtle. The ToolBox flyout is
either missing (or empty in VS2008, with a disturbing "there are no useable controls in
this group." ) You will also see a red-square ("stop") on the top toolbar as well as "lock"
symbols on the tabbed list.

Chapter 2 - Loops Page: 63


Stop the program by closing the MessageBox, then clicking the running program's red X,
or by clicking the Stop Button on the tool bar (which is a less-than-graceful hard-stop).

Chapter 2 - Loops Page: 64


Infinite Loops

No discussion about loops is complete without a glance at "infinite loops." Technically


this is a loop that never reaches a "false" condition. Or to word it differently, the while-
loop is always "true" and thus never ends.

Infinite loops are surprisingly easy to write, especially by accident, and the most
common problem is when you forget to increment the loop counter. For example, in the
programs above, if you neglected to grow the loopCounter, the variable never reaches 10
and the loop never ends. In this part of the chapter, you will explore how to make an
infinite loop and how to escape from them.

Writing an Infinite Loop:

There is no harm in writing an infinite loop, but it does get weird. Read this section
before starting the loop so you will know what to expect.

Modify Program 2.1b by doing the following:

A. Delete or comment-out (using slash-slash) the loop-increment statement at line 10. You
may have used "loopCounter++":

// loopCounter = loopCounter + 1; or
// loopCounter++;

B. For the first test, add the diagnostic MessageBox at line 10, just before the now-removed
loopCounter increment. This will be used to explore the concept:

:
6 while (loopCounter <= 10)
7 {
8 textBox1.Text = textBox1.Text + "hi ";
9
10 MessageBox.Show("Loop Counter = " +
Convert.ToString(loopCounter));
11
12 // loopCounter = loopCounter + 1;
13 }
:

C. Press F5 to run the program.


Click button1 to begin the loop.

Because the loopCounter was removed, the variable remains at 1 and never "grows."
The program is in an infinite loop, tempered by the MessageBox. You will be pestered
by MessageBoxes. Click OK a few times to get a feel for the loop.

Chapter 2 - Loops Page: 65


Breaking into the Loop:

There are several ways to break the loop. When you 'break" the loop, the editor
highlights the line(s) in the program where it is looping, giving a clue on how to fix the
problem. When breaking into the loop, you are in "debugging mode:" Clicking the tool
bar's "red-square" completely stops the running program and does not enter into the
debugging (break) mode.

Ignore the running program and its diagnostic MessageBox for a moment. Click
anywhere inside of the editor's code view window. This activates the editor, bringing it
to the foreground. You must do this first in order to enter debugging mode.

While the program is still running, and while you have the editor active, do one of the
following:

• Press ctrl-alt-Break (the keyboard's PAUSE key); this is not a "Ctrl-Break".

• Alternately, click the toolbar's Pause button (illustrated).

• Alternately, click inside of the editor's workspace (again, ignoring the looping
program). From the top menu, select Debug, "Break All".

Chapter 2 - Loops Page: 66


When the program breaks, the editor highlights where the program's current command
was running – in this case, it is the diagnostic MessageBox statement, but in a real
infinite loop it would probably be the "while statement." Notice your program (Form1)
is still running on the task bar. See below for additional instructions.

Diagnostics in Break Mode:

While in a debugging mode (break), hover the mouse over any variable name (especially
loopCounter) to see its current value. In this example, notice how loopCounter is always
"1" – even though you may have been through the loop a thousand times. This is the
very definition of an infinite loop.

Once in a break/debugging mode, press the toolbar's green "Continue" arrow to jump
back into the program, as-if the break hadn't occurred (in this case it would simply loop
around and stop again at the same MessageBox statement).

Ending the Program:

Although the editor and the debugger may have pushed the program into the background,
the program is still running. You will see "Form1" in the Windows task-bar. End the
program using any of the following methods:

Method 1: Bring the running program (taskbar, "Form1") to the foreground by clicking
on the taskbar icon. Once the Form appears, click the close-X. Be aware
this part of the interface can be disabled, so this option is not always

Chapter 2 - Loops Page: 67


available and be aware the MessageBox may prevent you from closing the
underlying program.

Method 2: Click the "red-square" icon on the toolbar.

Method 3: On the taskbar, look for the running program, labeled as "Form1" (not the
Visual Studio icon). Other-mouse-click the "Form1" taskbar button and
select Close. Windows will prompt to "end task".

You will likely be bugged about sending an error report to Microsoft. As humorous as
this sounds, click "Don't Send." See the Appendices (Common Error Messages) for
information on how to disable this prompt.

The Second Infinite Loop Test:

For a second more realistic test, return to the program editor and remove the diagnostic
MessageBox. When you do this, breaking into the program's debug mode is more
challenging.

A. Remove the diagnostic MessageBox, at line 10.


Confirm the loopCounter increment is still deleted or commented.

Chapter 2 - Loops Page: 68


B. Press F5 to run the program again.
Click button1 to start the loop.
This time there will be no dialog boxes to pester you and surprisingly, there won't be any
screen activity.

C. Wait a few seconds and click inside the running program.

Notice there is no screen activity!


Interestingly, the textBox does not fill up with a bunch of "hi"'s.

You can't move the running program's title bar.


The title bar may show "not responding".
Finally, if inclined, Windows task Manager shows CPU utilization at 25 to100%,
depending on the number of CPU's.

• Break the program by clicking inside of the editor and pressing ctrl-alt-Break (the
pause toolbar) and hover over the loop-counter variable, noting it stuck at 1.

• Click the toolbar's red-square (Shift-F5) to stop the program. Do this before the next
editing step.

Why the Display Does not Update:

You may be asking why textBox1 doesn't fill up with an infinite number of "hi"'s? The
computer is so busy looping that it doesn't have time to re-paint the screen.
(After stopping the program), you can force the screen to re-paint with a Refresh
statement:

Chapter 2 - Loops Page: 69


:
while (loopCounter <= 10)
{
textBox1.Text = textBox1.Text + "hi ";
this.Refresh();

// loopCounter = loopCounter + 1;
}
:

where:

• "this" refers to the currently-opened Form and it begins with a lower-case 't'. Visual
Basic and Microsoft Access programmers may recall the similar "Me" prefix.

• Refresh ( )'s parenthesis indicate the "Refresh" is a method of the "this" class.
Parenthesis are always required after a method name.

Try running the program again. Results:

Ultimately the program will crash when the textstring or textBox control exceeds limits.
Windows Task Manager will also report "Program not responding" even if the loop were
still active because it would not respond to other mouse and keyboard events. Ctrl-alt-
Break into, and stop the running program.

You now know how to identify and break out of a loop.

Chapter 2 - Loops Page: 70


"while" Loop - Printing Numbers 1-10

Modify the example program, (Program 2.1), so it prints the numbers 1 through 10
(instead of "hi"). You should have enough knowledge to complete this task and you are
encouraged to try this before reading on.

Important: you must un-do the previous infinite-loop logic from the section
above; returning the program to a normal loop. Also, remove the Refresh
command and other internal diagnostic MessageBox commands.

Steps:

1. Return the program to its original state by removing the diagnostics MessageBox
statement and the refresh line. Return the loop counter to normal.

2. Replace the textBox1 concatenation statement (line 8), replacing:


textBox1.Text = textBox1.Text + "hi ";

with this code:


textBox1.Text = textBox1.Text + Convert.ToString(loopCounter);

This is the statement that prints the numbers 1 through 10.

3. The final code should look like this:

Program 2.2 - while-loop with 1-10, complete with a formatting flaw


1 private void button1_Click(object sender, EventArgs e)
2 {
3 int loopCounter;
4 loopCounter=1;
5
6 while (loopCounter <= 10)
7 {
8 textBox1.Text = textBox1.Text +
Convert.ToString(loopCounter);
9
10 loopCounter = loopCounter + 1;
11 }
12 MessageBox.Show("Done");
13 }

5. Run the program by pressing F5, then clicking on button1.

Results: textBox1 displays "12345678910"

On line 8, append a space after the "Convert.ToString". The space is inserted at each
iteration loop and is part of textBox1 assembly.

Chapter 2 - Loops Page: 71


Comments on the 1-10 while-loop:

This program is no different than the "hi" version. The only minor complexity was
adding the loopCounter to the assembled text as it was moved into the textBox. Numeric
data (loopCounter) should be converted to a string before appending to a text field.

Question - Looping to 1000 - Why so slow?:

What happens if the while-loop changes from 10 to 100? What about 1000? If you have
a moderately old computer, a 2000-iteration loop takes a minute or so to calculate. At
increasingly larger numbers your program may appear to hang or it may appear to be in
an infinite loop. But if you wait long enough (and your upper limit is reasonable), the
program will complete the task.

Change your program to loop 1000 times now. Why so slow, knowing you probably
have a reasonably fast computer and 1000 iterations is nothing to a computer? The
appending statement:
textBox1.Text = textBox1.Text + Convert.ToText(loopCounter);

has to move a lot of text when it runs for a thousand times. All of the text, along with all
of the previous text, has to move in-and-out of the box with each iteration and each time
there is a larger amount of text to move.

And there is another problem. Since textBox1 is a visible field on the screen, C# spends
a lot of energy worrying about the user-interface, fonts, word-wrapping and the like.
There is a lot of behind-the-scenes work and it takes time.

The example can be slowed down even further by inserting "this.Refresh();" near the
bottom of the loop.

Speeding up the loop (in a contrived example):

Running the loop 1,000 times takes a surprising amount of time, even on a modern
computer. This non-sense program can be significantly improved by making a minor
change to the program's logic.

To do this, create a new string variable to house intermediate results. As the loop runs,
append all of the text to this intermediate variable. Since this variable is not part of a
display, the loop runs significantly faster. When the loop finishes, move the final (fully-
assembled) string to its final resting place in "one fell swoop." With this, the user-

Chapter 2 - Loops Page: 72


interface portion (font, word-wrapping, etc) runs only once, instead of 1,000 separate
times.

Make these changes to Program 2.2:

1. Near the top of the routine, at line 4a, declare a new string variable, "tempString" and
initialize it with an empty string, signified by a "" (quote-nospace-quote).

Replace line 8 with a new version:

1 private void button1_Click(object sender, EventArgs e)


2 {
3 int loopCounter;
4 loopCounter=1;
4a string tempString = "";
5
6 while (loopCounter <= 1000)
7 {
8 textBox1.Text = textBox1.Text + //Change these lines
Convert.ToString(loopCounter);
8a tempString = tempString + Convert.ToString(loopCounter) + " ";
9
10 loopCounter = loopCounter + 1;
11 }
12
12a textBox1.Text = tempString;
13 MessageBox.Show("Done");
14 }

The loop now manipulates tempString instead of the textBox. The structure and logic of
the statement remains the same

Notice variables do not have a ".Text" property; they are not "objects."
To compare:

textBox1.text = textBox1.Text + ....


tempString = tempString + ...

2. After the end of the loop's closing brace, at line 12/12a (see code above), move
tempString into textBox1.Text. This is key to the new design. Because of its position
outside of the loop, the move becomes a one-time event.

:
10 loopCounter++;
11 }
12a textBox1.Text = tempString;
13 MessageBox.Show ("done");
14 }

If you have played with the "this.Refresh();" command, remove it.

Chapter 2 - Loops Page: 73


Running the Faster Version:

The revised program reads like this.


Run the program (F5) and click button1 to execute the code.

Program 2.3 - 1000-iteration loop with speed, complete


1 private void button1_Click(object sender, EventArgs e)
2 {
3 int loopCounter;
4 loopCounter = 1;
4a string tempString = "";
5
6 while (loopCounter <= 1000)
7 {
8 tempString = tempString +
Convert.ToString(loopCounter) + " ";
9
10 loopCounter++;
11 }
12
12a textBox1.Text = tempString;
13 MessageBox.Show ("done");
14 }

Results: Lightning fast. 1,000 numbers, lickety-split.

Comments on the faster version:

The new string variable, tempString, was allocated above the loop, at line 4a, and was
initialized as an empty string. Initializing to an empty string is required because the
value must be "seeded" with data prior to the while-loop – even if initialized to "".

Without this, the compiler complains with "Use of unassigned local variable 'tempString'
when it first tries to work with the value in line 8. Previous 'textBox' versions of this
program did not have this problem because textBoxes automatically initialize with empty
strings when created.

It is a good idea to always initialize variables when they are


first declared because it can save headaches and problems
in later code. Pre-initialized variables get their memory
allocated immediately.

Initialize strings with "" (an empty-string); initialize


numbers, usually to zero.

Empty-strings "" are not null - they actually contain a value.

When progressively larger strings were appended to textBox.Text, the program labored
as larger and larger strings were moved in and out. The program had to worry about
wordwrapping and scrollbars. The improved version moves the data to a temporary
variable, tempString and does not concern itself with visual overhead.

Chapter 2 - Loops Page: 74


When the loop ends, textBox1 is still empty. Almost as an afterthought, at line 12a,
tempString is shoveled into textBox1, completing the program's mission.

Although this is a contrived example, it demonstrates a different way of accomplishing


the task. For normal textBox routines, you would never bother with a temp variable; the
performance gain isn't there. But in this example, a minor change in logic reduced the
runtime on my computer from 45 seconds to 1 second.

Multi-Lined textBox and Scrollbars:

You probably noticed the 1000+ loop's results did not fit in the allotted space within the
multi-lined textBox. It needs a scroll-bar.

After closing the program return to the Form Designer by using the top-row of tabs.
Highlight textBox1 (the field) and look at the Properties panel on the lower-right. You
may find from your previous testing the Output tab overlays the Properties tab. The
Output tab can be closed:

In textBox1's Properties pane, find the setting for "Scrollbars." (It is helpful to click the
A-Z sorting button at the top of the pane.) In other words, "Scrollbars" is a property of
text. Select "Vertical".

Chapter 2 - Loops Page: 75


Run the program and confirm the scrollbar is present; all values should display.

Chapter 2 - Loops Page: 76


"do" Loops

Compared to while-loops, "do" loops are anti-climatic. These types of loops are
identical to "while" loops in every respect except one: the conditional is checked at the
end of the loop and this is reflected in the code.

do
{
<code inside of loop>

} while (condition-test);

Why bother? The difference is subtle, but sometimes useful. A do-loop always runs at
least once, then it checks the conditional. Contrast this with a while-loop – it checks the
conditional before attempting the first iteration.

do-loops:
• Always runs at least one time
• Checks the condition at the end of the loop - always running at least once
• loopCounter must be incremented

while-loops:
• May or may not run, depending on the condition
• Checks at the start of the loop, can bail early
• Like a while-loop, the loopCounter must be incremented

do-loops are otherwise identical to a while-loop but the syntax is somewhat unusual and
there is a seemingly oddly-placed semicolon, sitting at the end of the while-clause.
Meanwhile, like all other loop statements, there is no semicolon on the "do" keyword.

In my experience, these types of loops are used infrequently. Since this loop is similar to
the while-loop.

Program 2.3b - 1000-iteration do-loop with speed, complete


1 private void button1_Click(object sender, EventArgs e)
2 {
3 int loopCounter;
4 loopCounter = 1;
5
6 do
7 {
8 textBox1.Text = textBox1.Text +
Convert.ToString(loopCounter) + " ";
9
10 loopCounter++;
1 } while loopCounter <= 1000;
2
3 MessageBox.Show ("done");
4 }

Chapter 2 - Loops Page: 77


for-next Loops

"while" loops are useful because they can run when the total number of loops are
unknown, running until a condition is true; the condition (the count) is usually not
known at the start of the loop. For the illustrations, the while-loops were limited to 10
(or 1000) iterations, but in real life, they would use a variable or perhaps another user-
event to control how many times the loop ran. Commonly, while-loops process input
files while "not End-of-File" (and the end-of-the-file is not exactly known), or "while
string_Input is equal to Smith", etc.

But there are times when you know exactly how many times to run the loop. For
example, you may need to skip across each month in a year, or each character in a string,
examining one character at a time. In both of these examples the number of iterations
are known (12 months in a year, length of the string). This type of loop is called a "for"
loop, but I call them by an older name, "for-next loops." Although you can make a
while-loop do the same thing as a for-next, there are reasons to use this new species of a
loop.

"for-next" loops have these characteristics:

• Total number of iterations must be known (unlike most while-loops).


• All loop-control variables are defined on one line.
• No need to "manage" the loop counters within the loop; self-counting.
• Loop condition is checked before the loop runs; the content may or may not run,
depending on the comparison. Similar to a while-loop.
• Concise, tight syntax.

Probably the best reason to use for-next loops is it keeps track of its own counters and
variables – all within one line of code. Since the controls are in one place, the loops are
often easier to read and interpret than other types of loops.

Even though C# doesn't use the keyword "next," I still call the loop a
"for-next" out of habit, from other programming languages. Basic
used the word "next" to close the loop. Calling the loop a for-next is
easier and more descriptive to read in a book because the word
"for" by itself is odd.

Basic for-next loops:

Using the same program from other examples in this chapter, follow these steps, with
new logic attached to button1. The program will print the numbers one through ten,
displaying the results in the multi-lined textbox, textBox1.

1. From either the form's design view, double-clicking button1, or from code view, locate
the button1_Click event and remove all statements between the event's opening and
closing braces, leaving an empty event:

Chapter 2 - Loops Page: 79


private void button1_Click (object sender, EventArgs e)
{

2. In the button1_Click event, write these statements. Line 5 is the heart of the loop and the
details are described in a moment:

Program 2.4 - Print the numbers 1-10 with for-next, complete


1 private void button1_Click(object sender, EventArgs e)
2 {
3 int loopCounter = 0;
4
5 for (loopCounter = 1; loopCounter <= 10; loopCounter++)
6 {
7 textBox1.Text = textBox1.Text +
Convert.ToString(loopCounter) + " ";
8 }
9
10 MessageBox.Show ("done");
11 }

3. Run the program with an F5.


Press button1

Results: textBox1 contains: "1 2 3 4 ... 10", where each loopCounter is converted to a
text-string and then appended to textBox1.

for-next Structure and Design:

for-next loops are composed of three separate phrases and each phrase handles either the
beginning, middle or end of the loop.

Chapter 2 - Loops Page: 80


where:

• The first phrase is the starting value of the loop - initializing the loop's first value, in
this case, "1". After this, the phrase is ignored for the remainder of the loop.

• The second phrase is the conditional - run the loop while the counter's condition is
true. This condition is checked *before* each iteration.

• The third phrase, at the end of the statement, increments the loop's counter.
Although this is the third phrase in the loop, this part of the statement takes effect at
the bottom of the loop, just before the condition is checked.

• For the increment, you can use either of these three styles and the differences are
immaterial:

loopCounter++ (recommended)
++loopCounter
loopCounter = loopCounter + 1

• Each phrase is separated from the other with an internal semicolon. And, like a
"while" loop, there is not a closing semi-colon at the end of the statement.

Declaring and Initializing at the Same Time:

The variable that controls the loop must be declared and initialized before the loop
begins. On line 3, loopCounter is declared as an integer, "int loopCounter;" and out of
habit I typically, and optionally, initialize the variable with a value, "int loopCounter =
0".

At line 5 phrase-1, the value is (re-) initialized to 1, as in:

for (loopCounter = 1; loopCounter <= 10; loopCounter++)

The variable's declaration and initialization can be combined into one by declaring the
integer and assigning an initial value with the first clause of the for-next loop. Because
this integer is only used for the loop, most programmers prefer this design:

The resulting statement at line 5:

for (int loopCounter = 1; loopCounter <= 10; loopCounter++)

Chapter 2 - Loops Page: 81


If you declare within the loop, you must remove the statement at line 3 or
the two declarations will collide with a compiler error.

loopCounter as a Throw-Away Variable:

Because loopCounter exclusively controls the for-next loop, it is disposable. Usually the
variable isn't needed anywhere else, and for that reason it does not even warrant a formal
declaration or even a legitimate name. Being so trivial, many programmers simply use
the letter "i" for the variable name (this goes back to the Fortran Programming language
in the 1960's).

For the sake of these illustrations, a descriptive name "loopControl" is being used (or as
you will see in future chapters, "iloopCounter" where the "i" means "integer"), but even
the author often uses a variable simply named "i". It is a sad commentary on the laziness
of programmers. The loop is most-often written like this:

for (int i = 1; i <= 10; i++)

How to Read a for-next Loop:

Reading a for-next loops takes practice. Here are some English hints.

In the old days (QuickBasic, VB), this loop was typically read as "for eye equals one to
10, step one" but that didn't really explain the loop to the un-initiated. It would be more
precise to say it like this: "Start eye at one and count forward by one. Do this 'while' eye
is less than or equal to 10. If eye climbs above 10, stop the loop." This type of verbiage
emphasizes the fact that a "while" loop and a "for-next" loop essentially operate in the
same way because both use "while" in their definition.

Incrementing a for-next loop:

The details inside the loop's opening and closing braces are nearly the same as the while-
loop examples – with one important exception: there is not a loop-incrementing
statement in the loop's details.

Chapter 2 - Loops Page: 82


In a "while" loop, a incrementing statement was absolutely required (if not, think infinite
loop). But for-next loops handle this automatically with the third-phrase of the
statement. The increment lives in the loop's definition - not in the loop's details.

How a for-next loop works:

When the loop is first encountered, C# initializes the loopCounter variable as shown in
the first part of the phrase. Next, it checks the conditional to make sure it has the
authority to run the loop – is the loopCounter less than 10? If the condition allows, the
code within the opening and closing brace runs.

For most for-next loops, the count almost always starts with
"1" and loops until some other value. But values other than
one could be variablized, meaning the loop could start at
"9" or "100". The for-next loop still checks for permission
before running.

When each iteration, the loop reaches the closing brace at line 8; the counter increments
by one; using the third phrase and then it loops back to the top and checks the condition.
Consider the closing brace as the "next" part of the loop. Ultimately loopCounter
reaches the cut-off point and the loop ends with the next statement after the closing brace
as the next command.

Semi-Colon Errors:

Out of habit, it is easy to add an errant semicolon at the end of the for-next statement, as
in:

The compiler will complain: "Possibly mistaken empty statement." Delete the semi-
colon.

Using a Carriage-Return/LineFeed (CRLF):

The examples in this chapter have printed their results in a horizontal list. With a minor
change, the numbers can display vertically by introducing a carriage-return/linefeed:

Chapter 2 - Loops Page: 83


The previous examples appended a space (e.g. " ") as each new value was written:
textBox1.Text = textBox1.Text + Convert.ToString(loopCounter) + " ";

Change the space to CarriageReturn/LineFeed with these characters:


+ "\r\n"

where the string "backslash-r, backslash-n" represents two ASCII character codes,
CHR$(10) + CHR$(13). (ASCII being a "character set" which includes the letters A-Z,
a-z, numbers, tabs and other special characters).

In C#, a \backslash indicates a "special character" (reserved, usually for non-viewable


characters). Thus, a "\r" is a "return - or carriage return" and "\n" is a new-line
(linefeed). "\r\n" is often called CRLF. This harkens back to manual typewriters and
mechanical line printers.

The completed code:


Program 2.45 -Print the numbers 1-10 vertically, complete
1 private void button1_Click(object sender, EventArgs e)
2 {
3 int loopCounter;
4
5 for (loopCounter = 1; loopCounter <= 10; loopCounter++)
6 {
7 textBox1.Text = textBox1.Text +
Convert.ToString(loopCounter) + "\r\n";
8 }
9
10 MessageBox.Show ("done");
11 }

There is more about special characters in future chapters.

Chapter 2 - Loops Page: 84


Vertical Scrollbars:

As before, the resulting list may not fit in the space allotted in textBox1. If the textBox
did not have a vertical scroll bar, users can still place the cursor in the box and arrow
down to reveal the remaining numbers - but without a scrollbar, users have no indication
this is possible. If you have not already done so, close the program and return to design
view and set textBox1's ScrollBar property to "Vertical."

Exercise: Use a for-next Loop to Add the Numbers 1 to 100:

Write a for-next loop that adds all the numbers between 1 and 100. Attach the logic to
button1. Display the results in a MessageBox or in textBox1. The answer is calculated
as: 1 + 2 + 3 + 4...+ 100 = 5050. A for-next loop is being used because the count is
known.

A new, intermediate variable is needed to hold the results of the calculation. Although
any variable name will do, one named grandTotal seems descriptive. Attempt to write
the code now, but as a hint, the loop's logic will work like this:

The loop starts out with a grandTotal of zero.

- On iteration 1, the grandTotal is the previous total, zero, plus 1

- At iteration 2, the grandTotal is the previous grandTotal plus the value of the
loopCounter (2), giving a total = 3

- On loop number 3, the new total is the previous grandTotal, 3 plus the value of the
loopCounter, 3; 3 + loopCounter=3 = 6.

Before reading further, attempt writing the routine now. You may be surprised to learn
the interior of the loop is one line long. Here is the solution:

Chapter 2 - Loops Page: 85


Exercise: Adding the Numbers 1 thru 100 using a for-next loop, complete
1 private void button1_Click (object sender, EventArgs e)
2 {
3 int grandTotal = 0;
4
5 for (int loopCounter = 1; loopCounter <= 100; loopCounter++)
6 {
7 grandTotal = grandTotal + loopCounter;
8 }
9
10 MessageBox.Show
("The Total value is: " + Convert.ToString(grandTotal));
1 }

where:

• Above the loop, a variable "grandTotal" is declared and initialized to zero.


Initializing the grandTotal to zero is important because a value must pre-exist before
it can be used on the right-side of the equal-sign in statement 7.

• The integer loopCounter is declared and initialized as a throw-away variable within


the loop's definition, line 5. It exists and is only needed for the duration of the loop.

• With each iteration, the current loopCounter's value is added to grandTotal and the
results are put back into grandTotal. This is the same type of formula as i = i + 1.
The only difference is a loopCounter-value is being added instead of a simple
number 1.

• The MessageBox shows the results *after* the end of the loop because it is after the
loop's closing brace. No details are being written in TextBox1.

Exercise: What is the result if looped 1,000 times? 10,000 times? This is
mathematically interesting and you are encouraged to try. There is a simpler
mathematical formula to calculate this, but with the computer, we can use brute force.

Exercise: Write this same loop using a while-statement.

Chapter 2 - Loops Page: 86


Variations on "for-next" Loops

for-next loops are interesting because they can have different starting values. You can
start the loop below zero or it can start at an intermediate number. The loop can grow in
increments other than one and it can run backwards, counting from high numbers to low.

Granted, "while" and "do" loops can also do this, but a for-next loop does this with
particular grace and it is self-documenting, looking at one line of code. Below are some
examples on the different ways to start a for-next loop.

Starting at a number other than 1:

There is no law that says you have to start a for-next loop at 1. They can start at zero, a
negative number, or any other number. "Arrays" often need to start at positions other
than one.

Consider this version of a for-next loop: It starts at 5 and results in "5 6 7 8 9 10":

for (int iloopCounter = 5; iLoopCounter <= 10; iloopCounter++)


{
textBox1.Text = textBox1.Text +
Convert.ToString(iloopCounter) + "\r\n";
}

Incrementing by a number other than 1:

You also don't have to increment by one. Consider this snippet which results in this
string: "2 4 6 8 10" and uses a variable "i":

for (int i = 2; i <= 10; i = i + 2)


{
textBox1.Text = textBox1.Text +
Convert.ToString(i) + "\r\n";
}

where:

• the increment is by twos. Because of this you must use the older-styled increment
"i = i + 2" instead of "i++".

Exercise: Loop from 1 to 50, stepping by 5.

Looping Backwards from 10 to 1:

for-next loops can start at a higher number and run backwards. When this is done, the
conditional flips from a normal "while less-than or equal-to" to "greater-than or equal-to"

Chapter 2 - Loops Page: 87


and the increment becomes a decrement. The results are a count-down from
"5, 4, 3, 2, 1, 0". This type of loop is often used by NASA.

for (int i = 5; i >= 0; i = i - 1)


{
MessageBox.Show ("Tee-minus " + Convert.ToString(i));
}

MessageBox.Show ("Blastoff!");

The decrement, "i = i - 1", could have been written as:

i--; (eye minus-minus)

If your code ran – but displayed a single Tee-minus and then jumped immediately to
blastoff, then the conditional was set incorrectly; it was probably set to "while i >= 5".

//Incorrect countdown timer


for (int i = 5; i >= 5, i--)
{
:

With this improperly-set conditional, i starts at 5 and since "5" is greater-than-or-equal-to


"5", it does not meet the condition and the loop stops prematurely, having not run a
single iteration.

Study the correct line for a moment. "Start eye at 5 and count backwards by 1. Do this
'while' eye is greater than or equal to zero. When eye falls below zero, stop the loop."

for (int i = 5; i >= 0; i--)


{

The other common way to incorrectly write this loop is to write the conditional as
"while i <= 0" where the greater-than was written as a less-than. This results in an
instant "blastoff," with no countdown, and it always surprises the launch crew.

Starting too High:

There is this possibility: What if your starting value is greater than the conditional? In
other words, what if "i" is initialized at 5,000; in this case none of the code within the
loop runs.

for (i=5000; i <= 10; i++)


{
textBox1.Text = textBox1.Text +
Convert.ToString(i) + "\r\n";
}

Results: The loop "runs," but does not execute because the loop-counter failed the "less
than or equal to 10". Naturally, in the real world, the 5,000 would not be hard-coded in
the loop. Instead, it would be a variable set by another routine.

Chapter 2 - Loops Page: 88


Controlling Loops with Variable textBoxes

In previous examples the loops were controlled with hard-coded values (i <= 10, etc.).
Now, using skills from Chapter 1's textBoxes, use a typed value to change the loop's
iterations based on a variable. For these next examples, use textBox2 to control the for-
next loop. For example, type "15" in textBox2 and loop 15 times.

Converting Strings to Integers:

By definition, textBoxes are strings and their values cannot be used in numeric
calculations or comparisons until converted to a number. You have seen the opposite,
where numbers are converted to strings using: "Convert.ToString(loopCounter)"

There is a converse command called "Convert.ToInt32".

Exercise:

Modify the example program, exchanging the hard-coded <= 10 with a converted
textBox2.Text. Detailed steps are documented next.

Controlling a Loop with a textBox Entry:

1. Using Program 2.2, and others in this chapter as a model (counting 1 to 10), modify
button1's logic so it uses textBox2 to control the duration of the for-next loop. Write the
results with CRLF's after each printed number. Here is the final code:

Chapter 2 - Loops Page: 89


Program 2.6 - Controlling loops With textBox Controls, initial
1 private void button1_Click(object sender, EventArgs e)
2 {
3 int loopCounter;
4
5 for (loopCounter = 1;
loopCounter <= Convert.ToInt32(textBox2.Text);
loopCounter++)
6 {
7 textBox1.Text = textBox1.Text +
Convert.ToString(loopCounter) + "\r\n";
8 }
9
10 MessageBox.Show ("done");
11 }

When testing, type only numeric values in textBox2; audit logic has not been written and
the program will crash if otherwise. If you find you cannot type a value into textBox2,
check the "Enabled" property, confirming it is set to "True" (set in an example from
Chapter 1).

where:

• The for-next loop's conditional uses a new term at line 5: "Convert.ToInt32". This
converts textBox2 from a dot-text string to a 32-bit integer – making the string
suitable for numeric calculations. An audit should be written here, intercepting
invalid numbers. This is covered in later chapters.

• Because of the length of the new statement, the for-next loop was written on three
separate lines, making for easy-to-read code. This is considered good programming
style.

Chapter 2 - Loops Page: 90


Possible Error - The infamous forgotten ".Text":

As you keyed the phrase "Convert.ToInt32(textBox2.Text);", what would happen if


you forgot to type the 'dot-Text' and then tried to run the program?

The program would crash with a runtime error, generating this difficult error message:
"System.InvalidCastException: Unable to cast object of type
'System.Windows.Forms.TextBox' to type 'System.IConvertible'". Press the tool-bar's
red-square to end the program and return to the editor.

You will see a similar message if textBox2 is blank: "System.FormatException: 'Input


string was not in the correct format.'

Exercises: Running the Program with a variety of inputs:

Run the program and answer the following questions.

1. Run the program (F5)


Type a reasonable numeric value in textBox2
Click button1.
(Results: The program runs as expected.)

2. Try a negative number (e.g. -15) in textBox2 and click button1.

(Results: The program runs but no new results are displayed (previous run results
remain unchanged. Note that the previous results were not erased; this will be fixed in a
moment.)

Question:
Why doesn't the loop run? (It is important that you understand the reason for this. The
answer: Looking at the 'while' clause – "is 1 less-than-or-equal-to negative 15?" No, the
first iteration, 1, is greater than the negative number – the loop does not have permission
to run.)

Chapter 2 - Loops Page: 91


3. Type a non-numeric value, such as "dog" in textBox2 or leave textBox2 blank.
(Results: A crash. Routines to intercept this are covered in the future. For now, note
how the compiler helps or hinders in debugging the problem. Click the ribbon's red-
square to return.)

Exercise: Clearing TextBox1 with Each Run:

Modify the code so each time button1 is pressed, textBox1 starts with an empty box and
the previous results are erased. Try this now. Hint: Can you move anything to clear or
erase textBox1.Text prior to the loop?

Solution: At line 3a, add this statement, where quote-quote represents a null string:
textBox1.Text = "";

or better yet, use this method, specifically designed for the task:

textBox1.Clear();

or use the performance-enhancing technique of using a temporary string variable instead


of writing directly to the text box (see line 3a and 7).

Using an intermediate string variable to clear the previous run, complete


1 private void button1_Click(object sender, EventArgs e)
2 {
3 int loopCounter;
3a string tempString = "";
4
5 for (loopCounter = 1;
loopCounter <= Convert.ToInt32(textBox2.Text);
loopCounter++)
6 {
7 tempString = tempString +
Convert.ToString(loopCounter) + "\r\n";
8 }
9
9a //Notice how this clears the previous test-runs and
9b //acts as a performance-enhancement
9c textBox1.Text = tempString;
10 MessageBox.Show ("done");
11 }

Why does this technique work without using a .Clear( )?


Answer: line 9c completely replaces textBox1.Text.

Chapter 2 - Loops Page: 92


Interrupting Loops

Sometimes, while running a loop, you may need to skip that iteration's processing and
jump to the next item. For example, while looping through payroll records, you may
discover a record or condition that shouldn't be processed due to a data-entry error or
some other flag. Perhaps certain account-codes should be ignored or transactions prior
to a certain date are bypassed. The need for this happens often enough that C# provides
two loop controls just for this purpose:

continue; The current loop iteration immediately ends and starts with the next loop
cycle. When "continue;" is reached, all remaining lines in the loop's details
are skipped and control is passed to the top of the loop. Usually, a continue-
statement is used when a record has some kind of error or exception, but the
error is not serious enough to stop the program. Use with care in a while-
loop.

break; Causes the entire loop to immediately end and control is passed to the next
statement after the loop's closing brace. This does not end the program or
the current routine, only the loop. Usually break statements are used when a
catastrophic data-error is discovered and all processing of a (file) should be
immediately stopped.

Use these statements in any type of loop, including "while", for-next, and do-loops - but
in my experience I most often see them in for-next loops.

'continue' works especially well in a for-next loop but use with great care in
a while-loop. Details later in this section.

Decisions like these always use an "if-statement" somewhere in the interior of the loop to
make a decision. Although "if-statements" are covered in the next chapter, you should
be able to follow these examples.

"continue" Example:

This example uses program 2.4 as a base. The goal is to loop 1 through 20, but skip
items 7 and 13. And just to demonstrate the break command, stop all processing at item
15. This is a contrived example.

Program 2.4, repeated here


private void button1_Click(object sender, EventArgs e)
{
for (int iloopCounter = 1; iloopCounter <= 20; iloopCounter++)
{
textBox1.Text = textBox1.Text +
Convert.ToString(iloopCounter) + "\r\n";
}
}

Chapter 2 - Loops Page: 93


Continue statements are almost always used near an if-statement. The clause needs to be
placed high enough in the loop so it skips the work being done. The "if" syntax is:

if (iloopCounter == 7)
continue;

The next chapter discusses if-statements in more detail but here are the important points:

• if-statements use double-equals (= =) for the comparison.


• The condition you are checking is enclosed in parenthesis
• Like many of the loop commands, it does not end with a closing semicolon.

Program 2.7 - Continue-Statements


private void button1_Click(object sender, EventArgs e)
{
for (int iloopCounter = 1; iloopCounter <= 20; iloopCounter++)
{
if (iloopCounter == 7)
continue;

if (iloopCounter == 13)
continue;

textBox1.Text = textBox1.Text +
Convert.ToString(iloopCounter) + "\r\n";
}
}

In this example, I prefaced the variable "loopCounter" with the


letter "i" (iloopCounter), acting as a visual reminder this is an
integer. The prefix "i" for integer, "str" for string, "lbl" for
label, etc., is called Hungarian Notation and I will be using this
notation in all of the later examples to help you understand the
variable's "type." Understand that Microsoft, and many shops,
do not condone this type of variable naming.

Test the "continue" Statements:

1. Press F5 to run the new program.


Click on button1.

Confirm that the output shows all numbers, 1 - 20, skipping 7 and 13.

Chapter 2 - Loops Page: 94


break Statements:

Now add the "break" statement that ends the loop pre-maturely at 15. (The loop should
have been written "while iloopCounter < 15" instead of using obtuse logic like this
example.)

Program 2.71 - break-Statements


private void button1_Click(object sender, EventArgs e)
{
for (int iloopCounter = 1; iloopCounter <= 20; iloopCounter++)
{
if (iloopCounter == 7)
continue;

if (iloopCounter == 13)
continue;

if (iloopCounter == 15)
break; //Numbers 16-20 never run

textBox1.Text = textBox1.Text +
Convert.ToString(iloopCounter) + "\r\n";
}
}

Test the "break" Statements:

Test the program by pressing F5 and clicking button1.


Confirm 7 and 13 are still missing
Confirm 14 is the last number printed.

Chapter 2 - Loops Page: 95


comments:

• Having two if-statements, with two separate 'continue' clauses is inefficient and
wordy. The two phrases could be combined into one statement using a double-"OR"
clause. This is described in more detail in the next chapter.

if (iloopCounter == 7 || iloopCounter == 13)


continue;

"while-loops and 'continue':

Using a 'continue' in a while-loop requires more thought. If a 'continue' is called, control


jumps to the end of the loop, directly to the closing brace, bypassing all intermediate
code – including the loopCounter increment. This can put a program into an infinite
loop, especially if the if-statement is using the loopCounter. Consider this code-snippet:

:
int iloopCounter = 0;

while (iloopCounter <= 10)


{
if (iloopCounter == 7)
continue;

textBox1.Text = textBox1.Text +
Convert.ToString(iloopCounter) + "\r\n";

iloopCounter++;
}
:

The 'continue' will jump to the while-statement's closing brace, bypassing the
loopCounter. Because of this, iloopCounter will never grow past 7 and will never reach
the upper-limit, 10.

Real-world examples of this problem will be seen in the string-parsing chapters. To


resolve this issue, more knowledge about if-statements is needed. In summary, the
solution is this:

:
if (iloopCounter == 7)
{
iloopCounter++; //A duplicate increment is needed
continue;
}
:

Variable Scope Concerns:

In a for-next loop, integer declarations can happen above the loop with a stand-alone
"int iloopCounter = 1;" or the variable can be declared and initialized within the for-
next statement using "for (int iloopCounter = 0; ...)".

Chapter 2 - Loops Page: 96


Declaring the variable within the for-next loop has the benefit of being compact and
quick and it has another benefit: Once the loop ends, the variable is discarded, freeing
memory. But what happens if you wanted to use the variable's value later in the
program?

Consider this example, which declares the variable "iloopCounter" above the loop and
then displays the final loop-counter value after the loop completes; note how
"int iloopCounter" is declared above the loop:

Program 2.75 - Variable declared outside of scope


private void button1_Click(object sender, EventArgs e)
{
int iloopCounter;

for (iloopCounter = 1; iloopCounter <= 10; iloopCounter++)


{
textBox1.Text = textBox1.Text +
Convert.ToString(iloopCounter) + "\r\n";
}
MessageBox.Show("The final value of 'i' is: " +
Convert.ToString(iloopCounter);
}

Now change where integer iloopCounter is declared, moving it into the loop definition:

Program 2.76 - Flawed variable out of scope


private void button1_Click(object sender, EventArgs e)
{
int iloopCounter;

for (int iloopCounter = 1; iloopCounter <= 10; iloopCounter)


{
textBox1.Text = textBox1.Text +
Convert.ToString(iloopCounter) + "\r\n";
}
MessageBox.Show("The final value of 'i' is: " +
Convert.ToString(iloopCounter);
}

When running this version, you will get a compiler error, flagging the MessageBox
statement with, "The name 'iloopCounter' does not exist in the current context."

In this case, the integer was declared within the for-next and died when the loop ended.
Any code referencing the loopCounter fails because it is un-declared and un-available.
Similarly, no matter where the variable is declared within button1's Click event, it is
unavailable to any other button or routine elsewhere in the program. Once button1
reaches it's closing brace, all references to variables within are destroyed. In other
words, the "scope" of the variable is limited to the routine that declared it. This topic
will be explored in more detail when Functions and Methods are described.

Chapter 2 - Loops Page: 97


Nested Loops

A loop within a loop is called a "nested loop." This topic may seem esoteric, but nested
loops are often used in array work and when processing files with sub-files or other 2
and 3-dimensional data sources. As scary as they sound (and look), nested loops are
interesting to work with and can do things that no other construct can.

In this section write a routine that generates a simple multiplication table and second
routine that displays an ASCII-character Christmas tree. These examples demonstrate
nested loops and they are purposely simplified in order to explain the concepts.

Building the Example Program:

Construct the example program by taking the same routines you've been working with in
the for-next examples (Program 2.2 or 2.4) and making the following cosmetic changes:

1. "Stretch" Form1 to make it a little taller.


2. "Stretch" textBox1, making it a bit taller; in order to do this, the Multi-line property must
be equal to true.
3. Select textBox1 and set the "Font" Property to "Courier New"

The form should look similar to this illustration:

Multiplication Table with Nested Loops:

The goal of the first nested-loop example is to generate a short 7x5 multiplication table
where

Chapter 2 - Loops Page: 98


row 1 = 1,2,3,4,5
row 2 = 2,4,6,8,10
row 3 = 3,6,9,12,15
etc.

Although not pretty, the on-screen results will look like this:

Consider how this routine is likely built. A normal multiplication table takes a row-
number times a column number. Thus,

row-2, column 3 equals 2 x 3 = 6


row-5, column 4 equals 5 x 4 = 20

This can be exploited with two loops, one dependent on the other. The first loop
controls the number of rows (7) and the second controls the columns (5). The column-
loop will be inside of the row-loop.

Building the Table:

Program 2.8: Multiplication Tables

1. Begin by deleting all previous code in button1's Click event, leaving the event's opening
and closing braces.

2. Since the number of rows and columns are known (7x5), for-next loops are the best
candidates to use with the loops.

Begin with the "row" loop within button1's Click event:

private void button1_Click (object sender, EventArgs e)


{
for (int row = 1; row <= 7; row++)
{

}
}

Chapter 2 - Loops Page: 99


The for-next loop has its own set of braces and the indenting keeps them visually
separate from the button's braces:

where:

• The for-next loop declares and initializes the "row" variable at 1


• The loop will count from 1 to 7
• Increment using row++

Consider the inner Loop:

Each row has five columns. Imagine row #1: while on the row, skip from column 1, to 2,
to 3.... Then, when you are done with the five columns, move to the next row (#2) and
start over with a new column-count, 1, 2, 3....

3. This is accomplished by placing a column loop within a row loop:

private void button1_Click (object sender, EventArgs e)


{
for (int row = 1; row <= 7; row++)
{
for (int col = 1; col <= 5; col++)
{

}
}
}

j For each outside row-loop, the columns run from 1 to 5. This means the interior
column loop is busier than the outside loop. Notice how the column variable, "col",
is re-created / re-initialized to 1 each time the row-loop runs.

Chapter 2 - Loops Page: 100


4. Inside the inner-most (col) loop, insert a statement that calculates the current row-
number times the current column number, printing the result. Be sure the statement fits
between the two nested braces; it will be indented the furthest:

:
for (int row=1; row <= 7; row++)
{
for (int col = 1; col <= 5; col++)
{
textBox1.Text = textBox1.Text +
Convert.ToString (row * col) + " ";
{
:

Notice the appended space (" "), which separates one digit from the next:
1 2 3 4 5

This is testable now, with a flawed result.

5. This leaves one problem: If the program were to run now, all the results would print as
one horizontal line, where each row appends to the previous:

1 2 3 4 5 2 4 6 8 10 3 6 9 12 15 ... etc.

A CarriageReturn/LineFeed is needed after the column-loop, giving

1 2 3 4 5
2 4 6 8 10
3 6 9 12 15 ...

This is a trick but is typical with nested loops. After the column loop completes its
count, 1-5, and before looping back up for the next row, append a CRLF to textBox1 at
the end of the column loop (outside the col-loop, but before the next Row loop):

Chapter 2 - Loops Page: 101


The completed multiplication table is shown here.

Program 2.8 - Multiplication Table, complete


private void button1_Click (object sender, EventArgs e)
{
textBox1.Text = ""; //clear previous run

for (int row = 1; row <= 7; ++row)


{
for (int col = 1; col <= 5; ++col)
{
textBox1.Text = textBox1.Text +
Convert.ToString(row * col) + " ";
}

//Insert a linefeed after each row


textBox1.Text = textBox1.Text + "\r\n";
}

Testing the Multiplication Table:

Press F5 to run the program; then click button1.


Results: A 7x5 multiplication table appears in textBox1.

Chapter 2 - Loops Page: 102


where:

• A loop-within-a-loop gives a graceful way to calculate each cell in the multiplication


table. The entire routine consists of two loops and two statements.

This logic is more useful than in multiplication tables. Arrays and other table work
often uses this type of logic.

• When a row's columns are completed, insert a CarriageReturn/LineFeed (\r\n). In


other types of record processing, this same location usually calculates subtotals or
preps for the next transaction.

• In order to display better, the textBox was set to a non-proportional font (Courier
New). This helps line up the columns. But the first and second rows clearly do not
line up because they are one digit wide. Fixing the spacing issues can't be resolved
with the tools discussed so far, but in summary, any of these methods would work:

a) Use an if-statement on the numeric-size, padding extra spaces


b) Use formatted printing techniques.

These methods will be discussed in future chapters.

Exercise: Change the table to a 10x10 array.

Chapter 2 - Loops Page: 103


Exercise: Nested Loops: A Christmas Tree

As an optional exercise, use a nested loop to print a triangular-shaped Christmas tree.


Granted, this is not a particularly useful but it further explains the nature of a nested
loop. If you understand this example, you'll have a solid grasp on nested loops.

The goal is to build a triangular shape using pound-signs (hash marks). The "tree" has 7
rows with a varying number of columns. The end result will look like this:

Building the Tree:

1. As before, modify button1's Click event by removing previous logic. Be sure to leave
the event's opening and closing braces. textBox1 remains a multi-lined textbox,
formatted with a non-proportional font.

Ultimately, there will be three loops:


The "row" loop controls the number of rows for the tree – rows 1-7.
A second, nested column loop controls how wide that part of the tree is.
The third and final loop is unrelated and it builds the tree-trunk.

2. Following the multiplication table example, add an outside "row" loop with a nested,
inside "col" loop.

On row 1, there is 1 column


On row 2, there are 2 columns
On row 3, there are 3 columns

Chapter 2 - Loops Page: 104


In the multiplication table, the column-loop was set to 5-wide. With this new
requirement, tie the column to the row number. If on row-2 then limit the columns to 2
columns. This is accomplished by replacing the (previously-hard-coded "5" (7x5)) with
the row-variable:

private void button1_Click (object sender, EventArgs e)


{
for (int row = 1; row <= 7; row++)
{
for (int col = 1; col <= row; col++)
{

}
}
}

3. Except for the tree-trunk logic, the rest of the loop remains the same, printing hash-marks
"#" instead of a calculation. As before, at the end of a column loop, print a CRLF.

Use either the traditional appending statement or the newer method:


textBox1.Text = textBox1.Text + "#";
textBox1.Text += "#";

Here is the code for the completed program. Note little changed from the previous
multiplication table example:

Program 2.85 - Nested Christmas Tree, nearly complete


private void button1_Click(object sender, EventArgs e)
{
for (int row = 1; row <= 7; row++)
{
for (int col = 1; col <= row; col++)
{
textBox1.Text = textBox1.Text + "#";
}

textBox1.Text = textBox1.Text + "\r\n"; //line feed


}

// * Trunk logic goes here

Chapter 2 - Loops Page: 105


The loop, explained:

In this code, notice how the column for-next loop is within the row-loop. This means for
each row (1,2,3), the column loop runs multiple times. Without actually running the
program, study the code for a minute and mentally keep track of what happens to the
column variables when the row-count is equal to 1. Then, mentally bump the row-count
to 2 and study the column loop a second time.

The interior column loop is tied to the row-count. When the row is 1, the column count
runs from one-to-one. When the row is 2, the column count runs from 1 to 2. When the
row is 7, the column count runs from 1 to 7.

Additional Comments on the Tree loop:

This is admittedly a strange concept. Here is the logic of the loop; look at the code and
follow along. The code is testable now:

• The row loop cycles from 1 to 7. Note the 7-tree-rows.

• As each row is reached (1, 2, 3...), the col loop runs, cycling through each column
before moving to the next row. This is the key to the program. The number of
columns varies with the row-count.

• At the end of each column-loop, after that row's #'s are printed, append a
CarriageReturn\LineFeed "\r\n". The CRLF's are added at these locations:

Chapter 2 - Loops Page: 106


The Tree-Trunk:

In the code above, after the loop, look for a comment: "\\ Trunk logic goes here".
Add this logic at that location:

Tree-trunk logic, partial code, complete


:
for (int row = 1; row <= 2; row++)
{
textBox1.Text = textBox1.Text + "@@\r\n";
}

This is a normal, non-nested loop that prints the tree trunk. The loop literally prints
"@@" twice, with a CRLF after each print. The "\r\n" can be inserted into an existing
string or appended as a separate string.

The same row-variable is being used because it was no-longer needed in the tree's
construction. In other words, the tree-trunk does not have to begin with row 8 because
this part of the program is unrelated (logically) to the other. If you wanted to start at 8,
then you would have to modify the program to read like this:

for (int row = 8; row <= 10; row++)


{
textBox1.Text = textBox1.Text + "@@\r\n";
}

Because the original nested-loop's row-variable was destroyed when the main tree-loop
ended, this new loop needs to re-declare the "int row" variable. As an aside, you could
have declared a different variable name, such as "trunkCount", making it self-
documenting.

Chapter 2 - Loops Page: 107


foreach Loops

"foreach" loops are specialized loops that work only with arrays and they are
superficially introduced here because of the chapter's topic. Arrays and foreach loops
are covered in more detail in Chapter 22 (Arrays) and in Chapter 13 (Parsing).

If you are new to programming, arrays will be a foreign concept and you can safely skip
this section, but in many respects, these are the simplest of all loops because they handle
all aspects of the loop automatically.

Arrays are a variable that can hold one or more items in a list. You can think of them as
a mini-spreadsheet. For example, an array of names might appear like this, where the
array is named "alistOfNames":

Processing an array with a foreach-loop:

In your test program, add a button2 with this logic:

Program 2.90 - Simple "foreach" loop, complete


private void button2_Click (object sender, EventArgs e)
{
//Declare and populate a string array:
string [] alistOfNames = {"Bob", "Tom", "Tim", "Jane", "Marge"};

foreach (string tempString in alistofNames)


{
MessageBox.Show (tempString);
}
}

where:

• The string array is arbitrarily called "alistofNames" and the compiler knows it is a
string array because of the square-brackets [ ]. I like to preface the variable name
with an "a" to remind me it is an array.

• "string [ ]" declares a string array of an undetermined size. The array will grow to
accommodate the string values inserted into it on the other side of the equal sign. In

Chapter 2 - Loops Page: 108


reality, the array is fixed to the size of the names; don't infer more capabilities into
this statement than what you see.

• On the same line, literal strings, encased in braces, insert data into the array. This is
one of several ways to put data into an array.

• Each item in the array is addressed by a numeric counter, starting at base zero.
For example, alistNames [0] = "Bob", alistNames [3] = "Jane".

"foreach" loops know how many items are in the array and the loop knows how to begin
the loop, increment and when to stop. Within the loop there are no loop-counters or
increment statements.

Because each item in the array has a unique, numbered name, the foreach loop needs to
"variablize" each item, giving it a predictable name that can be used reliably inside the
loop. The "string tempString" statement takes the indexed value and copies it to this
temporary variable.

Arrays and Lists are covered in more detail in Chapter 22 and 13.

Chapter 2 - Loops Page: 109


Loop Summary

I chose to introduce loops early in this book because they are visually interesting – and
they are definitely more entertaining than integer-size and variable-naming discussions.
Admittedly, the examples were somewhat contrived, but I hope this topic captured some
interest in programming. With this foundation you'll soon have your computer doing real
work – looping through files, modifying databases, and a host of other tasks. Loops are
the key.

If you are new to programming, this may have been a challenging chapter, with many
new concepts. Most new programmers struggle with loops and especially nested for-next
loops. The following exercises should help you understand the topic.

while-Loop Summary:

• "while" loops are useful when you don't know how many times to execute. For
example, "while not end-of-file" – where the number of records is undetermined.
The ASCII File Chapter covers this in greater detail.

while (iloopCounter <= 100)


{
//Do stuff here, then increment the loop counter
iloopCounter++;
}

and

while (EndOfFileSwitch == false)


{
//Do loop-stuff here
:
//Then retrieve next record; if end-of-file, set an EOF flag
}

• Do not use a closing semicolon on the "while" statement.

• Loops always require a pair of opening and closing braces.

• Be sure to manually increment the "while" loop's condition, which is often a counter
or some kind of boolean true-false flag. This usually happens at the end of the loop's
details. Think iloopCounter++.

Infinite loops are generally bad. However, faster computers can get through
them faster. Don't forget to increment the counter or set the conditional's
flag.

Chapter 2 - Loops Page: 110


do-Loop Summary:

do
{
<code inside of loop, including...>
iloopCounter++
} while (iloopCounter <= 100);

• "do" loops are exactly like "while" loops except they check their condition *after*
executing at least once. Note they have an unusual closing semicolon on the
conditional.

for-next Loop Summary:

for(int i = 1; i <= 100; i++)


{

• for-next loops are used when the loop count is known or when the loop count can be
pre-determined.

• for-next loops read them like this: "Start <variable i> at 1, loop while <condition> is
true; at the end of the loop, increment the variable." Unlike other types of loops, the
entire loop is self-contained in one line of code - making them succinct and easy to
interpret.

• for-next loops automatically increment themselves when they reach the closing
brace.

• Within the for-next statement, all variables should point to a <temporary> variable;
often called "i".

• Usually the for-next counter-variable is declared and initialized all in one line, within
the for-next loop using for (int i = 1; ...)

• for-next loops can start at values other than 1 and can easily increment in any
number of steps. For example, two-by-two. They can start at high numbers and
work backwards.

foreach Loops:

foreach (string strtempString in arrayName)


{
MessageBox.Show("found value is: " + strtempString);
}

• Only works with arrays, array lists, and List<T>. See the Array chapters for details.

• Note how you must create a temporary holding value and that value is used within
the details of the loop.

Chapter 2 - Loops Page: 111


• All items in the array are processed by this type of loop, even if the array is over-
allocated (see Array chapter).

• You cannot use the tempString to modify the values in the original source array.

Chapter 2 - Loops Page: 112


Exercises
Exercises are meant to be challenging. These can be solved using concepts covered in
this chapter or earlier.

A. Write a while-loop that prints your name 10 times in textBox1, one name per line. Get
your name from a separate textbox and do not hard-code your name.

Hints: You will need a carriage-return/linefeed ("\r\n") and don't forget to increment the
loopCounter.

B. Using the same while-loop in Exercise A, print the number "1.", "2.", etc, in front of your
name.

1. John Smith
2. John Smith
3. John Smith
:

Re-write the same program, again using a while-loop, counting backwards.

10. John Smith


9. John Smith
:

C. Using a for-next loop, write a program that displays the numbers 0 through 10 in a multi-
lined text box. Then write the numbers in reverse order.

D. Write a program, in the style used in this chapter, that counts from 1 to 100.
Use textBox1 to hold the multi-lined results and use textBox2 to control the loop-
increment. Instead of row++, use textBox2 to control the growth of the loop.

In other words, in textBox2 type a "2" then the loop will increment by 2:
0, 2, 4, 6... to 100. Type a 5 and the loop jumps from 0, 5, 10, 15... to 100.
Display all of the numbers in the multi-lined textBox1.

E. Using the Christmas-tree program (program 2.85), make a minor change and print the
triangle-tree up-side-down. Leave the "trunk" on the bottom.

F. Modify the Christmas Tree to print only odd-numbered columns. The first row will have
one "#", the second row will have "###" (3), the third row will have 5 pound-signs, etc.
Keep printing the tree as a triangle.

G. Using a program similar to the Christmas tree (program 2.85) and from Exercise G, build
the tree symmetrically, using 2 for-next loops that control the rows and columns.

Chapter 2 - Loops Page: 113


Use a third for-next loop to control indenting.

The only way to make this tree look presentable is to print only odd-numbered rows "*".
The first row gets 1 character; the second row gets 3 characters, etc. This way the tree
can line up. This can be handled automatically by the row-loop that increments by 2.

The tree-trunk needs to self-center; print it with two rows of "@@@"-signs.

Hint: Use a separate variable to track which row you are on when indenting. Also,
remember, when "printing", the characters on the "next" print line won't go to a new line
unless a carriage-return was used ("\r\n") – sometimes you don't want a crlf.

H. Write Exercise-G in such a way that the number of rows are adjustable with either a
variable or a text box. For example, print a 5-row-high-tree, as in the illustration above,
or set it as a 7-row tree. When this variable changes, the indents should happen
automatically without code changes and the tree-trunk should self-center under the new
tree.

This is a difficult exercise, but everything can be done with simple integer variables and
three for-next loops.

Chapter 2 - Loops Page: 114


Chapter 3 - Conditional Branching

Conditional Branching is a fancy way of saying an "if" statement: "If this is true, then do
that...". The concept was first introduced in the previous loop chapter, where each loop
contained an imbedded if-statement (do while valueA is less than 10), but the nature of
the conditional was glossed over.

Topics:

• Boolean true / false


• Comparison Operators (==, < , >= , !=)
• if-statement construction; braces and semicolon rules
• Required then's; Optional else statements
• Formatting Traps
• && (AND), || (OR), ^ (XOR)
• Nested if's, else if's
• Math.Min, Math.Max
• Compounded if-statements
• switch (Case-logic)
• .ToLower() and .ToUpper()
• goto
• Ternary (? :)

Overview:

if-statement Summary
if (valueA == valueB)
{
//"Then" statements;
}
else
{
//optional "else" statements;
}

if (valueA == 1 && valueB == 15)


{
//and-statement logic here
}

if (valueA ==1 || valueB == 1)


{
//or-statement logic here
}

Chapter 3 - If Statements and Branching Page: 117


switch-statement Summary
string myColor = "Red";

switch (myColor.ToUpper())
{
case "RED":
case "DARK RED":
//statements;
//Notes
// no {braces} in section;
// Colons, not semi-colons, on case-statements
// break; statements are absolutely required
break;

default:
//acting as an else or otherwise; optional section;
break;
}

There are two major conditional branching statements: "if" statements (which I call "if-
then" statements) and "switch" statements (also called "case" or "case-select" in other
languages).

Additionally, time will be spent on the "goto" command, which is a non-conditional


branching statement and the little-used "ternary" command.

Technically a loop is a branching statement but because how


they are used in every-day coding, I categorize them differently.
In any case, they have the same underlying construction:
through some mechanism, look at a condition and decide how to
proceed.

"Boolean" as a data-type:

When speaking about "if" statements and their brethren, you are speaking about
"boolean" terms. A boolean is a data type, like "integer" or "string," that resolves to
either 'true' or 'false'. Declare boolean variables similarly to how other variables are
declared:

bool myBooleanVariable;
bool continueReading = true;

Variables can be initialized with either a 'true' or 'false' (case-sensitive) or leave un-
initialized. Unlike other programming languages, you can not use "Y/N" or "T/F" or
"0/1" - just true/false.

Chapter 3 - If Statements and Branching Page: 118


Numeric and String Booleans

There are obvious and not so obvious ways to generate true or false values in an if-
statement. You are probably familiar with the first group of operators - such as greater-
than, less-than, and not-equal-to, as in "if valueA > valueB then...". There is a second
group of more intimidating operators, &, &&, ||, and "^".

Comparison Operators

Some of these same operators can be used to compare strings - but are more limited in
scope. More details about string comparisons are found in the next chapter.

Chapter 3 - If Statements and Branching Page: 119


Basic if-statement Construction:

When a boolean is paired with an "if-then" statement, you have a conditional branching
command. Mentally, it reads like this: "If this is true then do this stuff; else do that
stuff." You will find if-statements have a somewhat confusing set of rules when it comes
to braces and semicolons and it is further complicated by optional "else clauses," which
act like another set of statements.

When building an if-statement, the 'if' always has a pair of (parenthesis) and in the
parentheses is the conditional-test. For example, if(iValueA > iValueB). Recall
while-loops also used parenthesis in their conditionals. Following the if-statement are
one or more lines of instructions, delimited (or blocked) with a pair of {braces}.

if (iValueA > iValueB)


{

Chapter 3 - If Statements and Branching Page: 120


The if-statement ends at the closing brace and if it is false, logic jumps to this same
closing brace. You can think of the closing brace as the end-if. Visual Basic
programmers will notice there is no "end-if".

The "else" clause is optional and mentally you should treat it as its own separate
statement with its own set of braces.

Many think of if-statements in this fashion: "If this, then, else that" – a very Excel-like
mentality, where the opening brace is the "true" or "then" side.

Alternates:

Alternately, use the "Equals" method as in:

if (testString.Equals("Brown"))
or
if (Equals(testString, "Brown"))

The "Equals" method, which works on strings, integers and a variety of other types, can
be made a less more sensitive to non-English comparisons, using this syntax:

if (testString.Equals("Brown", StringComparison.CurrentCulture)).

Finally, one more introductory comment: When comparing two integers:


if (7 == 5) or
if (valueA == valueB)

you must use an equality operator of some type, "==", ">=", ">", etc.. But, if you are
testing an already-created boolean variable (such as 'myFlag', which can be True of
False, from the illustration above), then the true-false comparison is assumed. For
example, these statements are synonymous:

if (myFlag == true) if (boolVariable == true)


if (myFlag) if (boolVariable == true)
If (!myFlag) eg: "! = not" if (boolVariable != true) - !=False

where an exclamation-point ("!", often enunciated as "bang") means "not", as in


"if not myFlag".

Numeric "if" Statements:

Similar to Program 2.2, begin a new project. On Form1, place two textboxes and a
button. Modify the button1_Click event by double-clicking the button and adding the
following code. As usual, pay attention to the braces and indentation.

Chapter 3 - If Statements and Branching Page: 121


Button1 will compare two hard-coded integer variables and displays a message saying if
they are equal or not. Note: textBox1 will hold the results of the compare; textBox2 will
be used in later examples.

Program 3.1 - Numeric if, complete


private void button1_Click (object sender, EventArgs e)
{
int valueA = 10;
int valueB = 5;

if (valueA == valueB)
{
textBox1.Text = "ValueA and B are equal";
}
else
{
textBox1.Text = "ValueA and B are not equal";
}
}

Common Mistakes:

The compiler requires "double-equals" ( == ) when comparing variables. This is easy


to forget. When a single equal-sign is typed, the compiler complains with "Cannot
implicitly convert type 'int' to 'bool'"

if (valueA = 10) //Will error; needs double-equals


{
or
if (strtemp = "dog") //Will error; needs double-equals
{
:

A single equal means "gets," as in, valueA = 10, where valueA gets or is assigned the
value 10, while a double-equals means a comparison or boolean.

Why such a strange message? Here is what happens when you forget the double-equals.
The compiler sees the statement:

Chapter 3 - If Statements and Branching Page: 122


The equal-sign tells the compiler to assign "dog" to the temp-string, which results in a
string being fed into the if-statement. Using the example above, the statement converts
to "if (dog)", which is nonsensical because if-statements only know about "boolean"
True/Falses and, by design, they cannot handle the string. It wants to resolve to either
"if (true)" or "if (false)".

Different Brace Style:

Knowing that white space is ignored by the compiler, some programmers style their
opening if-statement braces at the same level as the "if". This is a legal and somewhat
common way to block your code and is common in Java scripting. The editor will
dissuade you from doing this and will try to impose a more standard design:

if (valueA == valueB) {
<do stuff here>
}
else {
<do stuff>
}

Optional Braces:

The statement following the if-clause is normally blocked with a pair of braces. But,
when an if-statement clause has only one instruction, braces are optional. These two if-
statements are functionally equivalent:

//Two if-statements; one with braces and one without:

//Style 1:
if (valueA == valueB)
{
textBox1.Text = "Value A and B are equal";
}

//Style 2:
if (valueA == valueB)
textBox1.Text = "Value A and B are equal";

In the second version, the braces were discarded – ! but this can only be done when the
if-clause has a single statement and the closing semicolon replaces the closing brace.
Braces are absolutely required If more than one statement exists inside the if-statement
clause.

Chapter 3 - If Statements and Branching Page: 123


Recommendations:

There are light-hearted arguments in the development community about having the
opening brace on the same line as the if-clause, and about not using braces on a one-line
if-statement. Obviously it saves vertical space and typing; others argue it is inconsistent.

I recommend using opening and closing braces for all blocked code – even if there is
only one line of text. And I recommend the braces typed on their own lines. This seems
to be most prevalent style and in practice, it is easy to type and read. Admittedly, it takes
more vertical space.

Recommended if-statement brace formatting


if (valueA == valueB)
{
textBox1.Text = "ValueA and B are equal";
}
else
{
textBox1.Text = "ValueA and B are not equal";
}

Consistently using braces means one-less decision while coding. In my experience, an


added second or third line of code will invariably be required so you might as well type
the braces now.

Optional "else":

With all if-statements, the first "then" clause is required but the else-side is optional.
What if your logic has nothing to do on the "then" side and all the logic needs to be done
on the "else" side? You have to fool the compiler with one of two tricks: Either put in a
non-functioning then-clause or flip the conditional around.

The following code does not have an executable {statement} after the "if" – comments
are not executable and this will fail:

if (valueA > valueB)


// do nothing - but this doesn't work; the compiler complains
else
{
MessageBox.Show("stuff to do when false");
}

Adding a "dummy" clause after the "if" solves the problem – a comment is not enough.
Everything in the braces act as a single executable statement, even if there is nothing
within. The braces replace the closing semicolon used in the single-statement example:

Chapter 3 - If Statements and Branching Page: 124


Program 3.2 - Dummy if-clause with no activity, complete
if (valueA > valueB)
{
// This is a valid Dummy if-statement
// if-clauses cannot be blank and it takes more than a comment
// to satisfy the compiler. There must be something here, an
// executable or a {block} of code with comments.
}
else
{
MessageBox.Show ("stuff to do when false");
}

or more simply:

if (valueA > valueB)


{

}
else
{
MessageBox.Show ("Stuff to do when false");
}

An alternative is to flip the conditional by using a Less-than (or "not-equal" in the case
of an equality):

Alternate Dummy-if
if (valueA < valueB)
{
MessageBox.Show("stuff to do when 'false'");
}

or

if (valueA != valueB)
{
MessageBox.Show("stuff to do when 'false'");
}

(English falls apart in this last example. It is almost like a double-negative: a not-equal
result gives the if-statement a 'true' response. Remember, if statements only think in
terms of true and false.)

if-statement Semicolon Rules:

Like the loop commands, there are no closing semicolon on the if-statement itself, but
out of habit, you may type one.

if (valueA == valueB) //No semicolon

Chapter 3 - If Statements and Branching Page: 125


When a superfluous semicolon is added, the compiler complains with several error
messages (brace expected, "Possible mistaken empty statement" and "Invalid expression
term 'else'"). Notice how the semicolon does not tuck against the if-statement.

By now you should be wondering why loop and if-statements don't use closing semi-
colons at the end of their phrase when almost every other command does. Remember
that all statements in C# are one command long and each command ends in a semi-colon.
if-statements and loops are no exception - they can only be one command long. Reading
the command in English explains why: "If valueA is equal to valueB then do this one
thing. Semicolon."

The compiler works this way for reasons only known to the compiler-gods, but for
aesthetic reasons, humans like to write the statement as a two-lined phrase with the "if"
on one line and the "then" on the next. This makes it appear as two lines when it really
is not. Of course, C# doesn't care about "white space" and the compiler treats it as one
line no matter how many physical lines you type.

With this knowledge, Program 3.1 could be written correctly, without braces. The 'else'
can be considered a separate command. Two commands; two semi-colons:

if (valueA == valueB)
textBox1.Text = "ValueA and B are equal";
else
textBox1.Text = "ValueA and B are not equal";

Because both halves of the if-statement are only one line long – and that line ends in a
semicolon – the two-line method is legal. To continue the example, you can roll up the
lines, removing all white space. For aesthetic reasons, this style is not recommended:

if (valueA == valueB)textBox1.Text = "A and B are equal";


else textBox1.Text = "A and B are not equal";

Chapter 3 - If Statements and Branching Page: 126


or to further demonstrate the lack of respect for white-spaces, the statement could be
written in this non-recommended fashion:

if (
valueA ==
valueB)
TextBox1.Text =
"A and B are equal";

Braces:

And of course the statements could be written with braces to delimit the if-clause and the
else-clause. (But as always, braces are required if the clause has more than one
statement.) This is the key to braces: They act as a single statement – think of them as a
closing semicolon for a single statement.

if (valueA == valueB)
{
//one or more statements can go here
textBox1.Text = "ValueA and B are equal";
}
else
{
//one or more statements can go here
textBox1.Text = "ValueA and B are not equal";
}

The examples above contained only one line of code for each phrase but you could put
any number of statements between the braces. The compiler treats the braces as a block
of code and internally it groups them as one logical statement, with a call to a sub-
routine. In pseudo-code, it looks like this:

if (valueA == valueB)
{ GOTO a subroutine to do other steps, then return after the closing brace; }
else
{ GOTO another subroutine and return when done; }

When compiled, the code within the braces is shuttled to a new location; then the
computer calls the subroutine and returns when done. This happens under the hood.
With this, the compiler can stick to it's rule about all statements being one line long.

Beware of Formatting Traps:

However, as mentioned before, many programmers discard the braces on single-line if-
statements. This can introduce hard-to-find bugs. Consider these examples:

If you have more than one line of code within either the "if" or "else" clause, then braces
are absolutely required. Consider this flawed code-snippet, which to a casual eye looks
perfectly logical:

Chapter 3 - If Statements and Branching Page: 127


With textBox2 indented, it looks like it belongs to the "if-then" side of the clause. When
braces are missing on the "if" side, the older versions of the compiler generated an odd
compiler error: "Invalid expression term 'else'". With Visual Studio 2017, you get a
better "} expected" along with an odd "possible mistaken empty statement.

Fix the problem by adding braces around those two lines of code on the if-side.

//This is corrected; but is cosmetically inconsistent


//with the else-clause, which does not have - or need braces:

if (valueA == valueB)
{
textBox1.Text = "ValueA and B are equal";
textBox2.Text = "This is an error";
}
else
textBox1.Text = "ValueA and B are not equal";

But watch what happens when the else-side has two or more statements, but
you forget the braces, as in this snippet:

//This is a flawed if-else statement:


if (valueA == valueB)
textBox1.Text = "ValueA and B are equal";
else
textBox1.Text = "ValueA and B are not equal";
textBox2.Text = "This is in error and you'll never know";

You will find textBox2 (underlined) runs unconditionally – regardless of the outcome of
the if-statement; it will run whether the test was true or false, even though it was 'clearly'
intended to be part of the else-clause. The trouble is, even though it "looks" like it is part
of the else-clause, it is not. Flaws like this are hard to see. In this case, the compiler will
not show an error. Half the time the if-statement takes the top route and the other half it
takes the bottom, making the error appear intermittent. The indentation fools your eye
and you will have a devil of a time finding this bug.

Chapter 3 - If Statements and Branching Page: 128


Fix these problems by adding braces around if and else-clauses,
even when not needed. For example, the braces around the if-
clause are technically redundant because that clause is only one
statement long. On the else-side, the braces are absolutely
required:

Chapter 3 - If Statements and Branching Page: 129


&& (AND) || (OR) Booleans

Boolean statements can be combined with "and"s and "or"s to make a "compound"
statement. With logical ANDS, think "if this-is-true AND that-is-true then the whole
condition is true." if (myColor == "white" AND paintType == "Latex)... then paint
the wall. Both sides of the condition must be true before the if-statement resolves to
true.

Instead of using the word "AND", C# uses the symbols "&" and "&&". Both mean
"AND" and both work similarly. Consider this sample code:

Program 3.4 - AND &&, complete


private void button1_Click (object sender, EventArgs e)
{
//AND with double-ampersands
int valueA = 10;
int valueB = 15;

if (valueA == 3 && valueB == 15)


{
textBox1.Text =
"This would resolve as true, if it were so";
}
else
{
textBox1.Text = "In this case the answer is false";
}
}

With an AND, both booleans must be true; otherwise 'else'. In the example, even though
B equals 15, A's value does not match and the statement resolves false because of the
AND.

Both halves of the AND must be Boolean – you can think of them as individual if-
statements. Ultimately, the if-statement can be reduced to this English-like text: "If true
and true then..."

Important Difference Between "&" and "&&":

There is no difference in how "&" or "&&" compute – both arrive at the same
conclusion. But the "&&" can be more efficient because it bails-out of the comparison if
a first 'false' is encountered.

Chapter 3 - If Statements and Branching Page: 130


In the example above, where A is being tested for the number 3 – but its real value is 10,
it will fail the first half of the AND. Because of the AND, the compiler knows it is
impossible for the entire statement to resolve True. With the double-&&, you are giving
it permission to immediately jumps to the else-clause. And most importantly, it does not
even bother to calculate the second half – saving a millisecond or two of CPU resources.
This may seem unimportant but it has a more practical use.

With a single ampersand ("&" vs. "&&"), both values are calculated, regardless of the
outcomes. Then they are "AND-ed". Sometimes, single-ampersands can get you in
trouble. Consider the following statement, where there is a possibility of dividing by
zero. Here the developer knew about the possibility and attempted to trap the condition
and used a single-AND:

With a single-ampersand, both sides of the AND are computed prior to deciding the "if".
The first half checks to see if valueZ happens to be zero while the second half runs the
division. Both sides are calculated before applying the AND. Since it is against the law
to divide by zero, the program literally crashes when the single-ampersand forces the
divide-by-zero.

The double-ampersand elegantly (and sneakily) avoids the problem:

Chapter 3 - If Statements and Branching Page: 131


Program 3.5 - Test for Divide by Zero, complete
:
//Shows the value of a double-ampersand if there is a possibility
//of a divide by zero...

private void button1_Click (object sender, EventArgs e)


{
//AND with double-ampersands
int valueA = 10;
int valueZ = 0;

if (valueZ != 0 && valueA /valueZ > 10)


{
//Do the calculations or other steps here
}
}

If valueZ happens to be zero, the remainder of the if-statement does not even bother to
calculate – and the divide by zero does not happen. If valueZ is greater than zero, the
first clause resolves as true and the computer then moves ahead and computes the second
phrase, as needed.

Code like this is graceful and subtle and the details can become hazy when it is pulled for
maintenance a year later; comments are certainly warranted here. However, this type of
problem (Divide-by-zero) happens frequently in programming and in one statement you
can prevent your program from crashing. The test could be re-written using a construct
called a nested-if (described in the next section). This takes more vertical space but in
some respects it is more self-documenting and easier to interpret:

if (valueZ != 0)
{
if (valueA / valueZ > 10)
{
// greater than 10 stuff
}
else
{
// less than 10 stuff
}
}

Recommendations for &&:

A double-ampersand's performance gain is small, but if it were inside a million-record


loop it would be noticeable. In general, when ANDing, you should almost always use a
double-ampersand.

Chapter 3 - If Statements and Branching Page: 132


The "|" and "||" (OR) Boolean:

The split-vertical-bar (" | ", also known as a pipe-symbol) represents a "logical OR". If
either half of the compound if-statement is true, then the entire statement is considered
true. Think, "If either this or that is true, then..."

Using the values from above, where (valueA equals 10) and (valueB equals 15) – but the
test is against a different value, this statement resolves true.

Program 3.55 - OR clause, complete


private void button1_Click (object sender, EventArgs e)
{
//This is a Double-Or (split-vertical bar) if-statement

//Again, the variables are set one way, but the example if-
//statement will test for a different value

int valueA = 10;


int valueB = 15;

if (valueA == 3 || valueB == 15)


{

}
}

The single-bar or dual-bar logic works along the same lines as the "&&" logic. As a
general rule when working with OR, always use the double-" || "'s.

To carry the example further, a test that read as

if (true || false || false)

would still resolve 'true' because the first instance and, because of the double-pipe, the
other statements would not be examined.

If using single-pipes for the three comparisons, it would still resolve as true but the
compiler is forced to compute each of the three phrases. Using the double-pipe is more
efficient.

The "^" (XOR – Exclusive OR) Boolean:

XOR is an unnatural and little-used boolean. With the "carrot" (sometimes called a "hat"
symbol), if one side or the other resolves as true, then the statement is true; this is the
same as a standard OR. But if both sides are true or both sides false, then the statement
resolves false.

Chapter 3 - If Statements and Branching Page: 133


XOR Table (boolean-result A ^ boolean-result B)

The entire statement is Phrase I Phrase II

TRUE true false

TRUE false true

*FALSE true true

FALSE false false


* this is different than a regular OR

Consider this true XOR example, where:

• A equals 10 (any number other than 3)


• B equals 15

XOR Example, result = true


private void button1_Click (object sender, EventArgs e)
{
int valueA = 10;
int valueB = 15;

//This resolves to "true":


if (valueA == 3 ^ valueB == 15)
{
textBox1.Text = "XOR: This is True";
}
else
{
textBox1.Text = "XOR: This is a false";
}
}

XOR Example; Result false


private void button1_Click (object sender, EventArgs e)
{
int valueA = 3;
int valueB = 15;

//This resolves to "false":


if (valueA == 3 ^ valueB == 15)
{
textBox1.Text = "XOR: This is True";
}
else
{
textBox1.Text = "XOR: This is a false";
}
}

Chapter 3 - If Statements and Branching Page: 134


I am hard-pressed to find a concrete example for an XOR. But if you did need this type
of logic the XOR can resolve the command with one statement vs a series of nested-if's.
If you use this construct, be liberal with documentation.

Chapter 3 - If Statements and Branching Page: 135


Nested if-statements

if-statements can be embedded within other if-statements. When you do this, indentation
is the key to human understanding, but as usual, the computer does not care about white-
space.

Consider this situation: You have three numbers (say these are product weights) and you
need to see which is the largest. (Mathematicians should refrain from laughter: what
happens if the numbers are equal? Let this slide.)

For this example, hard-code the numbers as integers to leave the demonstration
uncluttered; pretend they come from other data sources:

private void button1_Click (object sender, EventArgs e)


{
int valueA = 10;
int valueB = 5;
int valueC = 25;
string theAnswerIs;
:

Finding the largest number using if-statements is tedious:

If A > B, then check if A > C;


If A is still greater, then it must be the largest.
Otherwise, check if B > C
If B > C, then B is the largest, otherwise C is the largest.

It reads easier in code:


Program 3.6 - Nested-if Example
private void button1_Click (object sender, EventArgs e)
{
//Finding the larger of three numbers using nested-ifs

int valueA = 10;


int valueB = 5;
int valueC = 25;
string theAnswerIs;

if (valueA > valueB)


{
if (valueA > valueC)
theAnswerIs = "A is largest";
else
theAnswerIs = "C is the largest";
}
else
{
if (valueB > valueC)
theAnswerIs = "B is the largest";
else
theAnswerIs = "C is the largest";
}
MessageBox.Show (theAnswerIs);
}

Chapter 3 - If Statements and Branching Page: 136


When the if-statement resolves down the first path (when A > B), the "else" is ignored.
Try the example and test various non-equal numbers as A, B, and C.

If you had more than three values, this type of logic becomes unmanageable. In this
case, consider loading the values into an array and sorting, using concepts in future
chapters.

"else if" – Alternate Nested if:

Nesting if-statements are simply a matter of indenting and punctuation. Done well, the
code is understandable but nested-ifs can be difficult to look at when indented more than
three conditions deep. Although there is no real limit on nesting depths, indenting is
frustrating to type. There is an alternate method, called "else if", which acts like a new
if-statement.

Consider this example, which tries to determine which color was selected:

Program 3.65 - else-if, alternate design


//Finding which hard-coded color was typed by the end-user

string myColor = "Red"; //Assign the color here as-if typed


string theAnswerIs;

if (myColor == "Blue")
theAnswerIs = "Blue is my color";

else if (myColor == "Green")


theAnswerIs = "Green is the color";

else if (myColor =="Yellow")


theAnswerIs = "Stinkn yellow is the color";

else if (myColor == "Black")


theAnswerIs = "The new Black";

else if (myColor == "Red")


theAnswerIs = "Red is my color";

else
theAnswerIs = "None of these colors";

MessageBox.Show (theAnswerIs);

comments:

• Visual Basic programmers typed "elseif" – as one word – instead of "else (space) if".
The VB editor corrected the misspelling automatically, but the C# editor does not. If
you forget, the error reports "; (semicolon) expected".

Also, in the example above, I did not type opening or closing braces, saving on that
vertical space that paper books have limits about. In real life you would probably
have a liberal selection of braces scattered throughout the code. The code above
works, however, because all the statements are single-lined statements.

Chapter 3 - If Statements and Branching Page: 137


• The last statement is an "else", not an "else if", representing a "none-of-the-above"
choice. As usual, this else-statement is optional but good programming suggests
accounting for the possibility – especially in a statement such as this, where a color
like chartreuse might saunter by.

"else if" commands are somewhat of a nuisance and they take


practice to code. To me, they are hard to interpret, but at the
same time, they are better than an endless series of nested-ifs.
This example was clean and straight-forward, but by the time
you add braces and other logic, they can get almost as messy as
nested-ifs. With nested-ifs and else-if's, there are usually better
ways to code. Consider a 'switch' statement or using additional
modules and functions.

Chapter 3 - If Statements and Branching Page: 138


Math.Min - Numeric Testing for Smaller Value

Earlier, a nested-if was used to test which of the three values was the largest. C#
provides a vaguely similar function that returns the larger or smaller of two numeric
values. It is limited to testing only two values at a time, but it makes for a simpler test
than an if-statement.

Math.Min
Math.Max

Program 3.68 - Using Min Max, complete


private void button1_Click (object sender, EventArgs e)
{
//Finding the larger of two numbers using Min/Max

int valueA = 10;


int valueB = 5;
int valuex;

valuex = Math.Max(valueA, valueB);

MessageBox.Show ("The larger number is " + valuex);


}

Compare this to a similar if-statement:

:
if (valueA > valueB)
valuex = valueA;
else
valuex = valueB;
MessageBox.Show ("The larger number is " + valuex);

Chapter 3 - If Statements and Branching Page: 139


Compounding if-Statements

Compound boolean statements can be written with phrases that contain a mixture of
ANDs and ORs. The phrases are often surrounded with parenthesis to clarify the logic
or to make it easier to read. For example, consider this compound if-statement:

Example Compound Boolean


if ((Category == "A" || Category == "B") && factory == "SFC")
{

With the parenthesis, it reads this way: "If the product category were either A or B and
the factory were "SFC", then proceed with the then-clause." If the product category were
"C", the entire clause would be skipped. If the factory were any Factory other than
"SFC", the entire clause would be skipped.

The statement could also be indented in this fashion, which is easier to read and
understand:

if ((Category == "A" || Category == "B") &&


factory == "SFC")

Confusion:

Notice how the parenthesis surround the A and B tests. This is important. Using the
same logic as any mathematical calculation, parenthetical phrases are examined first, in a
left-to-right order.

Without the parenthesis, the intent can be confusing. What happens with this statement?

if (Category == "A" || Category == "B" && factory == "SFC")

The question is whether to read this as [(A or B) and SFC] or as [A or (B and SFC)]. In
this poorly-punctuated instance, read the statement left-to-right. In this case the results
would be the same with or without parenthesis but you would have to agree a non-
parenthesis statement is difficult to interpret. With compound booleans always use
parenthesis for clarity.

At some point, statements can get too complicated for most people to comprehend. Take
a look at the following, nonsensical if-statement. When statements get this involved,
consider a different construct – usually a combination of switch and nested-if-statements:

if ((Category == "A" || Category == "B") &&


(factory == "SFC" || factory == "DEN" || factory == "SEA") &&
((shipping == "RAIL" && weight > 10000) ||
(shipping == "AIR" && weight < 500)))

Chapter 3 - If Statements and Branching Page: 140


This convoluted statement has a flaw: What happens if weight is
less than 10,000 but not Air? When you have an if-statement
this complicated, there will be a better way to write the code.
The better way will be wordier but easier to follow.

Chapter 3 - If Statements and Branching Page: 141


switch Statements

In the previous nested-if example, program 3.65, the variable "myColor" was compared
against various colors - Blue, Green, Red, etc. Once the value was discovered, a text-
phrase was stored in a separate string. Finally, at the end of the routine, the string value
was regurgitated in a MessageBox. The if-logic looked like this:

string myColor = "Red";


string theAnswerIs = "";

if (myColor == "Blue")
theAnswerIs = "Blue is my color";

else if (myColor == "Green")


theAnswerIs = "Green is the color";
etc...

With each possibility, Red, Blue, Green, etc., the nested-if only looked at "myColor".
When the same variable is being compared with multiple choices, there is a better
construct called the "switch" statement. VB programmers know this as a "Select Case".
Although it has limitations, a 'switch' is easy to use and is easy to read. It can handle
dozens of separate tests, without the bother of nested-if's.

Case "Value":

Consider this code, which tests the variable "myColor" against a list of colors and reports
which was selected. Notice the individual Case-value-colon statements. If none of the
colors match the available choices, display a "None of the above" message. For
simplicity in the example, myColor is declared and initialized at the top of the program,
as a hard-coded value:

Chapter 3 - If Statements and Branching Page: 142


Program 3.7 - switch statement, complete
private void button1_Click (object sender, EventArgs e)
{
string myColor = "Red"; //As-if the user had typed this color
string theAnswerIs = "";

switch (myColor)
{
case "Blue":
theAnswerIs = "Blue is my color";
break;

case "Green":
theAnswerIs = "Green is the color";
break;

case "Yellow":
theAnswerIs = "Stinkn yellow is the color";
break;

case "Black":
case "Red":
theAnswerIs = "Red or Black is my color";
break;

default:
//acting as an "otherwise" statement...
theAnswerIs = "None of the above!";
break;
}
MessageBox.Show(theAnswerIs);

//This switch is risky - the color is case-sensitive.


//A better test is switch (myColor.ToUpper()) and "BLUE"
}

The trade-off is this: When making their comparisons, nested-ifs can look at more than
one variable. For example, nested-if's can look at myColor, the type of material, and the
day of the week; they can compare any other variable, at any time, even within a nest.
Switch-statements are restricted to a single variable, in this case, myColor.

switch-Statement Punctuation:

"switch" statements are oddly punctuated. To begin, notice none of the cases are
"blocked" – that is, they don't use braces. Compared to all other commands you've seen,
this is counter-intuitive – until you notice the colons (not semi-colons) at the end of each
"case." The colons are actually a "goto label." The "break" ends the goto-section and
tells the compiler to jump to the first statement after the switch.

If you've had prior programming experience, the implication of a goto statement explains
much about how the switch-statement works. In the example, you can think of a switch
as a "goto Green:" or "goto Red:". Goto statements are explained in more detail in the
next section, but for now, their purpose should be fairly obvious.

Chapter 3 - If Statements and Branching Page: 143


switch statements are not limited to just string values; numeric switches are also
acceptable:

Program 3.75 - Numeric switch, complete


int valueA = 5;

switch (valueA)
{
case 5:
<do stuff here>
break;

case 10:
<do stuff here>
break;
}

This example numeric-switch only considers the numbers 5 and 10. All other numbers,
and I do mean all other numbers, do not participate.

default Clause:

An optional "default" clause processes all other possibilities not met explicitly with a
case-statement; you can think of this as an "else" or "otherwise." Inserting a default into
the numeric switch example from above, substantially changes the logic: Now, all
numbers are processed - but 5 and 10 are given special consideration.

Program 3.76 - switch with default clause


int valueA = 5;
switch (valueA)
{
case 5:
<do stuff here>
break;

case 10:
<do stuff here>
break;

default:
<do stuff for all other possibilities here>
break;
}

The default clause requires a break-statement even though it is the last phrase in the
group. Technically, default can be anywhere in the construct – top, bottom, or in-
between – but common sense dictates it should be at the end.

Unlike other languages with similar verbs, there is no "do-always" clause.

Chapter 3 - If Statements and Branching Page: 144


Comparison Types - Sadly, Equalities Only :

You can only use equalities in a switch-comparison. For example, case "red": means the
case is equal to 'red' – but it will not test for 'Red' or "RED'.

Greater-than, less-than and not-equals are also not allowed. By saying case "red": you
are implying an equal sign. Unless you like compiler errors, do not type the equals (as in
case == red).

This restriction is noticeably different than other languages, such as Visual Basic, which
allows inequalities. In those languages you could, for example, switch-case on numbers
<= 10. With C#, keep in mind the :colons act as a GOTO label and case <= 10 does
not make a good destination label.

More on Required breaks:

Each "case:", including the last, must have a "break"1. This is the same type of command
as a "break" in a loop; control is passed to the switch-statement's closing brace and the
break essentially tells the compiler to GoTo the end of the switch.

The compiler does not allow logic from one clause to flow into another and the next
case-statement label is not enough to prevent it. Forgetting a break statement will fail

1
Or a "return" can be used in lieu of a break. return would exit the current method and return to
the calling routine. But you cannot use a return and a break in the same clause. Return-statements are
discussed in a future chapter.

This would be illegal syntax:


case 5:
<do stuff here>
return;
break; (Unreachable code detected)

case 10:
:

Chapter 3 - If Statements and Branching Page: 145


with a "Control cannot fall through from one case label ...". However, you can stack
two case-statements together; this is described next.

Compound case Statements:

The myColor switch example had two labels next to each other (Black and Red); this
acts as an "OR":

Program 3.77 - Compound case statements, partial


:
case "Black":
case "Red":
theAnswerIs = "Red or Black is my color";
break;

There can be no intervening instructions between the compounded case statements. In


this example, if myColor equals Black or Red, then do the code within the case. Stack as
many choices as needed in this manner.

Other languages allow you to stack you choices in this manner: case "Black", "Red":,
but this is not a valid "goto" label and the syntax is not allowed in C#.

Case-sensitive switches - .ToLower(), .ToUpper():

switch statements are case-sensitive. For example, to compare "myColor" to "Red",


"RED", or "red", you could use this syntax:

switch (myColor)
{
case "RED":
case "Red":
case "red";
<do this stuff>
break;

But this logic does not work with "rEd" and it can get cumbersome with other choices.
A better solution is to transform the switch's conditional checking for a more generic
version of "myColor." Imagine your program prompts the user for their favorite color
(and imagine they never misspell it). The comparison-logic would be smoother and more

Chapter 3 - If Statements and Branching Page: 146


accurate if the user's input were converted to all lower or upper-case. A new keyword
(more appropriately called a new "Method") is needed:

Using .ToLower( ) with a switch-statement, recommended


switch (myColor.ToLower())
{
case "red":
//Do red stuff
break;

case "blue":
//Do blue stuff
break;
}

".ToLower()" temporarily changes what ever is in the variable myColor to all lower-
cased letters. From here, each case-statement need only check for lower-cased
possibilities (e.g. "red", "green", etc.). Notice the two empty parenthesis, which are
required by the syntax. There is more on this in the next chapter.

With string switch-statements it is almost a requirement to


convert the case before comparing.

Finally, notice this about the convert ToUpper() and ToLower():

switch (myColor.ToLower())

The conversion only changes "myColor" for the duration of the switch statement. The
original myColor value"RED" remains upper-cased. Prove this to yourself by watching
the MessageBox.Show, as it displays the myColor variable; you will find it is always
cased as-it-was. The reason? The cased value was not assigned to a new variable.

Chapter 3 - If Statements and Branching Page: 147


If you wanted the variable to permanently change to (lower-case), use a command similar
to this:

myColor = "RED";
myColor = myColor.ToLower(); //Now 'red'
switch (myColor)

Trimming Text Swtiches:

When switching against strings, especially if these are values typed by end-users, it is
wise to shift the case and it would be even wiser to "trim" the results, where .trim
removes leading and trailing spaces (more on this in the next chapters). This would be a
recommended test:

switch (myColor.Trim().ToLower())
{
case "red":
:

Do's and Don'ts with switch-Statements:

switch statements have numerous rules on how they are used:

• Multiple case-commands can not be combined into one line. For example, say that
both black and red execute the same logic. You can't use this syntax:
case "black", "red":

However, the statements can be "stacked" as long as there is not intervening code
between the conditions. This works because the "case" statements are goto-
destination labels. Again, notice the colons:

case "black":
case "red":
case "dark purple":
<do the same stuff here if any of them>
break;

• C# does not allow logic from one case-statement to "flow" into another. In other
words, you can't move from blue's case-logic and expect to flow into "green's".
Break statements are absolutely required at the end of the code-sections. "default"
also needs a break.

• You can use a "goto" statement, going to another label within the case - such as
goto default; or goto blue;. Just because you can does not mean you should.

• case choices are "case-sensitive". "Red" does not equal "red". Usually this means
you should use a variable.ToUpper() or variable.ToLower(). I like using
lowercase tests because they are easier to type.

Chapter 3 - If Statements and Branching Page: 148


• Strings should almost always be Trimmed and cased in the switch statement:

switch (myColor.ToLower().Trim())

or as a two-step operation, where myColor's value is permanently changed:

myColor = myColor.ToLower().Trim();
switch (myColor)

• The internal case-destinations can only compare against hard-coded literals or


numbers; you cannot use a variable in the conditional test. For example, this is
illegal:

switch (myColor)
{
case strMainColor: //Illegal to use a variable here

case red:

However, the top switch-compare is always a variable [switch (myColor)].

• switch statements cannot use equalities or inequalities (==, >=, etc.) in the
comparisons. This is different than in other programming languages. If these types
of comparisons are needed, use if-statements.

These switch-comparisons are illegal:


int valueA = 10;

switch (valueA)
{
case <= 10:
<Illegal comparison>
break;

case > 10:


<Illegal comparison>
break;
}

switch-statement read as a "goto-like" label and this is why inequalities are not
allowed. Phrases such as "case valueA <= 10:" is more a calculation than a label.

• There is no limit to how many "case" statements you can have within a switch-block.
Switch statements can have other embedded switch statements but this is poor
programming style. Call another module from within the first switch if you need to
do this (techniques described in a future chapter).

• The "default:" section is optional but recommended. Your program should have
logic that handles unexpected tests - even if that logic is nothing more than a
displayed error message alerting of an internal problem.

• Variables within the switch-statements must be initialized with some value, even if
an empty-string "", prior to entering the statement. In the examples above,

Chapter 3 - If Statements and Branching Page: 149


"theAnswerIs" was initialized to "". This is especially true if you are not using a
default clause

Without initializing internal variables, you will get a "Use of unassigned local
variable 'theAnswerIs'" at compile-time – even if a valid switch would have been
selected. The Default-clause, where "theAnswerIs" can be set to a default, insulates
you from this problem. However, I prefer to initialize above the routine.

• Empty phrases that no action are sometimes needed. For example:

:
case "yellow":
theAnswerIs = "Yellow is the color";
break;

case "pink":
break; //execute no logic
}

MessageBox.Show ("The answer is: " + theAnswerIs);

In this snippet, if "pink" were selected, no logic was run, and more importantly, the
variable "theAnswerIs" would not be populated. A run-time error is at risk when
later MessageBox statements tried to use the variable, which never had a populated
value. This is why "theAnswerIs" variable should be pre-initialized with a default
value, even if that value is an empty string, "". Also, even if there were a default
switch, default would not run because pink was the choice, so again, the variable
"theAnswerIs" would not be set.

Syntax Errors:

Here are some of the common syntax errors you may encounter.

• Forgetting the "break" clause:

case "yellow":
theAnswerIs = "Yellow is the color";
// break;

Results: An error message: Control cannot fall through from one case label "'case
Yellow". The Yellow "case" statement is highlighted.

• Remove the parenthesis from the main 'switch' statement:


Change switch (myColor)
to switch myColor

Results: Syntax error, " ( " expected.

Exercise:

Chapter 3 - If Statements and Branching Page: 150


What is wrong with the following switch statement? The intent: Switch based on the
length of a string. This uses a text field's .Length property:

Exercise
:
switch (strfoundString.length)
{
case 1:
//do stuff here
break;
case 2:
//do stuff here
break;
default:
//do stuff here
break;
}

Compiler error: " 'string' does not contain a definition for 'length' and no extension
method 'length' accepting a first argument of type 'string' could be found (are you
missing a directive or an assembly reference?)". Solution: Capitalize ".Length"

Chapter 3 - If Statements and Branching Page: 151


switch Statement Summary:

It may seem the switch statement has too many restrictions to be useful but the statement
is succinct and easy-to-use. Most programs find a good use for it. A well-placed switch-
statement simplifies nested-ifs and other convoluted logic. A year from now, when
reviewing this code, switch statements will be refreshingly clear and easy to understand.

Here is a summary of the syntax:

Chapter 3 - If Statements and Branching Page: 152


goto Statements

The nature of goto statements has been touched upon earlier. A goto instructs the
computer to "jump" to a new location in code. The location is defined with a destination
– called a "label".

Labels are a non-executable marker in your code – a place for the goto to land on and are
punctuated with a colon:

Program 3.8 - A Shameful goto Example


private void button1_Click (object sender, EventArgs e)
{
string myColor = "Red";
string theAnswerIs;

if (myColor == "Red")
goto bypassLogic;

theAnswerIs = "Some other color";


goto allDone;

bypassLogic:
theAnswerIs = "Red is my color";

allDone:
MessageBox.Show (theAnswerIs);

comments:

goto statements often beget other goto statements. If you have one, you invariably need
another, and soon the program is a convoluted mess. Use other constructs and simplify
the logic.

goto Statement Rules:

• Do not use goto statements, if for no other reason than to avoid the scorn of fellow
programmers. This verb is considered crude and blunt2.

• goto jump labels end in a colon (not a semicolon). As you type, the compiler shifts
the label one tab-stop to the left so the label stands out from the rest of the code. The
labels themselves are "non-executable" and do nothing other than mark a place in the
program.

2
I originally wrote this sentence as "They are considered crude and blunt" but it was unclear if I were
referring to the GOTO verb or your fellow programmers. Generic pronouns are imprecise, aren't they?

Chapter 3 - If Statements and Branching Page: 153


• goto jumps cannot land in the middle of other constructs, such as the middle of an if-
statement or the middle of a loop. A goto can land just above a loop or if-statement,
or just after – as long as you haven't violated another construct's opening or closing
brace. They cannot target another module or function.

• goto jump labels must have some executable code beneath them. In the example
above, remove the MessageBox.Show line and you'll get a compiler error "Invalid
expression term "}" (closing brace).

• Unscrupulous people use goto's to bypass part of a loop. Almost always a "break" or
"continue" is more graceful. However, in the past several years, there has been
discussion in programming circles that a well-placed, simple goto can be acceptable,
perhaps as an early-exit out of a loop or other routine. I agree to this, if used with
care.

Chapter 3 - If Statements and Branching Page: 154


Ternary Operator

The last conditional statement to discuss is the seldom-used Ternary Operator,


punctuated with a "?" (question mark). This command is unusual because it uses three
operands on one line: A test-"?", a true-assignment and a false-assignment. Think of it as
a single-lined 'if-then-else' statement that works similarly to an Excel if-statement. I
generally avoid using this verb because of its rarity and it is somewhat difficult to
interpret.

Basic Syntax:

= (test or comparison) ? value-if-true : value-if-false;

Program 3.9 - Ternary Example, complete


int valueA = 10;
int valueB = 5;
string theAnswerIs;

theAnswerIs =
(valueA >= valueB) ? "Answer is A" : "It is B";

MessageBox.Show (theAnswerIs);

Results: MessageBox will show an "Answer is A" because A > B.

comments:

• Note the question-mark and the colon. Think if-then-else, much like an Excel
formula.

• The results of the test (valueA >= valueB) must resolve to a boolean.

• The two phrases after the "?" are "what to assign if True :colon, what to assign if
False". The results are sent to the variable at the front of the statement. Ternary
Operators must assign their results to another variable – in this case I used the
variable "theAnswerIs".

• You could combine the string definition for "theAnswerIs" and the Ternary into one
line, as in:

string theAnswerIs = (valueA >= valueB) ? "Answer is A" : "It is B"

I mention this command because you might see it occasionally in the wild. Although you
can accomplish several things with one line of code, the abilities of the Ternary is
limited. Unlike a standard if-statement, you cannot add additional statements within the
"then" or "else" clause.

Chapter 3 - If Statements and Branching Page: 155


Exercises

In the style used in this chapter, attach logic to "button1_Click" solving the following
problems. Have your results display either as a MessageBox or in textBox1.Text.

A. Assume your program controls a production line.

• If a product-code is "AB-123" and its weight is less than 25 grams, mark this product
as "Underweight".
• If the product is 25 grams to 30 grams, mark the product as "OK".
• If above 30 grams, mark the product for "Rework".

Write an if-statement (or series of if-statements) that processes this request.


Display the results in a MessageBox.

Hard-code the test variables at the top of the routine using these statements. For
simplicity, pretend they are input by end-users:

private void button1_Click (object sender, EventArgs e)


{
string productCode = "AB-123";
int productWeight = 22;

Be sure to test with different product-codes and weights by simply changing the hard-
coded values.

Is this type of test a good candidate for a "switch" statement? Why or why not?

What if you had several product codes with similar tests?

B. Modify the switch program 3.7 (the switch myColor program) so it tests against all
lower-cased variables – regardless of what case was used to populate the original test-
variable.

myColor = "RED";
myColor = "Red";
myColor = "rEd";

Have all possibilities properly detect as "red".

Chapter 3 - If Statements and Branching Page: 156


C. Write a program that tests a Prefix-salutation field (e.g. Mr., Mrs., Ms., Dr., etc) for valid
data. As before, simulate the user's input using a hard-coded test variable:

string prefix = "Mr."; //Change this variable to other possibilities for testing

e.g. Mr. (Pass, with no changes)


Mr (Pass, but append period "Mr.")
MR. (Pass, but change to "Mr.")
mr. (Pass, but change to "Mr.")
Mrs. (Pass, with no changes)
Xx. (Fail with error message)

If the field is valid, allow the value to pass and MessageBox what was typed.
If not in the list, display a MessageBox error of some type.

As an added feature, if the salutation were typed with improper case (all caps, lower or
mixed-case) or is missing a period (Mr vs Mr.), correct what is being tested and allow it
to pass.

Of interest: This can be solved using a switch statement or if-else statements. No other
fancy code is required. This should be challenging.

D. With the following pizza variables, display a message showing if you've built my favorite
type of Pizza. I happen to like Round Pizzas that are Medium or Large, never Small and
a Combo or Vegi are acceptable, with Thin crust.

Shape: "Square", "Round"


Size: "Small", "Medium", "Large"
Style: "Combo", "Cheese", "Vegi", "All Meat"
Crust: "Thin", "Chicago"

Hard-code the test variables at the top of the routine:

private void button1_Click (object sender, EventArgs e)


{
string pizzaShape = "Round"; //Manually change to other
string pizzaSize = "Medium"; //values in order to test
string pizzaStyle = "Cheese"; //all possibilities
string pizzaCrust = "Thin";

Then, write logic testing what was entered in the string variables. If the pizza matches
the requirements, display a messagebox saying "Yes, pizza is editable" or "No it is not".
Run a variety of different pizza-models through your logic. No matter what is entered, it
should compute to a proper Yes or No.

This can be written with nested-if and or compound if-statements.

Chapter 3 - If Statements and Branching Page: 157


Solutions:

There are many possible solutions to these problems. Here are some suggestions:

Production-Line Problem with Product-Code AB-123


if (productCode == "AB-123")
{
if(weight < 25)
MessageBox.Show("Product under weight");
else
{
if(weight >= 25 && weight <= 30)
MessageBox.Show("OK");
else
MessageBox.Show("Rework");
}
}
else
MessageBox.Show ("Not the correct Product Code");

Detecting all possible cases for Color "Red"


myColor = "rEd";

switch (myColor.ToLower())
{
case "blue":
MessageBox.Show("Blue was found");
break;

case "red":
MessageBox.Show("Red was found");
break;

case default:
MessageBox.Show("Color not in list");
break;
}

Chapter 3 - If Statements and Branching Page: 158


Prefix Validation
string strPrefix = "Mr."; //Change this to various test values

switch(strPrefix.ToUpper())
{
case "MR.":
case "MR":
strPrefix = "Mr."; //Force a known value regardless!
break;

case "MRS.":
case "MRS":
strPrefix = "Mrs.";
break
}

MessageBox.Show("The Prefix is: '" + strPrefix + "'";


//Notice how this solution corrects missing punctuation.
//Notice how this solution corrects casing problems: "mRs."

The pizza problem with two likely solutions:

if (shape == "round")
{
if (size == "medium" || size == "large")
{
if (style == "cheese" || style == "vegi")
{
if (crust == "thin")
MessageBox.Show("This is it!");
else
MessageBox.Show("The crust is wrong);
}
else
MessageBox.Show("Wrong style");
}
else
MessageBox.Show("Not the right size");
}
else
MessageBox.Show("I only like round pizzas");

Pizza Problem
if(pizzaShape == "round"
&& (size == "medium" || size = "large")
&& (style == "combo" || style = "vegi")
&& crust == "thin")

MessageBox.Show("Eatable");
else
MessageBox.Show("Send back!");

Chapter 3 - If Statements and Branching Page: 159


Chapter 3 - If Statements and Branching Page: 160
Appendixes
Appendix A - Compiler Error Messages

Alphabetic Listing

This is an alphabetic listing of various compiler messages with likely solutions. These are
from Visual Studio 2005, SP1 through VS 2014.

Errors and warnings are sorted alphabetically. Search by the first non <variable> word.
e.g. "Argument '2': cannot convert from 'double' to 'float' will be found under "cannot..."
Messages such as "The type arguments..." will be under "The"; Messages that begin with
punctuation ("; expected") are listed first.

Additional Comments on Error Messages:


If you have repaired a mis-typed line in your program but the same error message
continues to show, try the following from the editor window: Select menu choice: "Build,
Rebuild Solution".

"; expected" (semi-colon expected)


Also: Invalid Expression Term "."

Symptoms:
The compiler normally shows exactly where a semi-colon is expected and when you get this
error it is normally flagged at the very end of a line. If the compiler shows it in the middle
of a line, it can get confusing.

Problem:
This incorrectly typed command would show an expected missing semi-colon at the
Convert.ToString phrase.:
MessageBox.Show Convert.ToString(loopCounter); //missing paren

Solution:
In this MessageBox example, note that the MessageBox.Show phrase was incorrectly typed;
it is missing a set of parenthesis. This confuses the compiler like something awful. The
correct syntax is:

MessageBox.Show (Convert.ToString(loopCounter));

Problem
In this incorrectly typed command, the word "if" is 'misspelled' with a capital "I" instead of
a lower-cased "if":
If (IsBlank(testString)) //Capital "If" is wrong

A110: Database problems, various

See "A Network-related or instance-specific error occurred while establishing a connection


to SQL Server"

A constant value is expected

Common Error Messages and Solutions Appendix A: 3


Possible Solution:
In a 'switch' statement, a 'case' is using a variable instead of a hard-coded literal or a
constant. Replace the case <variableName> with case "quoted-string" or number.

A list that is this enumerator is bound to has been modified. An Enumerator can only be used if the list
does not change. (Sic)

Symptoms:
Attempting to delete an item from an array, comboBox, listBox, etc, while in the middle of
a foreach loop.

Issue:
You cannot delete an array-item while in the midst of a foreach loop.
Mark the item's position (counter) – typically in another temporary array and use a separate
loop to remove them, after the first loop completes.

A local variable named 'e' cannot be declared in this scope because it would give a different meaning to
'e', which is already used in a 'parent or current' scope...

Symptoms:
The top of the module, typically button1_Click, already has an 'e', as in "EventArgs e" and
you probably have a try-catch that also uses "(Exception e)"

Recommendations:
See button1_Click's signature line and compare it with the catch statement's signature lines

Change the "(Exception e)" to "(Exception e2)" with corresponding changes to e2.Message.
Or consider moving (most) of the logic from button1_Click to its own routine: e.g.
A100_Process(); which won't have an 'e' in its declaration.

A Namespace does not directly contain members such as fields or methods.

Possible Solution:
Do not define variables or methods (functions) above the form level; form-class level.
If you are trying to make a "global" variable, see Chapter 7.

A Network-related or instance-specific error occurred while establishing a connection to SQL Server

Symptoms:
While opening a form that uses SQL server resources.

Solution:
Confirm that the SQL Server is running and you have rights to the database.
If the SQL Server is running locally, on the LocalHost, confirm the Microsoft SQL Server
Services are started. From Windows, Start-Run, "Services.msc"; Confirm SQL Server
(SQLExpress) is started

An Error has occurred while establishing a connection to the server.


When connecting to SQL Server 2005/2008, ... (this failure may indicate) ...

Common Error Messages and Solutions Appendix A: 4


SQL Sever does not allow remote connections.
Could not open a connection to SQL Server.

See SQL: An Error has occurred while establishing a connection to the server....

An object of a type convertible to 'string' is required.

Issue:
"return" is not returning the correct 'type'.

Solution:
Examine the method's signature line to see if it returns a string, integer or other type of
object. The corresponding return statement(s) within the module must also return that same
'type'.

For example, a string function needs to return a <string> value.


It cannot return (nothing)

example code:
private string myFunction()
{
if (util.IsBlank(mystring))
return mystring;

Meanwhile, a void function can only return (nothing):

private void myFunction2()


{
stuff
return;

An object reference is required for the nonstatic field....

This is a generic error that generally means the compiler cannot find the variable or an
associated class was not instantiated.

Possible Solution:
If the variable or method in question is in a different Class, do one of the following:
a) Declare the variable as "public" or "internal" and instantiate the class within your
Form/Class using the "new" keyword. See Chapter 6, External Class Libraries, for
details.

b) Declare the variable as "public static" <string> or

c) If the method is in error, consider declaring the method as "public static...." or better
yet, "internal static" as in

internal static string B000_INILoad.B021_DiscoverINIFileName();


//returns string

Possible Solution:

Common Error Messages and Solutions Appendix A: 5


If calling a class.method within another 'sub-Class' (see Chapter 6; where cl800_Util was
placed within the PayrollTools.cs Class), there may not be a "constructor" in the new
(payroll) class and because of this, the (cl800_Util) declaration may not have a place to run.

Move the declaration into another method, directly above the Instantiation.
In simpler terms, move "cl800_Util util;" just above the line "util = new cl800_Util();"

Possible Solution:
If you have just switched a variable from a local variable to a "public static" variable, re-
compile the program using menu Build, Rebuild Solution.

Possible Solution:
Misspelled or wrong case variable name.

Possible Solution:
Especially when using a (Form's) properties. Do not use the current Form's name (it was not
instantiated within itself); instead, use "this."

ProgramGlobal.IformLeftPos = frmA000Form.Left;
ProgramGlobal.IformLeftPos = this.Left;

Note: You could also simply use "... = Left;", which is considered too vague for
most people even though the code would work.

ArgumentoutOfRangeException was unhandled

Argument out of range exception (s) are always due to an array being unalloacted, un-
available or a value [x] within square-brackets was using a larger number than the size of
the array. This always indicates a logic or counting problem and often the problem happens
at the end of a loop, where you over-shoot by one position. Remember, arrays are base-0; a
ten-item array's last position is [9].

See also "Index out of range"

If manipulating SQL data, statements, such as:


dataGridView1.Columsn[0].xxxxx = "yyyy"
may indicate the SQL Server service has not started on your development machine or the
remote SQL server is not available.

In any case, array arithmetic should be protected with a try-catch (if using a for-next loop or
are addressing [addresses] directly. Consider using a for-each loop, if logic is appropriate.

Argument '1': cannot convert from 'object' to 'string'


Also: The best overload method match for '<form(parameter)>' has some invalid arguments.

Issue:
The parameter you are trying to send is something other than a <string>; often the results
are an object-type or a "collection".

example:
frmA031CategoryAdd addCat = new frmA031CategoryAdd
(dataGridView1.SelectedRows[0].Cells[0].Value);

Common Error Messages and Solutions Appendix A: 6


...SelectedRows[0].Cells[0].Value is not necessarily a string. You can test this
by adding a .ToString() method and placing a debugging breakpoint at the statement. While
debugging, hover the mouse before the ".ToString()" method to see the value is missing
quotes – indicating it is not a string.

Possible Solution:
Convert it to a string using one of these two techniques:
... (dataGridView1.SelectedRows[0].Cells[0].Value.ToString());
... ("" + dataGridView1.SelectedRows[0].Cells[0].Value);

Argument '1' must be passed with the 'ref' keyword


Also: The best overloaded method match for '<class>(ref string, string)' has some invalid
arguments.

Solution:
When using "By Reference" (ref), both the calling and the called functions need the 'ref'
keyword. C# requires this for documentation purposes.

Example:
appendDefaultAreaCode (ref myPhoneNumber, locationDefaultAreaCode)

private void appendDefaultAreaCode


(ref myPhoneNumber, locationDefaultAreaCode)
{

Array creation must have array size or array initializer

Issue:
Sometimes you can declare an open-ended array with a simple statement, such as:
string [] afoundFields;
but if the array is used inside of a loop (while-statement), C# often requires that the array be
initialized with a starting value or by declaring a fixed array size. This is incase the while
statement never runs and downstream commands may panic.

Solution:
Initialize the array with an item count. Consider over-allocating.
string [] aFoundFields = new string [100];

Array does not have that many dimensions

Possible Solution:
Assuming a single-dimension array (a linear array),
aArrayName.GetUpperBound(0);

where (integer 0) is the first dimension of the array, "aArrayName".

Presumably you used aArrayName[x,x], when the array only had one dimension,
aArrayName[x].

'<btnClose>' is a 'field' but is used like a 'method'

Common Error Messages and Solutions Appendix A: 7


<btnClose> is a field but is used like a method

Summary:
Typed as btnClose()
Should be typed as an Event: btnClose_Click(null, null);

See also: "is a field but is used...."

Build Failed - with no compiler error messages

Likely solutions - Do all:


Close Visual Studio
Using Windows Explorer, open the Project's folder; delete all "*.suo" files
Re-Launch VS; select top-menu View, Output Window
Rebuild solution.

If still an error, look in the output Window. (See top-menu, View, Output)

Related: See "The type or namespace name 'Tasks'....

Cannot access a closed registry key

Symptoms:
Usually while performing a .GetValue(stringName)

Solution:
Move the RegKey.Close command below the GetValue statements. If the GetValues are in
a loop, be sure the Close is after the loop.

Cannot Assign to '<string name>' because it is a 'foreach iteration variable'

Symptoms:
Attempting to manipulate an array-element from within a foreach loop.

Issue:
Within a foreach loop, you cannot modify or change the values used by the foreach loop.
More to the point, you cannot transform or change the array's internal elements with a
foreach loop.

Solutions:
If you are merely trying to change the value of the array's element, move the value to a
secondary (intermediate) temp-string. Consider this example, with particular attention on
strtempString:

foreach (string strtempString in aTestArray)


{
//Note: strtempString cannot be manipulated directly
//within the loop

//Use an intermediate value to manipulate


string tstring = strtempString;

Common Error Messages and Solutions Appendix A: 8


tstring = tstring.ToUpper();
}

If your intent is to actually change the value(s) of the items in the array, you cannot use a
foreach loop. Instead, use a for-next loop.

for (int ii = 0; ii <= aTestArray.Length - 1; ii++)


{
//This actually modifies the values in the array...
aTestArray[ii] = aTestArray[ii].ToUpper();
}

Note the loop runs to the Array's length, minus-1 – a base-0 calculation
See the Array Chapter, "Transforming Array Elements" for more details.

Cannot connect to <Server> (SQL Server Management Studio)

Symptoms:
When attempting to launch SQL Server Management Studio

Possible Solution:
Are the services (Start, Run, Services.msc) "SQL Server" started?

<argument>: cannot convert from 'double' to 'float'

Solution:
A numeric parameter must be specified as a floating point number "F"
e.g.
Pen myPen = new Pen (Color.Black, 0.3) should be
Pen myPen = new Pen (Color.Black, 0.3F)

Cannot convert method group '<various: GetLength, etc>' to non-delegate type 'int'. Did you intend to
invoke this method?

Possible solution:
ilastHighlighted = myFiles.GetLength();

Did you remember the closing parenthesis?

Cannot Convert method group '<name>' to non-delegate type 'bool'. Did you intent to invoke this
method?

Possible Solution:
if using an implied comparison in an if-statement:

if (A100_SomeMethod_ThatReturns_Bool)
{
//Incorrect, missing ()
}

you forgot the parenthesis after the method name. Instead:

if (A100_SomeMethod_ThatReturns_Bool() )

Common Error Messages and Solutions Appendix A: 9


{
//corrected with ()
}

if (A100_SomeMethod_ThatReturns_Bool() == true)
{
//optional
}

Cannot currently modify this text in the editor. It is read-only

Solution:
Close the running program before attempting to modify either the code or the design-view.
You cannot edit while the program is running.

Either close the running VS program (your program) or in the Visual Studio Editor (ISE),
click ribbon-bar "Red Square" icon to abruptly close your program.

Cannot convert null to 'System.DateTime' because it is a non-nullable value type

Issue:
A DateTime method is attempting to return a null value to the calling module when only
"DateTimes" are allowed. This often happens in a try-catch error condition.

Solution:

Change the signature line from


private DateTime A100_MethodName()

to a "nullable" DateTime, where the <brackets> are required.

private Nullable <DateTime> A100_MethodName()


{
try
{
//Do stuff here
}
catch
{
return null;
}
}

Test in the calling routine using


if (returnedVariable.HasValue)

where the HasValue method only operates on items with a nullable data-type. See below for
more information on this.

Optionally, in the case of this examle's DateTime value, you could also use this command,
bypassing the Nullable solution: return DateTime.MinValue;

Solution:

Common Error Messages and Solutions Appendix A: 10


Change the original DateTime value to a "nullable" variable by using a question-mark in the
declaration.

:
DateTime? dtValue = (some date/time or null if not available);

With this, the downsteam function can return a null, if it has the need to do so.

:
if (dtValue.HasValue)
return dtValue.Value;
else
return null;

<procedureName> cannot declare instance members in a static class

Symptoms:
Usually when building a new method or function near the "static class program" / "static
void Main" class – the main driving procedure for your program. You have tried to use a
"private void <functionName>" within a "static" class.

Solution:
Consider changing
private void <functionName> to
private static void <functionName>

Cannot implicitly convert type 'int' to 'string'

Symptoms:
Code is trying to display a text message, a MessageBox, assign a text label, or assign a text
field with both text (string) data and numeric data. The numeric data refuses to cooperate.

Solution:
Use Convert.ToString on any numeric fields (or other non-string data-types) before moving
them or concatenating them to another string [field].

MessageBox.Show ("variable 'i' is set to this value: " + Convet.ToString(I));


textBox1.Text = "The number is equal to " + Convert.ToString(valueA));

also: <variableName>.ToString();

Cannot implicitly convert type 'long' to 'int' (are you missing a cast?)

Possible Solution:
Examine the return values of the command you are using. It likely is returning a 'long'
value, not an integer. The error will be flagged deep within the code, but it is the function's
(method's) signature line where you may need to make the fix.

For example:
private int A630_ReturnFileLength (string strpassedFileName)

Common Error Messages and Solutions Appendix A: 11


and: return fi.Length; <error complains here

but the fix may be changing the "int" to "long" on the Signature line.
Change "private int ..." to "private long ..."

CS0029
Cannot implicitly convert type 'string' to 'System.Windows.Forms.Label'

Solution:
Be sure to use a ".Text" when populating a label.
For example, assigning a blank string to a label:

Incorrect: lblmyField = "";


Correct: lblmyField.Text = "";

Cannot implicitly convert type 'System.Data.CommandType' to 'SystemLdata.Sqlclient.SqlCommand'

Issue:
Missing method name ".CommandType"

example code:
SqlCommand refCategoryCMD = new SqlCommand("RecordCategoryDelete");
refCategoryCMD = CommandType.StoredProcedure; //In error

Solution:
refCategoryCMD.CommandType = CommandType.StoredProcedure;

Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a
cast?)

Symptoms:
You are using a string array and attempting to assign a value to another text field.

For example:
lblDisplay.Text = aNames[1]; //fails
MessageBox.Show(aNames[1]); //fails

Solution:
Convert to String prior to assigning. This can be done explicitly or implicitly:

lblDisplay.Text = aNames[1].ToString();
lblDisplay.Text = (string)aNames[1];
MessageBox.Show("" + aNames[1]);

Common Error Messages and Solutions Appendix A: 12


CS0029
Cannot implicitly convert type 'string' to 'bool'
Cannot implicitly convert type 'int' to 'bool'

Symptoms:
In an "if" or other conditional.

Likely solution:
Did you use a required double-equal in the conditional?
if (testString = "Smith") vs
if (testString == "Smith")

Cannot implicitly convert type 'string[*,*]' to 'string[]'

Likely explanation: A multi-dimensioned (dynamically-sized) string array was declared in a


higher scope using and later dimensioned with actual sizes:

string [,] aCollectionNames; //Declared at a higher scope

and then later, in a different method, initialize with a fixed size, as in:

aCollectionNames = new string [15,4]; //Dimensioned

The author had this error after several mistyped array definitions. But once the array was
declared, as described above, the error persisted. Finally, after selecting menu "Build,
Rebuild Solution"; the problem went away.

Cannot implicitly convert type 'string' to 'System.Windows.Forms.TextBox'

Likely Solution:
You neglected the ".Text" appendage.

For example:
Incorrect:
textBox1 = textBox1 + Convert.ToString(<variable>);

Correct:
textBox1.Text = textBox1.Text +
Convert.ToString(<variable>);

For example:
MessageBox.Show("'" + pnlCategoryCode + "'");
vs
MessageBox.Show("'" + pnlCategoryCode.Text + "'");

Cannot implicitly convert type 'System.DateTime?' to 'System.DateTime'. An explicit conversion exists


(are you missing a cast?)

Issue: You are using a Nullable <DateTime> and since a Null is allowed, you must re-
convert to the same type. This seems redundant in code because the called function may
already be returning a Date Time. Re-cast the returned value:

Common Error Messages and Solutions Appendix A: 13


Example Syntax:

DateTime dtfileDate;
dtfileDate = (DateTime)A700_ReturnFileCreateDate(textBox1.Text);

See also "Cannot convert null to 'System.DateTime' because it is a non-nullable value


type" and consider a "nullable" declaration or cast (e.g. DateTime? dtValue;)

See Chapter 24 for further discussions.

Cannot Insert an explicit value into a timestamp column (SQL)

Issue:
SQL data field was defined as 'Timestamp' but C# code is trying to insert a Date. Change
the SQL field definition to a date-time or date format.

Cannot use local variable '<variable>' before it is declared

Likely Solution:
Declare (and possibly initialize) the variable before using:
string myString = "";
if (myString = "House")

Also, you can see this message if an if-statement, either above or below the first error has a
mis-spelled "Else" (vs "else") or missing parenthesis. This may take a while to locate in
large modules.

Possible (and likely) Solution:


In the function or method, is the last "return" statement mis-spelled, as in capital-R-Return?
or is the "return" clause otherwise mal-formed.

Changes are not allowed while code is running...

Solution:
Your program is still running from your last compile (F5 / Run). Locate the program on the
task bar and close before attempting to run it again. Alternately, from the Editor, press
Shift-F5 to force-close the program.

Command Line Arguments not parsed; Command Line Arguments ignored

By default, Visual Studio will not allow passed command line arguments, even
though the Start Options are set in the Project's properties.

Symptoms:
The program will behave as if no command-line arguments were passed, especially
if you compile a Release version of the program. Make this additional change in
the program:

Common Error Messages and Solutions Appendix A: 14


In Project Properties, Security, click [x] Enable ClickOnce Security Settings.
Then click "This is a partial trust application".
Click the Advanced button
Unclick "Debug this application with the selected permission set"
Click OK
Click "This is a full trust application"

Alternately: In Project Properties, left-nav, click Security.


Uncheck "Enable ClickOnce Security Settings".

Control cannot fall through from one case label ('case "<label>":') to another

Solution:
In a 'switch' statement, a "case" statement is missing a break; command, as in

case "Green":
<do stuff here>
break;
case "Red":
<do other stuff here>
break;

<formanme> does not contain a constructor that takes 1 arguments

Solution:
The call, typically on btnFormName_Click, instantiates a new form, as in:
frmA031CategoryMaint catMaint = new frmA031CategoryMaint("");
catMaint.InstanceRef = this;
catMaint.ShowDialog();

where it is passing one parameter, in this case, null, typed as ("").

But in frmA031's constructor, at

public frmA031CategoryMaint()
{

(this example) does not show any parameters.

The method's signature line must match the calling statement's (values). The two must
match the same count of parameters.

<DataGridView> does not contain a definition for Cells and no extension method Cells accepting a first
argument....

See below: "does not contain a definition for 'Cells'....

CS1061

Common Error Messages and Solutions Appendix A: 15


'<Classname>' does not contain a definition for "<MethodName>' and no extension method
'<MethodName>' accepting a first argument of type '<ClassName>' could be found
(are you missing a using directive or assembly reference)

For example: 'MainProgram' does not contain a definition for "A000_Base' and no extnsion
method 'A000_Base' accepting a first argument of type 'MainProgram' could be found (are
you missing a using directive or assembly reference)

Likely solution:
In another class (e.g. MainProgram.cs), you have not yet created or have misspelled a
method called "A000_Base".

<formname> does not contain a definition for <event such as 'checkBox1_CheckChanged'>


<formname> does not contain a definition for <'textBox1_TextChanged'>

Symptoms:
This is an Event problem where the original Event's code was either deleted or renamed in
Code View, but the pointer to the event was not changed in the Event Properties.

Solution(s):
There are two ways to correct this error. Either is acceptable.

1. Double-click the error and the editor will take you to the [Form1.Designer.cs] class;
and as scary as this may look, delete the entire highlighted line.

2. Or, open the <event> properties (Lightning Bolt) for the control in question and delete
the event information from the property screen. For example, if this were a
textBox1_TextChanged event, delete the detail-text after the (lightning-bolt) event.
Doing so still leaves the "textChanged" code, orphaned, in the program. It should be
deleted by hand.

Common Error Messages and Solutions Appendix A: 16


<System.Windows.Forms.DataGridView> does not contain a definition for Cells and no extension
method Cells accepting a first argument....

Solution:
In the foreach clause, did you use "DataGridViewRows" (and not just "DataGridView")?

foreach (DataGridView currentRow in dataGridView1.SelectedRows)


MessageBox.Show("Selected: " + currentRow.Cells[0].Value;

Entering Break Mode failed for the following reasons: Source file <server-drive....form.cs> does not
belong to the product being debugged.

Cause:
A previous project was moved from a server-drive to a local disk.
Reference paths still point to the (old) server location.

Solution:
With Visual Studio 2005 or above, select menu Build, Clean Solution followed by Build,
Rebuild Solution.

With Visual Studio Express, these menu choices may not be present. Do the following:
a. Close the Visual Studio Project
b. Using Windows Explorer, locate the solution; delete the "bin" and "obj" sub-
directories. Re-open the Solution and the problem should be fixed.

error CS0234: The type or namespace name 'Tasks' does not exist in the namespace
'System.Threading'

Error visible in the View, Output pane.


See error message "The Type or namespace name 'Tasks'

ExecuteNonQuery: Connection Property has not been initialized.

When using a Stored Procedure and attempting a SAVE or INSERT (ADD) operation.
Missing a connection clause with the SqlCommand. For example:

Incorrect:
SqlCommand refCategoryCMD =
new SqlCommand("RecordCategoryUpdate");

Corrected:
SqlCommand refCategoryCMD =
new SqlCommand("RecordCategoryUpdate", refCategoryConn);

where "refCategoryConn" was the connection defined earlier in the routine, as in:
string strConnection = "Data Source = <servername\\SQLExpress;" +
"Initial Catalog = <database name>;" +
"User ID=<sa>; Password = <password>";
refCategoryConn = new SqlConnection(strConnection);

Common Error Messages and Solutions Appendix A: 17


<method name> hides inherited member 'SystemWindows.Forms.<object>' Use the new keyword if
hiding was intended.

example message: Form1.left(string, char)' hides inherited member


'system.windows.forms.control.left'.

Cause: The name of your function/procedure/method is the same as a built-in name. e.g., if
you built a function called "Left". This is a new warning, starting with Visual Studio 2010.

Solution:
This error can be ignored. But consider renaming your function. For example, instead of
"Left", use "LeftStr". In general, single-word functions, such as Left, Mid, Right should not
be used.

Field '<name>' is never assigned to, and will always have its default value null (warning)

Possible Solution:
A variable was declared but was never set equal to anything. The 'variable' does not need to
be a normal variable, it could be a class name. Consider this example when declaring an
external class library with the "new" statement either commented or not typed in the proper
location:

clSiteGlobals SiteGlobals;
//SiteGlobals = new clSiteGlobals();

IDE1006 Naming rule violation: These words must begin with upper case characters: <button1_Click>

This is an informational message. Rename the procedure or method, shifting the first
character to upper-case. This is to follow recommended naming standards for cross-
platform programs.

Identifier Expected

Possible Solution:
When declaring a function, are all the parameters in the parameter list prefixed with a data-
type? Missing "string", "int", etc.

Incorrect: static bool IsNumeric(passedString)


Correct: static bool IsNumeric(string passedString)

'<btnClose>' is a 'field' but is used like a 'method'


<btnClose> is a field but is used like a method

Likely Solution:
You are calling a button-event from another location but forgot or mis-typed the event-
name.
btnClose ("", null); //Incorrect – not just the btn name
btnClose_Click ("", null); //Corrected: _Click was missing

Index Out of Range (DataGridView)

Symptoms:

Common Error Messages and Solutions Appendix A: 18


Commands that use a column index position, such as

dataGridView1.Columns[0].HeaderText = "SEQ";
dataGridView1.Columns[1].Width = 55;

must be written after the statement that populates the actual grid. See Chapter 27 for
examples.
GetMyData(strSelectString)

Confirm the SQL Server (SQLExpress) services are running (Services.msc) or the remote
server is available.

See also: ArgumentoutOfRangeException was unhandled

Index was outside the bounds of the array

Symptoms:
A generated error, usually from a try-catch, where array operation attempted to access a
point not in the array – usually one position beyond the end of the array [max n + 1].

If you are not looping through the array and are directly accessing the array (e.g. variable
[n]), then likely the array was not populated with data; especially with a previous .Split
command.

Possible Diagnostics:
If using a foreach loop, place a debug break point at the top of the loop and monitor the
loop. If you suspect the error is (1000 records) into the loop, add this diagnostic logic to the
program and break within the if-statement:

foreach ....
{
if (recordCount > 999)
MessageBox.Show
("Reached suspected error; put break point here");
// <regular processing here>>
}

If using a ".Split" and a subsequent command accesses a variable-field [n] directly, likely
the split found an empty record and had nothing to split into the array. After the split, check
for blank records before executing the (parsing) logic within the loop.

Symptoms:
If you are processing a CommandLine (arguments list), what happens when no command-
line arguments are passed? If you reference aargs[1], the program would abend. Consider
this statement:

if (aargs.Length >= 2 && aargs[1].ToUpper() == "/DIAG")

where the double-ampersand is absolutely required in the test.

Index was outside the bounds of the Array (SQL)


Also: Index was out of Range

Symptoms:

Common Error Messages and Solutions Appendix A: 19


After executing a SQL statement, such as a ExecuteReader.

Possible Solutions:
Confirm the SQL Service is running (Start, Run, Services.msc; look for MSSQL/SQL
Server).

Confirm you have the proper "strConnection" (Data Source=<Franken8>)

Confirm the SQL SELECT statement (an assembled string) includes the field-name you
need and it is punctuated with appropriate commas and spaces, especially within the
assembled string.

Are you trying to reference a [column] position before a DataGridView was populated?

Invalid Expression Term ',' (plus "; expected") when using a picture clause

Likely symptoms:
You are using a picture clause (with a String.Format).

Solution:
Did you forget the words "String.Format ("?

Invalid Expression Term '{' (when using a picture clause)

Solution:
String.Format requires a string, even if a single numeric variable is being formatted.
Encompass the phrase with quotes:
textBox1.Text = String.Format ({0:dddd}, dtValue); //Incorrect
textBox1.Text = String.Format ("0:dddd}", dtValue); //Correct

Invalid Expression Term "."


See "; expected".

Invalid Expression term 'else' and ";expected"

Possible Solutions:
The "if" clause above the errored line requires braces for the "then" section. Sections with
more than one command require braces to group them.

if (valueA == valueB)
{
<stuff>
<more stuff>
}

Possible Solutions:
Does the if-statement-clause have an unneeded semicolon on the if-clause itself? Remove
the semicolon.

InvalidCastException was unhandled

Common Error Messages and Solutions Appendix A: 20


See Error: Unable to cast object of type 'System.Windows.Forms.TextBox' to type
'System.IConvertible'.

Invalid Column Name '<field>' (SQL Read)

Symptoms:
An Invalid Column Name <field name> during a SQL Read or SQL ExecuteReader and the
field name is obviously right, when examined in SQLServer Management Studio.

Possible Solution:
The assembled strSQLstmt (the SELECT statement) is mal-formed, usually a space is
missing in a quoted string, especially on the last field-name, just before the FROM clause.
Set a breakpoint at the ExecuteReader and examine the IntelliTrace. For example, in this
illustration, notice how the "FROM" is crammed next to the field "Comment":

Invalid token '{' in class, struct, or interface member declaration

This message generally means the compiler is confused about an opening or closing brace
or there is a mis-placed semi-colon that confuses where the compiler expects a brace.

Possible Solution:
You have a semicolon at the end of an if-statement; while-loop or for-next-loop or remove
an unnecessary semi-colon from the end of a statement:

Common Error Messages and Solutions Appendix A: 21


private void xxxxx (object sender, EventArgs e); (bad semicolon)
while (loop stuff); (bad semicolon)
if (condition); (bad semicolon)

Possible Solution:
There is a statement or group of statements typed after the module's closing brace. Make
sure all your code is above the closing brace (e.g. above button1_Click's closing brace).

Possible Solution:
Check to make sure that all opening braces have a closing brace and all braces are lined-up
properly. Especially near the end of the program/namespace.

Invalid token 'string' in class, struct, or interface member declaration

Likely solution:
In a statement, such as:

public string SomeMethodNameHere()

where "string" is flagged as an error.


Be sure the word "public" (private, etc.) is lower-case. Not "Public".

Login Failed for user <xxxx>. Reason: Server is in script upgrade mode. Only the administrator can
connect at this time. Error 18401.

Solution:

The SQL Server service just started and the engine is updating tables. Wait a few minutes
and try the SQL connection again.

<method> is inaccessible due to its protection level

Solution:
The method in the Class Library are "private".
Set to either:
"public" if the Class is instantiated, or if the class is in the same namespace as the calling
routine.

Set to "public static" if the Class is not instantiated and it is in another namespace.

MessageBox is a 'type' but is used like a 'variable'

Solution:
You forgot to use a dot-method with the command.
For example: MessageBox.Show (...)
where the .Show was missing

Must declare the scalar variable "@<variable name".

Common Error Messages and Solutions Appendix A: 22


Symptoms:
During a btnSave event, when using replaceable parameters

Solution:
A field was used in the UPDATE/INSERT statement but it was not defined with an
"AddWithValue" clause. For example:

refCategoryCMD.Parameters.AddWithValue
("@NonRequiredField",
util.StripSQLinjections(pnlNonRequiredField.Text));

Also check the SQLstmt in two places (once for INSERT and once for EDIT), making sure
the field-name is listed, and within the parenthesis of the field list:

strSQLstmt = "INSERT INTO refCategory " +


"(RecordCategoryCode, RecordCategoryDesc, DeleteInhibit, NonRequiredField) " +
"Values ( @CategoryCode, " +
"@CategoryDesc, " +
"@CheckBox, " +
"@NonRequiredField )";

strSQLstmt = "UPDATE refCategory SET " +


"RecordCategoryCode = @CategoryCode, " +
"RecordCategoryDesc = @CategoryDesc, " +
"DeleteInhibit = @CheckBox, " +
"NonRequiredField = @NonRequiredField " +
"WHERE (RecordCategorySeq = '" + strEditPassedRecordSeq + "')";

Newline in constant

Likely Solution:
You are appending a "\" backslash character in a string, probably to build a directory-path.
Backslash is a reserved character. Use double-backslashes to represent a single backslash.

serverName + "\" + directoryName //error


serverName + "\\" + directoryName //corrected

No overload for method '<method name>' takes 0 arguments

Likely solution:
Typically with a button or other on-screen event, such as a button, notice the signature line
of the button; there are two parameters. For example, btnSomething_Click(object
sender, EventArgs e). When calling an event like this, make your call in this fashion:

btnSomething_Click(null, null);
Passing a null value for each item in the signature line.

No overload for method '<method name>' takes '1' arguments

Solution:

Common Error Messages and Solutions Appendix A: 23


You are attempting to pass <1> parameter via the signature line to a function that either
needs more than one value or no values. In other words, the two signature lines need to
match, parameter-to-parameter. Double-click the error to locate the invalid call statement.
Then locate the routine it is calling; once found, look at its signature line (the items in
parenthesis; they must match).

Non-invocable member 'System.IO.FileInfo.Length' cannot be used like a method


Non-invocable member 'System.Windows.Forms.Control.Text' cannot be used like a method

Solution:
You are attempting to use a 'property' as-if it were a method. In other words, remove the
trailing parenthesis. For example:
fi.Length ( ); //is incorrect; use instead:
fi.Length;

or: pnlMsg.Text ("some text in quotes here"); //incorrect; use instead:


pnlMsg.Text = "some text here";

Object reference not set to an instance of an object


Use the "new" keyword to create an object instance.

This is a generic error that can be hard to resolve.

Solution:
Generally it means something is mis-spelled.

For example: RegKey.GetValue("ApplicationzzzName").ToString()


has a mis-spelled parameter.

Solution:
You are calling another method, in another library, without having first instantiating the
object. For example, when using the cl710_Formatting.cs library's "ProperNames"
function, you may need to declare the library either at the top (Class level) or within the
current function (e.g. button1_Click):

formatting = new cl710_Formatting();


then: formatting.ProperNames(<stuff>);

Solution:
You have declared a variable, such as a string, an array, a number, but have not initialized it
to a value; the variable still contains nulls.

For instance, a string declared but not initialized:


string myName;

if (myName.Length == 5) ... Generates this error

For instance:
string [] aMyArray;

with: aMyArray[1] = "Dog" will generate the error.

Common Error Messages and Solutions Appendix A: 24


In each case, initialize the variable with a non-null value before using it in any equation or
comparison. With an array, instantiate the value with the "new" keyword:

Only assignment, call, increment, decrement, and new object expressions can be used as a statement.

Symptom 1:
In a for-next statement you have mis-keyed one of the three required phrases. For example,
this statement has an error in the first phrase:
for(i; i <= 10; ++i)

Possible Solution:
you can't use a simple variable in the first part of the phrase; it must have an assignment
clause. The statement correctly typed is:
for(i=1; i <= 10; ++i)

Possible Solution:
A method, such as
sr.Close(); or
A180_ClearDateEntryFields();
was typed without opening and closing parenthesis.

Operator '&' cannot be applied to operands of type 'string' and 'string'

Solution:
Use the "+" symbol to concatenate strings. You used to be a Visual Basic programmer,
weren't you?

Operator '&&' cannot be applied to operands of type 'bool' and 'string'

Possible solution:
In a complex if-statement or while-loop clause, would an extra set of parenthesis help?

while (lineCount < linesPerPage &&


( strReadLine = myAsciiFile.ReadLine() ) != null)

Operator '==' cannot be applied to operands of type 'string' and 'method group'
Operator '==" cannot be applied to operands of type 'method group'
Operator '+' cannot be applied to operands of type 'string' and 'method group'

Likely Solution:
You forgot a "( )" after a function name.
"ToString" vs "ToString()" is commonly missed.

For example:
If using a method, such as .ToLower; as in ...textB.ToLower
did you remember the required parenthesis, as in: textB.ToLower()
if (textB.ToLower == "dog") //incorrect
if(textB.ToLower() == "dog") //correct

For example, also in a written method call:


if (A027_CheckPreviousCount == 15) //incorrect
if (A027_CheckPreviousCount() == 15) //correct

Common Error Messages and Solutions Appendix A: 25


Operator '>=' cannot be applied to operands of type 'string' and 'string'

Symptoms:
You are using a > or < conditional when comparing two strings, as in:
if (testString >= "Brown")

Solution:
You can't use >, < operators against two strings. This is different than (VB). Instead, see
string.Compare(string1, string2, T|F);
string.CompareOrdinal(string1, string2);

Operator "||" cannot be applied to operands of type 'int' and 'int'

Symptoms:
You are using an equal sign in an if-statement; you need double-equals for the comparison.
e.g.
if (passedPhoneNumber.Length = 7 || passedPhoneNumber.Length = 8)

should be:
if (passedPhoneNumber.Length == 7 || passedPhoneNumber.Length == 8)

CS0019
Operator '+' Cannot be applied to operands of type 'TextBox' and 'TextBox'
Operator '+' cannot be applied to operands of type 'System.Windows. Forms.TextBox'

Solution:
You forgot to include the object's (field) dot-property after the object's name.

Consider this code:


label1.Text = textBox1 + textBox2;
label1.Text = textBox1.Text + textBox2.Text;

CS0642
Possible mistaken empty statement

Symptom:
On an if-statement, while, or for-next statement

Likely Solution:
Although this is a warning, it is most likely a true error. Do you have a superfluous
semicolon after an if-clause, for-next, or other loop statement?
Remove the semicolon and let the next line (or the next set of braces) act as the end-of-line.

if (stringA == stringB); // <- Remove this semicolon


{

Warning: Possible unintended reference comparison; to get a value comparison, cast the left hand side
to type string

Common Error Messages and Solutions Appendix A: 26


Symptom:
When comparing a string array to a string:
if (alSomeArray[iposition] == "some fixed string")
The ~ warning appears after runtime, not during editing

Solution:
Do one of the following by casting explicitly or implicitly:

if (alSomeArray[iposition].ToString() == "some fixed string")


if ((string)alSomeArray[iposition] == "some fixed string")

Note: The error will only clear after run-time; it will not clear during the editing session
(VS2010).

Property of indexer '<class.variable>' cannot be assigned to – it is read only.


Property or indexer '<class variable>' cannot be assigned to - it is read only.

Solution:
If this is in an if-statement, did you remember to use double-equals (==)?

Solution:
In the "get/set" routines, typically in a Global External Class Library, there is not any logic
for the "set". If your intention is to make a read-only variable, either remove the logic in
your program that is trying to set the variable's value (e.g. myName = "Smith") or add an
empty-set routine, which ignores the myName = Value statement. Fixing the error is
preferable.

Property Value Not Valid (Dialog box)


Property Not Valid

Solution:
Your program is still running while trying to edit the source code. Close your running
program before changing the code or an object's property (e.g. Click the editor's ribbon
icon: "Red Square").

Send Error Report / Don't Send "Please tell Microsoft about this problem"

Symptom:
When you ctrl-alt-Break your program and your program may be in an infinite loop or
otherwise crashed. Microsoft sees this as a problem and offers to send a diagnostic error
report to Redmond. This message is annoying and should be disabled.

Solutions:
Click "Don't Send," then make this registry key change to your workstation.

Start/Run/Regedit
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PCHealth\ErrorReporting. Dword
Value: DoReport, 0 = Don't Send.

Common Error Messages and Solutions Appendix A: 27


SQL: An Error has occurred while establishing a connection to the server. When connecting to SQL
Server 2005/2008, ... (this failure may indicate) ... SQL Sever does not allow remote
connections. Could not open a connection to SQL Server.

Possible Solutions:
1) If you are connecting from a remote computer: In Microsoft's Surface Area
Configuration tool (SQL 2005), confirm that "Local and remote connections" is
selected. Choose TCP/IP

See: "C:\Program Files\Microsoft SQL Server\90\Shared\SqlSAC.exe"

2) If you are connecting from a remote computer: Consider starting the Windows Service:
SQL BROWSER

3) Does the SQL Express Server have a software Firewall installed? For example, if using
Windows XP SP2 with Windows Firewall, open the Firewall's control panel; click
Exceptions: Add this program: sqlserver.exe. Also add SQLbrowser (udp port 1434).

Other Solutions:

4) You are using the wrong server name in the connection string.

With SQL Server 2008:


string strConnection =
"Data Source = Franken8;" +
"Initial Catalog=Address;" +
"User ID=sa;Password=<yourpassword>";

With SQL Server 2005:


string strConnection =
"User ID=sa;Initial Catalog=Address;Data Source=FRANKEN8\\SQLEXPRESS";

or use ...Data Source=LOCALHOST\SQLEXPRESS If a local database

This can be especially true if you have moved your application from one computer to
another (when in development) and your Development SQL Server also moved. The
author had this problem when moving from a Desktop to a Laptop.

5) You are using the wrong Catalog (database name). e.g. from Chapter 16 "Address"

6) And of course, the wrong username and or password. If the password is encrypted; did
you decrypt it prior to executing the command?

SQL: Login Failed for user <xxxx>. Reason: Server is in script upgrade mode. Only the administrator
can connect at this time. Error 18401.

Solution:

The SQL Server service just started and the engine is updating tables. Wait a few minutes
and try the SQL connection again.

Common Error Messages and Solutions Appendix A: 28


Static member '<namespace.class.variablename>' cannot be accessed with an instance reference;
qualify it with a type name instead.

Solution:
Was the External Class Instantiated (with a "new" keyword)?
If so, remove the "static" modifier from the variable's declaration and use the "new"
variable's name as a variable prefix.

e.g. SiteGlobals = new clSiteGlobals ();


then: MessageBox.Show (SiteGlobals.CompanyName)

If the External Class was not Instantiated (using Quick and Dirty Global Variables), prefix
the variable name using the physical Class Name, as seen in Solution Explorer. Do not use
the "new" keyword.

For example:

Use:
MessageBox.Show(<namespace name.> clSiteGlobals.CompanyName);

'String' does not contain a definition for 'length' and no extension method 'length' accepting a first
argument of type 'string' could be found (are you missing a directive or an assembly
reference?)

Solution:
Capitalize the .Length property, as in:

switch (strfoundString.Length)
{
:
}

'System.Configuration.ConfigurationSettings.AppSettings' is obsolete: 'This method is obsolete, it has


been replaced by System.Configuration!
System.Configuration.ConfigurationManager.AppSettings (depricated)

Symptoms:
When attempting to use an app.config file and
MessageBox.Show (ConfigurationSettings.AppSettings ["<variable name>"]);
And yet, the statement still works properly, except for a compiler warning.

Solution:
In Solution Explorer, References, add a Reference to .NET, System.Configuration.dll
Then change the call statement to
MessageBox.Show (ConfigurationManager.AppSettings ["<variable name>"]);
See the App.Config chapter for full details.

'System.DateTime.Now' is a 'property' but is used like a 'method'

Solution:
Remove the parenthesis from the .Now. This is not Excel.

Common Error Messages and Solutions Appendix A: 29


= DateTime.Now; //Not DateTime.Now()

System.FormatException: 'Input string was not in the correct format.'

Likely solution:
You are attempting to Convert.ToInt32(textBox1.Text) and the textBox was empty or
contained non-numeric values, such as a hyphen or other character.

This is a run-time error that should have a try-catch clause.

System.InvalidCastException: 'Unable to cast object of type 'System.Windows.Forms.TextBox' to type


'System.IConvertible'.'

You will see this message if a textBox or other string field is blank and is trying to be
converted to a numeric value - Visual Studio 2012 and older.

You will also see this if Convert.ToInt32(textBox1.Text) - where the .Text property is
missing.

Note: This is a run-time error (an unhandled exception) and your program has crashed.
Provided this is not a syntax error, consider a try-catch block.

System.Windows.Forms.TextBox - various:

Issue: Likely missing ".Text" appendage.


See:
Cannot implicitly convert type 'string' to 'System.Windows.Forms.TextBox'

CS1061
'System.Windows.Forms.<field>' does not contain a definition for 'text'

Possible Solution:
Usually this means you mis-typed or more likely, mis-capitalized a field's property value.

Consider:
Field1.text (with a lower-cased .text) vs
Field1.Text

Other properties exhibit similar type messages when mis-spelled.

CS1061
'MainProgram' does not contain a definition for "A000_Base' and no extnsion method 'A000_Base' accepting
a first argument of type 'MainProgram' could be found (are you missing a using directive or
assembly reference)

Likely solution:
In another class (e.g. MainProgram.cs), you have not yet created or have misspelled a
method called "A000_Base".

'System.Windows.Forms.MessageBox' is a 'type' but is used like a 'variable'.

Common Error Messages and Solutions Appendix A: 30


Solution:
You forgot to use a method: MessageBox.Show (
e.g., you forgot the ".Show"

This is incorrect: MessageBox("Hello World");


Corrected: MessageBox.Show("Hello World");

The best overload method match for '<form(parameter)>' has some invalid arguments

See "Argument '1': cannot convert from 'object' to 'string'.

The best overloaded method match for 'string.PadRight(int,char)' has some invalid arguments

Solution:
When PadLeft or PadRight, the pad-fill is a character, not a string. Delimit a single
character with tic marks, not quotes.
e.g. strtestValue.PadRight(ipadLength, '*');

The best overload method for 'System.Windows.Forms MessageBox.Show(string)' has some invalid
arguments.

Possible Solution:
MessageBox.Show must have a string as the first item in the "show list". For example:

MessageBox.Show (comboBox1.SelectedItem); //errors; not really a string.


MessageBox.Show (Convert.ToString(comboBox1.SelectedItem)); //works

You can also trick the method by appending an empty-string before the first object being
displayed. Often, the object is converted to a string automatically, but the MessageBox
doesn't know this. Force it to get past the compiler by putting an empty-string in the front:
MessageBox.Show ("" + comboBox1.SelectedItem);

Another way around this problem is to use a ".ToString()" method. For example, with this
RegistryKey example (snippet):

MessageBox.Show (RegKey.GetValue("ApplicationName").ToString());

The class name '?' is not a valid identifier for this language

Likely Solution:
Close all frm (forms), then close and re-open the solution. It appears the development
environment can get confused, especially if you have been deleting methods.

The current project settings specify that the project will be debugged with specific security settings

Symptoms:
When using File-IO functions or Command-Line arguments (and others)

Select Project, Properties, Security


Uncheck the "Enable ClickOnce Security Settings"

Common Error Messages and Solutions Appendix A: 31


The Insert Statement conflicted with the Foreign Key constraint <key name>... (SQL)

Symptoms:
When attempting a SQL Insert where the main table has a relationship with a sub-table. For
example, adding a new NAMES record, pointing to Record Category = 1, when using:
tblNamesCMD.Parameters.AddWithValue("@RecordCategorySeq", 1);

Problem:
@RecordCategorySeq = 1
Looking in the RecordCategory table, there is not a record with a value "1"

The left-hand side of an assignment must be a variable, property or indexer.

Example Problem Statement:


if (String.Compare (strReadLine, null) = 0)

Possible Solution:
With if-statements, use a double-equal signs (not single) when comparing values; as in:
if (String.Compare (strReadLine, null) == 0)

The name 'ConfigurationManager' does not exist in the current context.

Solution:
See "System.Configuration.ConfigurationSettings.AppSettings' is obsolete: "

Common Error Messages and Solutions Appendix A: 32


CS0103
The name '<variable>' does not exist in the current context.

Symptom 1:
You have not declared the variable using "string", "int", "float", etc, as in:

string myString;
int aNumber;

Or you have declared the value as "myString" but used the variable later as "mystring"
(case-sensitive).

Or you declared the variable in another (routine or module) and that declaration is outside
the scope of your current routine/method/module. A variable was declared in another
construct (such as within a for-next loop) and that construct has ended.

Consider the integer i, which is declared as part of the for-next loop but was used in a
MessageBox outside of the loop; in this case, variable "i" was out of scope and cannot be
used.

for (int i=1; i <= 10; ++i)


{
<stuff to do>
}
MessageBox.Show ("Variable i = " + Convert.ToString(i));

Solutions vary. Check variable declarations.

Symptom 2:
Error: The name '<FixedSingle>' does not exist in the current context.
You are setting a Property incorrectly, such as
textBox1.BorderStyle = FixedSingle

Possible Solution:
The item on the Right-side of the equal sign may need to be prefixed with a property name,
as in:
textBox1.BorderStyle = BorderStyle.FixedSingle

Note: BorderStyle requires a "using System.Windows.Forms;" statement or you can fully-


qualify the name, as in:
textBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle

Possible Solution:
If the 'name' is a keyword-like name:
The name 'IsNumeric' does not exist.... do you need a Class Library prefix, such as:
util.IsNumeric?

The type arguments for method 'System.Array.Resize<T>(ref T[], int)' cannot be inferred from the
usage. Try specifying the Type Arguments explicitly

Issue:

Common Error Messages and Solutions Appendix A: 33


Array.Resize only works with single-dimensioned arrays. You cannot resize multi-
dimensioned arrays; you cannot resize List<T> arrays.

Solution:
Manually copy existing array to a new, larger array -- but you will have problems that the
new array will have a different name. There does not seem to be a good solution to this
problem.

The type or namespace name 'boolean' could not be found (are you missing a using directive or an
assembly reference?)

Consider this called function, which returns a boolean:


static boolean IsBlank(string passedString)

Should be typed as:


static bool IsBlank(string passedString)

C# is inconsistent in how one should spell boolean. When used in a function, use "bool".

The type or namespace name 'CurrentUser' | 'Local Machine' does not exist in the namespace
'Registry' (are you missing an assembly reference?)

Symptoms:
When attempting to read a specific registry key from the Windows Registry

Solution:
Confirm you have a "using Microsoft.Win32;" at the top of the program.
Then use this prefix in the RegistryKey command:

RegistryKey RegKey =
Microsoft.Win32.Registry.LocalMachine.OpenSubKey
(@"Software\Test");

The author is unsure why the "Microsoft.Win32.Registry" prefix is required when a "using"
statement is in place.

More generally:
You are missing a 'using' statement (e.g. using System.Management;).
if this does not resolve the problem, often you can add a new "Reference" (in Solution
Explorer). The name will usually be the same ("System.Management").

The type or namespace name 'DllImport' could not be found (are you missing a using directive or an
assembly reference?)

Possible Solution:
Add these two statements at the top of the (DllImport) class:

using System.Collections;
using System.Runtime.InteropServices;

Note "DllImport" is spelled with lower-cased -el's Dll's

Common Error Messages and Solutions Appendix A: 34


The type or namespace name 'Return' could not be found...

Solution:
Spell "return" with a lower-case 'r'.

If a Void function:
return;

If a non-Void function:
return (some-variable);

The type or namespace name 'single' could not be found (are you missing a using directive or an
assembly reference?)

Solution:
With floating point numbers,
Use Single (with a capital S) or "float" instead.

Unlike "int", Single does not have a shorter alias. Many programmers prefer "float".

The type or namespace name 'StreamWriter' / 'StreamReader' / 'WriteLine' could not be found (are
you missing a using directive or an assembly reference?)

Likely solutions:
Confirm "using System.IO;" near the top of the program.

Confirm you are using the variable name on the WriteLine method.

For example, this is incorrect:


System.IO.WriteLine("my text to write");

Use this:
myreviewFile.WriteLine...

The type or namespace name 'Tasks' does not exist in the namespace 'System.Threading'

You are likely using one of the Wait methods and System.Threading.Tasks is only available
in dot net 4.0 and higher.

Solution:
In the project, select top-menu "Project, Project Properties".
Change the target framework from .NET Framework (3.5) to version 4.0 or newer.
Re-compile.

The type or namespace 'Windows' does not exist in the namespace 'System' (are you missing an
assembly reference?) File: cl800_Util.cs

Likely solution:

Common Error Messages and Solutions Appendix A: 35


You are writing a Console application and have imported the cl800_Util library. cl800's
"Wait" routines call a System.Windows.Form module -- which Console applications do not
allow.

Recommended Solution:
Delete cl800_Util from Solution Explorer and re-add as a "Copy" (not as a link). Once
added, locate the WAIT routines and remove them from the cl800_Util library. Because
cl800 is copied, you are only damaging this program's local version. If you write a lot of
console applications and wish to continue using cl800, move the WAIT logic into its own
library.

Note: The text was changed to reflect this need. All Wait routines were moved into their
own class library. You would see this message if you tried to combine them contrary to
what the book recommends.

Related Solutions:
Console applications cannot call any "Windows-like" method. For example,
MessageBox.Show will not work in a console application. Adding a "using
System.Windows.Forms" defeats the purpose of a console application.

There were build errors. Would you like to continue and run the last successful build?

Symptoms:
When you compile (F5) your newly-written program.

Solutions:
Select checkbox "Do not show again" and click No. In other words, you would never want
to run the previous version of your code (before newly introduced bugs; you really want to
see the current bugs).

If you had already checked yes, see Tools, Options, "Projects and Solutions", "Build and
Run". Set "On Run, when build or deployment errors occur" to "Do not Launch".

Unable to cast object of type 'System.Int32' to type 'System.String' (SQL ExecuteRead)


Unable to read record (SQL)

Symptoms:
When attempting to read a SQL record.

Solution:
When assembling the SELECT statement (strSQLstmt), the "WHERE" clause's record
number (e.g. usually a SEQuence number), must be enclosed in tic-marks. For example:

Incorrect:
:
"WHERE NameSeq = " +
strEditPassedNameSeq;

Corrected:
:
"WHERE NameSeq = " +
"'" + strEditPassedNameSeq + "'";

Common Error Messages and Solutions Appendix A: 36


See also:
Invalid Column Name (SQL)

Unable to cast object of type 'System.Windows.Forms.TextBox' to type 'System.IConvertible'. When


casting from a number, the value must be a number less than infinity.

Likely Solution:
A Convert.To phrase is missing a dot-property

Consider this flawed for-next loop fragment:


for (int loopCounter = 1;
loopCounter <= Convert.ToInt32(textBox2); ....

The "Convert.ToInt32( )" does not point to a particular property.

It should read
Convert.ToInt32(textBox2.Text)

I bet you used to be a VB programmer.

Unable to Read Record (SQL)


See Invalid Column Name (SQL)
See Unable to cast object of type 'System.Int32' to type 'System.String' (SQL)

Unrecognized escape sequence

A string was found with a "\" (backslash) character. This is a reserved character needed for
"escape sequences." If you need a backslash character in a string (typically for a file-
name\path), double-up the backslashes, as in: "C:\\data\\filename.ext"

\t = tab
\r = carriage return
\n = newline
\r\n = crlf
\\ = backslash
\' = tic
\" = quote

See Appendix Special Characters for details.

Use of unassigned local variable <"myInteger"> | <"myString">, etc

Possible Solution:
In your declarations, usually at the top of your routine, a variable, such as

int <myInteger>; or string <myString>

was declared but not initialized with an explicit value. Or you attempted to use a variable
on the right-side of an (equals) statement when it has not yet been populated by another
statement earlier in the code.

Common Error Messages and Solutions Appendix A: 37


Or, the variable was declared, but because of logic, was never assigned a value before being
used in another statement or calculation.

The variable needs an initial value, either explicitly or programmatically. Remember,


declaring a variable does not initialize it..

Another likely scenario is the variable was not initialized and a "while" loop was going to
set the value but the loop never ran (or more likely, the compiler thought the loop had a
possibility of never running).

Recommendations:
Consider using this type of syntax:

int myInteger;
myInteger = 0

or declare and initialize on the same line, as in:


int myInteger = 0;

Possible Solution:
The variable was declared in another module and has fallen out of scope.

Visual Studio cannot start debugging because the debug target <your project name\bin\debug> is
missing. Please build the project and retry, or set the OutputPath and AssemblyName
properties appropriately to point at the correct location for the target assembly.

Solution:
The Program must compile at least one time without errors or you will see this message.
Delete or comment-out the line causing a compiler error.
Run the program again (even if the program does nothing but display the form)
Close the running program and re-introduce the errors. This error should go away.

Solution:
Immediately after starting any new project, press F5 to compile the first empty-screen.
Then immediately close the running program and begin your coding work.

Solution (untested):
Select Menu: Project, Properties.
Go to "Build"; check the "Output" section at the bottom
Browse to your project's main directory/path, choosing Bin\debug"; this is where the actual
exe/dll lives.

When casting a number, the value must be a number less than infinity...

See
Error: Unable to cast object of type 'System.Windows.Forms.TextBox' to type
'System.IConvertible'.

Common Error Messages and Solutions Appendix A: 38


Appendix B - Compile and Distribution

This section discusses how to compile and distribute an .EXE.

Background:

When developing and testing a program, pressing F5 (top menu Debug, Start
Debugging) compiles the program, writes a temporary executable, and then
launches that .EXE as a separate task on the Windows task bar.

On the disk, Visual Studio builds a Debug folder in the Project's directory and in
there you will find a compiled .EXE and other support files – but only the .EXE is
needed for distribution. If you compile for "Release" (described below), a new
directory, "Release" is populated similarly.

The debug version (the .exe) contains code overhead that helps you test and
develop and this version is about 10 to 15% larger than a release version.
Although you can distribute the debug version to end-users, it is not recommended.

How to Compile and Distribute EXEs Appendix B: 39


Cheap and Easy EXE Distribution:

Follow these steps to compile your program as a stand-alone executable and to


give it a formal version number and release date. The resulting .EXE is not a full-
fledged, installable program, but it can be manually distributed (without a setup
routine) and the executable can be run from a server or from a thumb-drive, etc.
This method works well in corporate environments.

1. Open your Visual Studio solution as you would normally.

2. Select top-menu Project, (project name) Properties.

a. In the Project Properties screen, click the left-nav "Application" tab.


Change the "Startup object" from "not set" to your program's main routine,
often <ProgramName.Program>.

b. Click button, "Assembly Information".

Type a short description for the program and fill out the company, copyright,
etc.
Manually set an assembly version (version number) and a file version. The
GUID is a random number, which you should leave as-is.

c. On the left-nav, select "Build".


Change the top Configuration menu from "Debug" to "Active (Release)"
Recommend leaving the platform at "Active (Any CPU)".

3. Close the Properties tab and return to the editor.


On the top ribbon, change from "Debug" to "Release".

How to Compile and Distribute EXEs Appendix B: 40


4. Build the final code by choosing top-menu "Build", then "Build <your project's
name>"

5. Once built, use Windows File Explorer to open the project's "bin\Release" folder
(for example: C:\data\Proj\VS\FileManipulation\bin\Release)

The file (e.g.) FileManipulation.exe is distributable to end users or can be


positioned on a server. The file version is visible from Windows File Explorer.

How to Compile and Distribute EXEs Appendix B: 41


Virus Risks:

EXE files placed on a file server / file share, like all executables, are susceptible to
being infected by viruses. Be sure the EXE is in a read-only directory and your
development staff does not have write-access to the EXE or any DLL's in this
directory. This includes you. Use a service account, from a secured workstation,
when updating shared executables.

Protecting executables from viruses is a real-world


problem experienced by the author. A helpdesk employee's
machine was infected and they in-turn infected a variety of
executables on a main login-script server. As each
workstation logged in, they all were infected. It wasn't
until the next day the company's virus signatures were
updated. The Author now believes it is safer to distribute
executables locally, on each workstation. This makes
house-wide infections less-likely but is more problematic
when updating.

How to Compile and Distribute EXEs Appendix B: 42


EXE Icons

The taskbar and shortcut icon will be a default Visual Studio icon and your
program deserves better. Unfortunately, you will have to create, buy or steal your
own icon. Of the three techniques, one of them requires a bit of artistry and it is,
of course the most fun.

Obviously, thieving an icon is reprehensible and can get confusing if your program
shares the same icon as another. To help, Microsoft provides a free library of
icons, which can be found in the Microsoft Visual Studio Image Library,
downloadable at this link:

http://msdn.microsoft.com/en-us/library/ms246582.aspx

As of 2015.01, this is a downloadable .zip file. Extract all


files in the archive, then tunnel to
ImageLibrary\Actions\ICO. Other ICO libraries are near-
by.

The number of (Application) icons is limited, but the number of toolbar icons is
expansive. Regardless, it provides a good starting point, especially if you want to
draw a complete set of icons, with more on this in a moment.

Icon Files:

Icon files (.ico) are peculiar because they contain multiple images, at different
resolutions and different color depths. A fully-populated icon has these images
embedded:

16 x 16, 4-bit color


16 x 16, 8-bit color
16 x 16, 32-bit color

32 x 32, 4-bit color

How to Compile and Distribute EXEs Appendix B: 43


32 x 32, 8-bit color
32 x 32, 32-bit color

48 x 48, 32-bit color

256 x 256, 32-bit

To do an icon properly, you need a full-fidelity version at 256 x 256 pixels and
another at 48 x 48 pixels, followed by progressively smaller and less-detailed
versions. You will have poor results if you take a full-sized version and attempt to
scale it down to the smaller sizes; color shading and pixellation will occur and it
is beyond the scope of this book to describe the intricacies. As you will learn,
there is an art to creating icons.

Editing Icon .ICO files

Contrary to popular belief, you cannot create icons with most photo editors and
you can't edit them properly with MSPaint (it only sees one icon within the file).
There are ways to draw an icon locally, saved as a PNG, and then upload to a
website for ico conversion, but these are generally limited to one size, one icon.

As a free solution, Microsoft recommends this web-based editor. It will not build
the larger Windows-8 style tiles, but it is generally workable:

www.xiconeditor.com:
http://msdn.microsoft.com/en-us/library/gg491740%28v=vs.85%29.aspx

Using Visual Studio to Edit Icons

Amazingly, Visual Studio, the editor, can also edit ico (icon) files, but it comes
with infuriating limitations, only editing the 16 and 32 pixel icons. And it does not
seem to give full control over color pallets.

With the editor, you can view, but not modify 48 and 256-pixel icons. Also, it
does not appear capable of building a new icon file – it only works against existing
ones, but this restriction is easily worked around.

Even with these limitations, it is worth a moment to explore. Do the following:

A. Because you cannot create a new ico file with Visual studio, you must begin your
work with an existing icon. Locate a larger, full-fidelity icon, one with multiple
icons within the file. For example, from the downloaded ImageLibrary (see
above):

How to Compile and Distribute EXEs Appendix B: 44


ImageLibrary\Objects\ico\ActiveServerPage(asp)_11272.ico

Or search C:\Windows for any *.ico files.

Protect the original file by copying the .ico to a temporary location before editing.
I recommend creating an "Images" folder within your project and storing icon and
clipart files there.

B. From any Visual Studio Project, select File, Open. Tunnel to and open the .ico file
and it will open in a tabbed-window, next to your code and form designs. The left-
nav shows each of the different sizes. Note the editing ribbon bar is only available
on 16 and 32-pixel icons.

Example Icon in Visual Studio, showing multiple sizes

Once edited, close the tab and save the changes.

Attaching Icon Files to your Project:

The icon needs to be attached in two locations: One for the file system and a
second for the running program.

How to Compile and Distribute EXEs Appendix B: 45


1. Open the top-menu, Project, (your project's name) Properties. Select left-nav
"Application". In the Resources section, set the icon file by browsing to the icon
file. Illustrated "ActiveServerPage(asp)_11272.ico". Once selected, File Explorer
will show this as the EXE's default icon and it will automatically pick the
correctly-sized icon.

2. Return to the Form Editor (Form1, Design View). In the form's properties, locate
the "Icon" setting. Browse to the same .ico file and select. Again, the editor will
choose the properly-sized icon from within the file.

How to Compile and Distribute EXEs Appendix B: 46


3. Re-compile the program for the changes using F5 or F6-re-build. The icon will
show in File Explorer, on the program's (form's) title bar, on the Task bar, and on
any desktop shortcuts created. The icon file appears as a resource in Solution
Explorer.

How to Compile and Distribute EXEs Appendix B: 47


Creating Publishing / Distribution Packages

The "cheap and easy EXE" distribution method described above is my favorite
way of distributing a compiled program, but you can build a setup.exe that
automatically installs the software and builds an un-install routine. The benefits of
this design are:

• Setup.exe installs your program in a location of your choosing


• Adds an un-install to the Control Panel's "Add Remove Software" (Programs
and Features).
• It builds a desktop icon for the current user (In Windows 8, adds a tile to the
All Apps menu.

For example, from Windows 8's Control Panel, Programs and Features:

and from the Tile screen:

Building a Distribution Package:

1. Create a Release version of your application, as described earlier in this chapter,


then close the project.

How to Compile and Distribute EXEs Appendix B: 48


2. If this is the first time building an MSI package, you must install a Microsoft-
recommended InstallShield ("InstallShield Limited Edition for Visual Studio" - a
free product from a third-party).

Flexera Software
http://learn.flexerasoftware.com/content/IS-EVAL-InstallShield-Limited-Edition-V
isual-Studio

From the web site, download and follow the instructions. Once downloaded and
installed, close and restart Visual Studio.

3. Re-launch Visual Studio, selecting


New Project, Other Project Types, "Setup and Deployment"
Choose the "InstallShield Limited Edition Project" template

For the Location, type a path that is near but separate from your original Visual
Studio Solution. For example: C:\data\Source\FileManipulation\ (Deployment),
where "Deployment" is the recommended name. As usual, I recommend leaving
Create Directory for the solution and letting the "Name" become the actual
directory.

The new solution opens into an Install Shield Wizard with a row of buttons/icons
showing each step, starting with "Application Information." This is called the
Project Assistant and if you get lost, look in Solution Explorer and double-click the
Project Assistant

How to Compile and Distribute EXEs Appendix B: 49


4. In the InstallShield wizard's first step, "Application Information", fill out your
company name, web-address, and version number (not illustrated).

5. In Step 2/Icon 2 – "Installation Requirements", choose any restrictions you may


have, such as only installable on Windows 7 or newer and choose any required
software, typically Microsoft.Net Framework version 4.x. The screen is self-
explanatory.

6. In "Application Files", rename the default [ProgramFilesFolder] from


"InstallShield" to "MyCompany" or "MyApplication". This becomes the default
installation folder.

In the right-hand Name section, "other-mouse-click" and browse to your Release


version and add your final compiled EXE to the list. Add any additional INI files,
Readme.txt, etc, in this same location.

How to Compile and Distribute EXEs Appendix B: 50


7. In the "Application Shortcuts" section, choose where you want icons built,
typically the Start Menu (In Windows 8 this is the All Programs tile screen).

8. In the "Installation Interview" section, choose options as needed. Typically:

No - Do not display license agreement


No - Do not make users type their company or username
Yes - Allow users to modify the Installation Location
Yes - Allow the user to launch the application after install

9. Finally, select top-menu "Build", "Build Solution". Note that this is not part of the
Wizard steps. This completes the MSI build.

10. In Windows Explorer, tunnel to ...\Express\CD_ROM\DiskImages\Disk1

This is your Deployment directory – not your original program solution!

Results:
Note the Setup.exe, Setup.INI and .MSI file.

This entire directory can be positioned on a Share, CD, thumb-drive, etc. and is
ready for use.

Testing:

Launch Setup.exe and allow the program to install.

How to Compile and Distribute EXEs Appendix B: 51


Results:
• In this example, installation arrives at "C:\Program Files
(x86)\MyCompany\*.exe"
• Note desktop icon (if selected)
• If Windows 8, note the All Programs Tile installed as "Launch (your program
name)"
• In Control Panel, Programs and Features (Add Remove), you can un-install.

Possible Warning:

Warning: -7235: InstallShield could not create the software identification tag
because the Tag Creator ID Setting in General Information View is empty.
ISEXP: Warning.

Solution:

This is a warning and can be ignored with no harm. It suggests files required for
automatic inventory scanning are not in place and implies a corporate install. As
of 2014.03, this design is not in wide-spread use.

The warning can be resolved in one of two ways.

1. Disable the Inventory feature: In the Deployment Package's solution, Solution


Explorer. Tunnel to "Orgainize your Setup", "General Information". Scroll
down to the Use Software Identification Tag. Set Use Tag = no

2. Or Enable the Software Inventory by filling out the fields in the "Software
Identification Tag" section.

In Solution Explorer, tunnel to "General Information"

How to Compile and Distribute EXEs Appendix B: 52


Complete these fields:
Tag Creator Name: Your business name
Tag Creator ID: (See below to generate)
– example: regid.2009-04.com.yourBusinessName

Generating a Creator Tag:

Go to this site:
Magnicomp Software Tag Maker (free)
http://www.magnicomp.com/cgi-bin/mcswtagmaker.cgi

Complete the online form and generate an XML tag file.


Download and store the Tag file in your deployment's root directory.

My tag file was named this way:


2009-04.com.keyliner\regid.2009-4.com.keyliner.examplefilemanipulation_13
96226547.swidtag

a. In your Package's "Application Files" section, add the tag file, so it installs
at the same level as your .EXE program.

b. A second copy of the tag file must also be copied, using "Application
Files": (example file name):

%PROGRAMDATA%\2009-04.com.keyliner\regid.2009-4.com.keyliner.e
xamplefilemanipulation_1396226547.swidtag

Note: The Vendor's Generated Tag webpage will have the exact link and
name you should use – cut and paste.

Your MSI package must also build the Program Data directory:
"%PROGRAMDATA%\2009-04.com.keyliner"

where %PROGRAMDATA% value is a Windows system environment


variable. On Windows Vista and later the value is usually
C:\ProgramData

Recompiling:

If your original program is pulled for maintenance or enhancements, you must


rebuild the Release version *and* rebuild the Deployment version. Remember,
your source code and the deployment solution are two different Visual Studio
projects.

There are other features, such as automatic updates when version numbers change.
This is beyond the scope of this chapter.

How to Compile and Distribute EXEs Appendix B: 53


This completes the Compile and Distribution chapter.

How to Compile and Distribute EXEs Appendix B: 54


How to Compile and Distribute EXEs Appendix B: 55
How to Compile and Distribute EXEs Appendix B: 56
Version History:

1.01 2015.04.10
Initial Release. Submitted to Wrox Publishers; declined due to length and
competition with other titles.
Advanced copy to D.Parks for review

1.02
Chapter 19 Wait States
Expanded Auto-launch example, 19.5

Chapter 20 Printing
Added reference to "Add Reference" for System.Printing on Console
apps

Chapter 23. Files


Added "Directory.Create" exception note dealing with
C:\Program Files (x86)
1.03
Split into three volumes
CH0 - 11 + Appendixes A,B (Through Multiple Forms)
CH12 - 21 ASCII - Formatting + Appendix C
CH22 - 27 Arrays - SQL + Appendix D

How to Compile and Distribute EXEs Appendix B: 57


An Absolute Beginners Guide to C# - Volume 1
Visual Studio C# 2017
Intro -Through Forms
by Tim R. Wolf
2017.06 Version 1.04
Table of Contents

9 Chapter 1 - Introduction to the Editor 3


Your First Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Variables and Scope. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Working with Text Boxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Naming Fields
Concatenation
Default Text Values

9 Chapter 2 - Introduction to Loops 45


"while" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
loopCounter
Appending a String
Incrementing a Counter
Incrementing with "++"
Concatenating to Self with "+="
MessageBoxes
Infinite Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Breaking into the Loop
"while" Loop - Printing Numbers 1-10. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
"do" Loops.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
for-next Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Carriage-Return/LineFeed (CRLF)
Variations on "for-next" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Controlling Loops with Variable textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Interrupting Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
continue;
break Statements
"while-loops and 'continue'
Nested Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
foreach Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Loop Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110

9 Chapter 3 - Conditional Branching 117


Numeric and String Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Basic if-statement Construction
Numeric "if" Statements
Brace Style
"else"
Semicolon Rules
&& (AND) || (OR) Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
"|" and "||" (OR) Boolean
"^" (XOR – Exclusive OR) Boolean
Nested if-statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
"else if"
Math.Min - Numeric Testing for Smaller Value.. . . . . . . . . . . . . . . . . . . . . . . . . . 139
Compounding if-Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
switch Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Case "Value":
default Clause
Required breaks
Compound case Statements
Case-sensitive switches - .ToLower()
goto Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
Ternary Operator. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

9 Chapter 4 - Strings 163


Declaring Strings.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Special Character Strings (Escape Codes) \t, \r\n. . . . . . . . . . . . . . . . . . . . . . . . . . 167
Reserved Backslash
Carriage-Return-Line-Feeds CRLF
Embedded Quotes
Verbatim Text Strings - @
ASCII Codes
String Concatenation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
string.Concat ( ) and "+"
Floating Point Numbers
string.Compare( )
<string>.EndsWith. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
Detecting .XLS file Extensions
<string>.Length. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
<string>.ToUpper( ), .ToLower( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
<string>.PadLeft(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
<string>.PadLeft(int, '*');
Date Padding with Leading Zeros
<string>.Trim( );. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
<string>.TrimStart()
<string>.TrimEnd()
Trimming with other Characters
char.IsNumber ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
<string>.Replace( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Character to Numeric Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Conversions with Casting
null and Empty Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
String.IsNullOrEmpty( )
Null-Conditional Operator
Testing for "blank". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
if ((strtest + "").TrimStart().Length == 0)
Finding Strings - IndexOf. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Reading an Overload
<string>.LastIndexOf
<string>.Contains
Parsing and Substrings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
<string>.Substring. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Classic Left-strings .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Classic Right-string. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
Mid-Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
"Mid-String" for any Length Delimiter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253

9 Chapter 5 - Numbers and Dates 265


Integers Defined. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Floating Point Numbers Defined.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Casting and Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
Implicit Conversions
Explicit Conversions
try-catch Error Trapping. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Converting Strings to Numeric. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
TryParse
Rounding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Math.Round
Truncating Decimals. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
Math.Truncate
Basic Math Functions (Division). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
DivRem (Divide Remainder) / Mod. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
Math.DivRem
Mod "%"
Other Math Functions (SQRT, etc.). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
Sqrt (Square Root)
Random Numbers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
Dates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
DateTime.Now
String.Format
Date Parts
Date Format Pictures
DateTime.TryParse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
UTC (Zulu) Time
Empty Dates - Nullable Dates
DateTime.Compare. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Date.Compare: Two Files
Less Exact Date Comparisons
Rounding Dates
Optional Project: MTWRFSU.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
Optional Project: AllowByHour. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327

9 Chapter 6 - Utility Functions - Methods 333


IsBlank( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
IsFilled( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
LeftStr, RightStr and MidStr string Functions. . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
Classic LeftStr ( ) "Left-string".. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
Left-string with String Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Classic Right-String with Numeric Parameters.. . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Right-string using Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Mid-string with Numeric Parameters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Mid-string Numeric Start Position but No Length (Overload). . . . . . . . . . . . . . . . 382
Mid-string with string Delimiters and NumberOfCharacters. . . . . . . . . . . . . . . . . 383
Mid-string with Two String Delimiters.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388

9 Chapter 7 - Advanced Utility Functions 395


StripSlashes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
StripTrailingComments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
StripLastCharacter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
StripNonNumerics ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
StripNonNumerics, Preserving Decimals and Signs. . . . . . . . . . . . . . . . . . . . . . . . 424
Overloading
ParseBetweenDelimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Overloading ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
ParseKeyName: Master Function. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
IsNumeric ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463
IsNumbers ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
StripDuplicateCharacters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475
VerifyYN.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Passing Variables by Reference. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481

9 Chapter 8 - Class Libraries 493


Building an External Class Library from Existing Code. . . . . . . . . . . . . . . . . . . . 496
Linking an Existing External Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
Using the util. Class Library (CL800 Util). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
Link vs Copy
Inline "Program" Class Libraries (PayrollTools). . . . . . . . . . . . . . . . . . . . . . . . . . 513
Using CL800_Util within the new Class.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 520
Constructors
Manually Building a Class Constructor
Creating an "External" Class Library from Scratch. . . . . . . . . . . . . . . . . . . . . . . . 525
Compiling and Using DLLs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526
Using the DLL
"Modularizing" with Program-Control Functions.. . . . . . . . . . . . . . . . . . . . . . . . . 532
"void" Functions
Passing Variables to Program-Functions
Returning Values from a Program-Function
Naming Standards for Functions and Class Libraries.. . . . . . . . . . . . . . . . . . . . . . 535
Object Prefixes

9 Chapter 9 - Variable Scopes 543


Variable Scope, Demonstrated. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
Scope within Loops
Form-Level (Class) Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 551
"Global" Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554
"Quick and Dirty" Global Variables
"static" Modifier
Form1 to Open Form2
Using a Global Class to Pass Values.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564
Building an External Global Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 568
Getting and Setting Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 577
Building Get/Set Properties
Passing Variables by Ref.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 582

9 Chapter 10 - Form Controls and Events 589


Default Editor Settings - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594
Snap To Grid
Compiler Errors
Starting and Naming a New Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 598
Rename the Form
Recommended Form Properties
"Form Load" event
"this.Show"
Link the CL800_Util Library
AutoClose Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 609
textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 613
MaxLength
Default Text Value
Enabled
Password Fields
Multiple Lines
Setting Field Properties at Runtime. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 619
textBox Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 625
Enter and Leave Events
TextChanged
KeyPress Events (Intercepting Keystrokes). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 629
Allowing only Numeric Values
UpperCasing as Typed
Intercepting Shift, Alt and Ctrl Keystrokes
Alt-Key events on a button or other object
comboBox and listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 638
Static (Hard-Coded) ComboBoxes
AutoComplete / Type-Ahead
Query the Selected Value
"No Selection"
Using the SelectedIndexChanged
ComboBoxes linked to an External Data
listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 648
Blank-line Testing
listBox Multiple Selection
Processing Multiple Selected Records
checkBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 660
ThreeState
CheckChanged
CheckStateChanged
Click
Radio Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 669
Radio Button Events
Grouping
ProgressBar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 676
monthCalendar and DateTimePicker. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 681
.MaxDate
.MinDate
Form Load Event
Date-Time Math
MenuStrip. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 689
Horizontal and Vertical Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 692
ToolTips. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 694
ToolBox: "PictureBox" Icons and Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
Launching Other Applications From the Graphic
Link Labels. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Label Tricks.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 704
Transparent Labels
Using Labels in Your Program
Tab-Order. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 706

9 Chapter 11 - Calling Secondary Forms 711


Opening Secondary Forms - Simple. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 716
Issues with a Simple Form Call
Using a Global Variable to Re-Open Form1.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 724
Using a FormReference to Open Form2 - Recommended. . . . . . . . . . . . . . . . . . . 727
"FormRef" properties
Modal and Modeless Forms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 735
Transferring Data Between Forms using Global Variables. . . . . . . . . . . . . . . . . . 737
Using Quick-and-Dirty Global Variables
Using a Global Class (Global Variables)
Retrieve the Stored Value in Form2
Passing Data with a Signature and a Constructor. . . . . . . . . . . . . . . . . . . . . . . . . . 741
The Parent's Call
The Child (Form2) Constructor
Passing Values using get-set Properties - Recommended. . . . . . . . . . . . . . . . . . . . 745
Form Starting Positions - Global Class. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 753
MessageBox Overloads. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 756
Custom Dialog Boxes: NS810_Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 759
Returning Results to the Parent via Properties
Custom InputBoxes: NS815_InputBoxDialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . 777
Detecting a KeyPress ENTER Key Event.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 785
Using NS815 to Pass Arrays instead of Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . 787
Multiple Input Screens in the Same Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 789

9 Appendix A - Compiler Error Messages 3

9 Appendix B - Compile and Distribution 38


Cheap and Easy EXE Distribution:.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Virus Risks
EXE Icons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Using Visual Studio to Edit Icons
Attaching Icon Files to your Project
Creating Publishing / Distribution Packages.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Generating a Creator Tag
Absolute Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 04 - Strings
Chapter 4 - Strings

"Strings," which have already been used in earlier chapters, are nothing more than a
'string' of character data. Examples include words and phrases, as well as non-numeric
data, such as "123 N. Elm Street", phone numbers, part numbers, and the like. This
chapter describes basic and advanced string manipulation techniques.

In my experience, string manipulation is the most useful


concepts in this entire book and this is where my applications
typically spent most of their energy. Dissecting and
manipulating strings are what most programs do for a living.

Topics:

• string declarations
• \escape codes, special characters, @verbatim strings, ASCII strings
• string.Concat and + - appending/ Concatenation
• string.Compare(A, B, true)
• .EndsWith
• .ToLower(), .ToUpper()
• .PadLeft(), .PadRight()
• .Trim() - for lobbing-off leading spaces and other characters
• .TrimStart(), .TrimEnd()
• .Length
• Character arrays

• null and empty strings tests using "+"").TrimStart( ).Length > 0"
• Testing for Blank/empty data
• .IndexOf("=",0) - Finding string positions

• Overloading, introduced
• .LastIndexOf - searches from the back-end forward
• if(<string>.Contains("="))
• IsNumeric, limitations

• .Substring
• (LeftString, RightString, MidString)
• Substring with multiple delimiters

Many of the examples in this chapter use the


MessageBox.Show (<some string value here>) to display the results of a string
manipulation. The MessageBox is a simple dialog box that appears on the screen and is
being used as a diagnostic and teaching tool. Naturally, the results could be put in other
locations.

Chapter 4 - String Manipulation Page: 163


Common string Commands, summary
<string>.ToUpper() //Shift to uppercase
<string>.ToLower() //Shift to lowercase

<string>.PadLeft(<int>, '*') //Pad with 'char' for total len.


<string>.Trim() //Trims leading and trailing
<string>.TrimStart() //Trims only leading
<string>.TrimEnd() //Trims only trailing

<string>.Replace("AB", "xy") //Replace "AB" with "xy"

Convert.ToString(integer-number) //Convert number to string


Convert.ToInt32(<string>) //Convert string to number
Convert.ToSingle("123.32") //Convert to floating point

string Search and Positions, summary


int = <string>.Length //base-1 length of a string

int result = String.Compare(strA, strB, true)


//Compares <, = > on strings.
//result: 0=equal, 1=GT, -1=LT
//true = ignore case

if (String.IsNullOrEmpty(<string>) //Recommend using util.IsBlank()

if ((testString + "").TrimStart().Length > 0)


//Test for null, empty,
//all-space
//Recommend using
//util.IsBlank()*

int = <string>.IndexOf("=", 0) //Searches for "string" and


//returns base-0 count,
//starting position zero.

int = <string>.LastIndexOf("=") //Searches from the end


//for "=";
//returns base-0 count.

if (<string>.EndsWith(<"value">)) //Returns boolean


if (<string>.Contains("=")) //Searches and returns T/F

*Function from Chapter 6

Chapter 4 - String Manipulation Page: 164


Manual Left, Right and Midstrings, summary
<string>.Substring (<start position base-0>)
<string>.Substring (<start position>, <number of characters>)

Left Strings always start at position zero:


<string>.Substring (0, 10) //Left 10 Chars.
<string>.Substring (0, delimiter-foundNDX).Trim()

Right Strings:
<string>.Substring (<string.Length> - 10).Trim()
//Right 10 Chars.

<string>.Substring (<delimFrontNDX> + <delimFront>.Length).Trim();


//Right, after delim

Mid Strings:
<string>.Substring (<start position base-0>,
<number of characters>)

<string>.Substring
(delimiterFrontNDX base-0 + delimiterFront.Length,
<number of characters>)

<string>.Substring (delimiterFrontNDX base-0 +


delimiterFront.Length,
delimiterBackNDX - delimiterFrontNDX -
delimiterFront.Length).Trim()

//See also the util.Left, .Right, .Mid commands in cl800_Util.

(Split) //See Chapter 10

if (Char.IsNumeric('<single-byte char-value>'))
if (Char.IsNumeric("string value", #-of-characters))

//See util.IsNumeric for strings.

•Setting up Example Programs for this Chapter:

For the remainder of this chapter, practice the examples by building a test program
similar to the ones used in prior chapters. All examples below use a screen with
"button1" and (sometimes) textBox1. See Chapter 1 for instructions.

Chapter 4 - String Manipulation Page: 165


Declaring Strings

String variables must be declared (defined) before they can be used. Do this with a case-
sensitive "string" command. The keyword "string" declares what type of variable is
being declared, followed by the name of the variable.

string <invented variable name>;

String variable names can be any single word or phrase, with no spaces. The name you
assign is an invented name of your choosing. This author likes to prepend the string-
name with "str" as in "strtestVariable".

Optionally, the variable can be initialized with a default value on the same line, or it can
be assigned a value separately with a different statement. Use quotes to surround hard-
coded text strings (also called a 'literal').

Examples:
string strtestVariable;
string strtestVariable2 = ""
string strtestVariable1 = null
string strtestVariable3 = "123 N. Elm Street";
string strtestVariable4 = textBox1.Text;
string strtestVariable5 = strtestVariable4;

strtestVariable = DateTime.Now.ToShortDateString();

A string can be declared as an empty-string (quote-quote) – meaning a string was


assigned, but no (real) value was applied. This is different than a "null" string or an
uninitialized string with more on this later in the chapter. Strings can also be assigned
values from other strings or from a textBox.Text or they can be populated from the
results of other functions or methods.

Chapter 4 - String Manipulation Page: 166


Special Character Strings (Escape Codes) \t, \r\n

Tabs, carriage-return-line-feeds, reserved characters, and other white-text are also strings
but these cannot be typed directly into the program's code. In other words, how do you
type a "tab" without invoking the keyboard's tab and how do you place a quote-mark
within a quoted string?

To work around this, some strings are represented in the editor with special characters,
called "escape codes".

Tab \t (ASCII 9)
Backslash \\ (ASCII 92)
Double-Backslash \\\\ (ASCII 92, 92)
Double Quote \" (ASCII 34)
Single-quote (tic) \' (ASCII 39)
Carrage-Return-LineFeed \r\n (ASCII 13, 10)
Unicode \uxxxx where xxxx = unicode number

These characters are typed as 'clear text' (typed in the editor as a literal value) with a
leading backslash (" \ "), signifying their special nature. For example, a tab is
represented with "\t". Although the "\t" is typed as two characters, the compiler treats
it as a single character – a tab.

string strtabExample = "\tJohn Smith";


string strtabExample2 = "John\tQ.\tSmith";
string strmultiLineField = "four score \r\n and seven years";

Thus, "\tJohn Smith" equals "tab John Smith"


and "John\t\Q.\tSmith" equals "John tab Q. tab Smith".

Reserved Backslash:

The backslash (" \ ") is reserved as an escape sequence marker. If that actual character is
needed in a string, use double-backslashes (" \\ "), again where two typed characters
represent a single character. Since double-backslashes are used in server and share
names, those would require four backslashes (" \\\\ "):

string strserverName = "\\\\payroll\\shareName"; e.g. \\payroll\sharename


string strinputFileName = "C:\\Data\\test.txt"; e.g. a DOS Path

This can get you in trouble if you make a mistake. Consider this flawed string:

string strinputFileName = "C:\\Data\test.txt";

where the embedded slash-t (\test.txt) was intended as a DOS path but was interpreted as
a tab – "C:\Data (tab\t) est.txt". In this case, the editor would not have flagged this as an
error.

Chapter 4 - String Manipulation Page: 167


See below for Verbatim text strings for additional information.

Carriage-Return-Line-Feeds CRLF:

Carriage-return-line-feeds (hard-returns or sometimes called CRLF) are represented by


this sequence: "\r\n" – a return plus a line feed, for a total of two characters. Escape
codes can be embedded directly in a quoted string or they can be assembled with
concatenation:

"four score \r\n and seven years" or


"four score" + "\r\n" + " and seven years"

Results - noting the line break and the word " and", with a leading space:
four score
and seven years

In the second half of the example, three literals were combined into one string, using a
"+" (concatenation) and both examples are functionally equivalent. Concatenation is
discussed in a few pages.

Alternately, Microsoft recommends:


Environment.NewLine

as in: "four score " + Environment.NewLine + "and seven years".

where "NewLine" is case-sensitive. Environment.NewLine theoretically translates into


other languages with less effort. See also ASCII Codes, below.

Embedded Quotes:

Since strings are usually initialized with a quoted literal (e.g. "Bob said"), an embedded
quote-mark ( " ) cannot be placed in the middle of the string. In these cases, use an
escape character: backslash-quote ( \" ).

Although this is somewhat hard to read, the assembled string starts with a regular quote,
followed by a backslash-quote. Note how the end of the string is delimited:

Chapter 4 - String Manipulation Page: 168


Verbatim Text Strings - @:

Character strings with embedded backslashes are difficult to read and many developers
choose an alternate technique called a "verbatim" literal. Preface the quoted string with
an at-sign (@) and spell it in the 'natural' way:

If embedded quotes or white-space characters such as a tab are needed, you


cannot use verbatim strings.

Normally, a literal cannot span multiple lines in the editor but with verbatim strings you
can. The results can be weird. Note how the following string has its closing semi-colon
on the third line:

string strlongString = @"Four score and seven years ago


our founding fathers
brought forth on this continent...";

The results can be seen with: MessageBox.Show(strlongString);:

ASCII Codes:

All typed characters are represented with an ASCII code. For example, with the English
code-set, the letter 'A' is represented with an ASCII 65 while a lower-case 'a' is ASCII 97.
The code can also show other, non-visible characters. A tab is ASCII 9 and
CRLF (\r\n) is ASCII 13 + ASCII 10.

Chapter 4 - String Manipulation Page: 169


ASCII is still widely used by programmers even though, with Windows XP and above, it
has been replaced with a more-capable, language-neutral, "Unicode" standard. Working
with ASCII is somewhat esoteric and is mentioned here as reference.

This code demonstrates how to convert from 'A' to the number ASCII 65 and back. Note
the new type of declarations, "char" and "byte".

Example: Character to ASCII; ASCII to String


private void button1_Click(object sender, EventArgs e)
{
//Convert from a single-Character value to an ASCII code:
char myChar = 'A';
byte myByte = (byte) myChar;
int myInt = (int) myByte;

MessageBox.Show(myInt.ToString());

//Convert from a numeric ASCII code to a string:


string myString = Convert.ToChar(65).ToString(); //A
MessageBox.Show(myString);
}

Chapter 4 - String Manipulation Page: 170


String Concatenation

String concatenation combines two or more separate strings into one. C# uses the "+"
(plus symbol) for string concatenation but Visual Basic and Excel programmers will
remember the "&" symbol. Previous chapters have used this idea several times.

For example, concatenate two strings into one result:

Example: String Concatenation, complete


private void button1_Click (object sender, EventArgs e)
{
string strtextA = "Cat";
string strtextB = "Dog";

MessageBox.Show(strtextA + strtextB);
MessageBox.Show(strtextA + " and " + strtextB);
}

Results: "CatDog", "Cat and Dog". Note the second example uses the word " and " with
leading and trailing spaces.

string.Concat ( ) and "+":

An alternate way of concatenating uses the string.Concat method and this is identical
to the "+" method above. There is no limit to the number of strings that can be
concatenated:

MessageBox.Show(string.Concat(strtextA, strtextB, strtextC))

or

MessageBox.Show(strtextA + strtextB + strtextC)

In C#, the "+" symbol actually translates to the 'string.Concat' method. Since this
happens at compile time, there is no particular benefit in choosing one method over the
other. Most developers use the plus.

comments about string.Concat( )

".Concat( )" is a "method" applied against "string." You can think of a method as a
subroutine or function and methods always require parenthesis. Some methods, such as
the .Concat, require values to be passed through the parenthesis, while others can be
empty, as in .ToString( ) or .ToUpper( ). There is more on this as the chapters progress.

Chapter 4 - String Manipulation Page: 171


Numeric Values and Strings:

Both string.Concat and "+" can append numeric values (as a string) to other strings,
without first having to explicitly convert the numbers to a string (This is a change,
starting in Visual Studio 2005/2010). For example, the following appends the letter "N"
and a numeric integer into one string, forming "N1030". The code example shows three
different techniques to do this, all yielding the same results:

Example: Convert.ToString( ), complete


private void button1_Click (object sender, EventArgs e)
{
//Each variation produces the same results: "N1030"

string strpartNumberPrefix = "N";


int iPartNumber = 1030;
string strassembledPartNumber;

strassembledPartNumber =
string.Concat(strpartNumberPrefix, iPartNumber);
MessageBox.Show (strassembledPartNumber);

strassembledPartNumber =
strpartNumberPrefix + Convert.ToString(iPartNumber);
MessageBox.Show (strassembledPartNumber);

//New in VS2008 and above:


strassembledPartNumber = strpartNumberPrefix + iPartNumber;
MessageBox.Show (strassembledPartNumber);
}

Results: Each results in "N1030".

Each of the examples are acceptable but the "Convert.ToString" most clearly indicates
your intentions. It is somewhat surprising that C# allows an implicit conversion from
numeric to string, especially in the third example. Prior versions of C# did not allow
this, especially if the first term were numeric, and out of habit, I still tend to use
Convert.ToString.

Starting in VS2012, the first parameter in a MessageBox statement can also be numeric
and it will be implicitly converted to a string.

Concatenating Floating Point Numbers:

Floating-point numbers (non-whole, numeric values with decimals, also known as


"Double" and "Single") can be concatenated to a string. Consider the following
example. Notice how floating-point numbers are declared and initialized (with an "F");
more on this in the next chapter:

string strpartNumberPrefix = "N";


float floatingPartNumber = 123.456F;

MessageBox.Show(strpartNumberPrefix + floatingPartNumber);
MessageBox.Show

Chapter 4 - String Manipulation Page: 172


(strpartNumberPrefix + Convert.ToString(floatingPartNumber));

Results: "N123.456"

In real life, no programmer would define a "part-number" as a


floating-point number, even if it has a decimal point. This
should really be declared as a string. Only declare numeric
values if you intend to perform mathematical operations.

Chapter 4 - String Manipulation Page: 173


String Comparisons, revisited

The if-statements in the prior chapter introduced the concept of equalities. Numeric
values were easily compared with double-equal-signs (==) and with greater-than and less-
than symbols (<, >). But string comparisons are more complicated. Keep these rules in
mind when comparing strings:

• All string-comparisons are case-sensitive, where upper and lowercase matters.


• You cannot directly test if a string is "less than" another string (e.g. "Abe" <
"Zebra").
• Use String.Compare (string1, string2) to compare strings.

Comparisons are Case Sensitive:

This example compares two similar part numbers, one with an uppercase prefix and the
second with a lowercase prefix - "N1030" and "n1030":

String: Equal Comparisons, no match because of case


private void button1_Click (object sender, EventArgs e)
{
//Mixed-case strings do not compare equal:

string strtextA = "N1030";


string strtextB = "n1030";

if (strtextA == strtextB)
MessageBox.Show("they are equal");
else
MessageBox.Show("they ain't equal");
}

Results: "they ain't equal"

Case-Insensitive Comparisons:

To compare two strings, regardless of case, use " .ToUpper()" or ".ToLower()".


Consider these flawed comparisons:

if (strtextA == strtextB.ToUpper())
if ("N1030" == "n1030".ToUpper())

These are unsafe tests because although strtextB was shifted to uppercase and
successfully tested, what would happen if strtextA ("N1030") were accidentally set to
lowercase by another routine? This is easily fixed by shifting both sides to upper-case,
in this time-honored tradition:

if (strtextA.ToUpper() == strtextB.ToUpper())

Chapter 4 - String Manipulation Page: 174


.ToUpper() and .ToLower() are string methods and all
methods require parenthesis, even if there is nothing to pass into
the function. See later in this chapter for more details.

string.Compare( ):

It will be a shock to VB Programmers, but C# does not allow ">" and "<" symbols in a
string comparison. This forces you to use a more cumbersome "string.Compare" method
for inequalities but it does have a minor improvement because you can make the
comparisons case-insensitive.

The syntax uses a lower-cased keyword "string" (type the word, 'string') followed by a
dot-Compare. Within the parenthesis, list the two variables to compare. A third
parameter specifies whether the comparison is case-sensitive or not (bool ignoreCase).
Oddly, string.Compare( ) returns an integer -1, 0, or 1, instead of a True or False:

integerResult =
string.Compare (string1, string2, IgnoreCase:True|False)

Example: string.Compare with inequalities (<, >, =), complete


private void button1_Click (object sender, EventArgs e)
{
string strtextA = "Abe";
string strtextB = "Zebra";
int iresult;

iresult = string.Compare(strtextA, strtextB, true)

//where integer result:


// +1 = A > B
// 0 = A = B
// -1 = A < B
// ",true" = case-insensitive; ",false" = case sensitive

MessageBox.Show("Results: " + Convert.ToString(iresult));

//Testing should be done with a switch statement,


//discussed shortly...
switch(iresult)
{
case -1:
MessageBox.Show(strtextA + " < " + strtextB);
break;
case 0:
MessageBox.Show(strextA + " = " + strtextB);
break;
case +1:
MessageBox.Show(strtextA + " > " + strtextB);
break;
}
}

Chapter 4 - String Manipulation Page: 175


where:

• string.Compare() uses the actual word "string" - you are not replacing this with
your variable names until inside of the parentheses. This command reads as "The
'Compare' method of the 'string' Class.

• string.Compare returns an integer, -1, 0, 1, and in this example, it is being written to


an explicit variable:

0 if equal
+1 if textA comes alphabetically before textB
-1 if textB comes alphabetically before textA

• The third parameter, a boolean "IgnoreCase: True|False", is optional but is almost


always set to "true". Use "True" for case-insensitive tests, use "False" when you
want an exact case-match.

Using string.Compare for Equality:

With the IgnoreCase flag, string.Compare can test if two strings are equal, saving the
trouble of shifting both strings to upper or lowercase before the test:

Example: string.Compare, case-insensitive, complete


private void button1_Click (object sender, EventArgs e)
{
string strtextA = "N1030";
string strtextB = "n1030";

if (string.Compare (strtextA, strtextB, true) == 0)


{
MessageBox.Show("they are equal");
}
else
{
MessageBox.Show("they ain't equal");
}
}

As you type "string.Compare", the editor's intellisense displays a pop-up help-box with a
short description of the command, along with a list of available parameters. Note the
"int" (integer) term on the first line, acting as a reminder on what is returned by the
function.

Chapter 4 - String Manipulation Page: 176


The results of the string.Compare can be tested with a series of nested-ifs or better with a
switch statement:

Testing a string.Compare with nested-if, completed but not recommended


private void button1_Click (object sender, EventArgs e)
{
//Testing a string-compare using a nested-if:
//Remember: string.Compare returns a -1, 0, 1
//(Recommend using a switch-statement instead)

string string1 = "Abe";


string string2 = "Wilcox";

int compareResults = string.Compare (string1, string2, true);

if (compareResults == 0)
MessageBox.Show ("Both values are the same");
else
{
if (compareResults == +1)
MessageBox.Show ("Value1 is first alphabetically");
else
MessageBox.Show ("Value2 is first alphabetically");
}
}

where:

• The compare-results (-1, 0, 1) is stored in an intermediate variable near the top of the
routine. This keeps you from having to perform the test a second-time within the
nested-if.

• As a reminder:
+1 string1 > string2 (alphabetically, string1 is first)
0 string1 and string2 are equal
-1 string 2 > than string1

• In the example, string1 is alphabetically first. The variable order makes a difference
in the results from the string.Compare.

• Nested-ifs are cumbersome. A switch-statement is a better way to code this; see the
next section.

Testing string.Compare using a switch Statement:

Using if-statements to test for a three-way string.Compare (0, +1, -1) is cumbersome.
Because of this, most programmers use a "switch" statement (see previous chapter).

Chapter 4 - String Manipulation Page: 177


Comparing multiple conditions with a "switch", recommended

The order of the case-tests do not matter; the first true condition is used. Notice a
declared integer holding-value for the compare-results is not needed; contrast this with
the nested-if example, which needed an integer iresult.

where:

• "string.Compare" begins with a lower-case keyword 'string" followed by a dot-upper-


case '.Compare'. The two stringed variables being compared are listed within the
parenthesis, comma delimited.

• The third parameter ("IgnoreCase": true|false) controls the case-sensitivity of the


command. "true" means the comparison is case-insensitive (where caT == CAT)
and this is how the function is almost always used. You must explicitly use the 'true'
switch to enable this feature.

• When inserting "string.Compare" into an "if" or "switch", keep in mind that the (if-
statement) needs its own parenthesis - which are separate from the parens used by
the string.Compare. Notice the two leading and closing parenthesis:

switch (string.Compare(strtextA, strtextB, true))

Chapter 4 - String Manipulation Page: 178


string.Equals():

"string.Equals" performs the same test as "==", returning a true|false. The test is strictly
case-sensitive and does not perform greater-than or less-than comparisons. It is
mentioned here because some programmers believe this command has a slight
performance gain over a simple ==; however, the compiler translates all string double-
equals to this function at compile time, so the gains are artificial. Regardless, this
function is self-documenting and syntactically consistent with other string functions.

string.Equals( ), complete
private void button1_Click (object sender, EventArgs e)
{
string strtextA = "n1030"
string strtextB = "N1030"

//string.Equals returns a true/false:

if (string.Equals (strtextA, strtextB))


{
// <stuff to do if equals>
}
}

Results: <stuff to do if not equal>. This is only a case-sensitive compare, where "n" and
"N" are not equal.

string.CompareOrdinal:

This is a seldom, if ever-used comparison and is here for


reference.

string.Compare( ), alphabetically compares two strings, using a 'dictionary-based'


comparison where "a" comes before "A". But internally computers represent data using
numeric ASCII codes (discussed earlier in this chapter). ASCII sorts in a numeric order
that is alphabetic within the group of lower and upper-cased letters, but not alphabetic
with mixed case.

Chapter 4 - String Manipulation Page: 179


For example, the letter capital 'A' is internally stored as an ASCII 65 while a lower-case
'a' is stored as ASCII 97. Here is a partial list of some of the characters represented by
the ASCII tables. Full ASCII charts can easily be found on the Internet.

Char ASCII Char ASCII Char ASCII

A 65 a 97 0 48

B 66 b 98 1 49

C 67 c 99 2 50

D 68 d 100 3 51

Z 90 z 122 9 57

The computer needs a way to track both visible and non-visible "characters," and this is
why DOS used an 128/256 character ASCII table. (Other computer systems use different
schemes - for example, IBM mainframes use an encoding called "EBCIDC" (or Hex).
Windows now uses a newer table called "Unicode" - which represents 65,000 characters
– giving the ability to display Asian languages.)

You can probably see where this is leading. string.CompareOrdinal compares two text
strings using the ASCII table for the sort-order. For example, "A" comes before "a"
(ASCII 65 < ASCII 97).

Use string.CompareOrdinal in the same manner as string.Compare with only one


exception: this command is not case-sensitive so there is not a 'true|false' switch.

string.CompareOrdinal( ), complete
private void button1_Click (object sender, EventArgs e)
{
//string.CompareOrdinal compares ASCII values,
//not by a Dictionary Compare

//Returns and integer where:


// 1 = A > B
// 0 = A = B
//-1 = A < B

int result;
iresult = string.CompareOrdinal(strtextA, strtextB)

MessageBox.Show("Results: " + Convert.ToString(iresult));


}

where:

result = integer 0 if equal


result = integer -1 if ASCII Code textA is before textB
result = integer -1 if ASCII Code textB is before textA

Chapter 4 - String Manipulation Page: 180


<string>.EndsWith

This method examines the end of a string, looking for a passed-value. If found, the
function returns a boolean true/false. A common use is to look at a filename's extension.
Typically, the command is paired with an if-statement.

General use:
<original-string>.EndsWith(<compare-string>)
<original-string>.EndsWith(<compare-string>, true, null)

where:

• <original-string> is the string you are comparing, e.g. "myfilename.xlsx"


• <compare-string> is the string you are looking for. For example, ".xlsx", "Jr."
• Parameter-2: "true" makes the comparison case-insensitive
• Parameter-3: "null" use current culture (English); and is required if using parm-2

Detecting .XLS file Extensions:

Consider this test to see if the filename is likely an Excel spreadsheet.

A simplistic test like this would likely fail because of upper and lowercase concerns (.xls
vs .XLS, plus a concern with Excel's newer style, .xlsx):

if (myFileName.EndsWith(".xls"))
{
//Do this stuff
}

Either of these is a better test because both are case-insensitive:

if (myFileName.ToLower().EndsWith(".xls"))
if (myFileName.EndsWith(".xls", true, null))

The next example carries the test to a more capable level, detecting both the traditional
Excel xls extension and the newer .xlsx, introduced with Microsoft Office 2007. For
efficiency, the test converts the filename to lowercase one time, rather than with each
sub-statement:

Chapter 4 - String Manipulation Page: 181


Example: EndsWith: Detecting Excel File Extensions, complete
private void button1_Click (object sender, EventArgs e)
{
//Detect if the filename is an Excel sheet or not;
//supports Office 2007's new file extensions, which made this
//test a little more complicated.

string myFileName = "C:\\data\\09Financials.xls";

myFileName = myFileName.ToLower();

if (myFileName.EndsWith(".xls") ||
myFileName.EndsWith (".xlsx") ||
myFileName.EndsWith (".xlsm") ||
myFileName.EndsWith (".xlsb"))
{
MessageBox.Show("This is an Excel File");
}
else
MessageBox.Show("This is not likely an Excel File");
}

Wild-cards cannot be used.

Example: Appending a Closing Backslash:

This example uses .EndsWith to help standardize end-user-typed path names, forcing a
closing backslash, preparing for a fully-qualified path (See also Chapter 23:
Path.Combine). For example, if the user typed: "C:\Data", change it to "C:\Data\". If
they typed "C:\Data\" or if they typed a complete path and filename, such as
"C:\Data\09Financials.xls", leave it as-is.

Chapter 4 - String Manipulation Page: 182


Eample: EndsWith - Appending Backslash to path, completed
private void button1_Click (object sender, EventArgs e)
{
//Example program that appends a trailing backslash to a path
//name if the slash doesn't already exist.
//e.g. C:\Data becomes C:\Data\

string strpassedUserPath = "C:\\Data";

if (strpassedUserPath.EndsWith(".xls", true, null))


{
// an entire xls filename was passed; leave as is...
}
else
if (strpassedUserPath.EndsWith("\\"))
{
// user typed a closing backslash; leave as-is; no append
}
else
{
// no closing backslash on path; help them out: append
strpassedUserPath = passedUserPath + "\\";
}
}

MessageBox.Show("Final User Path is: " + strpassedUserPath);


}

where:

• Note the strpassedUserPath = "C:\\Data" uses two backslashes to represent a single


backslash. Backslash is an escape character, as described at the top of this chapter.

• If a closing backslash is not found, append one with a simple concatenation.

• Presumably, another routine would append a filename to the user-path.

Note: This example is flawed in that it only considers ".xls" files but still demonstrates
the concept. A more substantial routine could be written by looking for a period
(extension).

Chapter 4 - String Manipulation Page: 183


<string>.Length

A string's numeric length is found by examining its ".Length" property. Lengths are
often needed in substring calculations and in data-entry validations. This example looks
at text typed in textBox1:

<string>.Length, complete
private void button1_Click (object sender, EventArgs e)
{
//Display the length of a typed-string in textBox1

int ipartNumberLength;

ipartNumberLength = textBox1.Text.Length;

MessageBox.Show
("The number of characters in TextBox1 is: " +
Convert.ToString(ipartNumberLength));
}

where:

• ".Length" is a property (not a method or function). Because of this, it does not have
parenthesis. The Length property can not be changed in code. See below for
information on how to identify a property from a method..

• Lengths are base-1 counters, where the first character is at position 1, the second at
position 2, etc. In C#, all lengths are base-1 while all other commands start counting
at base-zero, where 0 is the first position.

• If present, leading and trailing spaces are counted as part of the length.

• Empty strings ("" quote-quote) have zero lengths. Note this is not an undefined or
null length - the length is actually zero.

• textBox fields default as empty-strings and as such have lengths = 0.

• Null strings have undefined lengths. If you try to retrieve a null-length, the program
will abend and logic is needed to trap the event. Detecting empty and null strings
takes special care; see later in this chapter for details, and again in Chapter 6.

• Numeric variables do not have lengths and the program will not compile if
attempted.

Identifying Properties:

All functions and methods use (parenthesis) but properties such as .Length do not. The
editor gives a hint about which-is-which while typing. For example, as you type

Chapter 4 - String Manipulation Page: 184


"textBox1.Text" and press "period (for -Length)", the editor displays a list of available
functions and properties. Notice the .Length popup icon is a black wrench (a property)
while methods are boxes.

Hovering over the "Length" context menu displays additional fly-out help. The first
word shows "int" (the results of the property is an integer value) and it expects a string as
input (textBox1.Length). Now that all this has been explained, standard strings, such as
strtextA, have only one property – Length; but other objects, such as textBoxes, have
nearly a hundred properties, including Width, Background Color, base-fonts, border-
styles and the like. More about this later.

Chapter 4 - String Manipulation Page: 185


<string>.ToUpper( ), .ToLower( )

<string>.ToUpper( )
<string>.ToLower( )

.ToUpper( ) and .ToLower( ) convert strings to upper and lowercase.

strmyName = textBox1.Text.ToUpper();
strsomeValue = strsomeValue.ToLower();

Because this is a method (note the box-icon in the illustration below), parenthesis are
required. But in this case, the method does not accept parameters and the parenthesis are
always empty.

Strings and textBoxes are often shifted for if-statement comparisons and the shift
happens temporarily, just for the test. For example, the next code-block compares what
was typed in textBox1 with a hard-coded string "MY DEAR AUNT SALLY". Users can
type "my dear aunt sally" in any case: upper, lower or mixed, and the if-statement's shift-
to-upper will generate a positive match.

<string>.ToUpper( ) / <string>.ToLower( ); If-statement comparison


private void button1_Click (object sender, EventArgs e)
{
//Convert a string .ToUpper case (also available: .ToLower)
//Assuming textBox1 has this value typed in the field:
//"My Dear Aunt Sally";

if (textBox1.Text.ToUpper() == "MY DEAR AUNT SALLY")


MessageBox.Show ("This must be Sally: " + textBox1.Text);
else
MessageBox.Show("Someone else: " + textBox1.Text);

The if-statement's shift was only for the duration of the test because the .ToUpper was
not assigned to a new value. In other words, textBox.Text remains mixed-case once the
if-statement completes.

To permanently shift to uppercase, assign the variable to itself, as in:

textBox1.Text = textBox1.Text.ToUpper(); //Notice the dot-Text

Chapter 4 - String Manipulation Page: 186


As a reminder, in C# assignments are marked with an equal-sign, as in 'a value "gets"
another value.' Meanwhile, if-statements use double-equal-signs, indicating a
comparison.

Other string variables (not just textBoxes) can be shifted similarly:

strtestA = strtestA.ToUpper();

comments:

• Not much is accomplished if you have this line of code, by itself:


strtestA.ToUpper();

Since it was not assigned to a variable, the compiler shifts then discards the
uppercase results. Instead, use this command:
strtestA = strtestA.ToUpper();

• All Methods have parenthesis. This includes loops, if-statements, Concats, and
.ToUpper and .ToLower. Parenthesis are required even if no values are passed
through them. This is described in more detail in future chapters, but for now,
remember methods always have parenthesis while "properties," such as ".Length",
do not.

• If you forget the parenthesis after a .ToUpper or .ToLower, you'll be blessed with a
compiler error similar to this:

VS2012 - VS2015:
Cannot convert method group 'ToUpper' to non-delegate type 'int'. Did you intend
to invoke the method?

VS 2010 and older:


Operator "==" cannot be applied to operands of type 'method group' and 'string'

• With textboxes, it is easy to forget the .Text property. In other words, you cannot
assign a string to the object, you must assign it to the property:

textBox1 = textBox1.ToUpper(); //Incorrect


textBox1.Text = textBox1.ToUpper(); //Incorrect

textBox1.Text = textBox1.Text.ToUpper(); //Correct

As a reminder, properties appear this way in the Intellisense editor. You will also
see methods (such as ToUpper) or events (such as "Click" event). Events are
discussed in future chapters.

Chapter 4 - String Manipulation Page: 187


Exercise I: "ToUpper":

The .ToUpper() and .ToLower() methods work on any textual data – including
textBoxes, standard variables. Create a new program or modify an existing, giving it the
following characteristics: A single button, "button1" and two textBoxes, textBox1 and
textBox2.

When button1 is clicked, take a hard-coded string ("Space Ghost") and shift to
uppercase, storing the results in textBox1. Then store a lowercase result in textBox2.

A Reminder:

See Chapter 1 for details on how to build the initial program. Click in "Form1.cs
[Design]" to get to the form design view. Use the Toolbox flyout to populate the panel.
with the three objects. Attempt the program now before reading the steps.

Program 4.1: String Manipulation


Detailed Steps:

1. Build a new project or continue modifying the previous examples.

Chapter 4 - String Manipulation Page: 188


2. Open Form1.cs in design view by double-clicking "Form1.cs" in the Solution Explorer or
by pressing Shift-F7.

Then, from the Toobox flyout menu, "Common Controls", click and drag two "textBox"
objects from the menu to the panel. Drag a "button" object. Arrange them so they look
like the illustration above. If loading for the first time, the toolbox flyout may take a
moment to load.

Click each textbox and examine its properties (Name) to decide which is named
textBox1 and textBox2.

3. Double-click button1, taking you to button1's "Click event." (More on events in the next
chapters.) Add this code:

Example: ToUpper( ) and .ToLower( )


private void button1_Click(object sender, EventArgs e)
{
strtestString = "Space Ghost"; //A hard-coded value

//Convert the hard-coded string to upper and store in textBox1


textBox1.Text = strtestString.ToUpper();

//Convert the hard-coded string to lower and store in textBox2


textBox2.Text = strtestString.ToLower();

//Show the original hard-coded string was not changed by the


//manipulations:
MessageBox.Show
("strtestString is still equal to mixed: " + strtestString);
}

4. Press F5 to run the program; then click button1.

Results when button1 is clicked: textBox1 gets an upper-cased "SPACE GHOST" and
textBox2 gets a lowercased string. The subsequent MessageBox shows testString
remains unmodified (case). Notice the use of "textBox1.Text" – you must use the .Text
property.

Program 4.2: textBox1 ToUpper, directly

Chapter 4 - String Manipulation Page: 189


<string>.PadLeft()

<string>.PadLeft (int);
<string>.PadRight (int);
<string>.PadLeft (int, '*');

".PadLeft" and ".PadRight" append spaces (or other characters) to the string, bringing the
total length to the value of the passed integer. Left-padding with spaces can help to align
lists or columns of numbers, especially in printed reports.

This example takes two numbers and pads them left, for a total length of 10. The results
are displayed both on the panel and in a MessageBox. The panel will be set to a fixed-
space font while the MessageBox will remain a proportional font.

See also Chapter 21 Formatting for other alignment options.

Example Setup:

A. In Design View, change textBox1 to a multi-lined textBox.


Highlight the field with a single-click.
Click the little-black arrow in the upper right, marking "multi-line".
Drag the box's handles, making it larger, as illustrated.
If needed, delete textBox2 from previous examples.

Note: If, from previous examples, you have a textBox2, delete or move it down, making
room for the multi-lined textbox.

B. Highlight textBox1. In the Properties Window (lower, Right corner of the design view
window), change the font to Courier-New:

Chapter 4 - String Manipulation Page: 190


C. In Code View (double-click "button1"), modify the event with these new statements:

Example: .PadLeft( ), complete


private void button1_Click (object sender, EventArgs e)
{
//Pad a string on the left side with spaces
//and display the results in textBox1

string strString1 = "1000.00";


string strString2 = "99.00";

strString1 = strString1.PadLeft(10); //appends space by dflt


strString2 = strString2.PadLeft(10, ' '); //Explicit; note tics

//Show the results in textBox1 (on the panel)


//Assumes this is a multi-line textBox and courier-new font
textBox1.Text = strString1 + "\r\n" +
strString2;

//Show the results in a messageBox with a proportional font


//and embedded tic-marks
MessageBox.Show ("'" + strString1 + "\r\n" +
"'" + strString2);
}

Chapter 4 - String Manipulation Page: 191


Results: Both the textBox and the MessageBox were padded with 10 characters but the
view is different in the MessageBox because of the proportional-spaced font. The textBox
looks great. Typically padding is done for printed reports using fixed-space fonts.

where:

• Padding strString1.PadLeft(10) assumes a space as the pad-character.

• Padding strString2.PadLeft(10, ' ') explicitly used a space-character. Note


the tic-marks, which indicates "character data" – characters are single-position strings
and are delineated with apostrophes. More on this below.

• The length of the passed integer (e.g. 10) is expected to be longer than the starting
string's length or nothing appends. In other words, padding with 5 has no affect when
the length of the "1000.00" is already 7 long.

• Generally, when trying to align a stack of numbers, pick a length that is 3+ the widest
expected (string) number. This accounts for a two-character-wide column separator
plus room for a +/- sign.

• When padding, the output device almost certainly needs to be a fixed-width, non-
proportional font. Recommend the true-type font "courier new".

Chapter 21 describes how to format numbers with decimals,


commas, and other alignment issues. For simplicity, this chapter
looks only at string data.

Chapter 4 - String Manipulation Page: 192


Debugging Embedded Spaces with tics:

Since spaces are hard to see, the example appended a tic-mark (apostrophe) before and
after the MessageBox's testString. Notice the quote-tic-quote. The resulting punctuation,
(tic) ' 1000.00', shows where the spaces are. This is a trick and is only useful while
debugging with MessageBoxes.

<string>.PadLeft(int, '*');

.PadLeft is not restricted to a space as a fill-character, however, only single-characters are


allowed and they are always delimited with tic-marks – not quotes. For example, in
.PadLeft(int, '*');, the '*' (or any other character) is a new data-type called "char"
with more discussions about this in the .Trim section. Bank checks and the like, are often
printed with leading asterisks.

In this block of code, display a numeric value with a leading dollar-sign and asterisks.
The number is a floating-point number (not an integer or a string) and it is declared using
an "F". More on numbers in the next chapter:

Example: .PadLeft( ) with leading characters, complete


private void button1_Click (object sender, EventArgs e)
{
//Print a floating point number with leading dollar-sign and
//asterisks:
// ***$1800.99
//Note the parenthesis near the dollar sign; see text

float fDollarAmount = 1800.99F;

string strPrintedString =
("$" + fDollarAmount.ToString()).PadLeft(10, '*');

MessageBox.Show(strPrintedString);
}

Chapter 4 - String Manipulation Page: 193


where:

• Because of the parenthesis, the dollar-sign is tucked up against the number


(***$1800.99) rather than ($***1800.99). Notice how this is treated as one item
because of the parenthesis:

("$" + fDollarAmount.ToString())

Then these results are padded with asterisks.

• The same effect could be achieved in this fashion, reading left-to-right. But I like the
first method because the order is explicit:

"$" + fDollarAmount.ToString().PadLeft(10, '*')

In this example, hardcoding 10 characters for padding will cause problems for numbers
longer than 10 characters. More advanced number formats, discussed in future chapters,
need to account for decimals, commas and other punctuation. A better solution is to print
at least 3 leading "stars," regardless of the length. "Variablize" this by incorporating a
length calculation into the passed integer. Take the length of the existing string and add
three + 1 for the dollar-sign.

string strPrintedString =
("$" + fDollarAmount.ToString())
.PadLeft(fDollarAmount.ToString().Length + 4, '*');

See also Chapter 21, Formatting, for more information.

Date Padding with Leading Zeros:

Padding with leading zeros is often needed with day and dates, changing a single-digit
month like "5" to "05". Here is a typical example.

The Formatting chapter has a more graceful way to pad numeric


dates (using MM, DD).

Chapter 4 - String Manipulation Page: 194


Example: PadLeft on Month, changing '5' to '05', complete
private void button1_Click (object sender, EventArgs e)
{
//Append a leading zero on single-digit dates

string strtestMonth = "5";


MessageBox.Show ("'" + strtestMonth.PadLeft (2, '0') + "'");
}

Results: a single-digit "5" becomes "05". A double-digit,"12", remains unchanged


because it was already at the minimum length.

Chapter 4 - String Manipulation Page: 195


<string>.Trim( );

The .Trim method removes leading and trailing spaces from a text string. User-typed
data-entry fields always should be trimmed because users will type leading and trailing
spaces for reasons unknown. When parsing input text files, especially if the input files
were generated by human beings, you invariably need to trim the data.

For example, the text string " Space Ghost", with leading spaces, is trimmed to "Space
Ghost" when passed through the function. Often the result of the trim is assigned directly
back to the field being trimmed:

Example: <string>.Trim( ), complete


private void button1_Click (object sender, EventArgs e)
{
//Trim leading and trailing spaces from a test string
//or from a populated textBox:

strtestString = " Space Ghost ";

strtestString = testString.Trim() //standard trim


textBox2.Text = textBox2.Text.Trim() //trim a textBox
}

where:

• An unadorned .Trim method still requires a set of open-and-close parenthesis. By


default, this trims leading and trailing spaces. In a later section, other trim-characters
can be used.

C# does not trim redundant internal (embedded) spaces. For example, "Space Ghost",
with 3 internal spaces, remains the same once trimmed. This will require a specialized
function; see Chapter 8 for a SuperTrim function.

Combining Commands:

Often, the dot-trim method is combined with other commands. For example, you could
trim leading spaces, shift to upper-case and then pad with leading asterisks, all in one
command. The results of the statement are often assigned back on top of the original
variable. The command is read and processed from left to right:

Chapter 4 - String Manipulation Page: 196


Example: Combined Trim, ToUpper and Pad, complete
private void button1_Click (object sender, EventArgs e)
{
//Trim, Uppercase and append leading asterisks with
//one statement:

string strtestString = " Space Ghost";

strtestString = strtestString.Trim().ToUpper().PadLeft(14, '*')


MessageBox.Show (strtestString);
}

Results: "***SPACE GHOST"

<string>.TrimStart()
<string>.TrimEnd()

.TrimStart() trims leading spaces, leaving internal and trailing spaces, and .TrimEnd()
removes from the end. (Visual Basic used "LTrim" and "RTrim".) This example removes
leading spaces and uses tic-marks to show the results:

Example: .TrimStart( ) / .TrimEnd( ), complete


private void button1_Click (object sender, EventArgs e)
{
//Trim only leading spaces

string strtestString = " Space Ghost ";

MessageBox.Show ("'" + strtestString.TrimStart() + "'");


}

Results in 'Space Ghost '. (Trailing spaces survived; printed with tic-marks).

Trimming with other Characters:

By default, Trim removes leading and trailing spaces but the command can be modified to
trim other characters. For example, you could trim leading asterisks or quotes.

As neat as this sounds, this is less-than-useful because if you knew the characters you
needed to remove, you probably also know how many and where they were. The added
setup time isn't worth the trouble and other methods could be used to remove the text.
However, since you might occasionally see this command it is still worthy of discussing
and it introduces new concepts.

As an aside, the additional features of the Trim methods are


called "overloads" – this is when a command's capabilities are

Chapter 4 - String Manipulation Page: 197


extended with new features and abilities. You'll learn more about
this when you start writing your own methods in Chapter 6.

char Data-Types Explained:

In order to Trim with other characters, two new concepts are introduced: The 'char'
variable and "arrays."

A "char" (character) data-type is a single-character string. From the compiler's view, a


'char' data type takes a tiny amount of memory (one byte) and it is fast and efficient. To
aid the compiler, char data-types are always delimited with 'single-tics' (apostrophes),
instead of "double-quotes." For example, compare these two declaration statements:

string strtextString = "a" // This is a string


char charCharacter = 'a'; // This is not a string; Note the
tic-marks; no quotes

Earlier, Trim methods always end with a pair of empty parenthesis, as in:
strtestString.Trim();

However, inside the parenthesis you can pass 'char' data. For example, to remove the
letter 'C' from a prefixed-part number ("C1030"), use either of the two following
commands, where the second is more reliable:

Example: .Trim( 'characters') / .TrimStart ('c')


private void button1_Click (object sender, EventArgs e)
{
string strpartNumber = "C1030";
string strstrippedPartNumber;

//Method 1
//(Will fail if leading spaces are present)
//(Will fail if the 'c' is lower-cased)

strstrippedPartNumber = strpartNumber.Trim('C');
MessageBox.Show(strstrippedPartNumber);

//Method 2: Trims only from the front


//(Will fail if the original string has a leading space
//but works (for lower and uppercased partNumbers)

//for example: " C1030". Read further.

strstrippedPartNumber = strpartNumber.ToUpper().TrimStart('C');
MessageBox.Show(strstrippedPartNumber);
}

There are un-intended consequences with the two Trim statements shown above. The
first command removes both leading and trailing 'C's. If the part-numbers happened to to
be "C1030C", the 'C' is trimmed from both sides.

The second command resolves this problem by trimming only the leading character using
"TrimStart" and it also fixes the problem when you don't know if the part-number is upper

Chapter 4 - String Manipulation Page: 198


or lower cased. The entire string is shifted before trimmed with a capital-C. The order of
the commands is important; shift before Trimming. The following Trim statement, while
syntactically correct, would not work because the operations are in the wrong order:

strpartNumber = "c1030" // note lower-cased

strstrippedPartNumber =
strpartNumber.TrimStart('C').ToUpper(); //fails

Results: non-strippedPartNumber = "C1030" with the capital 'C' un-trimmed. Uppercase


first, then trim. If the user typed " c1030" (leading spaces), the command would still fail.

char-Arrays and Trim Statements:

In another contrived example, what if your part-numbers could have different prefixes,
such as "A", "B", "C" and possibly leading spaces and you needed to trim the prefixes?
You could use four separate Trim statements or you could use an extended feature,
passing an 'array' of individual characters for the trim. In one statement, all of these
possible prefixes could be trimmed.

An array is a list of similar, individual items. Each item stands


independent of the others, but they are contained in a common
group. (More details about arrays are found in later chapters,
but for now, the Trim command will introduce the topic.)

Changes in VS2012:

Starting in Visual Studio 2012, Trimming with multiple characters was blessedly
simplified. An array of valid trim-characters could be passed, via an explicit array, or
now you can trim using an implicit array, where the values are typed directly into the
statement. The new design is easier to code and is easier to explain.

To trim the characters 'A', 'B', 'C', and ' ' (space) from a part-number prefix, use an
extended Trim command (called an "overload").

Trim with an Implicit Array (VS2014, 2015), complete


private void button1_Click(object sender, EventArgs e)
{
//Trim selected prefixes from a part-number

string strpartNumber = " B1030";

strpartNumber = strpartNumber.Trim('A', 'B', 'C', ' ');

MessageBox.Show("New Trimmed PartNumber: " + strpartNumber);


}

Results: "1030" with the prefixes and leading and trailing spaces removed.

Chapter 4 - String Manipulation Page: 199


Notice you cannot use this syntax: .Trim("abc "). The Trim command insists on
'character' data and this bad example is using a "string."

Visual Studio 2010 and Older:

With VS2010 and older, you had to build an explicit array to pass into the Trim function.
Even with newer versions, it is still useful to learn this technique – especially if the
number of Trim characters are in a longer list, where the values are more easily
controlled.

Begin by building a character array. Declare a "char" variable, much like you would
declare a string, but after the char-data-type add brackets, where the brackets indicate an
array. Unlike Visual Basic, the brackets go after the data-type (rather than the variable
name).

//Declare the array...


char[] <then the name of the array>;

The variable's name can be any invented name and these examples I am using a long and
descriptive name, "aremoveTheseCharacters", where I like to use a prefix "a", reminding
me this is an array. Within the array, specify each individual (part-number prefix), using
character-tic-marks and comma-separating the values:

char[] aremoveTheseCharacters = {'A', 'B', 'C', ' '};

You may have correctly surmised there can be different types of arrays: string arrays,
numeric arrays, and 'char' arrays. However, Trim only accepts 'char' arrays.

char[] aremoveTheseCharacters = {'A', 'B', 'C', ' '};

The braces mark the beginning and ending of the list and this list becomes the array.
Notice how the array includes a space-character, which is an apostrophe-space-
apostrophe. There are other ways to load this array and this is left for Chapter 22, Arrays.

Chapter 4 - String Manipulation Page: 200


Feeding the Array into the Trim:

The array will be fed into the .Trim command and it will trim the letters A, B and C, as
well as spaces. Place the array's name (aremoveTheseCharacters) in the Trim's open-and-
close parenthesis.

Trimming other characters using an array, complete


private void button1_Click (object sender, EventArgs e)
{
//Declare a character array and trim various prefixes from the
//front of a fictitious part number. Get leading spaces as well
//as prefixes beginning with A,B, or C

string strpartNumber = " b1030";

char [] aremoveTheseCharacters = {'A', 'B', 'C', ' '};

string strstrippedPartNumber =
strpartNumber.ToUpper().TrimStart(aremoveTheseCharacters);

MessageBox.Show (strstrippedPartNumber);
}

Results:
PartNumber " C1030" becomes strippedPartNumber = "1030".
Passing " b1030" trims to "1030".
And of interest, "abc1030", "ccc1030" and "a b c 1030" all return "1030".
"Z1030" would not be trimmed.

Array-Trims are still flawed:

This is well-and-good but not as useful as it would seem. Consider these variations of a
user-typed phone-number. Assume the program needs to remove all punctuation, both at
the margins and internally:

(208) 383-1234
208-383-1234
208.383-1234
(208383-1234
208/383-1234

with a desired result: "2083831234".

Chapter 4 - String Manipulation Page: 201


Trimming with an array of dashes, dots, spaces and parenthesis will fail because trim does
not process interior positions. For example, this array would fail. Note the double-
backslashes (which represents a single '\'):

//This array fails to trim phone numbers properly:

char [] aremoveTheseCharacters =
{' ', '(', ')', '-', '.", '/', '\\'};

To clean these numbers, more advanced tools are needed and they are discussed next.

To properly format a phone-number, you need to remove (trim) all user-punctuation, then
insert your standard punctuation back into the string. A fabulous method that does just
that can be found in Chapter 21, Formatting.

Chapter 4 - String Manipulation Page: 202


char.IsNumber ( )

Regardless of the function's name, char.IsNumber examines character or string data for
numeric values (zero-9) and returns a boolean "true" if it contains all numbers. The
command is almost always used with an if-statement:

if (char.IsNumber ('9')
{

if (char.IsNumber ("123A", 4))


{

There are two variations on the command:

char.IsNumber (<character data – examines a single char-byte>)


char.IsNumber (<string data>, <required index number>)

As you will see, this function is too limited for most real-world
problems. Read through this section for more information and
other possible solutions.

Simple char.IsNumber Test:

Consider this simplistic example, which examines a character 'A' to see if it is numeric.
This version only looks at a single character:

Example: char.IsNumber(char-data), completed but not particularly useful


private void button1_Click(object sender, EventArgs e)
{
char testChar = 'A'; //Notice the tic-marks for char data
//char data can only be one character

if (char.IsNumber(testChar))
{
MessageBox.Show
("This is numeric: " + testChar.Convert.ToString());
}
else
{
MessageBox.Show ("This is not numeric");
}
}

Chapter 4 - String Manipulation Page: 203


Testing String Variables:

char.IsNumber has an "overload" that can examine strings instead of a single-characters.


"Overloads" are explained in the next several chapters but they basically a variation of the
original command and the variation accepts different parameters.

As you are typing the "char.IsNumber(" command, the popup-help shows a list of
variations in a scrollable list.

'IsNumber' has 2 overloads, where the first has the least number or parameters and the
second is slightly more complicated. Click the arrows to view each variation. The second
overload shows it returns "boolean" and accepts a passed string, along with a required
index/numeric value.

When you are reading an overload, the parameter list always and
only shows "required" variables – none are optional – but there
are usually multiple overloads, each with their own list. This
way Visual Studio does not have to document all variations on
required and optional parameters. Each entry is a self-contained
list of all required values.

Consider this routine that tests the first three characters in a string for "numeric-ness":

Chapter 4 - String Manipulation Page: 204


Example: Examine only the first 3-characters for numeric-ness, complete
string strTest = "123 Elm";

//Check the first three digits for numbers.


//Use an integer-index value of 2 – a base-0 count

if (char.IsNumber(strTest, 2))
MessageBox.Show ("The first three digits are numeric");
else
MessageBox.Show ("The first three digits are not numeric");

//A better example would use an IndexOf command to find the


//first space and use that as the numeric counter. This idea
//is discussed below.

where:

• The char.IsNumeric's "string" overload requires a string and an "index" (a numeric


value) before it will operate. The index tells the command how many characters to
test against.

The numeric value is listed as an "index," which in Visual Studio means a numeric
count, starting at zero – base-0. For example: char.IsNumber ("123 Elm", 2) only
tests for numeric against the first three digits. The index starts counting at position
zero. 2 is the third digit.

• Test the entire string ("123 Elm") by using the string's total length. In Visual
Studio/C#, lengths are always calculated as base-1 (by design), which starts
counting at "1". Comparing a base-0 to a base-1 count means you have to off-shift the
counts by one:

"123 Elm" is 7-long when counting base-1


but is 6 when using an index base-0

char.IsNumber requires an index and to test the entire length, take the calculated
length minus one: char.IsNumber ("123 Elm", 7 - 1 = 6)

Thus: if (char.IsNumber(strTest, strTest.Length -1))

j In C#, all indexes are base-0; all lengths are base-1.

A semi-real-world example would find the first space-character's position in an address


and would use that count as the index. This example uses a new "find" method called
"IndexOf", which is described shortly.

Chapter 4 - String Manipulation Page: 205


private void button1_Click(object sender, EventArgs e)
{
string strstreetAddress = "123 N. Elm St.";
int ifoundSpacePosition = strstreetAddress.IndexOf(' ');

if (ifoundSpacePosition > 0)
{
if (char.IsNumber(strstreetAddress, ifoundSpacePostion -1))
{
MessageBox.Show("Address has numeric prefix");
}
else
{
MessageBox.Show("Address not numerically prefixed");
}
}
else
{
MessageBox.Show("No space detected");
}
}

Limitations with char.IsNumber:

Interestingly, this is the only numeric test available in C# and it is limited in what it
detects. For example, char.IsNumber claims these string values are not numeric:

"-54"
"54-"
"6.2431"
"$54.12"

And what about these user inputs: should they be considered numeric?
"1,800.23"
" $100"

Should there be an option to allow them to be considered as numeric? The util.Chapter


addresses these concerns with a more powerful tool that can work around these problems.
In that chapter, see the functions "IsNumeric" and "IsNumbers."

Chapter 4 - String Manipulation Page: 206


<string>.Replace( )

The ".Replace" method replaces any string with another string and the general syntax is:

<string-variable>.Replace
(<search-original-string>, <new replacement-string>)

Example: <string>.Replace, complete


private void button1_Click(object sender, EventArgs e)
{
//Replace "Robert" with "Bob"
string strtest = "Robert Smith";

MessageBox.Show(strtest.Replace("Robert", "Bob");
}

Results: Starting with "Robert Smith", replacing to "Bob Smith"

where:

• Search and Replace strings are case-sensitive and the search must match exactly.

• Wild-cards are not supported (*, ?).

• Each (multiple) occurrence of the same search-string is replaced.


For example, replacing the letter "A" with "C" in "AAA Autobody"
results in "CCC Cutobody".

• This method does not have a way to replace just the first or last occurrence; use the
"substring" commands or Chapter 6's util commands.

• Search and replace strings can be differing lengths.


For example, replace "Robert" with "R.".

• The example above does not change the original string; it only replaces for the
duration of the MessagBox statement. To replace the original, re-assign strtest to
itself, as in: strtest = strtest.Replace("Robert", "Bob");

Parts of a string can be removed by replacing the Search-string with an empty-string. This
is most commonly used when the results are written back over the top of the original
string:

Chapter 4 - String Manipulation Page: 207


Replacing a string with an empty-string, complete
private void button1_Click (object sender, EventArgs e)
{
//Replace the letters "Q." with nothing
//Note the extra trailing space in the test, which
//helps isolate middle-initials. A better test might have
//been " Q. ".

string strtest = "John Q. Public"


strtest = strtest.Replace ("Q. ", "");

//Display the results in a TextBox:


textBox1.Text = strtest;
}

Results: Replacing "Q. " in "John Q. Public", resulting in "John Public".

Chapter 4 - String Manipulation Page: 208


Character to Numeric Conversions

Programs often need to convert string (character) data, usually from external files or from
data-entry text-box fields, into a pure numeric value. Numeric conversions are required
before mathematical operations can be performed. These operations are the opposite of
the Convert.ToString seen earlier and work in a similar fashion.

Syntax and Variations:

Convert.ToInt32( ) Convert to Integer


Convert.ToSingle( ) Convert to Floating point – numbers with decimals
Convert.ToDouble( ) Very large or precise Floating Point numbers

Consider this code, which converts a string "123" to a numeric value, then adds 10, then
another 8:

Example: Convert.ToInt32( ), unsafe – Needs a try-catch


private void button1_Click (object sender, EventArgs e)
{
string strInput = "123"; //Note the quotes – string data
int iNumber;

iNumber = Convert.ToInt32(strInput);

iNumber = iNumber + 10; //Adds 10 to 123


iNumber += 8; //Adds an additional 8 to 133

//Ironically, convert the number back to string in order to


//to see it in a MessageBox:

MessageBox.Show (Convert.ToString(iNumber));
}

where:

• Convert.ToInt32(string-to-convert) explicitly converts the string to a number and in


this example, results are assigned to a new variable before other steps act upon the
conversion. The conversion could also be in the middle of a statement or part of an
intermediate calculation, as in:

MessageBox.Show
("Intermediate answer: " + (Convert.ToInt32(strInput)+10);

• iNumber += 8; is short-hand for "iNumber = iNumber + 8"


(e.g. take whatever is in iNumber and add 8, then re-assign the value back over the top
of itself - iNumber).

Chapter 4 - String Manipulation Page: 209


• Dozens of other conversions are available, but probably most commonly include:
Convert.ToSingle( ) (think Convert to Float – numbers with decimals)
Convert.ToDouble( ) (think Large or very precise Float)

Comments on Numeric Conversions:

If the input string contain nonsensical data (123 N. Elm), you will incur the wrath of a
runtime error "Input string was not in the correct format." Use a try-catch to intercept
these types of data-problems; see "try-catch Exception Handling" near program 4.4,
below. Later chapters show more sophisticated techniques.

Notice a string, such as "123.10" cannot directly convert to an Integer (it fails with an
"Input string was not in the correct format" because of the period). To convert, first
move the variable to a floating-point number then to an Integer (truncating the fraction);
see below.

Converting to Other Numeric Types:

Converting from an integer to a floating point number is acceptable because the jump
from "123" to "123.00" is a lossless-change. In instances like these, C# allows "implicit"
conversions without question and the conversion can be made with a simple assignment
(equal):

Example: Implicit, lossless Conversion from Int to Float, complete


//Implicitly convert from Float to Integer, no problem:

int iNumber = 123;


float floatNumber;

floatNumber = iNumber; //Convert to float is implicit at assign

But converting from a floating (123.10) to integer (123) risks truncation and the compiler
will not allow it unless explicitly converted. Notice how floating-point literals are
suffixed with an "F":

Example: Convert Floating Point to Int, Explicitly Truncating


//Explicitly convert when data-integrity is at risk:

float floatNumber = 123.10F;


int iNumber;

iNumber = Convert.ToInt32(floatNumber); //Results: "123"

Chapter 4 - String Manipulation Page: 210


Conversions with Casting:

Numbers can also be converted to a new type using a technique called "casting". For
example, an integer can "cast" to a floating-point number and visa-versa. This is best
described with an example:

Example: Casting to an Integer, complete


float floatNumber = 123.1F;
int iNumber;

// Casting: These next two statements are equivalent.


// Both cast a floatingpoint number as an integer before assigning
// it to the variable:

iNumber = (int) floatNumber;


iNumber = Convert.ToInt32(floatNumber);

where

• (int) <variableName> is the casting statement – the word "int" in parenthesis. When
casting, prefix the variable with parenthesis and the type of variable you want it cast
to. The syntax is odd because the parenthesis surround the first part of the statement,
not the variable name.

• Cast as (int), (double), (float). Other types of objects beyond the scope of this chapter
can also be cast.

• Note the "F" in the "floatNumber = 123.1F" declaration. When initializing a floating-
point number as a literal, suffix the declaration with a capital F, letting the compiler
know this is a floating-point number. ('float' and 'single' are the same. More details in
the next chapter.)

• You can cast one type of number to another. But you cannot cast a string to a
numeric data type. Instead, use "Convert.To" – assuming the string is a valid
number.

Casting is Confusing:

When casting is and is not allowed can be confusing. As you research other people's code
on the web, you'll find that some will cast variables from one data-type to another – but
the idea for casting was really designed for changing classes and other object-oriented
features, none of which have been discussed yet. At the risk of over-simplification, I'll
generalize: For commands discussed in this chapter, cast as part of an intermediate
calculation or when moving from similar data-types. Use Convert.To if in doubt or are
having problems with casting. Convert.To is always a safe command to use. In any case,
the compiler will tell you if a cast is not allowed.

Chapter 4 - String Manipulation Page: 211


null and Empty Strings

Strings hold character data but they can also hold "nothing" – that is, they may be empty,
undefined, null, or they may have spaces. Unfortunately, with several types of
nothingness; testing for them is problematic and most programmers struggle with this.
This section shows how to best detect these states and it demonstrates a powerful tool that
Microsoft does not supply.

This section, along with Chapter 6, demonstrates the tools needed to tackle this subject
with skill, but in order to learn what is needed, I will be taking you on a scenic tour of the
available commands. There are a litany of suggested tests on the web and most have
subtle flaws.

Flawed ways to detect null and empty strings:

if (strtest == null)
if (strtest == "")
if (strtest.Trim() == "")
if (strtest.Trim().Length == 0)
if (strtest != null && strtest.Length > 0)

Microsoft's recommendation:
if (string.IsNullOrEmpty(<strstring>))

This book's recommendation:


if ((strtest + "").TrimStart().Length > 0) **Recommended

Even better, write two new functions (methods) to make this


test even easier. See Chapter 6:
IsBlank(strtest)
IsFilled(strtest)

Detailed Types of "blank" fields:

There are several states of "emptiness," each of which is different. Here is a string
variable in various states of declaration:

string strTextA; declared but not initialized; value null


string strTextA = ""; initialized with an empty-string (quote-quote)
string strTextA = string.Empty; same as an empty-string
string strTextA = " "; initialized with a space (unprofessional)
string strTextA = null; initialized with null – not an empty string

Declared but not Initialized:

When a variable is declared but not assigned a value, it contains a null. A null is not an
empty string nor is it a zero. It means the value is undefined or unknown.

Chapter 4 - String Manipulation Page: 212


Un-initialized variables cannot be used to populate any other variable or function; if you
try you'll get a compiler error: "Use of unassigned local variable <variable name>". This
is easy to detect because the compiler sees them while editing and these problems are seen
long before the end-user arrives at the finished program. To resolve, assign a value when
the variable is first declared – typically a quote-quote "" (or if a numeric variable, the
number zero is assigned).

string strtest = ""; //Assign an empty-string to initialize the


variable
int imyVariable = 0; //Assign a zero or other value to numeric
variables

Empty Strings:

Strings that are initialized or assigned "empty-strings" are also different than nulls or
undefined. An empty string is where the variable is assigned the value = "" (two double-
quotes – "quote-quote" or alternately with string.Empty(stringname)). An empty-
string is just what the name implies: it is a string with nothing in it. From the compiler's
point of view, this is a perfectly good string. textBoxes default to this value when first
created. The compiler considers this populated with data even though there is no "real"
data.

null:

"nulls" are strange from a data-processing view. They are not empty strings, nor are they
a numeric zero; technically, a null indicates no value was specified. Un-initialized
variables are null and often databases populate unknown values with nulls and certain
array operations, when they have nothing to work with, generate nulls. When most C#
commands encounter one, they implode. Because they are not a string or a number, they
have to be tested carefully and separately.

Spaces:

Setting a field's value to one or more spaces is mentioned because end users sometimes do
this in data-entry fields – that is, they type a space or a line of spaces in order to "blank
out a field." Because of this, you often receive an exported spreadsheet or file with field-
spaces. This type of data is a nuisance to program around. Because a space is an actual
value, it eludes all empty-field detection and yet for all practical purposes, the field is
empty. The IsBlank and IsFilled functions, described later, navigate this problem with
grace. In your code, do not initialize or assign variables with a space-character (quote-
space-quote) just to "clear them out." Use "" or string.Empty().

nulls are Problematic:

Nulls are a nuisance. If you suspect either a null or an empty string in the data, especially
if it comes from an outside source, you will have to test separately for each possibility.

Chapter 4 - String Manipulation Page: 213


Consider this code, which takes an integer, which is being simulated with a null, and tries
to add +1:

int numberA = null;


MessageBox.Show(Convert.ToString (numberA + 1));

Result: Compiler error: "Cannot convert null to 'int' because it is a value type." [this is
an implicit conversion]. The trouble is, this error can also happen at run-time and it will
cause the program to crash, long after it was compiled.

Many string functions also panic with nulls. For example, a string.Length against a null
value gives an error, "Cannot convert null to 'int'". Although empty strings have a
(numeric) length of zero, null strings do not have any length – not even zero. In general,
nulls cause all kinds of problems and they must be treated with care.

First-blush: Testing for nulls:

One way to resolve these problems are to test for null prior to any calculations.

if (strText == null) or
if (String.Compare (strText, null) == 0) //(Compare returns -1,0,1)

This adequately tests for null, but it does not solve the most likely problems encountered
in a program. Typically, the code needs to concern itself with more than a null value.
Empty-strings, null-values and "blank" fields all cause problems.

First-blush: Testing for empty and space-strings:

Testing for empty strings is relatively easy (remember, these are not "null" strings). Test
explicitly with an empty-string (quote-quote) or by checking for a zero-length. However,
these tests explode3 if they should happen to encounter a null value.

if (strTest == "")
if (strTest < " ") //Illegal: (Use String.Compare)
if (strTest.Length == 0)4

Testing for empty strings using .Length is favored by many programmers and you will see
numerous examples on the web. But because the test explodes when it encounters a null,
it is too dangerous to use in the real world.

Testing "space" strings (quote-space-quote – usually typed by end-users in data-entry


fields) is a more problematic. A .Length==0 test fails because the space counts as a
character and it fails because the number of spaces are not known – e.g. " " (x1 space) and
" " (x3 spaces) look the same on the screen.

3
"Explode" means a nasty run-time error - one that happens after you release your final program to
production – users will call.
4
If testing a Form field, a similar test would be if(textBox1.Text.Length == 0)

Chapter 4 - String Manipulation Page: 214


These problems can be worked-around with these next two tests but keep in mind neither
detects nulls or empty-strings, which are equally likely:

if (strTest == " ") //Ineffective and pointless

if (strTest.Trim() == "")
if (strTest.TrimStart() == "")
//more reliable test for multiple spaces but
does not test for nulls or empty-strings

The Trim tests are particularly interesting because it removes any number of leading
spaces that might have been typed by end-users before comparing with an empty-string,
however, it still dies if it finds a null.

Painful Ways to Test for Empty and Nulls:

To effectively test for an "empty" field – that is a field that contains nulls, empty-strings,
or blanks – you must combine several tests. The key is to test nulls separately, keeping
the rest of the routine from abending5.

if (strTest == null)
{
//The field is "null" and must test separately before a trim
//is attempted
}
else
if (strTest.Trim() == "")
{
//The field is "empty"
}
}

This test correctly checks for nulls and spaces but involves a half-dozen lines of code.
Since most programs frequently need to test for "blank" fields (data-entry fields, database-
fields, etc.), the test is too tedious.

String.IsNullOrEmpty( ):

If this is starting to look complicated, you are right. Using the techniques above, testing
for spaced, blank, empty and null strings each requires a different test. Starting with C#
version 2005, Microsoft agreed and developed a new boolean string method for these
scenarios.

Consider this code, which uses the new "String.IsNullOrEmpty( )" function:

5
Abend = Abnormal End; also known as a run-time error, an "Exception," or more commonly as a
program crash. These types of errors cause your program to die while end-users are using the program
and these are often hard to debug.

Chapter 4 - String Manipulation Page: 215


Example: String.IsNullOrEmpty, Microsoft's recommendation
private void button1_Click (object sender, EventArgs e)

//This is Microsoft's recommendation for testing null and


//empty strings, but it does not go far enough

string strTest = null; // The sample value being tested

if (String.IsNullOrEmpty(strTest))
MessageBox.Show("No data");
else
MessageBox.Show ("String has data");
}

where:

• Use the key phrase "String.IsNullOrEmpty( )" with a variable as a parameter within
the parenthesis.

• Much simpler than a nested-if.

The only issue I have with this statement is a space (" ") is considered an occupied data
field. You always have to remember those pesky users who pad fields with spaces in
order to clear them. For most programs, this should still act like it was an empty field,
even though it has 'valid' data within. Although this is a good first-step, the "IsBlank" and
"IsFilled", introduced below, solve this problem.

Nullable Tests:

As an aside, C# also supports a "nullable data-type," which is a value that can contain
either legitimate data or a null, and this can only be applied against numeric data types
and certain constructs, such as an array. The reason for using a nullable data-type is
because sometimes a (numeric) value is not known.

Consider what should happen when importing integer temperature values from a database.
What if the value was never set (e.g. a null). Should your program store a zero (0) – that
would not work because zero degrees is a valid temperature. If you attempted to store a
"null" to a standard integer, the program would either not compile or would crash at run-
time. Similarly, how should unknown dates, such as a birth-date, be represented when the
date-field absolutely requires a date? Storing a null would normally cause the program to
fail. This is where a "nullable data-type" is useful; it can store legitimate numbers *or* a
null.

A nullable data type is defined with a declaration: Nullable<variable>, but nobody


does this; instead, they use a question-mark, as in:

Chapter 4 - String Manipulation Page: 216


int? imyNumberVariable = 10;
double? d1 = 3.14;
char? myCharacter = 'z';
DateTime? myDate = null;
int?[] aimyNumberArray = new int?[10]; //An array of integers
string? myString; //Not allowed – not a nullable type

where the question mark indicates this data-type is a "nullable" type and the variable can
hold a null value. Note: "Strings" are not nullable (with a question-mark definition), but
they can be null – but you cannot use the ?-technique.

Once defined as "nullable", null values can be tested with a property called ".HasValue".
This property will not work on a standard integer, date or string – it must be defined as
"nullable" before this property can be used.

In a contrived and simple example, testing with the .HasValue property could look like
the following code and the if-statement prevents to program from having an "Exception"
(crash). You could also test for a not-equals-null (!=null), with similar results:

int? imyNumberVariable = null; //simulated; Note the question-mark

if (imyNumberVariable.HasValue)
imyNumberVariable = imyNumberVariable + 100;
else
{
//You cannot use this variable
}

Null-Conditional Operator:

Starting in Visual Studio 2015, a new "null-Conditional operator," which is represented as


a "?." (question-mark+period), became available6. With this new operator, the null-test
can be condensed, discarding a =null or .HasValue if-statement.

Consider this example, where a null-string is accidentally being passed into a substring
routine. If a null is encountered, a null is immediately returned and the remainder of the
command is not interpreted. Without a test (and without the ?.), this would normally
cause the program to abend:

private void button1_Click (object sender, EventArgs e)


{
//Accidentally set a string to null, say from some data-source...
string strSomeString = null;

//Attempt to substring the first four characters...


//Without the "?.", the substring would abend

MessageBox.Show
("The value is '" + strSomeString?.Substring(0,4) + "'");
}

Results: The value is ''. The Substring command is abandoned. The closing apostrophe is
appended to the MessageBox.

6
See also the Terniary ? command in Chapter 3.

Chapter 4 - String Manipulation Page: 217


More commonly, the null-test happens in a downstream function. Consider this example,
where a 'function' called "A100" needs to substring the first 4 characters from a string
(functions are described in Chapter 6):

private void button1_Click (object sender, EventArgs e)


{
//Accidentally set a string to null, say from some data-source...
string strSomeString = null;

//Call a routine (below) to do the work


string stranswer = A100_Routine (strSomeString);
MessageBox.Show ("The value is '" + stranswer + "'");
}

private string A100_Routine (string strpassedValue)


{
return strpassedValue?.Substring(0,4);

//This would abend if a null were accidentally passed, but the


//Null-Conditional Operator (?.) prevents the failure. Notice
//an if-statement is not required
}

This test is succinct and works well for a null-value, but the routine has other failures
which are not accounted for. If an empty-string is passed, or if the string is shorter than 4
characters, the program would still abend, but the intent of this section is to quickly
demonstrate the new 2015 operator.

Often, what is really needed, is a way to ask a question of the data, asking if the string is
null, empty or spaces - e.g. what I call "blank". This is discussed next.

Chapter 4 - String Manipulation Page: 218


Testing for "blank"

My belief: For most purposes, a null, empty-string, or spaces, all


mean the same thing – there is no data in the field. This is what I
call a "blank" field. This is non-standard nomenclature however,
'blank' or 'filled" are the terms I will use throughout the
remainder of this book. Technically, a field with spaces is not an
empty field – it has data – but more often-than-not your program
needs to behave as-if it were an empty field.

From the previous section, deciding when a field is "blank" requires three different tests:
• Is it null?
• Is it an empty-string?
• Is it only "spaces"?

In order to keep the program from crashing, all three conditions must be tested, in a
specific order, and it requires a tedious nested-if or a mixture of IsNullOrEmpty and other
checks, but there are coding tricks that can simplify this task. In this section you will
build a "function" to automate this idea. The next chapter takes these new functions and
puts them into a library, where they are always available. The final code will be easy-to-
write and more importantly, easy-to-read.

Below are several methods that test for "blank" strings; some
designs are better than others. You are encouraged to try them
in order to learn more about if-statements and logic. Try the
examples in your test program using button1_Click's event.

Checking for blank strings using a compound if-statement:

Using the techniques described in the previous sections, the null and .Length test can be
combined into one semi-complicated statement:

if(strTest != null && strTest.Length > 0)

This method is flawed because it doesn't test for a space-character and it relies on a oddly-
worded phrase "not-equal null (!=)" It also uses a sneaky double-ampersand, which first
tests for nulls before attempting the other tests – saving a possible abend. As a reminder,
Chapter 3 discusses the usefulness of the double-ampersand in this types of test. Both of
these "tricks" require mental gyrations to understand – but this is not a bad first step.

Adding another layer to test for "space-characters" would involve adding an "and" or "or"
clause to the command above and it would make the statement un-interpretable to most
humans. However, this idea is still worth exploring. Try this code by doing the
following:

Chapter 4 - String Manipulation Page: 219


Note:
In order to compile a test program, you must declare and initialize the string variable,
setting it to null. (Although un-initialized string variables are set to null, the compiler will
not let you run the program in that state. To work around this, the code forces a test
variable to null in order to simulate an uninitialized variable.)

1. In the button1_Click event, begin with this code:

private void button1_Click (object sender, EventArgs e)


{
string strTest = null; //You must explicitly set to null for test
//This is a simulation of a real-world event

// <more code below>


}

2. Create an if-statement that checks for both null and for string.Length.
Using a trick from previous chapters, use an AND statement within the if-statement
clause.

j Recall the double-ampersand first checks for null and if that fails, it doesn't resolve
the second-half of the "if", saving you from a run-time /abend.

Flawed test for blanks; but still interesting


private void button1_Click (object sender, EventArgs e)
//Test using a double-ampersand; this is actually a neat trick
//but still flawed because it does not test for spaces.

string strTest= null; // the sample value being tested

if (strTest != null && strTest.Length > 0)


MessageBox.Show("String has data");
else
MessageBox.Show ("No data");
}

Run the program with strTest as a null: Results "No data" -good
Change strTest from a null to an empty-string (""): Results "No data" -good

Change strTest to a (space): Results: "String has data". This is not the result wanted.

Chapter 4 - String Manipulation Page: 220


Best Practice: Test for blank strings using Trim-Append-Length:

The tests above have flaws in either their logic or complexity.


Here is the best way to test.

Best Practices Summary:


if ((strtest + "").TrimStart().Length == 0)

• Trim leading spaces first


• Append an empty string before testing; killing nulls
• Finally, check <string>.Length = 0 to detect empty-strings

Checking only a string's length is a common but flawed way to tell if a field is empty. It
works well on filled and empty strings, which have zero lengths, but crashes when the
string has a null value or is filled with spaces.

The null-value problem is solved using a trick: Take the string being tested and append
an empty-string; this immediately (and temporarily) converts the null to an empty string.
Once this is done, a Trim (to take care of leading spaces) and a simple string.Length,
makes a reliable "blank" test.

Best Practice: Testing for 'blank' strings


private void button1_Click (object sender, EventArgs e)
//This method is a best-practice method for testing blank data.
//The test properly detects nulls, empty and space-strings, but
//should be placed into a separate method, such as "IsBlank".

string strtest = null; // The sample value being tested

if ((strtest + "").TrimStart().Length > 0)


MessageBox.Show("String has data");
else
MessageBox.Show ("No data");
}

Conversely, the test could also be written this way:

Best Practice: Testing for 'blank' strings, alternate


if ((strtest + "").TrimStart().Length == 0)
{
MessageBox.Show("The string is blank");
}

Appending an empty-string is the key to this test and this protects the statement from
nulls. When an empty-string ( "" ) is appended to most other strings, the original string's
value doesn't change.

In other words, "Dog" + "" still equals "Dog".


If the original string were empty ( "" ), it would remain "".

Chapter 4 - String Manipulation Page: 221


nulls are the only fields changed by this operation and they get promoted to an empty-
string – simplifying the test.

j The key point is this: The null-step must be done before either the Trim or the .Length
calculation because both panic when they see a null.

In the middle of the test, "TrimStart" resolves the problem with "space" fields by
removing the "redundant spaces." Use a "TrimStart" instead of a more normal "Trim" for
a slight efficiency because there is no need to trim the back-side when you are only
looking to see if the string has a space. Words such as " " (space) or " Dog" (space-dog)
are correctly interpreted.

With the code above, change strtest and try these possibilities. All are detected as
"blank":

• "" (quote-quote)
• "bob" (Normal string)
• " bob" (leading space or spaces)
• "" (a single-space, quote-space-quote)
• " " (several spaces)
• null

The Best Solution for Checking Blank Fields:

All but the simplest programs have to check for "blank" data, usually in multiple
locations. But adding this best-practice-logic in so many places is cumbersome and would
soon become a chore. A better solution is to call a function that returns a true/false when
ever the function were asked, "is it blank?" Chapter 6 describes how to make this a
generic function but here is a hint on what it would look like in your program:

//This test is not yet developed; see Chapter 6


if (IsBlank(strTest))
{
<do stuff if blank>
}

The converse could also be asked:

//This test is not yet developed; see Chapter 6


if (IsFilled(strTest))
{
<do this stuff if it has data>
}

Starting with Chapter 6, you will be able to coin your own function names, essentially
creating your own keywords in C#. "IsBlank" and "IsFilled" are functions that C# should
have, but doesn't. These will become two verbs that you can't live without.

Chapter 4 - String Manipulation Page: 222


Finding Strings - IndexOf

Often, programs need to parse through strings, looking for information. For example,
assume your program reads an input file (perhaps an INI file), where a person's name is
listed with a keyword "Name" followed by an equal-sign-delimiter:

When parsing the line, you need either a delimiter or a known horizontal position in order
to snarf the information. The delimiter (in this case, the equal sign) does not matter as
long as it is predictable or identifiable. You can imagine other delimiters, such as
"Name: (colon) Smith, John", "Name - (dash) Smith, John", etc.. The key is to find the
position of the delimiter.

The concepts in this section to introduce new verbs and concepts.


To do this well, more sophisticated routines (such as functions
and methods) are required. Chapter 6 comes back to this topic in
detail.

<string>.IndexOf - Finding a String within a String:

The ".IndexOf" method searches for a string within another string, returning the numeric
horizontal position of the found string. This position is called an "index." (Visual Basic
programmers know this function as "Instr".) Typically, you are searching for a delimiter:
an equal sign, comma, tab, etc. The returned count is the first index position of the found-
string and the returned value is a base-0 count.

For example, searching the string "Name = Smith, John" for an equal-sign (=), shows it at
position 5 - not 6. Be sure to count the space before the equal sign and start your count at
zero.

Chapter 4 - String Manipulation Page: 223


.IndexOf Parameters: The Search String and Starting Index:

".IndexOf" typically uses two parameters. The first is the 'search string' you are searching
for and the second is a numeric count on where to begin the search, which is almost
always zero – starting at the first position.

The statement returns an integer, which is usually assigned to previously-built variable.


Out of habit, I suffix the variable's name with "NDX" (index position), reminding me this
is a horizontal counter into the string.

int delimFoundNDX;
delimFoundNDX = <your-string>.IndexOf('=', 0);

The first parameter is the text you are searching for, in this case , '='.
The second parameter tells where to begin searching; typically starting at position 0 –
base-zero – which if not specified, is the default. (Although not required, I usually specify
the starting value, even if zero, to make it abundantly clear about the search.)

Using IndexOf:

If the search-string (or search char) is found, the numeric position, base-0, is returned. If
the string is not found -1 is returned.

To locate the delimiter ('='), try this code in your test program's button1_Click event. This
example uses a hard-coded input string:

Chapter 4 - String Manipulation Page: 224


Example: Search for a delimiter using .IndexOf( ), complete
private void button1_Click (object sender, EventArgs e)
{
//Search for a delimiter (=) and report its position.
//This location can be used for a subsequent substring.

string strinputLine = "Name = Smith, John";


int delimFoundNDX;

delimFoundNDX = strinputLine.IndexOf('=', 0);

if(delimFoundNDX >= 0)
{
MessageBox.Show("equal found at position: " +
Convert.ToString(delimFoundNDX));
}
else
{
MessageBox.Show("no equals found");
}
}

Results: "equal found at position: 5"

comments:

• ".IndexOf" is not limited to finding single-characters. It can find words and phrases
within a source-string. When a match is found, the returned value is the first
character of the search-string.

For example, searching "System Part Number: 12345" for "Part Number:" returns 7
(the letter "P"). Add 14, the length of " Part Number: " including leading and trailing
spaces, to locate the actual part number. The next section, Substring, has more details
on this technique.

.IndexOf Rules:

• .IndexOf locates the first occurrence of the search string ('=') and the equal sign is
being used as a "delimiter."

Either char-based searches, '=' (tic), or string "=" (quotes) are allowed

• Searches are case-sensitive.

• It returns a base-zero integer value, showing the horizontal location of the found
string. Zero is returned if found in the first position. A negative-1 (-1) is returned if
not found.

• If searching for multi-character strings ("Smi"), the returned index is the location of
the first character in the search-string, in this case, the "S" in Smith.

Chapter 4 - String Manipulation Page: 225


• Subsequent delimiters, if present, are ignored. For example:
LastName = Smith FirstName = John

To find the second occurrence, use the (first-found index +1) as a starting point. See
below for more details on this.

• char ('=') searches are slightly more efficient than string searches but can only be used
when the delimiter is one position long. In other words, when searching with single-
characters, enclose the delimiter with tic-marks instead of quotes, for a minor
efficiency.

Related commands: ".LastIndexOf" and ".IndexOfAny"

Case-Sensitive Searches and Multi-character Delimiters:

The examples above searched for an equal-sign, which is not case-sensitive, but other
search strings could be. When searching for a sub-string, the program almost always
needs to shift the variables to all upper or all lowercase prior to using an ".IndexOf".

In a contrived example, imagine two pieces of data, from different sources, and you are
parsing on the word "Fees" (as in Fees charged).

Maintenance Fees 15.00


Initial Fees 20.00

The data may be in upper or lower-case, depending on how the data-entry clerk typed the
value. Keying off the keyword word, "FEES", the dollar amounts can be located:

Chapter 4 - String Manipulation Page: 226


Example: .IndexOf search, using .ToLower( ), complete
private void button1_Click (object sender, EventArgs e)
{
string strinputLine = "Maintenance Fees 15.00";
//string strinputLine = "Initial fees 20.00";
int delimFoundNDX;

delimFoundNDX =
strinputLine.ToLower().IndexOf("fees",0);

MessageBox.Show ("Original Line: " + strinputLine + "\r\n" +


"'Fees' found at position: " + delimFoundNDS.ToString());
}

where:

• The numberic found index position is stored in the integer variable delimFoundNDX.
This is a base-zero count.

• Reading left-to-right, the lowercase shift happens before the "IndexOf".

delimFoundNDX = strinputLine.ToLower().IndexOf("fees",0);

The shift changes the value only for the duration of the test; the original inputline is
un-changed.

• If lower-casing, be sure the search-string is also lower-cased. The technique ensures


mixed-case possibilities are properly detected: FEES, Fees, fEEs and fees.

Results:
The first inputLine finds "fees" at position 12 (the "F" in Fees).
Using the second test string, finds "fees" at position 8. All counts, base-zero.

To find the Dollar Values:

The numbers 15.00 and 20.00 can be found by adding the length of "fees" (+4) plus an
additional position for the trailing space, giving +5. Notice both example searches work
even though they are different input lengths.

delimFoundNDX =
strinputLine.ToLower().IndexOf("fees",0) + 5;

Reading an Overload:

The indexOf command (a string method) has 9 different variations ("overloads"). You
can search using strings or char-values; you can search starting at certain positions and
you can search for a selected number of characters.

Chapter 4 - String Manipulation Page: 227


As you type the "dot-IndexOf", the editor assists with a summary of the command. Notice
how it has a daunting list of 8 overloads, that is, 9 different variations of the same
command, starting with the base command:

Choose by pressing Enter, or by typing the full text, up to the opening parenthesis. When
you do, Intellisense pops up additional help. Click through the arrowed overload list,
until you arrive at the second string option:

It takes a minute to learn how to read an overload list, but once learned, you will find the
design is simple and efficient.

The "overload list" appears when you start typing the function's opening
parenthesis: .IndexOf(

As you type each comma-separated parameter, the bottom description


changes to show more details about that variation of the command.

Scroll through the list using the up and down arrows

1 of 9: int string.IndexOf (char value)


2 of 9: int string.IndexOf (string value)
3 of 9: int string.IndexOf (char value, int startIndex)
4 of 9: int string.IndexOf (string value, in startIndex)
:
9 of 9: int string.IndexOf (string value, int startIndex, int count, StringComparison)

Each overload shows a different list of parameters for that variation of the command. The
neat thing about each option is all parameters for that row are "required".

Chapter 4 - String Manipulation Page: 228


There are no 'optional' parameters. If you want "optional" parameters,
scroll to a different overload.

Often, the overload you want is near the front of the list. For example, in the list above,
the first overload shows one parameter – search for a char value. The fourth shows a
search for a string value, starting at position-x, as in .IndexOf('fees ', 0).

Results:

For this example, the completed command is:

delimFoundNDX = strinputLine.ToLower().IndexOf("fees", 0) + 5

which finds the delimiter at position 12; adds +5, setting the final index at position 16.
From here, we can identify where the dollar value of "15.00" begins.

By shifting everything to lower case, the word "Fees" becomes case-insensitive.


Changing the input line from "Maintenance" to "Initial fees 20.00", the same command
finds a different index, making this routine flexible:

<string>.LastIndexOf:

.IndexOf searches from the start of the string, forward. Use ".LastIndexeOf" to search
from the opposite direction, making a reverse search. For example, returning to string
"Name = Smith, John"

delimFoundNDX = strinputLine.LastIndexOf("=");

Results from the example above also returns 5 because the equal-sign is at an absolute
position 5 no matter which way you start looking. Even though you are searching
backwards, it returns the find-position by starting the count at zero. Searching the same
test string for a comma results in 12.

Chapter 4 - String Manipulation Page: 229


When searching backwards through a string, the optional "starting index-position"
defaults to the end of the string but you can specify your own number. Since
"Name = Smith, John" is 17 characters long:

delimiterFoundPos = strinputLine.LastIndexOf('=', 17);

or more generally:

.LastIndexOf with optional starting index


delimFoundNDX =
strinputLine.LastIndexOf ('=', strinputLine.Length - 1);

Length Offset -1:

Why use "inputLine.Length - 1"?

In C#, all length calculation are base-1 – which start counting at 1, not zero. If you start
counting at "1", the string "Name = Smith, John" is 18-characters long, but if you started
counting from zero, the length would be 17. Here is the problem: All length calculations
are base-1 but indexOf searches report back with a base-0 count.

This may seem inconsistent, but it is not. In C#, nearly everything from arrays to pixel
positions are base-0; the only exception are lengths.

If you forget to offset the Length calculation, the blunder will generate an "index out-of-
range" runtime error ("abend"). Runtime errors are nastier than compiler errors because
you, the developer, may not see them until after go-live. End-users typically panic when
they see these.

If you are lucky enough to accidentally generate a runtime error during testing (giving you
a chance to fix the bug), be aware it appears differently, depending if you are the
developer or the end-user. For example, this illustration shows both views of the same
error.

Chapter 4 - String Manipulation Page: 230


You can use 'try-catch' (see below, program 4.4) to intercept these errors – but this is
sloppy and does not fix the underlying problem. It is better to make sure the logic is not
flawed. "Index out of range" is a logic problem that should be fixed in code. Be sure to
test with a variety of data and test with missing strings, etc.

<string>.Contains:

The ".Contains" method is an alternate way of finding text within other strings. This
method returns a boolean true|false if the search-string exists. Unlike the ".IndexOf"
functions, the index-position is not returned.

Although this command can return stand-alone results, it is almost always used with an if-
statement. In an if-statement, results exist for about two milliseconds and the results can
be implicitly assumed – the boolean does not need to be declared beforehand.

Example: <string>.Contains; returns a True/False, complete


string strinputLine = "Name = Smith, John";

if(strinputLine.Contains("="))
{
<do true stuff>
}

where:

• The search-string is strictly case-sensitive.


• Searches of one or more characters are supported; e.g.
strinputLine.Contains("Smith").
• Character (char) 'a' searches are not allowed.
• A starting value of zero is assumed and cannot be changed.
• There are no interesting overloads to this method.

In general, use an IndexOf when the horizontal position (a count) is needed. Values
greater than -1 indicate the string was found, but you will have to write an if-statement to
test the results. Because of this, a string.Contains is a little easier to code, but not by
much.

Chapter 4 - String Manipulation Page: 231


Parsing and Substrings

Extracting part of a string is called "substring" and the act of locating and extracting is
called "parsing." There is somewhat of an art to parsing and the commands may seem
complicated. However, if your logic is assembled in sections, one step at-a-time, parsing
becomes easier to manage.

The following examples continue to work with the "Smith, John" statements previously
used. As before, imagine the input line is being read from an INI file or some other
location, but for the examples the input is simulated with a hard-coded string. The goal is
to separate the header from the data and then to separate the last-name from the first.

Name = Smith, John

Substrings require a known location in the source-string, usually a delimiter, that marks
where one part of the data ends and another begins. Sometimes this can be a fixed width,
such as always at position 5, but more-often the location varies. In the "Name =" line, the
equal-sign is clearly the delimiter but the position varies, depending on the header-data.
Imagine your program needs to process an INI file with these varying delimiter positions:

Full Name = Smith, John


Address Line 1 = 123 N. Elm Street
City = Anytown
Phone = 208.383.1234

Because the location of the delimiter varies, substrings seldom travel alone – they almost
always have an .IndexOf (string-search) lurking near by. Once the sub-string is identified,
there is a certain amount of fiddling usually required to skip past the delimiter.

Everything learned in this section will be revisited and refined in


Chapter 6. Substrings are used throughout the rest of this book.

Chapter 4 - String Manipulation Page: 232


<string>.Substring

Substring extracts part of a string and writes it into a new location, using a string method
called .SubString(). The remainder of this chapter discusses the different types of
Substrings – commonly called LeftString, MidString and RightString – each of which is
built with the same .Substring statement, but uses different arithmetic to find the results.

Left, Mid and RightStrings do not natively exist in C#, but are
easily simulated with your own routines. This chapter lays
the groundwork for this, and teaches the basics for modular
code.

General Substring Syntax:

A substring always starts at a position, usually somewhere in the middle, and then reaches
to the right, one or more characters. The results of the substring are usually assigned to a
new string or it can be part of another statement, such as an "if-statement."

".Substring" has two overloads. The first overload has one parameter – the horizontal
starting position, where the substring starts, using a base-0 count. By default, it substrings
all remaining characters. The second overload has a starting-position followed by a
length (number of characters to substring).

<string>.Substring (<start-position>);
<string>.Substring(<start-pos>, <length>);

For example, starting with this variable:

string strinputLine = "The rain in Spain";


string strfoundSubstring = strinputLine.Substring(4);

Results: strfoundSubstring = "rain in Spain"

string strfoundSubstring = strinputLine.Substring (4, 4);

Results: strfoundSubstring = "rain", starting at position four, for a length of four


characters.

From above, .SubString(4, 4) begins its count at position zero. The count looks like this:

Chapter 4 - String Manipulation Page: 233


With these two overloads, a program can extract substrings from
anywhere in the original source-string, including those from the
left-side, the far-right-side and from the middle. All possible
variations of a substring can be made from these two overloads.

Example Programs:

Each of the following examples parse this string:

where there is a space before and after the equal-sign. The equal sign becomes the
delimiter for the name and a comma delimits the last and first names. The examples will
extract from the front-half, the back half and from the middle.

These variables names should help simplify the examples. The data being parsed is being
simulated by hard-coded (literal) values:

string strinputLine Holds the data being substringed


string strfoundString A temporary variable holding the results of the substring
int delimFrontNDX An integer storing the delimiter's found position (index)

Chapter 4 - String Manipulation Page: 234


Classic Left-strings

Left-strings gather the data from the left-side of the string. In other languages, such as
Visual Basic, you can commonly request the "ten most left characters from a string" but
C# doesn't have such a verb; instead, it relies on a Substring variation. Compared to a
Right-string (later), the commands needed for this statement are simple. To capture the
left-most 10 characters, hard-code the Substring, starting at index position 0, for a length
of 10.

Left-strings always start at position 0, which is the definition of a left-string. The lengths,
or number of characters, are always in base-1. In general, a classic left-string looks like
this::

.Substring (0 , <number of characters>)

Using this input string, "Name = Smith, John" the left-most 10 characters are found
with this statement:

strfoundString = strinputLine.Substring (0, 10);

Example: Classic Left-String; find the 10-most-left characters


private void button1_Click (object sender, EventArgs e)
{
//Classic Left-string; Find the 10-most left characters
//with some flaws, such as what if a blank string were searched?

string strinputLine = "Name = Smith, John";


string strfoundString;

strfoundString = strinputLine.Substring (0, 10);

MessageBox.Show ("'" + strfoundString + "'");


}

Results: 'Name = Smi'. Even though the substring starts at base-zero, the length uses
base-1.

where:

Chapter 4 - String Manipulation Page: 235


• (0, 10) says to start at index position zero and capture the next 10 characters, using a
Length of 10.

The trouble with this definition of a "Left-String" is they seldom happen this way.
Usually, the variation needs to read like this: "Left-most characters starting from the
position marked with an equal-sign, but don't include the equal-sign or any spaces." This
idea is explored next.

There is this problem with a normal substring command: In the


middle of your program is a substring that you, the programmer,
has to look at and interpret each time you are debugging. It
would be better if it read "LeftString (strinputLine, 10)",
or even "LeftString (strinputLine, "=")". These techniques
are discussed in Chapter 6 and 8, where new keywords are
written.

Left-string: Using a Delimiter (.IndexOf) for the Length Calculation:

Fixed length left-strings (e.g. 10) seldom happen in the real world. More often a delimiter
needs to be located, and once found, get all the characters to the left (or right) of the
found delimiter.

Returning to Name = Smith, John

isolate the front-header "Name =" from the rest of the string, by using the equal-sign's
position as the length of the substring. Using the IndexOf function, from the previous
section, substitute the fixed-number ("10") with a variable using an ".IndexOf"
calculation. IndexOf will contain the horizontal position of the found equal-sign:

In code, it looks like this. Find the delimiter first, noting its location. Then use a
substring (Left-string) to grab the header. This will capture a trailing space, which can be
trimmed:

Chapter 4 - String Manipulation Page: 236


Example: Delimited Left-String, preliminary
private void button1_Click (object sender, EventArgs e)
{
//Delimited Left-String; using the equal-sign for the
//position. This routine does not detect not-found delimiters.
//From the test string, returns "Name"

string strinputLine = "Name = Smith, John";


string strfoundHeader;
int delimFoundNDX;

//Find the delimiter:


delimFoundNDX = strinputLine.IndexOf("=",0);

strfoundHeader =
strinputLine.Substring (0, delimFoundNDX).Trim();

MessageBox.Show ("'" + strfoundHeader + "'"); //Displays "Name"


}

Result: MessageBox displays 'Name' after finding the equal-sign.

where:

• "Left-strings" don't exist in C#. Use a Substring variation to simulate one.


In the next several chapters you will write your own method to automate the left-
string commands.

• By definition, all Left-strings start at index position zero.

• Using an .IndexOf, find the equal-sign and write the found position into a variable,
delimFoundNDX. The base-0 position is stored as an integer.

• The next line substrings all characters to the left of the found position, using a
standard .substring, substituting the length with the found index position. Results are
trimmed to discard the trailing space found just before the equal-sign.

• The variable delimFoundNDX could be discarded by combining the search into the
Substring like this:

foundHeader =
strinputLine.Substring
(0, strinputLine.IndexOf("=", 0).Trim());

Chapter 4 - String Manipulation Page: 237


• With a Left-string, the length of the found-delimiter (the equal-sign) is
inconsequential. In other words, the delimiter is one-character long, but could be
longer. With Mid-strings and Right-strings, the length of the delimiter matters.

• This routine is flawed. What should happen if the delimiter ("=") was not found? It
should return an empty string – but as-written, this routine abends. To test, search for
a pound-sign and re-run. Once the program crashes, click the tool-bar's Stop
Debugging button (red-square icon). This flaw is addressed shortly.

Keep in mind left-substrings start at base-zero but the length calculation is base-1. In
numbers, this means the equal-sign was found at position 5 ("=") and that happens to be
the length of the needed substring (e.g. "Name ").

Using the same delimiter, change the inputLine to this string:

string strinputLine = "Address = 123 N.Elm"

Re-run the program. Results: 'Address'. As before, it captured Address's trailing space,
but then trimmed it. Also, notice how it adjusted to the different length and a different
delimiter position.

Delimited Left-String Rules

Left-String: Iron-Clad Rules:

• Start a position zero


• For a length of the delimiter's found position
• Usually: Trim the results after the substring
• <variable-string>.Substring(0, <delimiterFoundNDX>).Trim();

A Matter of Style:

Chapter 4 - String Manipulation Page: 238


delimFoundNDX ("="'s position) is declared as an integer near the top of the routine. It
could be bypassed and the results of its calculation could be placed in-line, in the middle
of the Substring statement. For example, consider the original Substring statement versus
one that does not use a separate variable:

Original:

int delim1FoundNDX;

//Find the delimiter, then the substring using the delimiter:


delim1FoundNDX = strinputLine.IndexOf("=",0);
strfoundHeader = strinputLine.Substring(0, delim1FoundNDX).Trim();

vs this shorter method, which removes two statements from the routine:

strfoundHeader =
strinputLine.Substring (0, strinputLine.IndexOf("=",0)).Trim();

This new way is not necessarily better. The first example clearly shows the intent of the
IndexOf command – it is a delimiter-found position. The second example is more obtuse
and if you were to review the code a month later, you may have difficulties remembering
its purpose. As you will see in the next section, embedded values makes it difficult to do
other testing and debugging.

Missing Delimiters:

One should wonder what happens when a delimiter can't be found. Problems like this
happen for a variety of reasons, often beyond a program's control, and logic must trap this
possibility. In the following code, the found index position is checked for a value greater
than zero before continuing:

private void button1_Click (object sender, EventArgs e)


{
//Delimited left-string with not-found delimiter check

string strinputLine = "Name = Smith, John";


string strfoundHeader;
int delimFoundNDX;

//Locate the delimiter:


delimFoundNDX = strinputLine.IndexOf("=",0);

if (delimFoundNDX > 0)
{
// delimiter found; locate the foundHeader...
strfoundHeader = strinputLine.Substring
(0, delimFoundNDX).Trim();
MessageBox.Show ("'" + strfoundHeader + "'");
}
else
{
MessageBox.Show("Expected delimiter not found")
}
}

Later chapters take this design and generalize the routine and its error processing.

Chapter 4 - String Manipulation Page: 239


Classic Right-string

Capturing the right-most nn characters of a string requires minor arithmetic. To get the
right-most 10 characters, take the length of the string minus 10. In the example, "Name =
Smith, John", has a length 18. The starting position for the a Right-string (substring)
would be 18-10=8.

As a reminder, a substring with only a starting position tells


the program to grab all characters from that position to the
end of the string – which is the definition of a right-string.

Example: Classic Right-String: Capture the 10-most Right characters, complete


private void button1_Click (object sender, EventArgs e)
{
//Capture the 10-most Right characters in a string

string strinputLine = "Name = Smith, John";


string strfoundString;

strfoundString =
strinputLine.Substring (strinputLine.Length - 10);

MessageBox.Show ("'" + strfoundString + "'");


}

Results: 'mith, John'. To get the right-most 5 characters, the calculation is similar:
(<variable>.Length - 5)

Using the example data, the starting position calculates to 8 and the substring grabs all the
remaining characters to the end of the line. In code, the final calculation reduces to this
statement:

strinputLine.Sugstring(strinputLine.Length - 10); or
strinputLine.Substring(8);

where the substring starts at index position 8, for the remaining length of the original
string.

Chapter 4 - String Manipulation Page: 240


Doing the math, it works like this:

It is easy to get confused between base-1 and base-0. Do the length calculations in base-1
but the starting position is counted from base-zero. I typically write out an example on a
piece of paper and count with a pencil-tip.

Keep these ideas in mind: Subtract the number of characters you


want from the total length; do this math normally; this is the
starting position. But when counting where the first character is,
start the count with zero.

Now that this has been said, there is no real math involved
because the rules on a Right-string never vary. See below for the
iron-clad rules.

Right-String: Using a Delimiter (.IndexOf) for a Starting Position:

As much fun as a classic Right-string can be, they are seldom used with a simple fixed-
number of characters. Invariably you need the right-most characters, starting at a
delimiter.

Using the example "Name = Smith, John", gather all the data to the right of the equal-
sign. To locate the text after a delimiter, use an " .IndexOf" to set the base-0 starting
position; see earlier in this chapter for details:

Chapter 4 - String Manipulation Page: 241


By default, a Substring takes everything from the starting position to the end of the string
(because no length was specified). Examine this flawed code:

Slightly-flawed Right-String with Delimiter


private void button1_Click (object sender, EventArgs e)
{
//Flawed Right-string; it includes the delimiter in the
//found-string

string strinputLine = "Name = Smith, John";


string strfoundString;
int delimFrontNDX;

delimFrontNDX = strinputLine.IndexOf("=", 0);

strfoundString = strinputLine.Substring(delimFrontNDX);

MessageBox.Show ("'" + strfoundString + "'");


}

where:

• an integer, delimFrontNDX is declared near the top


• delimFrontNDX (index) starts at position zero (base-0), and locates the equal-sign
• The Substring starts at that position to the end of the string.

Run the program by pressing F5, then clicking on button1.


Results: "= Smith, John"; note the equal-sign and the space are included in the returned
results. The space might be forgiven, but nobody wants the delimiter.

Unexpected Results?

The result "= Smith, John" includes two uninvited guests: the delimiter and a leading
space. This is typical for a Right-side Substring: it snags the delimiter and other leading
spaces. Fixing this problem is easy. Change the substring by skipping one character past
the delimiter and trimming the results:

Chapter 4 - String Manipulation Page: 242


A Better version: Right-String with skipped-single-character delimiter, preliminary
private void button1_Click (object sender, EventArgs e)
{
//Corrected version that trims the results of the
//substring and skips past the delimiter.
//This version is close, but still not right, as it hard-codes
//the delimiter's length at +1.

string strinputLine = "Name = Smith, John";


string strfoundString;
int delimFrontNDX;

delimFrontNDX = strinputLine.IndexOf("=", 0);

// Substring the name into its own variable:


strfoundString =
strinputLine.Substring(delimFrontNDX + 1).Trim();

MessageBox.Show ("'" + strfoundString + "'");


}

where:

strfoundString = strinputLine.Substring(delimFrontNDX + 1).Trim();

The "+1" shifts the start of the Substring one character past the equal-sign (the found
delimiter) while the Trim lobs-off the leading spaces in front of "_Smith...".

Why Not Shift +2 and Save the Trim?

You could have shifted the substring +2, thus skipping both the delimiter and the space
but this is unsafe because the input file may not have imbedded spaces (consider this
scrunched-up input line: "Name=Smith, John". If the program were changed with +2, it
would remove the "S" in Smith.) Since the input data is not guaranteed to be in one
format or the other; Trim is safer.

Secondly, the code should consider what to do if the delimiter is not found. The example
below devotes some thought to this issue but still is somewhat flawed because it only
deals with single-character delimiters.

Chapter 4 - String Manipulation Page: 243


Right-String Delimited with Error Checking, preliminary
private void button1_Click (object sender, EventArgs e)
{
// "Right-String" example that works only for single-character
// delimiters

string strinputLine = "Name = Smith, John";


string strfoundString;
int delimFrontNDX;

delimFrontNDX = strinputLine.IndexOf("=",0);
if (delimFrontNDX > 0)
{
// delimiter found; locate the fullname...
strfoundString =
strinputLine.Substring(delimFrontNDX + 1).Trim();
MessageBox.Show ("'" + strfoundString + "'");
}
else
{
MessageBox.Show("Expected delimiter not found")
}
}

Results: Correctly captures 'Smith, John' without delimiter artifacts. Issue: It does not
take into account longer delimiters – delimiters can be longer than one character.

Right-string: Using any length Delimiters:

Single-character delimiters (e.g. "=") are the norm, but you can imagine other delimiters
such as, ">>" or "//". This next variation is more accurate and works for any length
delimiter. By expanding the code in this direction, you can build a solid routine that
works in a all circumstances.

The flaw in the simpler design is in the delimFrontNDX "+1" calculation – which
assumed the delimiter was always one character long. The +1 can be "variablized" by
using the delimiter's Length. The result is the same substring that finds the starting
position and then automatically skips past the last character in the delimiter.

For example, change the input string's delimiter from "=" to "==>"

Substring(delimiterPosition + strdelimiter.Length).Trim();

Chapter 4 - String Manipulation Page: 244


Replacing the +1 delimiter-skip with a length calculation, results in this new routine:

Example: Right-string with any Length Delimiter, complete


private void button1_Click (object sender, EventArgs e)
{
//Delimited Right-String with variable-length
//delimiters. This is a fairly solid routine.

string strinputLine = "Name = Smith, John";


string strfoundString;
string strdelimFront = "=";
int delimFrontNDX;

//Locate the variablized delimiter


delimFrontNDX = strinputLine.IndexOf(strdelimFront,0);

if (delimFrontNDX >= 0)
{
// delimiter was found; locate the fullname
// using a variable-length delimiter:

strfoundString = strinputLine.Substring
(delimFrontNDX + strdelimFront.Length).Trim();

MessageBox.Show ("'" + strfoundString + "'");


}
else
{
MessageBox.Show("Expected delimiter not found")
}
}

Testing Multi-character Delimiters:

1. Change inputLine from "Name = Smith, John" to


"Blue Sky Database C:\Data\BSKY\DB\Main.mdb"

2. Change strdelimFront from "=" to "Database" (making the word "Database" an eight-
character delimiter)

3. Run the program; Click Button1.

Results: The filename is pulled from the string using the word "Database" as a delimiter,
resulting in this final string: "C:\Data\BSKY\DB\Main.mdb". Notice the delimiter is
case-sensitive.

In other words, this same routine is now flexible enough to handle normal delimiters,
such as equal-signs, but it can also now handle words that are not normally thought as
delimiters. Most importantly, the routine knows how to skip past the delimiters –
regardless of their lengths. This makes the routine flexible.

Chapter 4 - String Manipulation Page: 245


comments:

• Right-strings don't exist in C#; but are easily simulated with Substrings.

• The formula is easy: After finding the delimiter, add the delimiter's length and use
this as the Substring's starting position. Usually this is +1, but can be longer.

• If a Substring is started from the middle of a string, and an optional length is used,
then you are essentially simulating a Visual Basic "mid-string." See the below for
additional examples.

Delimited Right-String; Iron-Clad Rules

Right-String with Variable-length Delimiters: Iron-clad rules

• Confirm the delimiter was found with an if-statement


• Shift past the delimiter's length (usually plus 1)
• Trim when done
• <variable-string>.Substring (<found-ndx> + <delimFront>.Length).Trim();

Chapter 4 - String Manipulation Page: 246


Mid-Strings

Earlier examples parsed the left and right sides of "Name = Smith, John", using the equal-
sign as a delimiter. This section discusses how to substring using two delimiters: the
equal-sign and the comma, in order to parse the last name, "Smith".

Previous sections covered how to select Left and Right-side


strings. This leaves pulling strings out from the middle. In
Visual Basic and other languages, these are called Mid-strings.
As before, C# does not have a verb for Mid-strings the function is
simulated with a Substring method.

Programs often need to parse more complicated data, such as financial information
typically found in tabbed or comma-delimited tables. Looping through this type of data is
covered in Chapter 13 and that chapter relies heavily on the concepts discussed here.

Classic "Mid-String" Using Two Numeric Parameters:

A classic Mid-string uses two numeric parameters, where the first is the starting position
and the second is the number of characters to snag. In the 'Smith, John' example, a Mid-
string starting at position 7 (base-0) for a length of 5, returns "Smith", or more generally:

string.Substring(starting position, number-of-characters);

However, seldom can one use such a simple (midstring) statement. Invariably both the
starting position and the number of characters have to be discovered - both being variables
and both depending on delimiters. Because of this, the simple-sounding idea becomes
complicated:

string.Substring
(front-side-delimiter position + delimiter's length,
length-calculation=backside-delimiter - frontside -1).Trim()

Parsing with a "Mid-String":

The goal: The input line "Name = Smith, John" contains three pieces of information:
There is the header, "Name"; a last-name and a first-name. In this section, manually
substring the person's last-name, "Smith". There are two delimiters: the equal-sign and
the comma; everything between is the target:

Chapter 4 - String Manipulation Page: 247


For these discussions, the equal-sign will be called the "frontside" delimiter and the
comma is the "backside" delimiter. These two index positions help calculate the starting
point of the mid-string and the length of the copy. Both locations are found with an
".IndexOf" command.

"Mid-String" Formula:

The "Mid-string" formula, for single-character delimiters, is always the same and it
involves a series of easy, but verbose steps. Assembling the command is best
accomplished in phases. Here are the steps:

• Using ".IndexOf", locate the frontside delimiter

• Locate the backside delimiter by starting one position past the front-side

• Calculate the length (number of characters) between the two and assemble the final
substring command. This calculation uses simple math but probably will not come
naturally to most people.

Read these explanations first. Actual step-by-step instructions


are in a few pages.

In general:

string.Substring (front-side-delimiter + delimiter's length,


length-calculation=backside-delimiter - frontside -1).Trim()

The final commands look like this:

Chapter 4 - String Manipulation Page: 248


Mid-string with Single-character Delimiters, preliminary

Note: This is not a perfect mid-string command because this example only considers
single-character delimiters. Longer delimiters are discussed shortly.

Detailed Explanation:

Step 1: Locate the front and back-side delimiters:

Begin by declaring variables that hold the front and back index positions. Then, capture
the two delimiter's locations, each using an IndexOf.

Chapter 4 - String Manipulation Page: 249


int idelimFrontNDX;
int idelimBackNDX;

idelimFrontNDX = strinputLine.IndexOf("=", 0);


idelimBackNDX = strinputLine.IndexOf(",", idelimFrontNDX + 1);

Locating the front delimiter uses a standard IndexOf command and should not be a
surprise. Notice the search for the equal-sign starts at position zero:

Step 2: Locating the backside delimiter involves a small trick: Begin the search after the
front-side delimiter. The command is nearly the same, it just uses a different starting
position (the equal-sign's position) plus 1:

Step 3: Assemble the Substring Command:

After establishing the Front and Back delimiters, write the actual Mid-string command.
Because the final command is lengthy, the command will be assembled as a four-phrase
statement:

• Decide which variable gets the final answer and which variable is being searched.
In this example, "strfoundLastName" is the variable that gets the answer while
"strinputLine" is the variable being searched.

• Set the starting position for the Mid-string (this is the Frontside delimiter, plus 1)

• Compute how many characters are needed for the Mid-string, in this case, 5

• Trim the final results.

Step 3a: Build the Substring in stages: The Substring-results need to land somewhere, in
this case, "strfoundLastName". Put this variable on the left-side of the equal-sign. Then,
put the variable being searched, along with the dot-Substring method-name:

Chapter 4 - String Manipulation Page: 250


Step 3b: Continue with the next phrase and set the starting position, which in this case, is
the frontside index + 1 ("Name =") – the equal-sign plus 1 skips past the equal-sign.

Step 3c: Then calculate how many characters the Midstring should snag. Recall that a
Substring overload has these parameters: (Start Position, Number of characters); this
subtracts the front index from the back, plus an offset described below. For ease-of-
reading, place the new phrase on its own line.

More on the Length Calculation:

The third phrase (3c) of the Mid-string command involves a simple "length" calculation
but if you've never done one before, they do require thought. Look at the
"Name = Smith, John" illustration on the previous pages. Count the characters after the
equal-sign and before the comma; you'll find there are 6 characters, including the space in
front of the last-name, after the equal-sign.

The length computation is basic arithmetic.


Take the backside delimiter position, 12, subtract the frontside delimiter position. From
that, subtract 1 (to account for the frontside delimiter's length).

More generally, "idelimBack - idelimFront - 1" or


Mid-string (12 - 5 - 1 = 6 characters long)

Chapter 4 - String Manipulation Page: 251


By coincidence, the starting position in this example is also 6, for a length of 6.
Replacing all of the variables with numeric values, the new substring could look like this:

foundLastName = inputLine.Substring
(5 + 1,
12 - 5 -1)
.Trim();

or, on one line: Substring (6, 6).Trim();

Results in "Smith"

Complete the Mid-string command:

Step 4: The last phrase Trims the results, removing the leading space captured after the
equal-sign. (When Mid-stringing, it is almost always safer to include the found spaces,
but then trim the results.) Notice the Trim is outside of the Substring's parenthesis. This
forces the computer to substring first, then trim.

Mid-String Command for single-character Delimiters, complete


private void button1_click (object sender, EventArgs e)
{
//Midstring using two delimiters:
//Parsing the "last name" from Name = Smith, John
//where "=" is idelimFront; comma is idelimBack
//* This example works for all single-char delimiters
//* A better solution is shown in the next code example

int idelimFrontNDX;
int idelimBackNDX;

idelimFrontNDX = strinputLine.IndexOf("=", 0);


idelimBackNDX = strinputLine.IndexOf(",", idelimFrontNDX + 1);

strfoundLastName = strinputLine.Substring
(idelimFrontNDX + 1,
idelimBackNDX - idelimFrontNDX -1)
.Trim();

MessageBox.Show("Last name = " + strfoundLastName);


}

In the end, a "mid-string" is nothing more than an overloaded Substring that specifies a
starting position, for a pre-defined length.

j This example works for all single-width-character delimiters. You can copy the
routine, line-by-line, changing only the inputLine variable, and use this in any
program.

See below for a formula that works with any delimiter length. Chapter 6 and 8
converts this routine into a function that can be called from anywhere.

Chapter 4 - String Manipulation Page: 252


"Mid-String" for any Length Delimiter

The single-character delimiter "Mid-string" described above works for a majority of the
mid-strings you are likely to encounter. But if the delimiters are more than one character,
the formula fails. With minor changes, the Mid-string can accommodate any length
delimiter and it will work in all Mid-string situations.

To make this work, "variablize" the delimiters, by replacing all "=" and "," (equals and
commas) with variable representations. Then change all hard-coded "+1" offsets to
length-calculations.

The new routine will look like this:

Chapter 4 - String Manipulation Page: 253


Mid-String for any length delimiters, complete
1 private void button1_Click (object sender, EventArgs e)
2 {
3 //This Mid-string formula works for all Mid-strings
4
5 string strinputLine = "Name = Smith, John";
6
7 //Variablize the delimiters:
8 string strdelimFront = "=";
9 string strdelimBack = ",";
10
1 //Declare, Locate the front and backside delims first:
2 //You are locating the equal-sign and comma:
3 int idelimFrontNDX =
strinputLine.IndexOf(strdelimFront, 0);
4 int idelimBackNDX = strinputLine.IndexOf
(strdelimBack, idelimFrontNDX + strdelimFront.Length);
5
6 //Then perform the mid-string with this constant formula.
7 //This formula works for all mid-strings:
8
9 string strfoundLastName = strinputLine.Substring
(idelimFrontNDX + strdelimFront.Length,
idelimBackNDX - idelimFrontNDX - strdelimFront.Length)
.Trim();
20 }

where:

• At line 8, the variable "strdelimFront" (not delimFrontNDX), represents the string


"=". This is the frontSide's variablized delimiter. Compute its length with:
(strdelimFront.Length).

string strdelimFront = "=";

• Line 9 declares the variable "strdelimBack". Keep in mind in real life, the delimiters
will be passed into the routine as a variable and this is demonstrated in the next
several chapters.

string strdelimBack = ",";

• Line 13 finds the frontside delimiter's (strdelimFront) position in the string in the
same way as before, and the numeric value is stored in the same idelimFrontNDX
variable. In other words, it still searches for the "=".

int idelimFrontNDX = strinputLine.IndexOf(strdelimFront, 0);

• Line 14 finds the backside delimiter's position, starting its search after the frontside
delimiter: note the idelimFrontNDX + (the delimiter's length).

int idelimBackNDX = strinputLine.InexOf


(strdelimBack, idelimFrontNDX + strdelimFront.Length);

Chapter 4 - String Manipulation Page: 254


If this seems similar to the "Right-string with any length delimiter" discussed earlier, you
would be correct. Remember, a Mid-string is nothing more than a Right-string with a
length calculation. Study the Right-string section as a reminder. See below for how to
use this in an example program.

Why Skip past strFrontSide Delimiter's Length?

Line 14 shows how the second delimiter doesn't begin its search until after the first
delimiter. "After" means after the last character of the delimiter. With single-character
delimiters, this difference is hard to see. But imagine if the delimiter were longer than
one character...

Consider this contrived example:


"Company: XYZ Data Base: Finance City: Boise"

Say you were trying to substring the Data Base name "Finance", using the phrase
"Data Base{colon}{space}" as the front-side delimiter and you needed to be concerned
about the City name, also on the same line. (For the sake of this discussion, the {space}
after Finance could be used as the backside delimiter or you could also use the word
"City".)

Since the front-side delimiter also contains a space, you must skip past it before starting
the search or else you will find the wrong {space} when looking for the backside
delimiter.

Chapter 4 - String Manipulation Page: 255


Exercise:
Manually Parse all three parts of Name = Smith, John

Write a program that manually parses all three parts of the input line. Store each part into
its own variable. This program will use Left, Right, and Mid-strings. The test string
breaks-out according to this illustration:

Program 4.3: Manual Parsing

Attempt this exercise now, before reading the detailed steps.

1. Delete all code from the button1_Click event in previous example programs (or start a
new project).

2. In button1_Click, declare assorted variables to hold each of the found parts of the string
along with two variables to hold the delimiter positions. To keep the example from being
cluttered, the input line is simulated with a hardcoded variable.

private void button1_Click (object sender, EventArgs e)


{
string strinputLine = "Name = Smith, John";
string strfoundHeader;
string strfoundLastName;
string strfoundFirstName;

//Setup delimiters:
string strdelimFront = "=";
string strdelimBack = ",";
int idelimFrontNDX;
int idelimBackNDX;
}

3. Use an ".IndexOf" to locate the front-side delimiter (the equal-sign), starting the search at
position zero:

//Locate delimiter positions:


idelimFrontNDX = strinputLine.IndexOf(strdelimFront, 0); //The =

4. Using the first-delimiter's position as a starting point, locate the second delimiter. You
always want to begin your search for the second delimiter one character past the first (the
length of the front delimiter):

Chapter 4 - String Manipulation Page: 256


The second-delimiter search could have started at position 0, because we know there is
only one comma in the string, but this is more efficient. More importantly, this logic is
required when searching for multiple commas or multiple tabs (say within the financial
data).

By now, the program should look like this snippet:

private void button1_Click (object sender, EventArgs e)


{
// <variable declarations went here; see above>

//Locate delimiter positions:


idelimFrontNDX = strinputLine.IndexOf(strdelimFront, 0);
idelimBackNDX =
strinputLine.IndexOf
(strdelimBack, idelimFrontNDX + strdelimFront.Length);

With the front and backside delimiters located, there is now enough information to
perform the Left-string, the Right-string and the Mid-string. The rest of the commands
are standard substring commands discussed previously. The most complicated command,
the Mid-string, can literally be copied from the description above; the calculations are
already resolved.

Continue the Program:

See below for a completed program; in the interest of space, only code-snippets are shown
here. Simply key the snippets below the previously-typed statements.

5. "Left-string" the header and store in previously declared "strfoundHeader" variable.


The first (frontside) delimiter's position, "=", was stored in step 3 as idelimFrontNDX
(=5). Trim the results:

//Left-string the Header and store the results:


strfoundHeader = strinputLine.Substring
(0, idelimFrontNDX).Trim();

Chapter 4 - String Manipulation Page: 257


6. "Right-string" the first-name and store the results. Notice you can use the already-located
backside delimiter, delimBackNDX (=12). Skip past the delimiter by adding (one – the
length of the delimiter); this way the delimiter isn't included in the substring:

//Right-string the first name by snagging from the


//comma, outwards:
strfoundFirstName = strinputLine.Substring
(idelimBackNDX + strdelimBack.Length).Trim();

By Substringing at position 12+1, and not specifying the number of characters to


substring, all characters to the end of the inputLine are found and assigned to the first-
name variable.

7. "Mid-string" the last name with this statement, literally copied from the section above.
The math is already done:

//Mid-string the last name using a standard formula:


strfoundLastName = strinputLine.Substring
(idelimFrontNDX + strdelimFront.Length;
idelimBackNDX - idelimFrontNDX - strdelimFront.Length)
.Trim();

8. Missing Delimiters?

It seems a tad dangerous to substring when a delimiter might not be found in the original
string. You should wrap some kind of error-logic around the Substrings. There are
several ways to do this; all are acceptable:

if (delimFrontNDX != -1)
{
<do all your Substring stuff here>
}

or

if (strinputLine.Contains(strdelimFront))
if (strinputLine.Contains(strdelimFront) == true)

The program could check for the second delimiter, using an && (AND):

if (strinputLine.Contains(strdelimFront) &&
(inputLine.Contains(strdelimBack))
{
<all code lives here>
}

Remember, the "&&" instructs the compiler to check the first half of the if-statement; if
false, don't bother checking the last half. This makes the statement slightly more efficient.
See the completed program below for how to position this statement.

Chapter 4 - String Manipulation Page: 258


The completed 'Smith, John' Parse:

After all this discussion, the completed program is probably shorter than expected and it
parses all parts of the Name = Smith, John statement. Notice the Substring commands
are sensitive to the length of their delimiters:

Program 4.3 - Full-name Parse, complete

private void button1_Click (object sender, EventArgs e)


{
string strinputLine = "Name = Smith, John";

string strfoundHeader;
string strfoundLastName;
string strfoundFirstName;

string strdelimFront = "=";


string strdelimBack = ",";

int idelimFrontNDX;
int idelimBackNDX;

if (strinputLine.Contains(strdelimFront) &&
(strinputLine.Contains(strdelimBack))
{
// locate both delimiters
idelimFrontNDX = strinputLine.IndexOf(strdelimFront, 0);
idelimBackNDX = strinputLine.IndexOf
(strdelimBack, idelimFrontNDX + strdelimFront.Length);

// parse the header (Left-string)


strfoundHeader = strinputLine.Substring
(0, idelimFrontNDX).Trim();

// Parse the first-name (Right-string)


strfoundFirstName = strinputLine.Substring
(idelimBackNDX + strdelimBack.Length).Trim();

// parse the last name (Mid-string)


strfoundLastName = strinputLine.Substring
(idelimFrontNDX + strdelimFront.Length,
idelimBackNDX - idelimFrontNDX - strdelimFront.Length)
.Trim();

MessageBox.Show
("The person's " + strfoundHeader + " is: " +
strfoundFirstName + " " + strfoundLastName);
}
else
{
MessageBox.Show ("Unable to parse - bad delimiters");
}
}

Chapter 4 - String Manipulation Page: 259


Results: From "Name = Smith, John", display "The person's Name is: John Smith"

Exercise:

Using program 4.3, consider testing with different input lines:


Name = Fennington, Mary Lou
Address = San Francisco, CA

The Left, Mid and Right-string routines described in this chapter contain the logic
necessary to parse delimited input strings. Chapter 6 moves these routines into
generalized functions that work on any input string, with any delimiter. Once completed,
all hard-coded variables will disappear and you will never have to write these routines
again because you will have essentially added new keywords to the C# language.

Chapter 4 - String Manipulation Page: 260


String Manipulation Exercises

A. Write (or modify program 4.1) so it has two textBoxes, textBox1 and textBox2. Convert
the string value typed in textBox1 to upper-case and show the results in textBox2 when
ever button1 is clicked. If you are using program 4.1 as your base, discard that program's
'testString' variable.

Also add logic to Trim the results.


Display a message showing textBox2's length.

B. Write a routine that looks at any path/filename and appends a closing backslash.
For example:

"C:\data" becomes "C:\data\"


"C:\data\" stays as "C:\data\"

Next, make the routine sensitive to filenames in the path. If there is a filename-extension,
do not add a closing backslash. For this exercise, assume that all file-extensions are 4
characters long, including the period (e.g. ".doc", ".xls", ".wpd", etc.):

"C:\data\myfile.xls" stays as "C:\data\myfile.xls"


"C:\Data\myfolder" becomes "C:\data\myfolder\"

C. Take a string value, "123.10", convert to a floating-point number and add 1.95.
Take this result and truncate the decimal, leaving 125.

D. Write a program that parses city-state-zip into three fields. Consider city names that
might have two or more words. Consider two character state codes as well as multi-word
state names spelled out. Assume a comma.

If zipcode is present, assume it is numeric. (This logic does not work with Canadian
addresses. [See interesting article at http://en.wikipedia.org/wiki/Canadian_postal_code])

Examples:
San Francisco, CA 12345
Boise, Idaho 12345-3456
Gilford, New Hampshire 12345
Portland, OR

Chapter 4 - String Manipulation Page: 261


Extra Credit: What if no comma is present? Can the routine still be reliable when city-
names and state-names can be more than one word (probably not, unless you have a list of
all possible state-codes)? Without writing the code, how do you think the logic would
work?

Can it be written if you assume a two-character State code?

Jefferson City NH 12345


Jefferson City New Hampshire 12345
San Francisco CA 12345
Boise ID 12345

Chapter 4 - String Manipulation Page: 262


An Absolute Beginners Guide to C# - Volume 1
Visual Studio C# 2017
Intro -Through Forms
by Tim R. Wolf
2017.06 Version 1.04
Table of Contents

9 Chapter 1 - Introduction to the Editor 3


Your First Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Variables and Scope. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Working with Text Boxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Naming Fields
Concatenation
Default Text Values

9 Chapter 2 - Introduction to Loops 45


"while" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
loopCounter
Appending a String
Incrementing a Counter
Incrementing with "++"
Concatenating to Self with "+="
MessageBoxes
Infinite Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Breaking into the Loop
"while" Loop - Printing Numbers 1-10. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
"do" Loops.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
for-next Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Carriage-Return/LineFeed (CRLF)
Variations on "for-next" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Controlling Loops with Variable textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Interrupting Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
continue;
break Statements
"while-loops and 'continue'
Nested Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
foreach Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Loop Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110

9 Chapter 3 - Conditional Branching 117


Numeric and String Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Basic if-statement Construction
Numeric "if" Statements
Brace Style
"else"
Semicolon Rules
&& (AND) || (OR) Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
"|" and "||" (OR) Boolean
"^" (XOR – Exclusive OR) Boolean
Nested if-statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
"else if"
Math.Min - Numeric Testing for Smaller Value.. . . . . . . . . . . . . . . . . . . . . . . . . . 139
Compounding if-Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
switch Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Case "Value":
default Clause
Required breaks
Compound case Statements
Case-sensitive switches - .ToLower()
goto Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
Ternary Operator. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

9 Chapter 4 - Strings 163


Declaring Strings.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Special Character Strings (Escape Codes) \t, \r\n. . . . . . . . . . . . . . . . . . . . . . . . . . 167
Reserved Backslash
Carriage-Return-Line-Feeds CRLF
Embedded Quotes
Verbatim Text Strings - @
ASCII Codes
String Concatenation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
string.Concat ( ) and "+"
Floating Point Numbers
string.Compare( )
<string>.EndsWith. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
Detecting .XLS file Extensions
<string>.Length. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
<string>.ToUpper( ), .ToLower( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
<string>.PadLeft(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
<string>.PadLeft(int, '*');
Date Padding with Leading Zeros
<string>.Trim( );. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
<string>.TrimStart()
<string>.TrimEnd()
Trimming with other Characters
char.IsNumber ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
<string>.Replace( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Character to Numeric Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Conversions with Casting
null and Empty Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
String.IsNullOrEmpty( )
Null-Conditional Operator
Testing for "blank". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
if ((strtest + "").TrimStart().Length == 0)
Finding Strings - IndexOf. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Reading an Overload
<string>.LastIndexOf
<string>.Contains
Parsing and Substrings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
<string>.Substring. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Classic Left-strings .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Classic Right-string. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
Mid-Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
"Mid-String" for any Length Delimiter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253

9 Chapter 5 - Numbers and Dates 265


Integers Defined. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Floating Point Numbers Defined.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Casting and Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
Implicit Conversions
Explicit Conversions
try-catch Error Trapping. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Converting Strings to Numeric. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
TryParse
Rounding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Math.Round
Truncating Decimals. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
Math.Truncate
Basic Math Functions (Division). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
DivRem (Divide Remainder) / Mod. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
Math.DivRem
Mod "%"
Other Math Functions (SQRT, etc.). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
Sqrt (Square Root)
Random Numbers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
Dates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
DateTime.Now
String.Format
Date Parts
Date Format Pictures
DateTime.TryParse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
UTC (Zulu) Time
Empty Dates - Nullable Dates
DateTime.Compare. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Date.Compare: Two Files
Less Exact Date Comparisons
Rounding Dates
Optional Project: MTWRFSU.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
Optional Project: AllowByHour. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327

9 Chapter 6 - Utility Functions - Methods 333


IsBlank( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
IsFilled( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
LeftStr, RightStr and MidStr string Functions. . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
Classic LeftStr ( ) "Left-string".. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
Left-string with String Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Classic Right-String with Numeric Parameters.. . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Right-string using Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Mid-string with Numeric Parameters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Mid-string Numeric Start Position but No Length (Overload). . . . . . . . . . . . . . . . 382
Mid-string with string Delimiters and NumberOfCharacters. . . . . . . . . . . . . . . . . 383
Mid-string with Two String Delimiters.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388

9 Chapter 7 - Advanced Utility Functions 395


StripSlashes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
StripTrailingComments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
StripLastCharacter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
StripNonNumerics ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
StripNonNumerics, Preserving Decimals and Signs. . . . . . . . . . . . . . . . . . . . . . . . 424
Overloading
ParseBetweenDelimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Overloading ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
ParseKeyName: Master Function. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
IsNumeric ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463
IsNumbers ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
StripDuplicateCharacters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475
VerifyYN.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Passing Variables by Reference. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481

9 Chapter 8 - Class Libraries 493


Building an External Class Library from Existing Code. . . . . . . . . . . . . . . . . . . . 496
Linking an Existing External Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
Using the util. Class Library (CL800 Util). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
Link vs Copy
Inline "Program" Class Libraries (PayrollTools). . . . . . . . . . . . . . . . . . . . . . . . . . 513
Using CL800_Util within the new Class.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 520
Constructors
Manually Building a Class Constructor
Creating an "External" Class Library from Scratch. . . . . . . . . . . . . . . . . . . . . . . . 525
Compiling and Using DLLs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526
Using the DLL
"Modularizing" with Program-Control Functions.. . . . . . . . . . . . . . . . . . . . . . . . . 532
"void" Functions
Passing Variables to Program-Functions
Returning Values from a Program-Function
Naming Standards for Functions and Class Libraries.. . . . . . . . . . . . . . . . . . . . . . 535
Object Prefixes

9 Chapter 9 - Variable Scopes 543


Variable Scope, Demonstrated. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
Scope within Loops
Form-Level (Class) Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 551
"Global" Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554
"Quick and Dirty" Global Variables
"static" Modifier
Form1 to Open Form2
Using a Global Class to Pass Values.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564
Building an External Global Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 568
Getting and Setting Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 577
Building Get/Set Properties
Passing Variables by Ref.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 582

9 Chapter 10 - Form Controls and Events 589


Default Editor Settings - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594
Snap To Grid
Compiler Errors
Starting and Naming a New Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 598
Rename the Form
Recommended Form Properties
"Form Load" event
"this.Show"
Link the CL800_Util Library
AutoClose Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 609
textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 613
MaxLength
Default Text Value
Enabled
Password Fields
Multiple Lines
Setting Field Properties at Runtime. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 619
textBox Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 625
Enter and Leave Events
TextChanged
KeyPress Events (Intercepting Keystrokes). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 629
Allowing only Numeric Values
UpperCasing as Typed
Intercepting Shift, Alt and Ctrl Keystrokes
Alt-Key events on a button or other object
comboBox and listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 638
Static (Hard-Coded) ComboBoxes
AutoComplete / Type-Ahead
Query the Selected Value
"No Selection"
Using the SelectedIndexChanged
ComboBoxes linked to an External Data
listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 648
Blank-line Testing
listBox Multiple Selection
Processing Multiple Selected Records
checkBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 660
ThreeState
CheckChanged
CheckStateChanged
Click
Radio Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 669
Radio Button Events
Grouping
ProgressBar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 676
monthCalendar and DateTimePicker. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 681
.MaxDate
.MinDate
Form Load Event
Date-Time Math
MenuStrip. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 689
Horizontal and Vertical Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 692
ToolTips. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 694
ToolBox: "PictureBox" Icons and Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
Launching Other Applications From the Graphic
Link Labels. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Label Tricks.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 704
Transparent Labels
Using Labels in Your Program
Tab-Order. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 706

9 Chapter 11 - Calling Secondary Forms 711


Opening Secondary Forms - Simple. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 716
Issues with a Simple Form Call
Using a Global Variable to Re-Open Form1.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 724
Using a FormReference to Open Form2 - Recommended. . . . . . . . . . . . . . . . . . . 727
"FormRef" properties
Modal and Modeless Forms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 735
Transferring Data Between Forms using Global Variables. . . . . . . . . . . . . . . . . . 737
Using Quick-and-Dirty Global Variables
Using a Global Class (Global Variables)
Retrieve the Stored Value in Form2
Passing Data with a Signature and a Constructor. . . . . . . . . . . . . . . . . . . . . . . . . . 741
The Parent's Call
The Child (Form2) Constructor
Passing Values using get-set Properties - Recommended. . . . . . . . . . . . . . . . . . . . 745
Form Starting Positions - Global Class. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 753
MessageBox Overloads. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 756
Custom Dialog Boxes: NS810_Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 759
Returning Results to the Parent via Properties
Custom InputBoxes: NS815_InputBoxDialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . 777
Detecting a KeyPress ENTER Key Event.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 785
Using NS815 to Pass Arrays instead of Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . 787
Multiple Input Screens in the Same Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 789

9 Appendix A - Compiler Error Messages 3

9 Appendix B - Compile and Distribution 38


Cheap and Easy EXE Distribution:.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Virus Risks
EXE Icons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Using Visual Studio to Edit Icons
Attaching Icon Files to your Project
Creating Publishing / Distribution Packages.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Generating a Creator Tag
Introduction

Audience:

This book is volume one of a three volume set and is intended for beginning Microsoft
Visual Studio C# (C Sharp) programmers, versions 2015 through VS 2017. This is a
hands-on book, with actual programs, starting in Chapter 1 and it talks about the
practical day-to-day, nuts-and-bolts programming that real people need to know.

It assumes no prior programming experience.


Little time is spent on theory.

Each topic has step-by-step instructions with numerous code examples and over 900
cropped and annotated illustrations. It explains the "why" of a program.

Prior Experience

If you already have moderate programming experience, especially in Visual Basic, this
book will expand your skills, giving confidence in the new language.

The goal is to have as much time on the keyboard, working with common business
problems. After studying this book and working through the examples, you will be a
proficient programmer – able to write real programs that do real work. You will be able
to read files, parse data, write to database, and build data input screens.

Why this book?

This book is different than most publications. Little time is spent on theories and
technical side-trips are rare. Starting in Chapter 1, you will immediately begin working
with loops, if-statements and string-manipulation. This means some topics, such as
conversions, numeric types, and other such concepts, are glossed over until they are more
germane to the concepts being taught.

Where other publications might spend a page or two on a topic, this book dives into the
most common and most useful ways to solve a problem. For example, over 80 pages are
devoted to opening multiple forms and how to pass data between them. The parsing
chapter devotes 70 pages to this subject, covering delimiters, CSV, Tab, Excel, and other
techniques. This is not over-kill. You will find these address real-world data-processing
problems. I cover the tips and tricks you will need to know.

Chapters often show different techniques for the same problem and the benefits and
drawbacks of each are explained. If there is a chance of making a mistake in
punctuation, style, or logic, the examples show how to resolve them. Compiler errors are
scattered throughout the book and there is a comprehensive alphabetic error reference in
the appendix, showing likely causes and recommendations.

A side-effect of this first volume will be a library of utility modules that can be used in
all of your programs. These utilities can automate mundane tasks, such as parsing
delimited files, punctuating phone numbers, street-addresses, and capitalizing proper
names. These libraries will save boat-loads of time and will be literally useful in all your
programs.

You will also notice none of the examples use Console-applications (DOS-like
programs) – all are Windows forms. Besides being more visually interesting, they allow
greater feedback while developing and the programs more accurately reflect what
happens in the business world.

What this book does not cover:

I do not cover some of the more theoretical underpinnings of modern programming.


Things such as polymorphism and object-inheritance are not discussed directly, but the
techniques are used throughout the book. Advanced programmers may take exception,
but I favor a more procedural background and I always approach problems from a
business-oriented perspective. You have a job to do, files to process, data-entry-screens
to write. These are the things this book is concerned about.

These volumes do not cover web-development but the programming skills taught are
100% transferrable. SQL databases are introduced, but four lengthy chapters only
scratch the surface. However, if you fear treading in this area, these four chapters will
get you started and will show relatively advanced techniques.

Why C#?:

Microsoft has built a wonderful programming environment which verges on magical.


The language is mature and consistent and can be applied in any conceivable situation.
It is a powerful tool that can solve complex problems.

The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.

How to Use This Book:

These three volumes are teaching books and because of that, it makes a poor reference
guide. To get the most utility, start at page one and work your way through chapters.
Each chapter builds on the previous. It takes time and effort to learn programming. As
you work through the chapters you must sit in front of the compiler and write the code.

This series was divided into three volumes, partly to aid in printing, and partly to make
the chapters more accessible.

Volume 1:
1 Introduction to the Editor
2 Introduction to Loops
3 Conditional Branching
4 Strings
5 Numbers and Dates
6 Utility Functions
7 Advanced Utility Functions
8 Class Libraries
9 Variable Scope
10 Form Controls and Events
11 Calling Multiple Forms
A Compiler Error Messages
B Compile and Distribute

Volume 2:
12 ASCII Files
13 Parsing Tab and CSV Files
14 INI Files
15 XML and App.config Files
16 Windows Registry
17 Reading Excel and Access
18 External Programs (Shell)
19 Wait, Delays, Pauses
20 Printing
21 Formatting

Volume 3:
22 Arrays
23 File Manipulation
24 Console Applications
25 SQL Databases
26 SQL Record Edits
27 SQL Data Grids
28 Data Grid Cell Editing
C Installing SQL Server Express
D Routines (of Interest)

Thank you

Thank you for purchasing this book. I hope you enjoy it as much as I have had writing it.
Comments and suggestions are welcome.

traywolf at keyliner com


www.keyliner.com

This book was written with Visual Studio 2013 through 2017's Community Edition, with
sections referencing Microsoft Office and Microsoft SQL Server 2016 Express.
© 2017 by Tim R.Wolf. All rights reserved.

Original text written with WordPerfect version X7. Illustrations created with Corel's
PaintShop Pro, version X8.
The Compiler and Other Tools:

As of mid 2017, Microsoft allows you to freely download a fully-capable version of


Visual Studio. The software is free and this book was written with these versions. See
this link:

http://www.visualstudio.com/en-us/products/visual-studio-community-vs

This book is applicable to VS 2010 and newer, with an emphasis in version 2017.

For the database chapters you will need a copy of Microsoft MS-SQL database or a copy
of Microsoft's free downloadable "MS-SQL Express". Details can be found in the
Appendixes.

Everything in this book was designed to run on a stand-alone (home) workstation,


including the SQL chapters. You will not need a server, external SQL database or
Active Directory to complete the chapters. However, when useful, references to these
other resources are made.
Absolute Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 08 - Class Libraries
Chapter 8 - Class Libraries

The utilities from the previous two chapters were written to be generic and were meant to
be useful in a variety of situations. Because of this, they are good candidates for moving
into a separate library of functions, stored as a separate file.

This file, a "Class Library", can be linked into your programs as needed, saving the
trouble of re-creating the code. The Class Library makes the routines re-usuable and, in
effect, the functions (now more properly called "methods") become new keywords in the
C# language.

For this chapter, the new library will be arbitrarily named "CL800_Util"18 and once
moved, it can be linked into the current or any other program. Once linked, the methods
become available for use anywhere in the code. With this, you will never have to write
another Mid-String or IsBlank routine again.

Topics:

In this chapter you will learn the following topics, all of which are variations on the same
idea:

• Build Class Libraries from Existing Code (e.g. utility functions)


• Adding (Linking) a Class Library to your program
• Using a Class Library's Module (e.g. util.IsBlank())
• Creating a new Class Library on the fly
• Compiling and using DLL's
• Modularizing with Functions
• Naming Standards for Functions and Libraries

For reference, here is an overview on how to use the soon-to-be-built Utility library:

Overview Steps: Using an Existing Class Library (Linking)

1. In Solution Explorer, Link to CL800_Util.cs


2. Add statement: using NS800_Util;
3. In "public partial class Form1": CL800_Util util;
4. After "initialize components": util = new CL800_Util();

Functions from Previous Chapters 6 and 7:

• IsBlank if ((strTestString + "").TrimStart().Length = 0)


• IsFilled

18
The reason for the name 'CL800' is explained at the end of the chapter.

Chapter 8 - Building Class Libraries Page: 493


• Left-strings (Leftstr), Leftstr with delimiters
• Right-strings (Rightstr), Rightstr with delimiters
• Mid-strings (Midstr), with delimiters

• StripSlashes
• StripTrailingComments
• StripLastCharacter
• VerifyYN
• StripNonNumerics
• StripNonNumerics, preserving decimals and signs
• ParseBetweenDelimiters
• ParseKeyValue, ParseKeyName
• IsNumeric (T/F)
• IsNumbers (T/F)
• StripDuplicateCharacters

• For CSV and TAB file processing, see Chapter 18.

Overview:

This chapter covers different ways to build class libraries. While all deal with seemingly
different steps, they are the same with the only real difference in how the source-code is
placed into the library.

j Building the CL800_Util library is required for all future chapters (See end-of-
chapter Exercise A).

Building a Class Library from Existing Code:19

Discusses how to take source-code from an existing program (e.g. Form1's LeftStr,
RightStr, and Mid-strings) and move it into a new "external" library that is
independent of Form1. When the functions are moved into the library, the are more
properly called methods. In these examples, the new library will be called
CL800_Util.

Creating an "Inline Program" Class Library:

Demonstrates how to create an on-the-fly class library within a program and this type
of library is not necessarily meant to be shared by other programs outside of the
current project. This can be used to segregate code within a project. For example, all
routines dealing with "Printing" could live in a separate printing library.

19
The terms "External" and "Internal" Class Libraries are coined by the author with the only
distinction being where the code is stored: on a separate disk file (.cs) or within the current project.

Chapter 8 - Building Class Libraries Page: 494


Creating "External" Class Libraries from Scratch:

This follows the same steps as building an Inline Program Library except the library
is saved outside of the current project. Once saved, the newly-built class library (for
example, CL800_Util) can be linked or copied into any program. Linking or copying
has benefits and drawbacks, and each is discussed.

Modularizing with Functions:

Describes how methods and functions within a program can be used to simplify logic
and compartmentalize code. This section also discusses a naming-standard for major
routines that uses a unique coding prefix (A100_Main, A200_Process, etc.).

As more complicated programs are developed, the ideas presented here will make them
easier to read and maintain.

Chapter 8 - Building Class Libraries Page: 495


Building an External Class Library from Existing Code

This section describes how to take existing code (such as the MidStr, LeftStr and other
functions from the previous chapter) and move them to their own Class Library. The
library will be saved to an external file, separate from Form1's code.

j For the CL800_Util library, building an external class library is a one-time step.
Once built, the library can be linked or copied into any program; see the section
"Using and Existing Class Library."

The Goal: Move the previous chapter's work, by taking most of the existing code and
placing it in its own class library. Once the class is built, other programs can
use the code and all subsequent chapters in this book rely on this class. This
chapter assumes the work in the previous two chapters was saved in a C#
solution called "CH06_Functions".

The Visual Studio editor does not have an elegant way for moving code from an existing
program to a new class library. The steps are easy but somewhat convoluted and there
are probably a dozen different ways to do this. The procedures recommended involve
launching two copies of the editor with a cut-and-paste between them. Steps require
careful naming and are detailed below.

Preparation:

1. Create a home directory for the new library. For these examples, a local drive will be
used, but these types of shared libraries are usually shared with other people and a server
location is preferred. Because the new class is not tied to a particular project, it should
get a dedicated directory, separate from other projects.

Using Windows Explorer, create this directory structure; you may need to create
higher-level directories first. For example:

Create this directory: C:\Data\Source\CommonVS

Note: If the class library is needed only for the current project, the
class is usually stored in the same project directory.

2. Launch Visual Studio and open the solution file from the last chapter(s),
CH06_Functions.sln (this is the project/solution with the LeftStr, RightStr, IsBlank, and
other functions from chapter 6 and 7). Confirm the code is visible in the editor.

3. This is the less-than elegant step:


Start a second (new) copy of Visual Studio (Start, Programs, Microsoft Visual Studio).

a. From the Start Page, select "More Project Templates,"or optionally, File, New
Project)

Chapter 8 - Building Class Libraries Page: 496


b. In the Project Types section: Confirm Visual C#, "Windows Classic Desktop" is
selected.
In the Templates section, highlight "Class Library .NET Framework"

c. In the Name: field, type: "NS800_Util"


The name "NS800" is an invented name and the reasons for this nomenclature are
explained shortly. The "NS" prefix stands for "namespace" and will help you
remember the various parts of the class when it is later linked into a program.

d. In the "Location" field, type or browse to: "C:\Data\Source\CommonVS"

Chapter 8 - Building Class Libraries Page: 497


This is the directory built in step 1 and is the parent Folder for the new NS800
Library. This directory is separate from other projects or solutions and since it is re-
usable, it should not be associated with other programs.

e. In the "Solution Name" field, type "NS800_Util".


As usual, uncheck the "Create Directory for this solution" - the directory will be
created anyway and this saves having a double-stacked NS800\NS800 folder.

f. Click OK to build the class library.


A blank Project Solution opens, waiting for code. In a few steps, the code from the
original project will be copied into this new project.

4. Rename the newly-built class-library.


(Technically this is not required but it makes future steps less confusing. It is poor
programming style to leave a class with a "class1" name.)

While still in the second copy of Visual Studio (the new Class Library), locate "Solution
Explorer." If Solution Explorer is not visible, choose top menu "View, Solution
Explorer."

a. "Other-mouse-click" Class1.cs, select Rename

b. Rename from "Class1.cs"


to "CL800_Util.cs" (You must type the ".cs" extension)

c. When prompted "You are renaming a file. Would you like to perform a rename in
this project of all references....", choose "YES"

Chapter 8 - Building Class Libraries Page: 498


Once renamed, look near the top of the code and notice how the namespace is
called "NS800_Util" and the class line is called "CL800_Util". As ususal, all
names are case-sensitive. Also notice the "public" modifier in front of the class
name.

j You cannot rename the module by simply typing the new name inside of the code;
you must use Solution Explorer. When done, the top section of code looks like
this:

namespace NS800_Util
{
public class CL800_Util
{
//IsBlank, IsFilled, ParseKeyValue, etc. will go here...
:
:

Copy Steps:

5. Follow these steps to cut the existing functions from Chapter 6 and 7, getting them ready
to paste into the new class:

a. Leaving the current Visual Studio session open, return to the original Visual Studio
editor – the first copy of Visual Studio, where the RightStr, and other utility functions
live. Use the Windows task bar if needed. Locate Form1's code-view.

b. In Form1.cs (the code), highlight all of the functions from the previous chapter. The
first function is probably "IsBlank". You will be highlighting several hundred lines of
code and the editor will be slow at scrolling.

Alternately, place the cursor at the first line to copy, then using the mouse and the
vertical scroll bar, scroll to the bottom of the coded. Then Shift-Click the last line of
code, highlighting all the text between.

• Some cautions: Near the top of the program, do not highlight "public partial class
Form1 : Form", nor its first opening brace. Begin at the first "public bool
IsBlank" or "public string ...." you find. If found, include the leading \\\ (triple-
slash comments.

• At the end of the code, do not highlight the last two closing braces; these belong
to that program's class (e.g. Form1).

• If possible, do not highlight the "button1_Click" module, probably near the top,
but depending on where it is, it may be difficult to avoid. If it is accidentally
copied, allow it to paste into the new module, then delete the module once it
arrives. Reason: The Class Library is not capable of using a button1.

Chapter 8 - Building Class Libraries Page: 499


c. Select Edit, CUT
(There is no need to copy; you will not need the code in its former position).

6. Move to the second copy of Visual Studio (this is the new, blank project; see Windows
Task Bar). Open CL800_Util.cs's code-view.

Between the opening and closing braces for "Class1/CL800_Util"


• Insert a blank line or two between the braces
• Place the cursor on a blank line
• Edit, Paste

Chapter 8 - Building Class Libraries Page: 500


7. If button1_Click was copied, delete that module from the newly-pasted section. Delete
the declaration, the opening brace, all code within, including the closing brace.

8. If you followed the instructions in the previous chapter, this step is probably already done:

Note: From Chapter 6: Because the previously written routines were written with
"public string..." , "public bool...", etc., you probably do not need to modify this
part of the code; all of the routines are already "publicly viewable".

Scroll down the list of functions, changing all that were typed with "static" to "public"
(with one exception):

static bool IsBlank(passedString) changes to


public bool IsBlank(passedString)

The only exception is the "findCommentPosition" module; leave this as static.

"static" functions are only visible to the class (e.g. Form1) they were defined in, while
"public" functions are visible to other classes. Notice also the entire class library,
"CL800_Util" is "public," for the same reason. There is more about this topic in
Chapter 9.

The findCommentPosition function remains static because it is called by another module


in this new class, and is only needed by that class. There is no need to expose this method
to the outside world.

9. File, Close Solution (closing the entire namespace "NS800_Util" solution).


Save all changes when prompted. Then exit that copy of Visual Studio.

This should return you to the original program, which is much smaller because of the
"cut." Leave it opened for the next section, where you will test the new class library.
This completes the steps needed to move exiting code into another class library. Next,
you will see how to link the new class library back into the original program.

Comments on Creating the Class Library:

• The "CL800_Util" class library is ready to use. It is somewhat of a nuisance to create


the code from an existing program, but this is often how it happens in the real world;
You will develop a series of routines for one program and then realize it could be
used somewhere else – a hallmark of a shared class.

• The "CL800_Util" class lives inside of the namespace "NS800_Util". Although the
file can contain more than one namespace, and it can contain more than one class,
most developers keep a one-to-one relationship.

• Naming the disk-directory, the "namespace" (NS), and class-name (CL), helps to
understand the linking steps, which are covered next. By no means is this a universal
practice, but I like naming them in this manner.

Chapter 8 - Building Class Libraries Page: 501


Linking an Existing External Class Library

The LeftStr, RightStr, MidStr, and other functions were cut-and-pasted into their own
solution and now that library can be added back into the original or any other program.
Inserting a class library takes four steps, which seems like a lot of work, but the steps are
easy; more time is spent describing than doing.

Summary Steps: Linking an External Class Library

1. In Solution Explorer, Link to CL800_Util.cs


2. Add statement: using NS800_Util;
3. In "public partial class Form1": CL800_Util util;
4. After "initialize components": util = new CL800_Util();

Linking an Existing Class Library:

For these instructions, confirm you have closed and saved the CL800_Util library. For
simplicity, it should not be opened in an editor during these steps.

1. From a new program (or from your last example program where you cut the code),
confirm you have a Form1 object. This project was built using the Windows Form
template.

2. In Solution Explorer (right side of the editor):


Highlight the project's name near the top of the tree. The name may be called Windows
Application 1, MyProgram, etc).

Other-mouse-click and choose Add, Existing Item

Chapter 8 - Building Class Libraries Page: 502


3. Hesitate before actually adding the Library. For this example the new library will be
"Linked" (not added) and it is easy to miss this step:

a. In the Add Menu, browse to C:\Data\Source\CommonVS\NS800_Util, where


"NS800_Util" is a folder within CommonVS.

b. Highlight (do not double-click) "CL800_Util.cs"

c. Instead of clicking the "Add" button, choose "Add As Link" from the pull-down next
to the button.

Chapter 8 - Building Class Libraries Page: 503


If you mistakenly click the "Add" button (instead of Add-Link), CL800_Util.cs is
copied into your project and becomes independent of the original library. If this
happens, delete the newly-added "CL800_util.cs" from Solution Explorer's tree-view
and repeat this step. See below for a discussion on Copy vs Link.

This step tells the compiler where to physically find the library; it could be on a
server or a local disk. But adding the file to Solution Explorer is not enough to use
the library in your code – more steps are required.

4. Still in the current project, in Solution Explorer, locate Form1.cs. "Other-mouse-click"


Form1.cs and choose View Code. Alternately, double-click anywhere on Form1's
background.

Near the top of Form1.cs source-code, type a "using" statement, usually at the end of the
existing using list; order does not matter. This line lets you use the library without
prefixing each method with the namespace-name and is a convenience while coding.

Required 'using' statement


using NS800_Util;

5. Next, declare the new library. Below the line, "public partial class Form1 : Form", type
this line of code: "CL800_Util util;", adding it after the opening brace, as illustrated.
You are declaring a class-level variable.

The position is important. Putting the statement here tells the compiler this is being
defined as a class-level object and it is available to all modules in Form1. The statement
must be somewhere after the namespace's opening brace, above other methods, and should

Chapter 8 - Building Class Libraries Page: 504


not be part of another method. Other class-level variables may also be declared in this
section (not illustrated) but the order and position in this area is not important.

namespace Windows FormsApplication1


{
CL800_Util util; //Type this line here

public Form1()
{
InitializeComponent();
{

With this statement you are declaring what is essentially a new variable type. Instead of a
"string", "integer", or "boolean", you are declaring a new type, "CL800_Util" and you are
naming it "util". For comparison, consider how a string variable is declared:

For example, "string myString" means a new string called "myString" is declared.
Similarly, with "CL800_Util util", a new CL800-class object is declared and it is being

Chapter 8 - Building Class Libraries Page: 505


assigned an arbitrary name "util" (think "myString or myUtil"). "util" is a friendly, local
name for the class library.

"util" is a true variable name; it could be called anything,


including "stringfunctions" or "bob". I break with my normal
camel-casing convention by naming the library with a lower-
cased "util", mostly out of laziness.

Location Matters

Where the class was defined is important. By placing the "CL800_Util util" statement
after the Form1's opening brace, the new class is visible to all other modules in Form1.
This was done purposely because most of the modules in a given program will likely need
the utility functions and this position expands the "scope" of the library. The next chapter
covers scopes in greater detail.

j Other class modules might only be needed for a limited time or for a single method or
function. In those cases, declare them within that method and let them fall out of
scope when the method is done.

6. It is not enough to declare the new class; it must also be instantiated.

Instantiation is a fancy word that means created. Variables are


declared, Objects are Instantiated. Put another way, humans are
born and a baby becomes an instance of a human. In
programming, an object is an instance of a Class.

Once instantiated, all of the methods within the class become available to the program.

A good place to instantiate the class is within the "public Form1" method (technically,
this particular method is called a "Constructor". A constructor can be identified because
it does not return a void or other value in it's "public form1" declaration).

Instantiate the new Class Library with this command:

public form1()
{
InitializeComponents();
util = new CL800_Util(); //Instantiate the Utility Library
}

Instantiation:

Since "util" was declared ("named") at a higher scope, the instantiation statement assigns
a value to the already-existing, but currently empty variable. As a loose analogy, when a
string variable is declared, as in "string myString", it can't be used until a value is assigned
to it. When assigned a value it is said to be initialized but a more proper term would be
"instantiated." With this statement, the "util" variable is assigned a new copy of the class
library.

Chapter 8 - Building Class Libraries Page: 506


Where a class is instantiated is somewhat important. It would be nice to do it when the
class was first declared, at the class-level, but C# does not allow non-declarative
statements outside of a method or function. This forces you to instantiate the class further
down in the code.

This statement would be illegal at the class level:


CL800_Util util = new CL800_Util();

For form-wide libraries, such as CL800_Util, often the best place to instantiate is in the
"Constructor" event "public Form1". As long as the Form is active, the new utility-
library is available.

Declaring and Instantiating CL800_Util in the Form Constructor


using NS800_Util util;

public partial class Form1 : Form


{
CL800_Util util;

public Form1()
{
InitializeComponent();
util = new CL800_Util();
}

You could instantiate in an event such as button1_Click, but the library functions would
only be available as long as that event were active. Sometimes this is a good idea.
Instantiating only when needed saves memory, at the expense of CPU cycles, but if the
routines are needed in other methods and locations, you should instantiate at the higher
scope (see Chapter 9, Scopes), as illustrated here.

The library is now ready to use.

Summary:

First, using Solution Explorer, Add-Link the (CL800_Util.cs) file to the solution.
Then, make these three code-changes, all near the top of Form1:

Chapter 8 - Building Class Libraries Page: 507


The next section describes how to reach the new utility modules.

Chapter 8 - Building Class Libraries Page: 508


Using the util. Class Library (CL800 Util)

Once the utility class library is linked and instantiated, all of the (public) methods within
the library are available to Form1. Because the methods are in another class, preface the
method's name with the class's variable name, "util." (util-dot).

For example, to use "MidStr" in a button_Click event, the code would read "util.MidStr":

Using a util.Method
private void button1_Click(object sender, EventArgs e)
{
string strinputLine = "Name = Smith, John";
string strfoundString;

strfoundString = util.MidStr(strinputLine, "=", ",");


}

As you type "util-dot...", the compiler displays a list of methods that are available to the
util object. Try this now in the Form1 button1_Click event:

Think about other commands for a moment. Commands such as "MessageBox.Show( )",
work the same way, where "MessageBox" is the class and dot-show( ) is a method. With
the new utility class, "util" is the object and it has its own list of methods, visible in the
editor, as-if these were built-in keywords.

If the popup list of methods do not appear when you type util-dot,
then you missed one of the four steps needed to link an external
class library. A common problem is forgetting to write the "using
NS800_Util;" statement.

Chapter 8 - Building Class Libraries Page: 509


Without the using statement, you could manually type the prefix:
NS800_Util.CL800_Util.MidStr(...). The using statement
allows you to abbreviate the call.

Name Collisions Cured:

When the utility class was declared and instantiated, the CL800_Util class was given a
name, "util". The prefix "util." is an invented name and any name could be used,
including "bob":

CL800_Util util; and CL800_Util bob;


util = CL800_Util(); bob = CL800_Util();
with, util.IsBlank(strtestValue) bob.IsBlank(strtestValue)

If you link this same library into five other programs, you can use a different name for
each program. For example, assume you linked the current util-library and later you
discover another library (from another developer), which also wanted to be called "util.";
the two prefixes would collide. Easily fixed. In your program, instantiate the second
library, replacing "util" with a different name, such as "util2" and the two libraries stay
separate. Indeed, "util2" might have written his own "IsBlank" routine that behaves
differently than yours. In your code, choose which version to use by prefixing with
either: util.IsBlank( ) or util2.IsBlank( ).

Interior .util Calls:

What happens when one of the utility methods (e.g. LeftStr or RightStr) needs to call
another utility method from the same library? For example, the "LeftStr" method calls the
IsBlank method. Should the code prefix the IsBlank-statement with a .util prefix? No, the
prefix is not needed because the two methods (LeftStr and IsBlank) live in the same class
and they can see their own local methods, much like Form1 can see all of its own
methods.

Functions vs Methods:

You have probably now realized there is no difference between a function and a method.
Starting now, the term "method" is preferred. However, I still draw a distinction between
the two that is not generally shared by other practitioners of this language. If the method
is in the current project, I tend to call it a function; if it is in a separate class, I call it a
method.

The difference between a function and a method is not because of the dot-prefix:

IsBlank vs util.IsBlank

IsBlank, and IsFilled were used without prefixes when they lived in Form1.cs, but in
reality there was always an assumed prefix, which could be typed explicitly:

this.IsBlank

Visual Basic and Microsoft Access programmers may remember a prefix "me.", which
meant the current module. Similarly, C# can use "this." and some developers recommend

Chapter 8 - Building Class Libraries Page: 510


using the "this." prefix on all methods and functions in order to make it abundantly clear
where the module lives. I find this redundant. If there is no prefix, the current class is
assumed.

Class Libraries – Link vs Copy:

When a class library is brought into a project, there are several ways to incorporate the
code. In the example above, the utility libraries were "linked" into the main program.
That same code could have been "added" (copied) or the code could be compiled into a
stand-alone DLL. Each design has benefits and drawbacks.

Linking
In the example above, the library was "linked" into the project.

When compiled, linked libraries are shared code and other programs see the same
version of the code. If changes and bug-fixes are made to "CL800_Util", then all
newly-compiled programs see the new code. Previously-compiled programs will not
see the change until re-compiled. This is good and bad.

If the program has no need to see the library changes, and if it never needs to be
re-compiled, then the existing EXE can continue as-is, even though the original
library may have chanaged. However, if the original program is pulled for
maintenance, the new linked libraries are brought into play and some of the library
routines may have changed how they work. For this reason, changes to shared
libraries are made gingerly with an eye towards backward compatibility.

In my work, I favor Link libraries and understand if the program


is ever pulled for maintenance, I need to glance at any changes
made to back-end utility libraries.

Adding or Copying into the Current Project

The other choice is to "add" the library to the project. When a library is added, a
snapshot of the code is copied directly into the current solution. Later, if the original
master copy is changed, the copies will not see the new code, even if re-compiled.
Bug fixes and new features would have to be re-coded into each copy of the library.

On the plus side, if the original class library is radically changed, the current project
would not care. It has the older copy standalone copy of the code and if that code is
behaving well, you may have little reason to change. But if a serious bug were found
in one of the methods, you must make the same change in all copies of the library,
including the Master and all programs that have the copy. Again, all of this happens
when your program is re-compiled; existing .exe's are unaffected (but they would
presumably have the bug).

Chapter 8 - Building Class Libraries Page: 511


DLL

A third choice compiles the CL800_Util into a .DLL (Dynamic Linked Library) and
place it in a common shared directory. This is what commercial vendors do with their
code.

There are benefits to a DLL class library. First, if you had a dozen programs that
needed the same routines, the library is compiled one time and it would not occupy
space in the main program's EXE. The executables would be smaller.

To distribute bug fixes and enhancements, recompile the DLL and distribute to the
workstations or servers. With this, there is no need to re-compile the original
program and all programs pointing to this same library would see the bug-fixes at the
same time. The downside is you have to distribute and register the DLL separately
from your other programs. This requires more sophistication when installing. This
method, even with these drawbacks, is still one to consider, especially if you have
several programs on the same computer. How to create a DLL is described shortly.

Exercise:

A. Start a new Visual Studio Project, using the Windows Forms template.
Link in the CL800_Util Class Library, as described above.
Add two buttons: button1 and button2.

In each button_click event, add these statements:

string strTest = "Name: Smith, John Q.";


string strLastname;

strLastName = util.ParseKeyValue(strTest, ":", ",");


MessageBox.Show(strLastName);

Results: Without having to write any of the mid-string, or parsing routines, all of the logic
and all of those nasty overloads are present and ready to use! Very little work on your
part.

Other types of class libraries are discussed next.

Chapter 8 - Building Class Libraries Page: 512


Inline "Program" Class Libraries (PayrollTools)

When large swaths of related code clutter the main program, but they are not general
enough to be made into an external library, that code could be moved into its own class
library – something I call an "Inline Class Library" or a "Program Class Library." For
example, a program may have a substantial printing routine; this could be moved into a
library, separate from the main form, but still within the same program. Segregating like
this helps organize the program's code.

Beyond organizing, program class libraries have another purpose in life. Much like any
other function, they can be used to share code between multiple forms within the same
project, saving duplicate code and maintenance. For example, consider a group of
routines called "PayrollTools." The tools are probably useful only in the payroll system
but the code could be shared with other forms and modules in the same project.

Except for semantics, there is no difference between this type of library and an external
class library (CL800_Util) – both behave identically – but where the code lives is
different.

Creating an Inline Program Class Library:

For the experience, create a PayrollTools class within any existing project by doing these
steps.

This section describes how to build an "Inline Program" class


library and is demonstrated here as an example. This technique
is not needed to complete the remaining examples in this book.

1. In Solution Explorer, highlight the project's name (e.g. MyApplication)

2. Other-mouse-click, select Add, Class

3. From the menu/dialog, choose the first "Class" template in the displayed list.
At the bottom of the screen, change from "Class1.cs" to "PayrollTools.cs"
(you must type the ".cs" extension). Click button Add.

If you forget to name the class 'PayrollTools.cs', use Solution Explorer to rename the
default class name from Class1.cs to the new name (e.g. "PayrollTools.cs").

The editor also gives a visual clue about the class library: If the library is a linked external
class, Solution Explorer's icon shows a shortcut arrow. If the library is stored in the same
solution, it displays without a shortcut arrow.

Chapter 8 - Building Class Libraries Page: 513


comments

• The new class (PayrollTools.cs) appears in Solution Explorer and is immediately


ready to accept code.

• Because this is an inline/embedded Program library, the new class does not have the
linked ("shortcut" indicator) on its Solution Explorer icon; compare this to the
CL800_Util library.

• This type of library shares the same namespace as the original, master program.
Because of this, you do not need a "using" statement. See below for details.

• You cannot use a MessageBox.Show (or other visual-form features, such as buttons)
within the new class without first adding a using-statement at the top of the class:
using System.Windows.Forms. Only add this statement if you need these features
within the new class.

Namespace is the same:

From the Visual Studio editor, click on the tab "PayrollTools.cs" (code-view) and notice
the namespace line near the top of the class. The namespace, "WindowsApplication1" is
the same namespace as "Form1.cs" – in other words, both of these classes are in the same
namespace, hence, my designation of "Program Class Library" or "Inline Class Library."

Chapter 8 - Building Class Libraries Page: 514


Where is the New Class?

If you were to use Windows Explorer, you can find the new class library (PayrollTools.cs)
in Visual Studio's default file location, at an address similar to this:

"C:\Data\VSProj\WindowsApplication1\WindowsApplication1\PayrollTools.cs"

The difference is this: PayrollTools.cs is a separate file, stored physically on the hard
disk. Because of this, Visual Studio displays it as a different tab at the top of the editing
session.

Alternately, a new class could have been defined at the bottom of Form1.cs, using the
words: "class PayrollTools". In this case, the one physical file (Form1.cs) would have
two classes, but it would only display as one tab in the editor. There are stylistic concerns
about having multiple classes within the same file and I generally avoid doing this.

Using an Inline Class Library (PayrollTools)

This section discusses how to call methods within the new Payroll Tools class. As you
will see, class libraries behave much like variables – they can have visibility (scopes),
which will be discussed in more detail in the next chapter.

This is a theoretical discussion and is not needed to complete the


exercises later in the book.

PayrollTools Public Methods:

Chapter 8 - Building Class Libraries Page: 515


In the PayrollTools.cs class, jump into code view by double-clicking Solution Explorer's
"PayrollTools.cs". Create a dummy PayrollTest method, typing the code, as illustrated.

Public Methods:

Normally methods like button1_Click are written as "private void....", where 'private' is
the important thing to pay attention to. Private methods are only visible to the class they
reside and can only be used or called within that class.

Chapter 6's CL800_Util library contained "public" methods, such as IsBlank, MidStr, and
these are visible to other classes (e.g., Form1, Form2, etc.). Once the CL800 class was
instantiated with the "new" keyword, the public methods could be referenced using
"util.MidStr". In other words, they could be called from across class boundaries.

PayrollTools, being a separate class, is brought into service with the same type of code.
As before, only the "public" methods are visible to other classes.

A. Open Form1.cs in (form) design view (see top-row tabs, looking for (Form1.cs [Design]).
If Form1 design view is not visible, other-mouse-click Solution Explorer's Form1.cs,
choosing "View Designer".

B. Double-click the form's background, stubbing-in the Form1_Load event. (This example
will use this event instead of button1_Click. Form_Load runs automatically, each time
the program is run.)

C. Make the PayrollTools class available to Form1 by instantiating the new class. This
follows the same steps used with the CL800_Utility Libraries.

Chapter 8 - Building Class Libraries Page: 516


j Because the class is in the same namespace as Form1 (WindowsApplication1) it
does not need a "using PayrollTools" statement at the top of Form1. And because
PayrollTools.cs is already visible in Solution Explorer, it is not "linked".

D. Declare the class, assigning it a friendly name "pt" – for PayrollTools - similar to "util".

Instantiate the "pt" instance and use the method in an event, such as Form_Load. Here is
the code:

E. In the Form1_Load event, call the PayrollTest method written earlier, using this syntax:

private void Form1_Load (object sender, EventArgs e)


{
MessageBox.Show (pt.PayrollTest("Hello"));
}

where:

• The method "PayrollTest" was called by typing "pt.PayrollTest( )". As "pt-dot" is


typed, a context menu displays, showing the available public methods:

Chapter 8 - Building Class Libraries Page: 517


Explicitly using public Methods:

Because the PayrollTools class is in the same namespace as the main application (and it is
in Solution Explorer), the methods can be called directly, with a little more typing. Not
that this is recommended, but it shows how the classes's methods are being called. Undo
the steps from the previous section and try this:

a. Remove the "PayrollTools pt" declaration

b. Remove the "pt = new PayrollTools();" instantiation

c. Change the call to an Explicit call:

private void Form1_Load (object sender, EventArgs e)


{
MessageBox.Show (PayrollTools.PayrollTest("Hello"));
}

This works because PayrollTools is in the same namespace (see the statement right after
the 'using' clauses in both Form1 and in the PayrollTools Class Library). When doing
this, keep in mind only "public" methods are visible.

Explicitly using public Methods in other namespaces:

If a class is linked or copied into Solution Explorer, and it has a different namespace, then
it must be instantiated before used; this means you cannot explicitly path-down to the
needed methods.

For example, this is *not* allowed because of the Namespace change:


ns800_Util.CL800_Util.LeftStr( )...

The "scope" of the library can be limited by declaring, instantiating and using, all within
the limits of the current method or module. In other words, instead of declaring the
CL800_Util library in the Form's constructor (as in past examples), the entire library can
be specified and then used, within one place.

For example, assume you need CL800_Util's LeftStr method, but it is only needed in
Form1's button1_Click event. Rather than instantiating the utility library for the duration
of Form1, you can limit its lifetime (scope) to this one event. This frees memory. As
soon as button1's event finishes, the utility library would be released. However, if the

Chapter 8 - Building Class Libraries Page: 518


utility library is called multiple times, it would have be re-instantiated, at the expense of
CPU cycles. Consider the following, from Form1's button1 event:

private void button1_Click (object sender, EventArgs e)


{
//Accessing the CL800_Utility Libraries for a short duration:
//A "using NS800_Util" statement is not needed

NS800_Util.CL800_Util util;
util = new NS800_Util.CL800_Util();
MessageBox.Show(util.LeftStr("Fido is a dog", 4));
}

As soon as button1_Click ends, the entire Utility library is discarded and freed from
memory; the library falls out of "scope." The next chapter discusses "scope" in greater
detail. Use this technique when the methods within the library are rarely used.

Chapter 8 - Building Class Libraries Page: 519


Using CL800_Util within the new Class

Suppose the main "WindowsApplication1" uses the CL800_Utility class and the class is
linked and instantiated, as you have done earlier. When the PayrollTools class is added,
can it also use the same CL800_Utility class from the main Form? – No, not without
instantiating its own copy.

When the code leaves Form1 (which is itself a class), on its way to PayrollTools, the
previously instantiated utility class falls "out of scope." Remember, the utility class was
defined and scoped in Form1. In order for PayrollTools to use the same methods, it must
re-instantiate its own copy of CL800_Util.

WindowsApplication1's View:

As a reminder, in Form1, CL800_Util.cs was linked into Solution Explorer and then, with
the bolded-code below, the util class was instantiated into the program:

using NS800_Util;

namespace WindowsApplication1
{
public partial class Form1 :Form
{
CL800_Util util; //Declare the variable/alias

public Form1 ()
{
InitializeComponent();
util = new CL800_Util (); //Instantiate the class
}

private void button1_Click (object sender, EventArgs e)


{
PayrollTools.payrollTest2 ("Howdy");
}
}
}

Chapter 8 - Building Class Libraries Page: 520


No Place to Instantiate in PayrollTools:

Assuming PayrollTools also needs the CL800_Util library, it will need to instantiate the
library in the same fashion as Form1. The steps are nearly identical, but there are minor
differences.

• No need to re-link CL800_Util.cs into Solution Explorer because it is already there


from the Form1 step. Once linked, it is available to all classes in the project.

• Add the "using NS800_Util;" statement to the top of the PayrollTools class as you
always have. This keeps you from having to prefix the namespace NS800_Util each
time a method is used.

• Attempt to declare the util class at the top of the PayrollTools Library and then ask
yourself where the instantiation (the "new" statement) should go...

(In PayrollTools.cs)

using NS800_Util;

class PayrollTools
{
CL800_Util util; //Attempt here, but you will fail...
util = new CL800_Util(); //as before, it can't be here

public static void payrollTest2 (string passedValue)


{
etc....

If compiled, you will get this error:

An object reference is required for the nonstatic field, method, or property


'<class>.<method>.util' This is a horribly difficult message to interpret. Remember,
you cannot have executable code outside of a method. The instantiation is more than a
simple name-type declaration.

Class Libraries do not have 'Constructors':

Here is a difference: In the past, the CL800_Util class was declared at the top of Form1's
class and then in a later step, instantiated in "public Form1's" constructor. (The
constructor is a method that automatically runs when the form is first brought online.)

All forms have a constructor and form-classes makes this easy to


instantiate libraries like this. But standalone class libraries do
not have a default constructor because they typically don't need
one. In these examples, CL800_Util needs to be instantiated so
all of the PayrollTools methods can use the same utilities.
Fortunately, manually building a constructor is easy.

In this example, PayrollTools is not a form and it does not have a default Constructor
method. This means there is no obvious place to instantiate the CL800 class. In other

Chapter 8 - Building Class Libraries Page: 521


words, without a "startup" class (the constructor), there is no event that triggers the
spawning of the CL800_Util class library.

There are two easy solutions. First, and most obvious, is to declare *and* instantiate the
utility class inside the individual PayrollTools method you are calling. For example,
within a particular method, such as "PayrollTest":

Declaring and Instantiating CL800_Util within a Method


class PayrollTools
{
//Instantiating the CL800_Util class within a method,
//bypassing a constructor

//CL800_Util util; //Can't declare here; it never runs

public static void PayrollTest(string strpassedString)


{
//Declaring and Instantiating CL800_Util within the
//PayrollTest method
CL800_Util util;
util = new CL800_Util();

return util.LeftStr(strpassedString, 4);


}

This means each time Form1's button1 is clicked, it calls the PayrollTools class and
instantiates the Utility class. When the PayrollTest event ends, the Utility-class definition
is discarded. This takes more processing but saves memory because the utility class is
only instantiated when needed.

This demonstrates how C# tries to limit variable scopes to only


the routines that need them. In the past, CL800 was declared
relatively high-up in the program; this meant the library was
available to all methods and functions and it occupied memory
whether it was used or not.

Many programmers code in this fashion. If a library is only


needed for one or two things, declare them as close as possible to
the routine that needs them and allow them to fall out of scope.

But the drawback is this: suppose the PayrollTools class had dozens of methods, all
needing the CL800 utility methods. This means the CL800 would need to be re-
instantiated each time, at each method. If a loop called a PayrollTools method 1000
times, it would have to rebuild the utility libraries a thousand times. Because this utility
library contains handy-dandy routines, they are probably needed in many different places.
It would be more efficient to declare the utility-class higher-up in the program.

Manually Building a Class Constructor:

In this example, "PayrollTools.cs" needs to instantiate a commonly-used class,


CL800_Util, but PayrollTools does not have a default (Form) "Constructor" method.

Chapter 8 - Building Class Libraries Page: 522


Using the code in Form1 as a template, manually create a "class constructor" in
PayrollTools by typing this code just below PayrollTools's class definition:

where:

• Constructors always have these features:

They are always "public"


They have the same name as the name as the Class; in this example, "PayrollTools"
They do not declare a type (string, integer, boolean, etc.)
The Signature is empty ()

• By convention, most developers place the constructor just below the Class definition.

• In a Form class, Visual Studio automatically builds the constructor and in here, you
will find an "InitializeComponent" statement. With a non-form class, constructors
are optional, and in most cases not required. They will never have, nor do they need
an "InitializeComponent" statement.

To complete the example, instantiate the CL800_Util class as you always have:

Chapter 8 - Building Class Libraries Page: 523


Manual Constructors with Instantiation, completed
using NS800_Util;

class PayrollTools
{
CL800_Util util;

public PayrollTools () //Manually-typed constructor


{
//Instantiate the utility class here...
util = new CL800_Util();
}

With this, the CL800_Utility class can be used in each method within the newly-built
PayrollTools class. Another way to say this is the utilities are "scoped" at the class level.

This completes the discussion on Inline (Program) class libraries.

Chapter 8 - Building Class Libraries Page: 524


Creating an "External" Class Library from Scratch

Earlier, you created an External Class Library from existing code (CL800_Util). This
section describes how to create one from scratch and it follows the same general steps as
the other class libraries created in this chapter. As a reminder, external class libraries are
useful for routines that need to be shared among multiple, differing projects. The details
are repeated here in a more concise fashion.

From your experience in building the original CL800_Util library, you discovered Visual
Studio is less-than graceful when a class library needs to be built in a different directory
than the current project. This is still a problem. Keep in mind they would rather live in a
directory such as C:\Data\Source\CommonVS – anywhere but the current program's
directory, but Visual Studio tries hard to keep the class with the current solution.

This example will be named "CLSiteRoutines," which is an invented name that implies a
group of methods that can be used by all projects in your development shop. Naturally,
you should name your library something more sophisticated.

1. Launch a fresh (second) copy of Visual Studio and select File, New Project

2. In the Project Types section, confirm Visual C# "Windows" is selected.


In the Templates section, choose "Class Library".

3. In the Name field, type: "NSSiteRoutines"

4. In the Location field, type or browse to "C:\Data\Source\CommonVS"


Do not check "Create directory for solution"
Click OK to build the solution.

5. In Solution Explorer, change the default class name, "Class1.cs" by:

Other-mouse-clicking "Class1.cs" and selecting "Rename".


Rename the class to "CLSiteRoutines.cs" (you *must* type the .cs extension)
When prompted "Would you also like to perform a rename in this project and all
references...", choose Yes.

In real life, you should save the name using a numbering scheme, such as:
in C:\Data\Source\CommonVS....
NS860_StandardINIFileRead
CL860_StandardINIFileRead.cs

The class is now ready to accept code. Remember, only the Public method is visible to
other programs. Link or Add this new class to other solutions following the same steps
used in CL800_Util.

Chapter 8 - Building Class Libraries Page: 525


Compiling and Using DLLs

So far this chapter demonstrated how to link the utility library into a current project,
either as in-line code or as an added, external module. When the final program is
compiled into an executable (exe), everything dealing with the program, including the
utility libraries, is included in the compiled .exe and it acts as a complete, stand-alone
program. (See Appendix B for detailed .exe Compile steps.)

This section is not required to complete the remaining chapters in


this book and is meant to be a reference.

If several programs needed to share the same library, it could be 'compiled' into its own
module called a DLL (Dynamic Linked Library) and this module would be shared by all
programs at runtime. In other words, one copy of the library could be used across dozens
of programs, making for smaller executables. Bug fixes and enhancements can be applied
once to the DLL without re-compiling the other programs. The drawback to this method
is the DLL and the EXE must be distributed together.

j For commercial software, the DLL and the exe should be installed with an MSI
package and the DLL should be properly registered in Windows. The steps for this
are beyond the scope of this chapter.

Creating a DLL

To create a DLL, close all running solutions and open the class library (these examples
demonstrate the NS800_util.sln (solution)). Recall, NS800 was created as a new solution,
choosing "Class Library" from the original Visual Studio templates. Once opened, follow
these steps to create a stand-alone DLL:

1. Open the solution. Then, from the Visual Studio editing environment, select menu
"Project", "NS800_Util Properties" (the name of your project).

Chapter 8 - Building Class Libraries Page: 526


Note the project's properties left-navigation:

2. Select "Build" from the left-nav.


Change the Configuration from "Active (Debug)" to "Release"
Accept all defaults within the panel's details.

By changing the module from "Debug" to "Release" mode,


you will have a smaller compilation, with less overhead and
better performance. Be sure you have solid code and you are

Chapter 8 - Building Class Libraries Page: 527


finished with your development, but if needed, it can be
changed back into debug mode from these same screens.

3. Return to the left-nav Application Menu and click button "Assembly Information...".
Populate the details to help identify the purpose and version of the DLL and these values
are visible from Windows File Properties. Increment the version number appropriately.

4. From the top menu, select Build, "Build NS800_Util"

Chapter 8 - Building Class Libraries Page: 528


Results:

Confirm the results in Windows Explorer by opening the Project's source directory, and
tunneling to the bin/Release directory.

Note "NS800_Util.dll"

Using the DLL

Follow these steps to use the DLL. For this example, start with a new Project, adding a
button1 object for an initial test.

5. (From a new project), go to Solution Explorer. "Other-mouse-click" References, choosing


"Add Reference".

6. Click "Browse" on the left-nav and then the Browse button.

Browse to bin\release\NS800_util.dll.

Chapter 8 - Building Class Libraries Page: 529


This adds the (utility) module to the current program, but unlike previous examples, the
source-code is not visible. Also note you were not given the opportunity to add a copy of
the source or to link; DLL's are only linked.

7. In your new Project, follow the same steps as described earlier in this chapter:

a. Add a using NS800_Util;

b. In public partial class Form1, add this statement:


CL800_Util util; //assigning a program name "util." to the namespace

c. Below public Form1(), typically below the InitializeComponent() statement, add


this instantiation line:
util = new CL800_Util();

d. Use the module by typing "util."; note the available methods appear, along with
the \\\ (triple-slash) comments you may have typed.

Caveats to this Idea:

Chapter 8 - Building Class Libraries Page: 530


When linking to the DLL, the entire path is recorded – this is an absolute, non-relative
path. This means if the DLL should move, or the directory it lives in changes name, the
program will crash. When distributing the final program to other users, the DLL will
have to be placed in the same, predictable location.

You can convert the DLL's absolute (hard-coded) path to a relative link with these
obscure steps.

a. After adding the reference for the DLL in Solution Explorer, close and save your
program.

b. Using Windows Explorer, copy the compiled DLL to both your program solution's
...\bin\Debug and to the \bin\Release folder (if present).

c. Again with Windows Explorer, locate the Project's Solution folder. Look for a file
called <your project's name>.csproj. Edit this file with Notepad.

c. Locate the <ItemGroup> section, <HintPath> and remove the path:

Change from (your path will vary)


<HintPath>..\..\..\..\..\..\Source\CommonVS2012
\NS800_Util\bin\Release\NS800_Util.dll</HintPath>

To:
<HintPath>bin\Debug\NS800_Util.dll</HintPath>

d. Re-open the project. In References, highlight the DLL's reference, choose Properties.
Note the DLL filename, minus a path. In the future, you would distribute your .EXE
and the .DLL in the same directory.

Chapter 8 - Building Class Libraries Page: 531


"Modularizing" with Program-Control Functions

You have seen how to write functions such as IsBlank and IsNumeric and how to
congregate them into a larger class library. While these certainly help, there are other
ways to organize code. In more complicated programs, it is helpful to have a function that
controls how other methods are called. These types of functions act almost as if they
were mini-programs within the main form or program.

Imagine if Form1 had a button that printed Payroll checks and this routine had several
hundred lines of code. Rather than placing all the code in the button's method, it could
call another routine, perhaps called "A710_PrintChecks". Although A710 accepts no
passed parameters and does not return a result to the calling routine, it is still useful in
organizing and documenting the program's intent.

private void btnPrint_Click (object sender, EventArgs e)


{
pnlMsg.Text = "Preparing to Print...";
A710_PrintChecks();
pnlMsg.Text = "Printing completed";
}

Having several hundred lines of code in "A710_PrintChecks" is an improvement, but still


a poor design. Instead, it should have its own set of sub-functions, such as
A715_InitializePrinter, A720_AdvanceFormFeed, A730_LoopRecords, etc. As you can
tell, I am a fan of using a numbering scheme when I write these routines.

There are no hard-and-fast rules about how to modularize, but in general, if a routine is
more than a few dozen lines, or if it has numerous or complicated steps, then it should be
broken into its own routine.

For lack of a better term, "Program Functions" or "void functions" exist only to block
code into smaller pieces. Clearly, A710_PrintCheck's intent is clear. As a side-benefit,
the code can be called from different locations and it does not need to be tied directly to a
button-routine.

Program "void" Functions:

Define a "Program Function" in the same way as any other, with one slight difference.
Instead of returning a boolean, string or number, they typically return "void" – which
means return "nothing." Keep in mind the purpose of these functions: they act only as a
program-control – guiding the logic into smaller and more manageable chunks of code.
Beyond that, there is nothing special about them.20

20
Program Functions (A710_PrintChecks) can return a value other than void. They can easily return
a "True" or "False" (success or failure) or other values, as needed. Program Functions are not restricted
to just void.

Chapter 8 - Building Class Libraries Page: 532


Limited Functionality:

Well-written functions should do one thing, what ever that "thing" is. If a function is
asked to de-punctuate a phone number, then that is all that function should do. Perhaps
another task loads an array of numbers and in the process it may need to open a file. This
is acceptable, as long as opening the file is part of the array-loading routine. But, if
opening the file requires numerous other steps, that smaller step should move to its own
subroutine.

Breaking programs into successively smaller bits of logic is the hallmark of a


programming style called "Structured Programming." Although you are writing "object-
oriented" programs using Visual Studio development tools, each of the granular steps are
still old-fashioned, structured programming.

Consider the following pseudo-code, where A710 begins its work. Notice how it might
call a dozen other smaller routines:

private void A710_PrintChecks ()


{
//Begin the Check Run

//Early Exit
If (boolUserAuthorizedToPrint == false)
return;

//Begin print routines:


A715_InitializePrinter();
A720_PromptUserForPaperLoad();
A730_PrintTestCheck();

//Begin other steps...


<various code here>

MessageBox.Show
("Printing completed: Last Check Number = " + iCount);

//return is assumed when the closing brace is reached (void)


}

returns are optional:

Because these are typically "void" functions, the keyword "return" is not required and
when the closing brace is reached, a return is assumed. However, a return can be used as
an early-exit, as illustrated in the code above.

Passing Variables to Program-Functions:

Most "classic" functions, such as "IsBlank( )", accept something passed through the
parentheses, and then return something else. void functions (aka Program functions) can
also accept passed values, using the same techniques.

A720_PromptUserForPaperLoad(strPaperFormName);

Chapter 8 - Building Class Libraries Page: 533


Returning Values from a Program-Function:

Calling a function a "void" function is a descriptive convenience. Because there is no


difference between this type of function and any other, program-control functions can
both send and return values, as would any other function or method:

bool boolErrorDetected = A730PrintTestCheck(sendsomevalue);


if (boolErrorDetected == true)
MessageBox.Show("Print Test Check failed");

where A730 might return a True|False, depending on what codes were detected on the
printer, etc. In this case, A730 is not a "void" function (because of the equal-sign) and a
"return" is now required within that routine.

Chapter 8 - Building Class Libraries Page: 534


Naming Standards for Functions and Class Libraries

Even in a moderately simple form, the number of program functions can grow to where
you lose mental track. Consider a small workstation inventory program I wrote. Even
though it was a small program, it contained nearly a hundred blocks of code. Below is a
partial list of the functions. From this you can tell what the program did for a living:

AcceptCommandParms ReadPriorInventory
WaitForFiles ExposeControls
GetSelectedRegistryValues HideControls
GetFreeDriveSpace SendMessage
GetMemory WriteInventory
GetIPAddressing PrintReport
InventoryServerDrives Abnormals
PopulatePanel OnlineHelp

Name Confusion Reigns:

Having a hundred different function names makes for mental bowl of confusing names.
Was the name of the inventory step "ReadInventory" or "PriorInventoryRead"? I was
constantly scrolling up and down the program looking for the routine.

Visual Studio's editor has no rhyme or reason on where a newly-created functions splash
down; they are not alphabetical nor are they in the order created; it depends on where the
cursor was when the module was created. Of course the compiler keeps track of all this
and could care less about order, but it gets confusing to the programmer. A time-honored
solution is to number the modules with a prefix.

Prefixing:

Function names like "ReadPriorInventory" are admirable but they are devilishly-hard to
find in a long and cluttered program. In order to organize this proliferation of windy
names, I adopted a naming standard that is fairly unique in modern programming – but
was common-place in COBOL mainframe programming circles.

Prefix all program-control functions with a Letter ("A") and a three or four digit numeric
code. A beginning letter is required because function names must begin with a letter.
Although there are no hard-and-fast rules about the numbering, I generally follow this
pattern:

Chapter 8 - Building Class Libraries Page: 535


Suggested Program Function Prefixes

A010_Command Start-up Modules; INI files;


Read Registry; Read CMD, etc.
A020_(misc) Startup-subroutines, various

A100_MainProcesses Major Processes; usually this is what the program


"does" for a living.
A200_(misc-subprocesses) Main Processes call these routines
A300_(sub-subprocesses) which in turn call these modules
A500_RecordProcesses Add/Delete/Update, etc.

A600_FileProcesses Open; Database, etc.


A700_Reporting Reporting and Printing routines
A800_Utility General routines
A900_Error Error and Exception routines
A990_Help Online Help, Help About, etc.

When the program is pulled for maintenance, initial startup routines are always found in
the A010-A020 range. Similarly, if there is a bug in a print routine, the code is most
likely in the A700-region.

The Inventory program had these prefixes:

A010_Command A110_ReadPriorInventory
A011_WaitForFiles A190_ExposeControls
A020_GetSelectedRegistryValues A720_SendMessage
A021_GetFreeDriveSpace A700_WriteInventory
A022_GetMemory A710_PrintReports
A023_GetIPAddressing A790_Abnormals
A100_PopulatePanel A990_OnlineHelp

Notice all of the function names begin with an action verb:


A020 Get SelectedRegistryValues
A110 Read PriorInventory
A700 Write Inventory
A720 Send Message

Another benefit from this numbering scheme is you seldom need to remember if the
routine is called "ReadPriorInventory", "ReadInventory", or "GetPriorInventory". All you
need to remember is the module's prefix, A110. Searches are also easier with this prefix.
Complicated routines, such as A710_PrintReports, might call dozens of other controls and
if all are named in the 700-range, it is easy to tell at a glance they are related.

Larger programs might contain B and C-Series, especially if Inline Program Classes are
used. For example, PayrollTools might be in the "B" class and that entire class can have

Chapter 8 - Building Class Libraries Page: 536


its own series of routines, such as B010-B990. Even the class libraries can be numbered
and this explains the CL800_Util class name.

j As you write and prefix your code, the editor drops the new methods willy-nilly into
the edit session. Manually cut-and-paste the modules, moving them into a numeric
order. Yes, this has to be done manually, but once done, your code will be a joy to
work with and modules will be easy to find with just a scroll-bar.

A000 Prefixing Exceptions:

Visual Studio automatically creates the code for button1_Click (or btnPrint) as it stubs-in
the function name. Assuming the button was named something other than "button1", I
accept the given name and do not prefix. Thus, btnPrint_Click remains with it's given
name.

j I like to manually cut-and-paste all button-routines and move them into a common
area in the program, usually placing them near the top of the class.

Starting in Visual Studio 2017, Microsoft imposed a compiler warning on


methods that started with lower-case. Thus, "button1_Click" is flagged with a
warning. Microsoft thinks all Public methods should begin with an upper-
case letter. Additionally, they rightly believe it should be changed from
"button1" to a more meaningful name. What ever name you choose, be sure to
capitalize the first letter.

Object Prefixes:

When naming C# objects, such as buttons and checkboxes, give them any name other than
the default "button1". Prefix the objects with a code, which makes it easier to identify the
object's type. Although there are no standards, and many disagree with the entire prefix
idea, I use these:

Btn button btnDeleteRecord, btnClose


lbl label lblStatusMessage
cbx checkBox cbxHomeAddress
sel (select) comboBox selInventoryItem
rbn radioButton rbnSex
pnl panel (textBox) pnlHidden or pnlControlField not tied to data

I generally do not prefix textBoxes, but you could make a case for "tbx".

Similarly, you have probably already noticed I prefix variable names with:
str String
i Integer
f Floating Point / Single
dbl Double
bool Boolean

20,000 Foot Organization:

Chapter 8 - Building Class Libraries Page: 537


Within a program (Form1, for example), I tend to cut-and-paste modules into these broad
sections (psuedo-code):

public partial class Form1 : Form


{
//Global Variables and declarations
CL800_Util util;
strProgramName = "B1784_Program";
strVer = "Version 2.13";
strAuthor = "Tim Wolf";

//Constructors and Load Events:


Form1_Load
Form1_Close //and other closing events
Other Form Events

//*******************************************************
button logic

//*******************************************************
Menu logic

//*******************************************************
//Initialization routines
A010 ... A090

//*******************************************************
//Main Logic:
A100 ... A999

//*******************************************************
Miscellaneous, un-numbered routines
(especially small program utility functions)

Avoiding button Clutter:

Except for the most simple tasks, button-events should not do any real work. Instead, they
should call a prefixed routine, letting that routine do the dirty-work. For example,
BtnDeleteRecord should not contain the actual delete logic, instead it should call a
prefixed routine, A510:

Button Events should Call other Modules; but should still do their own error checking
private void btnDeleteRecord_Click (object sender, EventArgs e)
{
//Button events should call other routines
//but leave exception checking here. This makes the downstream
//function useful for other routines

bool BtnDeleteSuccess =
A510_DeleteRecord(pnlRecordSeqNumber, ref bDeleteSuccess);

if (BtnDeleteSuccess)
pnlMessage.Text = "Record Deleted";
else
pnlMessage.Text = "Delete failed";
}

Chapter 8 - Building Class Libraries Page: 538


Error Logic:

A510 may have a horrendous amount of auditing, logging, and other transaction details,
but there is no need to clutter the top of the program. BtnDeleteRecord should be a model
of sparseness and simplicity.

However, most of the error and conflict resolution should live in the upper btn-calling
routine (see code, above). Why? If records can be deleted from multiple locations in a
program, you may, in one instance want to tell the end-user of a problem and in another
instance you may wish to silently log the results. If A510 were forced to handle this
logic, it would have to know "who called it" and this complicates the logic.

In other words, when calling A510_DeleteRecord, that routine should do nothing but
delete records. Anything else is fluff and should be handled by the calling modules.

Chapter 8 - Building Class Libraries Page: 539


Required Exercises
The remaining chapters in this book rely on the CL800_Util Class Library and as-such,
they must be moved using these techniques: Building an External Class Library from
Existing Code (CL800_Util)

A. Move all of the Utility Functions from Chapters 6 and 7 into the (External Class)
CL800_Util Library, as described at the front of this chapter: Building an External Class
Library from Existing Code.

Move all functions and their overloads:


IsBlank
IsFilled
IsNumeric
IsNumbers
LeftStr
RightStr
MidStr
VerifyYN
ParseKeyValue
ParseKeyName
StripDuplicateCharacters
StripNonNumerics
StripTrailingComments
(findCommentPosition)

In later chapters, additional modules will be added to the library.

Important: This Exercise must be completed in order to continue with the remaining
chapters. The CL800_Util library is used extensively throughout the remainder of
this book. Contact the Author if you would like to download the completed library.

B. Optionally, build a sample program with a textBox; then call several of the util.Methods
and perform various functions on the text.

Later chapters return to this topic. Name-and-address, Phone-number and other


punctuation routines, as well as INI File processing are all good candidates to move into
separate class libraries. Each of these routines are likely to be used in multiple
programs.

Chapter 8 - Building Class Libraries Page: 540


An Absolute Beginners Guide to C# - Volume 1
Visual Studio C# 2017
Intro -Through Forms
by Tim R. Wolf
2017.06 Version 1.04
Table of Contents

9 Chapter 1 - Introduction to the Editor 3


Your First Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Variables and Scope. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Working with Text Boxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Naming Fields
Concatenation
Default Text Values

9 Chapter 2 - Introduction to Loops 45


"while" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
loopCounter
Appending a String
Incrementing a Counter
Incrementing with "++"
Concatenating to Self with "+="
MessageBoxes
Infinite Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Breaking into the Loop
"while" Loop - Printing Numbers 1-10. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
"do" Loops.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
for-next Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Carriage-Return/LineFeed (CRLF)
Variations on "for-next" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Controlling Loops with Variable textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Interrupting Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
continue;
break Statements
"while-loops and 'continue'
Nested Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
foreach Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Loop Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110

9 Chapter 3 - Conditional Branching 117


Numeric and String Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Basic if-statement Construction
Numeric "if" Statements
Brace Style
"else"
Semicolon Rules
&& (AND) || (OR) Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
"|" and "||" (OR) Boolean
"^" (XOR – Exclusive OR) Boolean
Nested if-statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
"else if"
Math.Min - Numeric Testing for Smaller Value.. . . . . . . . . . . . . . . . . . . . . . . . . . 139
Compounding if-Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
switch Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Case "Value":
default Clause
Required breaks
Compound case Statements
Case-sensitive switches - .ToLower()
goto Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
Ternary Operator. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

9 Chapter 4 - Strings 163


Declaring Strings.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Special Character Strings (Escape Codes) \t, \r\n. . . . . . . . . . . . . . . . . . . . . . . . . . 167
Reserved Backslash
Carriage-Return-Line-Feeds CRLF
Embedded Quotes
Verbatim Text Strings - @
ASCII Codes
String Concatenation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
string.Concat ( ) and "+"
Floating Point Numbers
string.Compare( )
<string>.EndsWith. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
Detecting .XLS file Extensions
<string>.Length. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
<string>.ToUpper( ), .ToLower( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
<string>.PadLeft(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
<string>.PadLeft(int, '*');
Date Padding with Leading Zeros
<string>.Trim( );. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
<string>.TrimStart()
<string>.TrimEnd()
Trimming with other Characters
char.IsNumber ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
<string>.Replace( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Character to Numeric Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Conversions with Casting
null and Empty Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
String.IsNullOrEmpty( )
Null-Conditional Operator
Testing for "blank". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
if ((strtest + "").TrimStart().Length == 0)
Finding Strings - IndexOf. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Reading an Overload
<string>.LastIndexOf
<string>.Contains
Parsing and Substrings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
<string>.Substring. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Classic Left-strings .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Classic Right-string. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
Mid-Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
"Mid-String" for any Length Delimiter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253

9 Chapter 5 - Numbers and Dates 265


Integers Defined. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Floating Point Numbers Defined.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Casting and Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
Implicit Conversions
Explicit Conversions
try-catch Error Trapping. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Converting Strings to Numeric. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
TryParse
Rounding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Math.Round
Truncating Decimals. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
Math.Truncate
Basic Math Functions (Division). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
DivRem (Divide Remainder) / Mod. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
Math.DivRem
Mod "%"
Other Math Functions (SQRT, etc.). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
Sqrt (Square Root)
Random Numbers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
Dates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
DateTime.Now
String.Format
Date Parts
Date Format Pictures
DateTime.TryParse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
UTC (Zulu) Time
Empty Dates - Nullable Dates
DateTime.Compare. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Date.Compare: Two Files
Less Exact Date Comparisons
Rounding Dates
Optional Project: MTWRFSU.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
Optional Project: AllowByHour. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327

9 Chapter 6 - Utility Functions - Methods 333


IsBlank( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
IsFilled( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
LeftStr, RightStr and MidStr string Functions. . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
Classic LeftStr ( ) "Left-string".. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
Left-string with String Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Classic Right-String with Numeric Parameters.. . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Right-string using Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Mid-string with Numeric Parameters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Mid-string Numeric Start Position but No Length (Overload). . . . . . . . . . . . . . . . 382
Mid-string with string Delimiters and NumberOfCharacters. . . . . . . . . . . . . . . . . 383
Mid-string with Two String Delimiters.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388

9 Chapter 7 - Advanced Utility Functions 395


StripSlashes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
StripTrailingComments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
StripLastCharacter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
StripNonNumerics ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
StripNonNumerics, Preserving Decimals and Signs. . . . . . . . . . . . . . . . . . . . . . . . 424
Overloading
ParseBetweenDelimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Overloading ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
ParseKeyName: Master Function. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
IsNumeric ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463
IsNumbers ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
StripDuplicateCharacters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475
VerifyYN.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Passing Variables by Reference. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481

9 Chapter 8 - Class Libraries 493


Building an External Class Library from Existing Code. . . . . . . . . . . . . . . . . . . . 496
Linking an Existing External Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
Using the util. Class Library (CL800 Util). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
Link vs Copy
Inline "Program" Class Libraries (PayrollTools). . . . . . . . . . . . . . . . . . . . . . . . . . 513
Using CL800_Util within the new Class.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 520
Constructors
Manually Building a Class Constructor
Creating an "External" Class Library from Scratch. . . . . . . . . . . . . . . . . . . . . . . . 525
Compiling and Using DLLs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526
Using the DLL
"Modularizing" with Program-Control Functions.. . . . . . . . . . . . . . . . . . . . . . . . . 532
"void" Functions
Passing Variables to Program-Functions
Returning Values from a Program-Function
Naming Standards for Functions and Class Libraries.. . . . . . . . . . . . . . . . . . . . . . 535
Object Prefixes

9 Chapter 9 - Variable Scopes 543


Variable Scope, Demonstrated. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
Scope within Loops
Form-Level (Class) Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 551
"Global" Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554
"Quick and Dirty" Global Variables
"static" Modifier
Form1 to Open Form2
Using a Global Class to Pass Values.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564
Building an External Global Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 568
Getting and Setting Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 577
Building Get/Set Properties
Passing Variables by Ref.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 582

9 Chapter 10 - Form Controls and Events 589


Default Editor Settings - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594
Snap To Grid
Compiler Errors
Starting and Naming a New Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 598
Rename the Form
Recommended Form Properties
"Form Load" event
"this.Show"
Link the CL800_Util Library
AutoClose Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 609
textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 613
MaxLength
Default Text Value
Enabled
Password Fields
Multiple Lines
Setting Field Properties at Runtime. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 619
textBox Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 625
Enter and Leave Events
TextChanged
KeyPress Events (Intercepting Keystrokes). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 629
Allowing only Numeric Values
UpperCasing as Typed
Intercepting Shift, Alt and Ctrl Keystrokes
Alt-Key events on a button or other object
comboBox and listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 638
Static (Hard-Coded) ComboBoxes
AutoComplete / Type-Ahead
Query the Selected Value
"No Selection"
Using the SelectedIndexChanged
ComboBoxes linked to an External Data
listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 648
Blank-line Testing
listBox Multiple Selection
Processing Multiple Selected Records
checkBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 660
ThreeState
CheckChanged
CheckStateChanged
Click
Radio Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 669
Radio Button Events
Grouping
ProgressBar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 676
monthCalendar and DateTimePicker. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 681
.MaxDate
.MinDate
Form Load Event
Date-Time Math
MenuStrip. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 689
Horizontal and Vertical Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 692
ToolTips. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 694
ToolBox: "PictureBox" Icons and Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
Launching Other Applications From the Graphic
Link Labels. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Label Tricks.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 704
Transparent Labels
Using Labels in Your Program
Tab-Order. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 706

9 Chapter 11 - Calling Secondary Forms 711


Opening Secondary Forms - Simple. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 716
Issues with a Simple Form Call
Using a Global Variable to Re-Open Form1.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 724
Using a FormReference to Open Form2 - Recommended. . . . . . . . . . . . . . . . . . . 727
"FormRef" properties
Modal and Modeless Forms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 735
Transferring Data Between Forms using Global Variables. . . . . . . . . . . . . . . . . . 737
Using Quick-and-Dirty Global Variables
Using a Global Class (Global Variables)
Retrieve the Stored Value in Form2
Passing Data with a Signature and a Constructor. . . . . . . . . . . . . . . . . . . . . . . . . . 741
The Parent's Call
The Child (Form2) Constructor
Passing Values using get-set Properties - Recommended. . . . . . . . . . . . . . . . . . . . 745
Form Starting Positions - Global Class. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 753
MessageBox Overloads. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 756
Custom Dialog Boxes: NS810_Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 759
Returning Results to the Parent via Properties
Custom InputBoxes: NS815_InputBoxDialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . 777
Detecting a KeyPress ENTER Key Event.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 785
Using NS815 to Pass Arrays instead of Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . 787
Multiple Input Screens in the Same Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 789

9 Appendix A - Compiler Error Messages 3

9 Appendix B - Compile and Distribution 38


Cheap and Easy EXE Distribution:.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Virus Risks
EXE Icons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Using Visual Studio to Edit Icons
Attaching Icon Files to your Project
Creating Publishing / Distribution Packages.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Generating a Creator Tag
Introduction

Audience:

This book is volume one of a three volume set and is intended for beginning Microsoft
Visual Studio C# (C Sharp) programmers, versions 2015 through VS 2017. This is a
hands-on book, with actual programs, starting in Chapter 1 and it talks about the
practical day-to-day, nuts-and-bolts programming that real people need to know.

It assumes no prior programming experience.


Little time is spent on theory.

Each topic has step-by-step instructions with numerous code examples and over 900
cropped and annotated illustrations. It explains the "why" of a program.

Prior Experience

If you already have moderate programming experience, especially in Visual Basic, this
book will expand your skills, giving confidence in the new language.

The goal is to have as much time on the keyboard, working with common business
problems. After studying this book and working through the examples, you will be a
proficient programmer – able to write real programs that do real work. You will be able
to read files, parse data, write to database, and build data input screens.

Why this book?

This book is different than most publications. Little time is spent on theories and
technical side-trips are rare. Starting in Chapter 1, you will immediately begin working
with loops, if-statements and string-manipulation. This means some topics, such as
conversions, numeric types, and other such concepts, are glossed over until they are more
germane to the concepts being taught.

Where other publications might spend a page or two on a topic, this book dives into the
most common and most useful ways to solve a problem. For example, over 80 pages are
devoted to opening multiple forms and how to pass data between them. The parsing
chapter devotes 70 pages to this subject, covering delimiters, CSV, Tab, Excel, and other
techniques. This is not over-kill. You will find these address real-world data-processing
problems. I cover the tips and tricks you will need to know.

Chapters often show different techniques for the same problem and the benefits and
drawbacks of each are explained. If there is a chance of making a mistake in
punctuation, style, or logic, the examples show how to resolve them. Compiler errors are
scattered throughout the book and there is a comprehensive alphabetic error reference in
the appendix, showing likely causes and recommendations.

A side-effect of this first volume will be a library of utility modules that can be used in
all of your programs. These utilities can automate mundane tasks, such as parsing
delimited files, punctuating phone numbers, street-addresses, and capitalizing proper
names. These libraries will save boat-loads of time and will be literally useful in all your
programs.

You will also notice none of the examples use Console-applications (DOS-like
programs) – all are Windows forms. Besides being more visually interesting, they allow
greater feedback while developing and the programs more accurately reflect what
happens in the business world.

What this book does not cover:

I do not cover some of the more theoretical underpinnings of modern programming.


Things such as polymorphism and object-inheritance are not discussed directly, but the
techniques are used throughout the book. Advanced programmers may take exception,
but I favor a more procedural background and I always approach problems from a
business-oriented perspective. You have a job to do, files to process, data-entry-screens
to write. These are the things this book is concerned about.

These volumes do not cover web-development but the programming skills taught are
100% transferrable. SQL databases are introduced, but four lengthy chapters only
scratch the surface. However, if you fear treading in this area, these four chapters will
get you started and will show relatively advanced techniques.

Why C#?:

Microsoft has built a wonderful programming environment which verges on magical.


The language is mature and consistent and can be applied in any conceivable situation.
It is a powerful tool that can solve complex problems.

The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.

How to Use This Book:

These three volumes are teaching books and because of that, it makes a poor reference
guide. To get the most utility, start at page one and work your way through chapters.
Each chapter builds on the previous. It takes time and effort to learn programming. As
you work through the chapters you must sit in front of the compiler and write the code.

This series was divided into three volumes, partly to aid in printing, and partly to make
the chapters more accessible.

Volume 1:
1 Introduction to the Editor
2 Introduction to Loops
3 Conditional Branching
4 Strings
5 Numbers and Dates
6 Utility Functions
7 Advanced Utility Functions
8 Class Libraries
9 Variable Scope
10 Form Controls and Events
11 Calling Multiple Forms
A Compiler Error Messages
B Compile and Distribute

Volume 2:
12 ASCII Files
13 Parsing Tab and CSV Files
14 INI Files
15 XML and App.config Files
16 Windows Registry
17 Reading Excel and Access
18 External Programs (Shell)
19 Wait, Delays, Pauses
20 Printing
21 Formatting

Volume 3:
22 Arrays
23 File Manipulation
24 Console Applications
25 SQL Databases
26 SQL Record Edits
27 SQL Data Grids
28 Data Grid Cell Editing
C Installing SQL Server Express
D Routines (of Interest)

Thank you

Thank you for purchasing this book. I hope you enjoy it as much as I have had writing it.
Comments and suggestions are welcome.

traywolf at keyliner com


www.keyliner.com

This book was written with Visual Studio 2013 through 2017's Community Edition, with
sections referencing Microsoft Office and Microsoft SQL Server 2016 Express.
© 2017 by Tim R.Wolf. All rights reserved.

Original text written with WordPerfect version X7. Illustrations created with Corel's
PaintShop Pro, version X8.
The Compiler and Other Tools:

As of mid 2017, Microsoft allows you to freely download a fully-capable version of


Visual Studio. The software is free and this book was written with these versions. See
this link:

http://www.visualstudio.com/en-us/products/visual-studio-community-vs

This book is applicable to VS 2010 and newer, with an emphasis in version 2017.

For the database chapters you will need a copy of Microsoft MS-SQL database or a copy
of Microsoft's free downloadable "MS-SQL Express". Details can be found in the
Appendixes.

Everything in this book was designed to run on a stand-alone (home) workstation,


including the SQL chapters. You will not need a server, external SQL database or
Active Directory to complete the chapters. However, when useful, references to these
other resources are made.
Absolute Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 09 - Variable Scope
Chapter 9 - Variable Scopes

By design, variables have limited life-spans (scope). When a procedure is called, it can
build variables as needed but when the routine ends the variables are destroyed and their
memory reclaimed. Within a loop, variables can be created and they will fall out of scope
when the loop ends. Entire Forms and Classes can fall in and out of scope as those
routines are exited. C# works hard to keep the memory footprint small.

This chapter explores different ways to control a variable's scope. As you will see, there
are good ways, bad ways, and ugly. All are discussed.

This chapter looks at "Global" variables, which are permanent


and do not fall out of scope until the program ends. Depending
on how they are built, they can be horrible, going against all
object oriented programming principles. This has implications to
a program's design, where these variables can make and break
dependencies between modules.

Topics:

• Local/Private variable scope review


• Scope within loops
• Defining a variable "higher-up"
• Form-level variables
• Form_Load events for populating demographic data

• Quick and dirty Global variables (the good, the bad and the ugly)
• Multi-form Global variable example
• "public static" and "internal static" variables
• "Inline" Global variable class (Program Specific class)
• "External Global Class Library" (Site variables)

• Get and Set Variables - The recommended way to build Global Variables
• By Reference (ref) variables

Overview:
Quick and Dirty, Form-level (Global) Variable
namespace WindowsApplication1
{
public partial class Form1 : Form
{
//Quick and Dirty Global variable (Not recommended)
//For simple projects with multiple Forms and Classes.
//To use: Form1.CurrentProjectName

internal static string CurrentProjectName = "Payroll";


public static string CurrentProjectName = "Payroll";
}

Chapter 9 - Variable Scopes Page: 543


Program-Specific Global Static Class
internal static class ProgramGlobals (or)
public static class ProgramGlobals
{
//This is one option:
//Declare global variables in a separate class:
//The "static" keeps you from having to instantiate with a "new"
//To use: ProgramGlobals.CurrentProjectName

public static string CurrentProjectName = "Payroll";

Steps for Linking External Class Libraries


Link the Library in Solution Explorer
using NSSiteGlobals; //at the Top
CLSiteGlobals SiteGlobals; //at Class Initialization
SiteGlobals = new CLSiteGlobals(); //at Class Constructor

Passing Variables by Reference (Ref)


//The "Call":
FunctionName (ref strVariable);

//The called function:


FunctionName (ref string strVariable)

Chapter 9 - Variable Scopes Page: 544


Using Get/Set Variables, recommended

1. Link in a previously-built CLSiteGlobals class

2. In CLSiteGlobals: (with these example variables:)

private string privCompanyName = "ABC, inc.";

3. Build a CompanyName constructor:

public string PubstrGlobalCompanyName


{
get { return privCompanyName; }
set { privCompanyName = value; }
}

4. In your program/form, instantiate a SiteGlobals class


CLSiteGlobals SiteGlobals;
SiteGlobals = new CLSiteGlobals();

5. In your program, use the new GetSet routine:


MessageBox.Show(SiteGlobals.PubstrGlobalCompanyName);

Variable Scope Summary:

Variables that live beyond a normal method boundary are often called Global variables
but I categorize them in different ways, depending on where they are built and how they
are used. This list is a summary of the benefits of each method:

• Local or Private Variables


Standard, method and function variables, which are defined within the function and
die at the end of a function. These are the variables you have been working with up to
this point.

• Form-Level Variables (Class Variables)


More properly called "Class variables". These variables are defined near the top of
the form (Class) and all methods and functions within the class can use them. They
cannot be seen or used by other Class libraries. These are not "global."

• "Quick and Dirty" Global Variables


Expands a variable's scope so the variable can be used by other Forms and Classes in
the same program, building a dependency between the two classes. Declaring a
variable with "public static" or "internal static" modifiers makes them visible beyond
their current class. They are only really useful in multi-formed and multi-class
projects and they do not lend themselves to good object-oriented techniques.

Chapter 9 - Variable Scopes Page: 545


• Internal (Program) Class Libraries
A slightly better way to have "Global Variables," especially in multi-form or multi-
class project. The variables are useful in the current program. Consider these as
"Program-specific" Global Variables. This is mainly a way to segregate where the
variables are defined.

• External (Site) Class Libraries


Global Variables for multiple projects. Consider these as "Site-specific Global
Variables," useful for all programs in a shop. This is similar to Program-specific
libraries except for the initial build and where the library is stored. Changes to these
variables affect all programs that link to it - and those programs do not have to be re-
compiled to see those changes.

• Using "Gets and Sets"


A way to have variables across classes without the dependencies and drawbacks of
"public static" Global variables. This is the recommended way to pass variables
between classes.

• Singleton Classes
An even more complicated way to build a Global Class. This type of class works in
multi-user and multi-threaded applications but is more difficult to construct and use.

• Passing variables by Ref


A way to pass values in the same class to downstream methods without resorting to
Global Variables. This is a recommended way for passing data to other modules but
admittedly only works for a few variables at a time.

Chapter 9 - Variable Scopes Page: 546


Variable Scope, Demonstrated

Variables declared within a function die when the function or method ends. In other
words, variables have a limited scope. For instance, consider these two button events,
where button1 declares a variable called "testString" and button2 attempts, and fails to use
the string:

Results: Compiler Error: (In button2) "The name 'testString' does not exist in the current
context."

j As a hint, while typing the button2 testString variable, you may have noticed the
"intellisense" pop-up help did not offer assistance with the variable name. This is an
indication the variable is not in the current scope.

When button1 is clicked, the variable "testString" is declared and memory allocated. As
soon as button1's Click event finishes (about a millisecond after you click it), all variables
declared within the event are deleted. When button2 is clicked, the "testString" variable
is already gone. The editor catches this error and will not let you compile.

Variables defined within a function or method are called "local" variables. The very act
of defining them inside of a function makes them private to that function.

Variable Scope within Loops:

If a variable is declared within a loop (or other braced-construct), it is destroyed when the
loop ends. In the following for-next loop, variable "i" is declared in the loop's definition.
The variable lives as long as the loop is active and dies when the loop ends.

Chapter 9 - Variable Scopes Page: 547


After the loop, the editor detects the MessageBox's attempt to use the variable and
prevents the program from compiling. The message, "The name "i" does not exist in the
current context" means the variable fell out of scope, never existed, or is mis-spelled.

In the same example notice the string "loopString" was declared inside the loop –
declared with the word "string":

string loopString = "hello";

On each loop iteration, a new instance of "loopString" is created. Since the loop runs ten
times, the variable is created ten times (and destroyed ten times). This can get confusing.
What really happens is loopString falls out of scope at the bottom of the loop (the closing
brace) and is re-spawned at the top. This means the variable is automatically initialized
shiny-and-new each time. This is a neat concept.

What if the Variable is Needed Beyond the Loop?

In the example above, what if you need the final results for variable "i", after the end of
the loop? The solution is to declare the variable at a "higher position," before (or above)
the loop. In this next example, "i" is declared above the for-next statement and the
variable lives – at least until the end of the button1_Click event.

Chapter 9 - Variable Scopes Page: 548


Did you notice the for(i = 1...) statement – the "i" is not declared with
"int i"?

Declaring a Local Variable Does Not Allocate Memory:

Along these same lines, notice this oddity with a newly-introduced variable, "testString".
Both integer i and testString are declared (but not initialized) above the for-next loop.

As the for-next loop runs, the local variables i and testString are populated with data but
at the end of the loop, testString surprisingly falls out of scope while "i" survives.
Simply declaring a normal, local variable (string testString) does not allocate

Chapter 9 - Variable Scopes Page: 549


memory until it is used at least one time. Because testString was first-time populated
within the loop, it de-allocated (falling out of scope) when the loop ended. But "i" was
populated at the top-of, and outside of the loop. This behavior is unexpected and seems
somewhat inconsistent. (Contrast this with Form-level variables, described below, which
survive the disjointed declaration and initialization events.)

The variable's scope can be extended above the example loop by declaring and initializing
at the same time. The author recommends pre-initializing all declared variables in this
fashion:

string testString = "";

Stylistically, consider initializing the loop's numeric value when an extended scope is
needed, making your intent abundantly clear:

int i = 0;

Chapter 9 - Variable Scopes Page: 550


Form-Level (Class) Variables

Knowing that variables declared within a function (for example, button1_Click), fall out
of scope when the function ends, one can correctly surmise variables declared higher in
the program survive beyond the function. With this, a variable declared near the top of
Form1would be visible to all of the functions and methods within the form. (This does
not mean the variables are "public" – they just have a broader "form-level" scope, or more
technically, a "class-level" scope. "public" variables are described in a moment.)

Setting the Scope at the Form-Level:

In this next illustration, notice how integer "i" and string "currentFormName" are
declared above the button1_Click event. In this position, the two variables live as long as
the Form is open. All functions and methods within the Form (button1, button2, etc.) can
see these two values:

Declaring variables at the class-level entails risk. Not only can every function see the
variables, every function can change them and this can make debugging difficult. On the
other hand, the variables are handily visible to all routines.

In other languages (Visual Basic 6), these types of variables were considered "public" but
this is an incorrect way to describe a public label in C#. It is not the word "public" that
makes a variable broadly-visible, instead, it is where you define it that gives it a wider
scope.

Many consider these types of variables as "Global" because they are visible to all of the
functions within the form. But even here, they are limited in their scope – in this case,

Chapter 9 - Variable Scopes Page: 551


limited to the form. In a multi-form project, the variable "currentFormName" is not
visible to other forms. It is defined when the form opens and is lost when the form closes.

Improper Placement:

Do not make the mistake of building form-level (class) variables within the "public
Form1( )" constructor. This is the event that builds the initial form and the event ends
shortly after the program starts:

Technically, form-level (class) variables can be declared between any two functions, not
just at the top of the form. Where they are defined is not important (as long as they are
not within another function or method), however, it is poor style to scatter them
throughout the code. Define form-level variables in a consistent location, typically just
below the "public partial class Form1" statement.

j Variables and other objects can be declared and initialized outside of a function – but
you cannot type executable (command) statements outside of a function's opening and
closing braces.

Benefits of Limited Scope Variables:

As a function ends, the memory occupied by the variables is automatically released by a


process called "garbage collecting." This offers benefits to the program.

• The program's memory footprint is smaller because variables are released frequently.
And there are less chances for "memory leaks".
• Local variable names won't conflict with other modules using the same name. If a
variable, such as "loopCounter," is used in multiple routines, the names won't collide,
and more importantly, other routines do not accidentally absorb the values from
earlier iterations. But this is not true if the variable is defined at the form (class) level
– here the values survive from one method to the next, making them somewhat risky.
• Form-level variables are released when the form closes. If the form is re-loaded,
variables are re-initialized.

Chapter 9 - Variable Scopes Page: 552


Name Collisions:

If button1 declares a variable "loopCounter" and button2 declares the same variable name,
both variables are independent of each other; neither would see nor care about the other.
This holds true with all variables defined at different scopes. For example, say the
program sets loopCounter as a form-level (class) variable but later declares the same name
in button1:

At the class-level, loopCounter was declared and initialized to 99 and in button1, a second
variable was named and initialized at 1. While button1's logic is running, it uses the
second copy, ignoring the first. When button1's logic ends, its loopCounter falls out of
scope and the original value is remains unharmed and unmolested. The two variables
have the same name and type but are unrelated.

If button1 did not declare its own copy of the variable, it would use the class-level copy.
In other words, if button1 said "loopCounter = 1" (vs int loopCounter = 1), the top-
level variable would be changed from 99 to 1.

Chapter 9 - Variable Scopes Page: 553


"Global" Variables

In a larger project, with multiple classes and forms, you may need a common set of
variables that are visible to multiple classes. A common need would be a central location
for the currently-logged in username, the default company name, and perhaps a default
server or database name. These types of variables are often called "global" variables.

Problems with Global Variables – The Debate:

There is heated discussion in programming circles about global variables. Many


developers know other programming languages support the idea of global variables, but
C# goes out of its way to prevent them. The very idea of global variables is considered an
anathema by object-oriented developers. "If you are trying to do it with a global, then you
are doing it wrong." There are several problems:

• Any routine can read and change the variable from any form, at any time. When a
variable is unexpectedly changed, it can be devilishly hard to find who made the
change. You may say, "I won't have this problem – I always know where my code
changes a value." In real life, programs get complex and this will be a problem.

• Object-oriented programming means that each class should be independent of each


other. But when you define a global variable in Form1 and use it in Form2, it creates
a dependency (coupling) between the two. With this coupling, Form2 can no longer
stand by itself and can't be used in other projects without its friend, Form1. This
makes it hard to re-use code.

• Without going into details, there are other issues in multi-threaded applications and
with multiple copies of your program running simultaneously (especially in Web
Servers where several hundred people may be running your program at the same
time).

On the other hand, smaller (simpler) programs, that have no intention on running on a web
server or in multiple threads, may find conveniences in populating a "global" variable at
the beginning of the program.

Keep in mind that "Global" variables are really a problem with


true object-oriented programming. Searching the web shows a
variety of ways to work around these problems but remember the
very idea of "global" variables is flawed. This chapter explores
these ideas, both the good and the bad, because you will see all of
these techniques in the real world.

Some suggest even form-level variables, which act like quasi-global variables, are not
proper. I disagree. Form-level variables are self-contained in the form class and are
properly destroyed when the form is closed.

Chapter 9 - Variable Scopes Page: 554


None of this deters this chapter from at least attempting global-like variables, but as the
chapter progresses, there will be progressively better ways to accomplish the goal while
maintaining object-oriented principles.

In each of the examples below, remember the general concept:


Some variables, such as CompanyNames, CurrentUsers,
Database names, and the like, seem to fit well as Global
variables. At the beginning of your program, you would read and
populate the values (from an external source) and then use them
repeatedly throughout the project with the understanding of the
risks to object-oriented programming techniques.

You Cannot Define a Variable higher than the Class (Form):

The question is this: Can variables be defined at a level higher than the form? If a
variable could be declared above the form (class) level, it should be visible to all of the
forms and other classes in a project:

C# does not provide a direct mechanism for building "global" variables and it enforces it
here. A compiler error ensues "A namespace does not directly contain members such as
fields or methods." In other words, you cannot declare a variable above the class
definition.

Declaring "Quick and Dirty" Global Variables:

Although global variables should be frowned upon, it turns out there are several
distasteful ways to build them. The most common misconception is to declare them at the
form-level with a "public" modifier, ala Visual Basic:

namespace WindowsApplication1
{
public partial class Form1 : Form
{
//this does not work:
public string currentFormName = "Payroll";

Chapter 9 - Variable Scopes Page: 555


The "public" modifier alone (ala VB) does not make the variable "global;" it is the
position of the statement that makes the variable visible to other functions in the form.
But more is needed to make the variable a true global.

The "static" Modifier - Quick and Dirty Globals:

In conjunction with "public", a second modifier, "static", makes a variable "global." I like
to call these 'quick and dirty' global variables.

Building a Quick and Dirty Global Variable


namespace WindowsApplication1
{
public partial class Form1 : Form
{
//Building quick and dirty Global variables for inline
//and program-specific classes. Visible to all classes in
//the project.
//Use one of these two types:

public static string CurrentFormName = "Payroll"; //or


internal static string CurrentFormName = "Payroll";

Public Form1()
{
: etc.

where:

• Declare the variables in the same location as a class variable but prepend the
statement with either a public static or internal static modifier.

• "public static" makes it visible to all other classes in the project (typically other
forms, but it would also be visible to other classes, such as the previous chapter's
"PayrollTools" class). public static is also visible to other programs, which are not
necessarily part of this project, should they link these classes.

"static" tells the compiler to create the variable when the form is instantiated and
leave it active for all other forms or assemblies. A "static" variable punches a hole
from one class to the other. ("static" does not mean the variable is unchangeable; use
"constant" for that.)

• "internal static" limits the visibility to only the classes in this project/solution and
for most variables, this is a better choice, although it seems less-well known.
"internal static" is slightly less-expansive because it limits the variable to the current
program and its assemblies.

Either of these two ideas allows you to build "quick-and-dirty" global variables. Just
because you can does not mean you should.

Chapter 9 - Variable Scopes Page: 556


public static string CurrentFormName Is the most globally visible
variable.
vs

internal static string CurrentFormName Only routines in your assembly


can view and change the
variable.

Using a Global Variable:

If the global variable is defined and used in form1, use the variable as you would
normally:

MessageBox.Show("My form name is: " + CurrentFormName);

If the global variable is defined in form1 but used in form2, reference the variable in this
fashion:

//From Form2...
MessageBox.Show("That Form's name is: " + form1.CurrentFormName);

Problems with Dependencies:

When you see a modifier, such as form1.dot-something, your object-oriented brain should
derail. Like all global variables, the value can be changed by any routine in any form,
making it nasty to debug. But more importantly, if the Form2-class is brought into
another project, its dependency to Form1 for its global variable would be broken. The
new Project would also need to bring in Form1. This does not make for good, high-quality
re-useable code.

Finally, be aware there are other modifiers beyond private, public, internal, and static.
The nuances of all the modifiers are not important to this chapter.

Chapter 9 - Variable Scopes Page: 557


Multi-Form and Variable Scope Exercises

In this section, build a project that has two forms and pass values between them. Each
form will have buttons that can query the variable's current settings.

Important: The techniques for opening a second form, as described here, are too
simplistic for a real program. See Chapter 11 for better designs.

Follow these steps to create a multi-Form project:

A. Start a new project. Accept default names.

B. Add two buttons to Form1, changing the button widths to show the text, as needed.:

Button1: (Name) btnShowFormName


text Show FormName

Button2: (Name) btnOpenForm2


text Open Form2

Begin coding here:

1. Scroll to the top of the Form1's class and create a standard class level variable (a form-
level variable) just below the "public partial class Form1 : Form" statement. Note: This is
not yet a global variable.

public partial class Form1 : Form


{
string strCurrentFormName; //(This works in current form only;
//see below)
public Form1()
{
etc...

2. Double-click the form's background to create a "Form1_Load event." This is an event,


similar to Button1_Click and it fires when the program first runs. Add this statement to
the event's code:

Chapter 9 - Variable Scopes Page: 558


private void Form1_Load (object sender, EventArgs e)
{
strCurrentFormName = "Payroll";
}

where strCurrentFormName is just another variable.

3. Return to the Form Designer (see top-row of tabs, click [Design]).


Double-click the button, btnShowFormName to create the event, adding this code:

private void btnShowFormName_Click (object sender, EventArgs e)


{
MessageBox.Show (strCurrentFormName);
}

This completes the first form.

4. Create a second Form (Form2):

a. In Solution Explorer, "Other mouse-click" the project's name e.g.


WindowsApplication1

b. Choose Add, “Windows Form”


c. Confirm "Windows Form" from the dialogue

d. Type name "Form2.cs" - where the .cs extension is required.


Click "Add". Form2 displays in the designer.

Chapter 9 - Variable Scopes Page: 559


5. On Form2, create two buttons with these properties:

Form2, Button1: (Name) btnShow2FormName


text Show FormName

Form2, Button2: (Name) btnChangeFormName


text Change FormName

6. Double-click Form2's button "Show Form Name" to create an event similar to Form1's
event. Add this statement, which will not work properly.

btnShowFormName_Click(object sender, EventArgs e)


{
//Although you are in Form2, attempt to see what Form1's
//CurrentFormName is

MessageBox.Show (Form1.strCurrentFormName);
}

When typing this line, notice how Form1.strCurrentFormName is not available from the
pop-up help, indicating the variable is out of scope. If you were to compile the program
now, you would see this error "An object reference is required for the nonstatic field,
method, or property..." and "'strCurrentFormName' is inaccessible due to its protection
level".

7. Even though the variable is not yet within scope, and the editor will complain, double-
click form2's "btnChangeFormName", adding this statement:

Chapter 9 - Variable Scopes Page: 560


btnChangeFormName_Click (object sender, EventArgs e)
{
Form1.strCurrentFormName = "Accounting"; //Was Payroll
}

The compiler complains. Form2 cannot see Form1's variables, no matter how you try to
prefix them; strCurrentFormName is not in scope.

Build a global Variable:

When a variable is out of scope, it is out of scope. This can be changed by converting the
variable to what I like to call a "Quick and Dirty" Global Variable21.

8. Return to Form1.cs's code view by locating Form1.cs in Solution Explorer, "other-mouse-


clicking" and choosing "Code View" (alternately, click Form1.cs on the top tab-menu).

Change Form1's "CurrentFormName" to a "public static" variable:

from: string strCurrentFormName;


to: public static string strCurrentFormName;

This converts the variable to a global and it can be used from anywhere in the project by
using a "Form1-dot" prefix. Out of convention, most developers use PascalCase (initial
caps) to signify a Global variable, where the "C" in CurrentFormName is capitalized.

Form1.strCurrentFormName

Note: The compiler may get lost with the change from a form-level variable to a Global.
If you try to compile and the field "CurrentFormName" reports the error "An object
reference is required for the nonstatic field....", do the following:

Select top-menu Build, "Rebuild Solution"

21
Paul Knoll, a famous Structured Programming instructor, was fond of saying quick and dirty
programming tricks are always quick and always dirty.

Chapter 9 - Variable Scopes Page: 561


Code Form1 to Open Form2:

9. Using the top tabs, return to "Form1.cs [Design]" and create the code that will open the
second form. (This topic is discussed in greater detail in Chapter 11. Be aware this is not
the recommended way to open a second form. For now, code as indicated.)

Double-click button "Open Form2" to create the button-event


Add this logic to the event:

Form1 Logic to Open Form2, simplistic


private void btnOpenForm2_Click (object sender, EventArgs e)
{
//Open Form2 using Simplistic logic.
//See Chapter 11 for better Form-opening steps

Form2 myForm2 = new Form2();


myForm2.Show();
}

Testing Form2 and the Global Variable:

Press F5 to run the program.


Click "Show Form" in both Form1 and Form2.

Results: CurrentFormName "Payroll" displays in both forms. Note: The Show-buttons


display the value stored in the global variable.

Test the Global Variable by changing the name from "Payroll" to "Accounting":
In Form2, click "Change Form Name".
Confirm the new name sticks in both Form1 and Form2.

Chapter 9 - Variable Scopes Page: 562


Problems with public static Variables:

Creating "public static" variables solves the immediate problem of getting a variable like
CurrentFormName to show in both Form1 and Form2.

Notice how the code in Form2 specifically referenced the variable in Form1:

MessageBox.Show (Form1.CurrentFormName); //a dependency

Form2 is coupled to Form1, creating a dependency that defies proper object-oriented


techniques. In simpler terms, Form2 cannot be re-used in another program without first
considering Form1. The next section solves this problem but you will find many use this
easier, yet flawed method.

Chapter 9 - Variable Scopes Page: 563


Using a Global Class to Pass Values

You may decide you don't care about the coupling between Form1 and Form2 (from the
"quick and dirty global variables," above). Your project may be a one-of-a-kind program
and the issues raised earlier are moot. In this case, using global variables with "public
static" is probably adequate. But you can improve on the design by making relatively
minor changes to where the global variables are defined.

By moving the global variables to their own class, you can move the dependency from
Form1 to a new class. On the surface, this sounds like a waste of time, but this change
makes it easy to add these same global variables to any program without incurring the
penalty of being linked to a form. In a corporate environment, this might be useful.
Imagine a set of global variables every program needs to use. These can store such
information as the company name, database servers, and other such demographic
variables.

At a simpler level, a "program-specific" global class can be used to share variables


between a multi-formed program. In each of these cases, global variables force a
dependency between the objects and there are better ways to do things, but often, for a
simple, one-time program, these types of constructs can quickly solve problems.

Global Class modules can be added to your existing code in one of two ways. The first
declares the new class at the bottom of Form1's code and the second method declares the
class as a separate physical .cs file. The separate file is preferred because the code is
easier to find. In either case, once declared, the variables can be used by the current
project (other projects cannot as easily use this class) and because of this, I call it a
"program-specific" class library. Use this type of library for all variables that need to be
seen by all of the classes and forms within the current project.

Method 1: Inline, Program-Specific, Global Class:

To build an "inline" class library, scroll to the bottom of your program's source code,
below Form1's closing brace; it is important to stay above the namespace's closing brace.
Define the new class with this statement:

internal static class InlineProgramGlobals


{

Chapter 9 - Variable Scopes Page: 564


Method 1: An "inline" Program-Specific Class Library, completed

Within this new class, create the "public static" variables, as described in the previous
section. For example, in the code example above, notice the newly-declared variable
"MyProgramName".

In Form1's (button1) event, you can use this statement to display the value. Note how the
class name is prefixed in front of the variable:

MessageBox.Show(InlineProgramGlobals.MyProgramName);

comments:

• The Author is not fond of a Global class in this location and prefers the class to be a
separate file and this is covered next.

• The class name, "InlineProgramGlobals" is an invented name; use any name of your
choosing.

• Since the class is defined inline, and it is within the same namespace, the class did not
need to be instantiated with "new" keyword, nor do you need a "using" statement.
Preface all variable names with the class name (e.g. InlineProgramGlobals.<variable
name>).

More to the point, the word "static" allows the class to be used without having to
instantiate the class with the "new" keyword.

Chapter 9 - Variable Scopes Page: 565


• When declaring a Program-specific class, use "internal static", instead of
"public static" because the class truly belongs to the current project and there is no
reason to expose it to a larger scope or to other programs.

Method 2: An "Internal" Program-specific Global Class:

A new program-specific class can be built in Solution Explorer with the same effect as the
inline class and this is the preferred method.

As Solution Explorer builds the class, the file is stored with the project as a separate
physical file. Because the file lives next to the other modules, this type of class is usually
reserved for program-specific (Global) variables and methods. With this, especially in a
multi-form project, each form can reference the variables (and methods) within this class.

Follow these steps to build a 'Program-specific' Global Variable class:

A. From Solution Explorer, other mouse-click the project's name. In the illustration above,
you would highlight the bolded "WindowsApplication1":

Select Add, New Item, "Class"


Change the offered filename from "class1.cs" to a new name such as ProgramGlobals.cs"
(the ".cs" extension us required).

B. The new class appears in the tree, without a "shortcut" icon, indicating the file is within
this project.

In Solution Explorer, double-click "ProgramGlobal.cs" to open code-view.

Notice how the class has the same namespace (e.g. WindowsApplication1). Although the
editor shows the new class in a separate tab, it acts as if it were typed "inline." Create
"public static" variables, as before.

Chapter 9 - Variable Scopes Page: 566


namespace WindowsApplication1
{
class ProgramGlobals.cs
{
public static string strServerName = "Electra";
}
}

comments:

• The class is physically a separate file (ProgramGlobals.cs), stored with the other
project files, and the editor shows it as a separate tab. But it behaves exactly as the
inline class, described earlier.

• Because it is in the same namespace, do not use a "using" statement.

• Since the class is not instantiated with a "new" keyword, you must preface variable
names with the class name – in other words, ProgramGlobals.ServerName (think
"ProgramGlobals.cs").

j If you have not instantiated the Class Library with a "new" keyword, you must
preface the variable name with the class's physical name. An instantiated class, as
described in the next section, uses the class's variable name as a prefix.

If you have built all of the class libraries from this chapter, Form1's button1_Click
(btnShowCompanyName) would look like the following. Notice how the variables are
prefixed:

private void btnShowCompanyName_Click (object sender, EventArgs e)


{
MessageBox.Show (CurrentFormName); //From a Form-Level variable
MessageBox.Show (InlineProgramGlobals.MyProgramName);
MessageBox.Show (ProgramGlobals.ServerName);
}

Chapter 9 - Variable Scopes Page: 567


Building an External Global Class Library

Following the theme of this chapter, expand the idea of global variables one step further.
A new "External" Global Class Library allows the variable to be shared with other
projects. The difference between this type of library and the previous "Program Class
Library" is how you mechanically build the library. Beyond this, the library and its
functions are the same.

This type of Global Class Library still has the same flaws of the
previous global variables (variables are exposed to change from
many locations and and there are dependencies created between
the classes), but many developers use this method because it is
easy to understand and use.

When building, what I call an "External" class library, the source-code should not be
saved in the current project's program directory because it is expected to be shared by
other projects. Follow these steps to build the library and they are the same as those used
in Chapter 8, "Creating External Class Libraries from Scratch." Recall that Visual Studio
(2008 through 2017) does not allow you to build an external class library from within the
existing project, so you have to improvise.

Building the External Library using Visual Studio:

These same steps were described in the previous chapter.

A. Launch a fresh copy of Visual Studio and select File, New Project

B. In the Project Types section, confirm Visual C# "Windows Classic Desktop" is selected.
In the Templates section, choose "Class Library"

C. In the Name field, type: "NSSiteGlobals"

D. In the Location field, type or browse to "C:\Data\Source\CommonVS" (or other similar


generic location).
Do not check "Create directory for solution"
Click OK to build the solution.

E. In Solution Explorer, change the default class name, "Class1.cs" by:


Other-mouse-clicking "Class1.cs", select "Rename".
Rename the class to "CLSiteGlobals.cs" (you *must* use the .cs extension)

When prompted "Would you also like to perform a rename in this project and all
references...", choose Yes.

The class is now ready to accept code.


Close this copy of Visual Studio and return to your original Program. You will add
variables to the new class in a moment.

Chapter 9 - Variable Scopes Page: 568


Linking the Class Library:

Following the same steps as with CL800_Util, link this new class into your existing
project.

1. In Solution Explorer (Be sure you had exited the second copy of Visual Studio and had
returned to your original project.),

a. Select Add, Existing Item


b. Browse to C:\Data\Source\CommonVS\NSSiteGlobals
c. Highlight, do not double-click, "CLSiteGlobals.cs"
d. Choose the "Add" pull-down menu; choose "Add as Link"

Notice Solution Explorer's icon, showing a shortcut, indicating the library is external to
this project:

Quick and Dirty Variables in the External Class:

Just for the learning experience, create a few quick and dirty Global variables within the
new class. This is not particularly the best way to do this because in a Global Class
library, "public static" variables are probably more accessible than they should be. This
will be fixed in a moment.

2. In Solution Explorer, double-click the new External Class "CLSiteGlobals.cs"; this opens
code view. Notice the namespace "NSSiteGlobals".

3. Below the class CLSiteGlobals definition, create two Global variables; this is in the same
position where class-level variables are created.

namespace NSSiteGlobals
{
public class CLSiteGlobals
{
public static string strCompanyName = "ABC, inc.";
public static string strCentralServer = "\\Elrond\\vol1";
}
}

Chapter 9 - Variable Scopes Page: 569


4. Click the Form1.cs tab to return to Form1's code view.
Double-click button1 (btnShowFormName, from previous examples)

Comment the previous example MessageBox statements in button1_Click


(btnShowFormName) before adding the new statement.

Then add this new MessageBox statement:

private void btnShowFormName_Click (object sender, EventArgs e)


{
//MessageBox.Show (InLineProgramGlobals.strProgramName);
MessageBox.Show (NSSiteGlobals.CLSiteGlobals.strCompanyName);
}

comments:

• For illustration, the new namespace was not added to the top of the Form ("using
NSSiteGlobals"). Because of this, notice how the variable name needs a double-
prefix that has both the namespace and class name:

MessageBox.Show (NSSiteGlobals.CLSiteGlobals.strCompanyName);

With other variables, this was not a requirement because they were always in the
same namespace.

To avoid the first prefix, add "using NSSiteGlobals" at the top of Form1. However,
some developers prefer to fully-qualify the name, making it self-evident where the
variable came from.

• Because variable "strCompanyName" is a "public static" variable, Form1 can reach


across the class and can both read and change the value.

• The "static" modifier allows you to address the CompanyName variable without
instantiating the class with a "new" keyword.

Instantiating an External Class Library:

"public static" variables are sloppy. Remember, the "static" modifier punches a hole all
the way into the class and allows any routine to modify the variable. A slightly better way
to handle this is to instantiate the new class and discard the "static" modifier. Of course,
this changes how you address the variable. Try this exercise.

1. Add this statement to the top of Form1's code-view. This saves you from having to prefix
the namespace each time a variable is referenced. (Confirm CLSiteGlobals is linked in
Solution Explorer, as described in previously.)

using NSSiteGlobals;

Chapter 9 - Variable Scopes Page: 570


2. In Form1's class, directly below the Form's declaration, declare a new "CLSiteGlobals"
variable using this statement. This is the same type of statement used with CL800_Util
and there may be other variables from previous examples in this same location; leave
them as-is.:

public partial class Form1 : Form


{
CLSiteGlobals SiteGlobals; //Declare SiteGlobals

Declaring the External Class at the Form-Level (Class), inprogress

The statement declares a new variable, of type "CLSiteGlobals", giving it a user-defined


name of "SiteGlobals". (This is similar to declaring a "string myName", where the type is
"string" and the variable's name is "myName".)

Placing the declaration statement high in the class definition gives it a wider scope and
like any other variable positioned here, it allows the new class to be used anywhere in the
Form. But the instantiation (initialization "new" clause) has to wait until later because
executable code cannot live outside of a method.

3. Usually with Form1's constructor, "public Form1( )", instantiate the class using the "new"
keyword. (This could also happen in the Form_Load event):

public Form1()
{
InitializeComponent();
SiteGlobals = new CLSiteGlobals();
}

The Constructor would run and end in a millisecond and everything within it it would fall
out of scope, including the new class "SiteGlobals," but because SiteGlobals was named
above this routine, it survives. If you only needed the class (and its variables) for a brief
time, say for the duration of button1_Click, you could declare and instantiate in button1's
code.

Chapter 9 - Variable Scopes Page: 571


Instantiating the SiteGlobals class follows the same four-step process used by the
CL800_Util class libraries. First, link the CLSiteGlobal.cs in Solution Explorer, then add
three statements to the code.

The Four-steps to Instantiate an External Class Library

The class essentially copies itself into the current Form with the "new" keyword. The
"new" class (which gets a different name: CLSiteGlobals becomes "SiteGlobals") is a
perfect copy of the original, including all of the attributes, variables and methods of the
External class. Once instantiated, the copy becomes part of Form1.

Using and Testing an External Class Variable:

Because the CLSiteGlobals class library was instantiated into Form1, the object
essentially becomes part of Form1 and the class is a copy of the original. The "static" part
of the variable's definition no longer makes sense.

Changing "CompanyName"'s modifier from "public static" to "public" completes the


variable's transformation.

Make the following changes now:

a) In Form1.cs,

Chapter 9 - Variable Scopes Page: 572


In the event btnShowFormName (which is now a misnomer), change the MessageBox
statement by removing the hard-coded class name, replacing it with the new
instantiated variable name, "SiteGlobals".

private void btnShowFormName_Click (object sender, EventArgs e)


{
//Change
//MessageBox.Show (NSSiteGlobals.CLSiteGlobals.strCompanyName)to
MessageBox.Show (SiteGlobals.strCompanyName);
}

b) As a test, leave the CLSiteGlobals "strCompanyName" defined as "public static".


This generates an error.

c) In Form2, comment-out all of the 'Show' and 'Change' logic in button1 and button2
because those statements are now pointing to the wrong variables. You will return to
these statements in a few minutes.

d) Attempt to compile the program by pressing F5.

The error: "Static member 'NSSiteGlobals.CLSiteGlobals.strCompanyName' cannot


be accessed with an instance reference; qualify it with a type name instead". This
really means the compiler is confused. It wants to know your true intent: Are you
instantiating or using a quick and dirty static variable?

e) Return to CLSiteGlobals.cs, and change CompanyName from "public static string" to


"public string"

public static string strCompanyName = "ABC, inc."; to


public string strCompanyName = "ABC, inc.";

f) Recompile. Now Form1's btnShow will work correctly.

In summary, quick and dirty Global variables require the use of "public static" but
instantiating the class is a better way to declare the variables. When instantiating a new
class, do not use "static" variables.

comments on Global Static Classes:

• Because the class is defined in an external namespace, I recommend coding a


"using NSSiteGlobals" statement. This saves you from having to double-prefix
variable names with the namespace.

• With any External Global Class, instantiate the class with a "new" keyword. This
makes a copy of the variables, and as you will see in the next section, this helps
insulate the variables from changes.

Chapter 9 - Variable Scopes Page: 573


• Reference the variable's name using the "new" (variable) name. In other words, do
not use the actual class name. Instead, use the name assigned when the class was
defined. In these examples, "SiteGlobals" is the variable name:

CLSiteGlobals SiteGlobals;

SiteGlobals.strCompanyName //if instantiated with "new"

If you instead decided to use "public static" variables, preface strCompanyName with
the physical class's name instead of the instantiated name:

CLSiteGlobals.CompanyName" //if not instantiated with a 'new'

• The names "CLSiteGlobals" and "SiteGlobals" are arbitrary; use any name desired.
The class could have been named any name, including "Bob", or "SG" (Site Globals)
and used as Bob.CompanyName, SG.strCompanyName.

In these examples, the class names were prefixed with "CL"; this is not commonly
done in the real world - but I like to use this for clarity. Since the class name and the
variable name must be unique, many developers use the same name for both, but use
different initial-caps. For example, the class name could be in caps while the variable
name is in lower-case. I find this too subtle:

SiteGlobals siteglobals;
siteglobals = new SiteGlobals

Form2 and the Same External Class Library:

In the previous example, Form1 instantiated the global class library and the
CompanyName variable was displayed properly. With some experimentation, you would
find that Form2 can not see the global variables. For example, these statements fail:

(From Form2)
Form1.SiteGlobals.strCompanyName //fails
Form1.CLSiteGlobals.strCompanyName //fails

If Form2 needs the same global variables, it needs to instantiate the global class in the
same way as Form1. As you will see, there may be some surprises with this.

This section demonstrates how class libraries are instantiated


and how the copies stand independently from each other. A
second version of the global class is indeed a new copy and each
can change independently of the other.

Add the Global Class to Form2:

1. Instantiate the same global class library by making these (bolded) changes at the top of
Form2. To illustrate the point, notice how this example manually prefixes the namespace,
allowing you to avoid the namespace "using" statement:

Chapter 9 - Variable Scopes Page: 574


public partial class Form2 : Form
{
//Declare the new Global Class; linking to the same as before:
CLSiteGlobals SiteGlobals;

public Form2()
{
InitializeComponent();
//Instantiate the new Global class...
SiteGlobals = new NSSiteGlobals.CLSiteGlobals(); //alt design
}

2. Add this code to Form2's button1_Click (btnShow). If you have followed previous
examples, the button may have originally been designed to show the FormName; use the
same button to display the CompanyName.

private void btnShowFormName_Click (object sender, EventArgs e)


{
//Using the newly-created variable "SiteGlobals" (which is a
//different version than Form1's...

MessageBox.Show (SiteGlobals.strCompanyName);
}

3. Add this code to Form2's button2_Click (btnChange):

private void btnChangeFormName_Click (object sender, EventArgs e)


{
SiteGlobals.strCompanyName = "Johnson and Company";
}

Testing Form2 with the New Global Class:

Form2 instantiates its own copy of the class into Form2. To prove this, run the program,
then follow these testing steps:

• In Form1, "show" the CompanyName (ABC, inc.)

• Click "Open Form2"


Click the "Change" button (nothing appears on screen but the value does change)
Click Show the CompanyName from Form2's perspective (Johnson and Company)

• Close Form2, returning to Form1

• Display the CompanyName again (from Form1's perspective, it remained ABC, inc.)
• Return to Form2; check the CompanyName (it is ABC, not Johnson and Company)

Chapter 9 - Variable Scopes Page: 575


What happened? Form1's copy of the global class is completely independent of Form2's
copy. Because both started from the same Class, they initialize to the same value (ABC)
but notice how Form2 was able to change its copy. Additionally, when Form2 closed, the
entire class library was discarded, falling out of scope. When Form2 was re-loaded, the
class re-birthed itself, returning to the original CompanyName value. This behavior can
be good and bad – good because it enforces corporate standards, but bad because a
program can change the values at run-time.

Chapter 9 - Variable Scopes Page: 576


Getting and Setting Variables

The global variables defined so far have been somewhat fragile because any routine can
change the value of the "public static" variables. And in the case of an instantiated
external class, a form can temporarily change a global variable without the other classes
knowing, and the change would be discarded when the form closed, defeating the the
change.

This section builds a more secure class library that protects the variables from inadvertent
changes. Naturally, this is slightly more complicated but solves numerous problems.
This concept is the recommended way to build variables that can be shared among
classes.

Using Get/Set and Global Variables, summary

1. Link in a previously-built CLSiteGlobals class

2. In CLSiteGlobals:, populate with this example variable:

private string privstrCompanyName = "ABC, inc.";

3. In the CLSiteGlobals class, build a CompanyName constructor directly below the


private variable:

public string PubstrGlobalCompanyName


{
get { return privstrCompanyName; }
set => privstrCompanyName = value;
}

4. In your program/form, declare and instantiate a SiteGlobals class


NSSiteGlobals.CLSiteGlobals SiteGlobals;
SiteGlobals = new NSSiteGlobals.CLSiteGlobals();

Or, if "using NSSiteGlobals;"


CLSiteGlobals SiteGlobals;
SiteGlobals = new CLSiteGlobals();

5. In your program, use the new GetSet routine:


MessageBox.Show(SiteGlobals.PubstrGlobalCompanyName);

where:

• Formerly, in older Visual Studio 2015 and older, the syntax for the Set looked like
this:
get { return privstrCompanyName; }
set { privstrCompanyName = value; }

Chapter 9 - Variable Scopes Page: 577


• Starting in VS2017, Microsoft recommends all Public variables and constructors
begin with an upper-case letter (PubstrCompanyName). Following my normal
convention, local variables remain lower-cased (privstrCompanyName).

• The decision on "using NSSiteGlobals;" vs prefacing, as in


"NSSiteGlobals.CLSiteGlobals" is a matter of personal style. I happen to like the
preface when using these types of variables.

Setting up the Example:

Use the previous example or start a new project:

A. Start a new Project and add two buttons to Form1:


btnShowCompanyName
btnChangeCompanyName

B. Link in the External Class Library; see previous section for setting up an external class:

C:\Data\Source\CommonVS\CLSiteGlobals.cs

C. Declare and instantiate the class with:

public partial class Form1 : Form


{
NSSiteGlobals.CLSiteGlobals SiteGlobals;

public Form1()
{
InitializeComponent();
SiteGlobals = new NSSiteGlobals.CLSiteGlobals();
}

D. If your project from earlier examples has a Form2, delete the form and any logic that
points to it.

Building Get/Set Properties:

In the past, variables were declared as "public" so they would be visible across class
boundaries. With the Get/Set routines, the variables become "private" – making them
visible to only their current class, but through a technique using "accessors" (I like to call
them "object properties"), the values of the now-hidden variables can be returned to the
calling routine as a passed-value.

Make the following changes to CLSiteGlobals.cs (a class for holding "global" variables,
built in the previous section):

a) Change
from: public string strCompanyName = "ABC, inc.";
to: private string privstrCompanyName = "ABC, inc.";

Chapter 9 - Variable Scopes Page: 578


The name is being changed so it does not conflict with the Constructor's name, which
is built next.

b) Write a new "constructor", "PubstrGlobalCompanyName". See below for placement:

public string PubstrGlobalCompanyName


{

This new construct is not a standard variable, nor is it a "method". Notice how this
declaration does not have a set of parenthesis after the name (thus, it can't be a
function or method). It does not have a semicolon, noting the braces.

This type of device is called a "constructor" and you've seen them before; recall in
Form1:

public Form1()
{
InitializeComponent();
}

...which is the constructor for Form1. This logic runs when Form1 is first built and
runs before the Form1_Load event. Essentially, when ever C# sees this type of
statement, it knows to create the object. As an aside, the logic within
InitializeComponent contains all the statements that tell the compiler how and where
to draw the buttons, window sizes and all of the other components that were
automatically built by the editing environment.

"public string PubstrGlobalCompanyName" is a constructor that builds a string


object and like all objects, it has "properties."

c) With PubstrGlobalCompanyName, expose two controls that allow changes this


object's properties by using "get/set":

Building a CompanyName constructor, in progress


public string PubstrGlobalCompanyName
{
get
{

set
{

}
}

Of interest, when you say: MessageBox.Show(CompanyName), a "get" routine runs.


Likewise, when you say CompanyName is equal to "ABC, inc.", it passes through a
"set" routine.

Chapter 9 - Variable Scopes Page: 579


The finished CLSiteGlobals class looks like the following, with a private string
declaration, followed by a public Get/Set:

Get/Set Properties for External Class Global Variables, completed

The "get" routine takes the private variable, only visible to the SiteGlobals class, and
"returns" it to the calling statement, as any other function or method would return.

Remove the entire "set" routine to prevent other code from modifying the global value;
this makes the variable "read-only" and attempts to set a value for (strCompanyName)
results is a compiler error.

Usually, in the set-routine, you would add logic that examines the current
userID for rights, or perhaps examines a different variable to see if this routine
has permission to update the global variable. A normal "set" is written like
this:

:
set
{
//Allow changes to the global variable, without auditing
privstrCompanyName = value;
}

where "value" is a required keyword and was implicitly passed into the constructor.
Notice it updates the private variable.

Chapter 9 - Variable Scopes Page: 580


If a set-routine is stubbed-in but left empty, updates to the variable are ignored. That
means no error messages or compiler warnings. Users or other developers will think this
is spooky and this is probably not what you want to do. Remove the SET if you do not
want it to update.

Using or Calling the Global Variable using a Get:

The new global variable is called (from Form1, for example) with the same syntax as any
other call. From the outside, you always reference the "public" name. For example:

MessageBox.Show (SiteGlobals.PubstrGlobalCompanyName);

As you type "SiteGlobals-dot", the editor shows a "Property" instead of a variable.

With this design, global variables are protected from inadvertent changes because there is
no "set". This is a recommended technique for handling "global" variables.

See also: Chapter 11 (Calling Multiple Forms) for similar logic.

Singletons

There is another type of Class Library called a "Singleton", which technically is better
than all of the methods described above. A Singleton class resolves a few other problems
that were not discussed here. Issues with multiple copies of your program running
simultaneously (e.g., in a Webserver) and issues with multiple-threaded applications are
addressed with this class. You can search the web for animated discussions on this type
of Class Library.

Followup Notes:

Global variables can also be loaded and stored with an external file called "app.config".
Chapter 15 (XML Config Files) discusses this possibility and those routines liberally use
get-set variables.

Chapter 9 - Variable Scopes Page: 581


Passing Variables by Ref

Disregarding all of the discussions about different classes and global variables, there is
another way to pass values for update from one method to the next, without using global
variables or get/sets. By passing the variable as a "reference," the downstream function
can make changes directly to a variable.

As a reminder, from Chapter 6, recall how a routine can call pass one or more variables
through the function's parenthesis and the downstream routine receives a copy of the
value. Changes made to the passed value are lost when control returned to the calling
module.

For example, button1_Click creates a teststring, sets the value to'John Smith's', and passes
it to the A100 routine. If A100 attempts to modify testString, it will fail with a compiler
error "The name 'testString' does not exist in the current context".

It could modify the passed variable, "strpassedString", giving it a new value, 'Mary Ann',
but upon returning to button1, testString would still equal its old value.

Sometimes Global Variables are Overkill;


Can You Use "ref" Variables?

In the example above, the program could move testString up higher in the program and
declare it as a class-level variable, but then the variable would occupy memory for the
duration of the program. It could also be promoted to a "public static" variable, making it
visible (and changeable) by any other routine, but this comes with all of the ugliness
described earlier. Sometimes a variable is just an average nickle-and-dime variable and it

Chapter 9 - Variable Scopes Page: 582


does not warrant being treated with any extraordinary care because they are not that
important to the program's overall design.

An alternative to a global variable is to use a technique called "byRef" – where the actual
variable is passed to a downstream routine and that routine can modify the actual value -
not just the copy.

Converting a variable to a Reference variable is easy: prefix both the call and the
downstream module's variable name with the keyword "ref". For example, modify the
illustrated program so it passes testString byRef:

//Pass by Ref and avoid a global variable

private void button1_Click (object sender, EventArgs e)


{
string testString = "John Smith";
A100_CallProcedure (ref testString);

MessageBox.Show("button1 now has this value: " + testString);


}

private void A100_CallProcedure (ref string strpassedString)


{
//Assign a new value to the passed string; because of the by-ref,
//the original is also changed.
strpassedString = "Mary Ann";
return;
}

Results: testString is changed to 'Mary Ann", both in A100 and in button1_Click. The
value 'sticks' and did not require either a class-level or global variable.

When passing a variable by "ref", you are passing a pointer to the original variable – this
is not a copy of the variable's value, like a standard pass would use.

Style Notes:

ref testString was passed to A100. Because this is a by-ref variable, it might make
more sense to call the variable by the same name when it arrives at A100, but it is not
necessary:

private void A100_CallProcedure (ref string testString)

With our without the passed-variable rename, the results are the same – the original value
is changed.

Passing Multiple Fields by ref:

If a module needs to modify more than one variable, pass each in the signature line as ref.
The calling module must also have a by-ref:

Chapter 9 - Variable Scopes Page: 583


A100_CallProcedure (ref strVariable1, ref strVar2, ref intVarA);

A100_CallProcedure
(ref string myString1, ref string myString2, ref int myVarA);
:

If only two of the three passed values need to be modified, only use "ref" on those
variables:

A100_CallProcedure (ref strVariable1, strVar2, ref intVarA);

There is no particular limit to the number of variables that can be passed to a downstream
routine – but after a half-dozen or so, it gets cumbersome and there is probably a better
design to be found.

Chapter 9 - Variable Scopes Page: 584


Exercises: Global Variables and Class Libraries

A. Within Form1, create these form-level variables, populated as follows. These should only
be defined one time in your program:

string companyName = "ABC, inc.";


string companyAddr = "123 N. Elm Street";
string companyCity = "Anytown";
string companyState = "ST";
string companyPhone = "555-1234";

From button1, display a nicely-assembled address as a MessageBox.


From button2, display the same address on the screen, in a textbox of some sort.

B. Create an "inline" (Program-specific) class library by typing the new class name near the
bottom of your program. In the class, populate the same variables as were used in
exercise A. Name the Class Library "ProgramGlobals". Clearly, these variables need to
be Global of some type.

Use the same button1 and button2 logic, from above, to ensure they are set properly.

C. Create a second Form in the project.


Using button3_Click, display the fully-assembled companyName and address block.
See earlier in this chapter for steps on building a multi-Form project.

D. Move the same variables to a new Program-specific Class using the "Alternate" method.
This will put the new Class Library "ProgramGlobals" in the Solution Explorer tree. Be
sure to remove the inline Library built in step C.

Use the same button1 and button2 logic, from exercise A to test the variables.

E. Modify exercise D, so Form1's Form_Load event populates the variables, changing them
from the default "ABC, inc" to "XYZ, inc". Simulate the variable loading by hard-coding
the values within that event.

Chapter 9 - Variable Scopes Page: 585


Chapter 10 - Form Controls and Events Page: 586
An Absolute Beginners Guide to C# - Volume 1
Visual Studio C# 2017
Intro -Through Forms
by Tim R. Wolf
2017.06 Version 1.04
Table of Contents

9 Chapter 1 - Introduction to the Editor 3


Your First Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Variables and Scope. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Working with Text Boxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Naming Fields
Concatenation
Default Text Values

9 Chapter 2 - Introduction to Loops 45


"while" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
loopCounter
Appending a String
Incrementing a Counter
Incrementing with "++"
Concatenating to Self with "+="
MessageBoxes
Infinite Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Breaking into the Loop
"while" Loop - Printing Numbers 1-10. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
"do" Loops.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
for-next Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Carriage-Return/LineFeed (CRLF)
Variations on "for-next" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Controlling Loops with Variable textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Interrupting Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
continue;
break Statements
"while-loops and 'continue'
Nested Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
foreach Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Loop Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110

9 Chapter 3 - Conditional Branching 117


Numeric and String Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Basic if-statement Construction
Numeric "if" Statements
Brace Style
"else"
Semicolon Rules
&& (AND) || (OR) Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
"|" and "||" (OR) Boolean
"^" (XOR – Exclusive OR) Boolean
Nested if-statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
"else if"
Math.Min - Numeric Testing for Smaller Value.. . . . . . . . . . . . . . . . . . . . . . . . . . 139
Compounding if-Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
switch Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Case "Value":
default Clause
Required breaks
Compound case Statements
Case-sensitive switches - .ToLower()
goto Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
Ternary Operator. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

9 Chapter 4 - Strings 163


Declaring Strings.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Special Character Strings (Escape Codes) \t, \r\n. . . . . . . . . . . . . . . . . . . . . . . . . . 167
Reserved Backslash
Carriage-Return-Line-Feeds CRLF
Embedded Quotes
Verbatim Text Strings - @
ASCII Codes
String Concatenation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
string.Concat ( ) and "+"
Floating Point Numbers
string.Compare( )
<string>.EndsWith. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
Detecting .XLS file Extensions
<string>.Length. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
<string>.ToUpper( ), .ToLower( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
<string>.PadLeft(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
<string>.PadLeft(int, '*');
Date Padding with Leading Zeros
<string>.Trim( );. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
<string>.TrimStart()
<string>.TrimEnd()
Trimming with other Characters
char.IsNumber ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
<string>.Replace( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Character to Numeric Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Conversions with Casting
null and Empty Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
String.IsNullOrEmpty( )
Null-Conditional Operator
Testing for "blank". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
if ((strtest + "").TrimStart().Length == 0)
Finding Strings - IndexOf. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Reading an Overload
<string>.LastIndexOf
<string>.Contains
Parsing and Substrings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
<string>.Substring. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Classic Left-strings .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Classic Right-string. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
Mid-Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
"Mid-String" for any Length Delimiter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253

9 Chapter 5 - Numbers and Dates 265


Integers Defined. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Floating Point Numbers Defined.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Casting and Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
Implicit Conversions
Explicit Conversions
try-catch Error Trapping. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Converting Strings to Numeric. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
TryParse
Rounding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Math.Round
Truncating Decimals. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
Math.Truncate
Basic Math Functions (Division). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
DivRem (Divide Remainder) / Mod. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
Math.DivRem
Mod "%"
Other Math Functions (SQRT, etc.). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
Sqrt (Square Root)
Random Numbers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
Dates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
DateTime.Now
String.Format
Date Parts
Date Format Pictures
DateTime.TryParse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
UTC (Zulu) Time
Empty Dates - Nullable Dates
DateTime.Compare. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Date.Compare: Two Files
Less Exact Date Comparisons
Rounding Dates
Optional Project: MTWRFSU.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
Optional Project: AllowByHour. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327

9 Chapter 6 - Utility Functions - Methods 333


IsBlank( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
IsFilled( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
LeftStr, RightStr and MidStr string Functions. . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
Classic LeftStr ( ) "Left-string".. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
Left-string with String Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Classic Right-String with Numeric Parameters.. . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Right-string using Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Mid-string with Numeric Parameters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Mid-string Numeric Start Position but No Length (Overload). . . . . . . . . . . . . . . . 382
Mid-string with string Delimiters and NumberOfCharacters. . . . . . . . . . . . . . . . . 383
Mid-string with Two String Delimiters.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388

9 Chapter 7 - Advanced Utility Functions 395


StripSlashes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
StripTrailingComments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
StripLastCharacter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
StripNonNumerics ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
StripNonNumerics, Preserving Decimals and Signs. . . . . . . . . . . . . . . . . . . . . . . . 424
Overloading
ParseBetweenDelimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Overloading ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
ParseKeyName: Master Function. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
IsNumeric ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463
IsNumbers ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
StripDuplicateCharacters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475
VerifyYN.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Passing Variables by Reference. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481

9 Chapter 8 - Class Libraries 493


Building an External Class Library from Existing Code. . . . . . . . . . . . . . . . . . . . 496
Linking an Existing External Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
Using the util. Class Library (CL800 Util). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
Link vs Copy
Inline "Program" Class Libraries (PayrollTools). . . . . . . . . . . . . . . . . . . . . . . . . . 513
Using CL800_Util within the new Class.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 520
Constructors
Manually Building a Class Constructor
Creating an "External" Class Library from Scratch. . . . . . . . . . . . . . . . . . . . . . . . 525
Compiling and Using DLLs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526
Using the DLL
"Modularizing" with Program-Control Functions.. . . . . . . . . . . . . . . . . . . . . . . . . 532
"void" Functions
Passing Variables to Program-Functions
Returning Values from a Program-Function
Naming Standards for Functions and Class Libraries.. . . . . . . . . . . . . . . . . . . . . . 535
Object Prefixes

9 Chapter 9 - Variable Scopes 543


Variable Scope, Demonstrated. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
Scope within Loops
Form-Level (Class) Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 551
"Global" Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554
"Quick and Dirty" Global Variables
"static" Modifier
Form1 to Open Form2
Using a Global Class to Pass Values.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564
Building an External Global Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 568
Getting and Setting Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 577
Building Get/Set Properties
Passing Variables by Ref.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 582

9 Chapter 10 - Form Controls and Events 589


Default Editor Settings - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594
Snap To Grid
Compiler Errors
Starting and Naming a New Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 598
Rename the Form
Recommended Form Properties
"Form Load" event
"this.Show"
Link the CL800_Util Library
AutoClose Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 609
textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 613
MaxLength
Default Text Value
Enabled
Password Fields
Multiple Lines
Setting Field Properties at Runtime. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 619
textBox Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 625
Enter and Leave Events
TextChanged
KeyPress Events (Intercepting Keystrokes). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 629
Allowing only Numeric Values
UpperCasing as Typed
Intercepting Shift, Alt and Ctrl Keystrokes
Alt-Key events on a button or other object
comboBox and listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 638
Static (Hard-Coded) ComboBoxes
AutoComplete / Type-Ahead
Query the Selected Value
"No Selection"
Using the SelectedIndexChanged
ComboBoxes linked to an External Data
listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 648
Blank-line Testing
listBox Multiple Selection
Processing Multiple Selected Records
checkBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 660
ThreeState
CheckChanged
CheckStateChanged
Click
Radio Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 669
Radio Button Events
Grouping
ProgressBar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 676
monthCalendar and DateTimePicker. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 681
.MaxDate
.MinDate
Form Load Event
Date-Time Math
MenuStrip. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 689
Horizontal and Vertical Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 692
ToolTips. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 694
ToolBox: "PictureBox" Icons and Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
Launching Other Applications From the Graphic
Link Labels. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Label Tricks.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 704
Transparent Labels
Using Labels in Your Program
Tab-Order. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 706

9 Chapter 11 - Calling Secondary Forms 711


Opening Secondary Forms - Simple. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 716
Issues with a Simple Form Call
Using a Global Variable to Re-Open Form1.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 724
Using a FormReference to Open Form2 - Recommended. . . . . . . . . . . . . . . . . . . 727
"FormRef" properties
Modal and Modeless Forms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 735
Transferring Data Between Forms using Global Variables. . . . . . . . . . . . . . . . . . 737
Using Quick-and-Dirty Global Variables
Using a Global Class (Global Variables)
Retrieve the Stored Value in Form2
Passing Data with a Signature and a Constructor. . . . . . . . . . . . . . . . . . . . . . . . . . 741
The Parent's Call
The Child (Form2) Constructor
Passing Values using get-set Properties - Recommended. . . . . . . . . . . . . . . . . . . . 745
Form Starting Positions - Global Class. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 753
MessageBox Overloads. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 756
Custom Dialog Boxes: NS810_Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 759
Returning Results to the Parent via Properties
Custom InputBoxes: NS815_InputBoxDialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . 777
Detecting a KeyPress ENTER Key Event.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 785
Using NS815 to Pass Arrays instead of Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . 787
Multiple Input Screens in the Same Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 789

9 Appendix A - Compiler Error Messages 3

9 Appendix B - Compile and Distribution 38


Cheap and Easy EXE Distribution:.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Virus Risks
EXE Icons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Using Visual Studio to Edit Icons
Attaching Icon Files to your Project
Creating Publishing / Distribution Packages.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Generating a Creator Tag
Introduction

Audience:

This book is volume one of a three volume set and is intended for beginning Microsoft
Visual Studio C# (C Sharp) programmers, versions 2015 through VS 2017. This is a
hands-on book, with actual programs, starting in Chapter 1 and it talks about the
practical day-to-day, nuts-and-bolts programming that real people need to know.

It assumes no prior programming experience.


Little time is spent on theory.

Each topic has step-by-step instructions with numerous code examples and over 900
cropped and annotated illustrations. It explains the "why" of a program.

Prior Experience

If you already have moderate programming experience, especially in Visual Basic, this
book will expand your skills, giving confidence in the new language.

The goal is to have as much time on the keyboard, working with common business
problems. After studying this book and working through the examples, you will be a
proficient programmer – able to write real programs that do real work. You will be able
to read files, parse data, write to database, and build data input screens.

Why this book?

This book is different than most publications. Little time is spent on theories and
technical side-trips are rare. Starting in Chapter 1, you will immediately begin working
with loops, if-statements and string-manipulation. This means some topics, such as
conversions, numeric types, and other such concepts, are glossed over until they are more
germane to the concepts being taught.

Where other publications might spend a page or two on a topic, this book dives into the
most common and most useful ways to solve a problem. For example, over 80 pages are
devoted to opening multiple forms and how to pass data between them. The parsing
chapter devotes 70 pages to this subject, covering delimiters, CSV, Tab, Excel, and other
techniques. This is not over-kill. You will find these address real-world data-processing
problems. I cover the tips and tricks you will need to know.

Chapters often show different techniques for the same problem and the benefits and
drawbacks of each are explained. If there is a chance of making a mistake in
punctuation, style, or logic, the examples show how to resolve them. Compiler errors are
scattered throughout the book and there is a comprehensive alphabetic error reference in
the appendix, showing likely causes and recommendations.

A side-effect of this first volume will be a library of utility modules that can be used in
all of your programs. These utilities can automate mundane tasks, such as parsing
delimited files, punctuating phone numbers, street-addresses, and capitalizing proper
names. These libraries will save boat-loads of time and will be literally useful in all your
programs.

You will also notice none of the examples use Console-applications (DOS-like
programs) – all are Windows forms. Besides being more visually interesting, they allow
greater feedback while developing and the programs more accurately reflect what
happens in the business world.

What this book does not cover:

I do not cover some of the more theoretical underpinnings of modern programming.


Things such as polymorphism and object-inheritance are not discussed directly, but the
techniques are used throughout the book. Advanced programmers may take exception,
but I favor a more procedural background and I always approach problems from a
business-oriented perspective. You have a job to do, files to process, data-entry-screens
to write. These are the things this book is concerned about.

These volumes do not cover web-development but the programming skills taught are
100% transferrable. SQL databases are introduced, but four lengthy chapters only
scratch the surface. However, if you fear treading in this area, these four chapters will
get you started and will show relatively advanced techniques.

Why C#?:

Microsoft has built a wonderful programming environment which verges on magical.


The language is mature and consistent and can be applied in any conceivable situation.
It is a powerful tool that can solve complex problems.

The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.

How to Use This Book:

These three volumes are teaching books and because of that, it makes a poor reference
guide. To get the most utility, start at page one and work your way through chapters.
Each chapter builds on the previous. It takes time and effort to learn programming. As
you work through the chapters you must sit in front of the compiler and write the code.

This series was divided into three volumes, partly to aid in printing, and partly to make
the chapters more accessible.

Volume 1:
1 Introduction to the Editor
2 Introduction to Loops
3 Conditional Branching
4 Strings
5 Numbers and Dates
6 Utility Functions
7 Advanced Utility Functions
8 Class Libraries
9 Variable Scope
10 Form Controls and Events
11 Calling Multiple Forms
A Compiler Error Messages
B Compile and Distribute

Volume 2:
12 ASCII Files
13 Parsing Tab and CSV Files
14 INI Files
15 XML and App.config Files
16 Windows Registry
17 Reading Excel and Access
18 External Programs (Shell)
19 Wait, Delays, Pauses
20 Printing
21 Formatting

Volume 3:
22 Arrays
23 File Manipulation
24 Console Applications
25 SQL Databases
26 SQL Record Edits
27 SQL Data Grids
28 Data Grid Cell Editing
C Installing SQL Server Express
D Routines (of Interest)

Thank you

Thank you for purchasing this book. I hope you enjoy it as much as I have had writing it.
Comments and suggestions are welcome.

traywolf at keyliner com


www.keyliner.com

This book was written with Visual Studio 2013 through 2017's Community Edition, with
sections referencing Microsoft Office and Microsoft SQL Server 2016 Express.
© 2017 by Tim R.Wolf. All rights reserved.

Original text written with WordPerfect version X7. Illustrations created with Corel's
PaintShop Pro, version X8.
The Compiler and Other Tools:

As of mid 2017, Microsoft allows you to freely download a fully-capable version of


Visual Studio. The software is free and this book was written with these versions. See
this link:

http://www.visualstudio.com/en-us/products/visual-studio-community-vs

This book is applicable to VS 2010 and newer, with an emphasis in version 2017.

For the database chapters you will need a copy of Microsoft MS-SQL database or a copy
of Microsoft's free downloadable "MS-SQL Express". Details can be found in the
Appendixes.

Everything in this book was designed to run on a stand-alone (home) workstation,


including the SQL chapters. You will not need a server, external SQL database or
Active Directory to complete the chapters. However, when useful, references to these
other resources are made.
Absolute Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 10 - Form Controls and Events
Chapter 10 - Form Controls and Events

"Forms" are the panels and screens in a Windows program's user interface; this is the part
your users see and interact with. Although you can write "Console Applications" (DOS-
like programs - see Chapter 24), a "windowed" program is almost always preferred, even
for simpler programs.

This chapter dives into a form's textBoxes, check-boxes, pull-down combo lists, buttons
and other form controls. Each type of object has properties that can be set at design or
run-time, and each has events, such as mouse-click, mouse-over and got-focus.

Topics:

• Default Editor Settings


• Steps for any new project
• Recommended (basic) Form Properties
• Adding BtnClose
• Auto-Close
• Setting Field Properties at Runtime (font.Bold, etc.)
• this.Show( ); this.Hide()

• textBox events (Enter, Leave, TextChanged)


• KeyDown events (intercepting numeric keystrokes; replacing characters)
• Using Control-C to Copy to the Clipboard
• comboBoxes, comboBoxes linked to a database (Chapter 17, 26)
• listBoxes
• checkBoxes, Radio Buttons

• Progress Bars
• monthCalendar; date processing, date arithmetic
• Hiding controls on the Form
• menuStrips and Menu Bars
• ToolTips
• Horizontal lines, label tricks; transparency
• Tab-order

For Mutex (duplicate Form loading), see Chapter 18 "Shells".


For delayed starts and splash screens, see Chapter 19.
For disabling a Form's Close-X, see Chapter 19.

Chapter 10 - Form Controls and Events Page: 589


Form Setup Summary:

Initial Form Build and Properties, summary

• Build the initial project/Solution, saving into a dedicated directory on the C: drive.

• Rename the internal name from form1 to "FrmProcess.cs", Frm100Process.cs; any


name but Form1.
• Change the Form.Text property (Title bar) to a user-friendly name

• Control Box = True


• Form Border Style = Fixed Single
• Minimize Box = True
• Maximize Box = False
• Window State = Normal

Form Load Event, recommendation


this.Show();

BtnClose Event
this.Close();
Application.Exit();

Optional: CL800 Utility Class Setup

1. In Solution Explorer,
Link class library CL800_Util (See Chapter 8 for details)

2. Add this using Statement:


using NS800_Util;

3. In the Form-level variable section:


CL800_Util util;

4. In the Constructor, below "InitializeComponent":


util = new CL800_Util( );

Other Summaries:

Chapter 10 - Form Controls and Events Page: 590


textBox Settings
textBox1.Enabled = false;
textBox1.Visible = false;

textBox1.BorderStyle = Borderstyle.FixedSingle;

textBox1.Text = strValue1 + strValue2; //Concatenate


textBox1.Text = textBox1.Text.ToUpper();
textBox1.Text = textBox1.Text.ToLower();

Font Settings
Changing a Font Color: two methods displayed:

Using a text color (Recommended):


textBox1.ForeColor = Color.Red;

Using a Red-Green-Blue (RGB) Numeric value:


textBox1.ForeColor = Color.FromArgb (255,0,0); //Red

textBox1.Font = new Font(textBox1.Font, FontStyle.Bold);


textBox1.Font = new Font
(textBox1.Font, FontStyle.Bold | FontStyle.Underline);

Changing Fonts:
textBox1.Font = new Font("Ariel", FontStyle.Regular);

comboBox, summary

Recommended Settings:
Sorted = True
MaxDropDownItems = n
AutoCompleteSource = ListItems
AutoCompleteMode = SuggestAppend

Retrieve and Modify Values:


comboBox1.SelectedIndex //Returns numeric value or -1
comboBox1.SelectedItem.ToString() //Returns text

comboBox1.Items.Add("Tokyo");
comboBox1.Items.Insert(0, "Boise"); //Inserts at position zero
comboBox1.Items.Remove("Tokyo");
comboBox1.Items.RemoveAt(0);
comboBox1.Items.Remove(comboBox1.SelectedItem);

comboBox1.Items.Clear();
comboBox1.SelectedIndex = 3; //highlight third item

comboBox1_SelectedIndexChanged //Recommended Event

Chapter 10 - Form Controls and Events Page: 591


checkBox, summary

Setting:
cbxCheckBox1.CheckState = CheckState.Checked;

Testing - Simple CheckBox:


if (cbxCheckBox1.Checked == true)

Testing - ThreeState:
if (cbxCheckBox1.CheckState == CheckState.Indeterminate)
//no selection logic
else
if (cbxCheckBox1.CheckState = CheckState.Checked)
//checked logic
else
//unchecked logic

Recommended Settings:
ThreeState = false;

Recommended Events:
See text

Chapter 10 - Form Controls and Events Page: 592


Radio Buttons, summary

Setting:
rbxRadioButton1.Checked = true;

Testing:
Each button must be tested individually:
if (rbxRadioButton1.Checked == true)

Events:
Use separate events for each buttons. Events do not fire on default values!
rbxRadioButton1_CheckChanged
rbxRadioButton1_Clicked

Chapter 10 - Form Controls and Events Page: 593


Default Editor Settings - Recommended

As a one-time setup, consider making C:\Data\Source (or other directory) the default for
all projects and solutions.

Begin by using Windows Explorer to manually create the data folders: C:\Data and
C:\Data\Source. Notice these are separate from your Windows Profile MyDocuments
folder. If you have followed previous examples, this is near the
C:\Data\Source\CommonVS folder. Avoid embedded spaces in the path names.

Then, from Visual Studio's opening screen, select the top menu Tools, Options. A tree-
diagram appears on the left. Locate and make changes to the following items (illustrated
below). Close the Options window to save the changes.

• In "Projects and Solutions"

Visual Studio Project Location: C:\Data\Source


User Project Templates Location: C:\Data\Source\VSProjTemplate
User Item Template Location: C:\Data\Source\VSItemTemplate

• Uncheck [ ] Warn user when the project location is not trusted (for storing code on a
file server).

• Tunnel to Environment, Documents


Uncheck "Detect when file is changed outside the environment."

Chapter 10 - Form Controls and Events Page: 594


• In "Environment, Keyboard",
set: Keyboard Mapping: Visual C# 2005

Snap To Grid:

Objects on the form move in quantum jumps, aligning to an invisible form grid. For more
granular control, and to help move objects more precisely, make this change in the editor's
options:

• Select Tools, Options, "Windows Form Designer", General.


(If using Visual Studio Express, check the "Show all options" checkbox.)

• Change "Snap to Grid" to False.

Some versions of Visual Studio (especially 2010) do not seem to


pay attention to the snap-to-grid setting. You can move objects
with the keyboard by highlighting, then pressing arrow-keys to
move the control one pixel at a time.

Compiler Errors:

If you made a typographical error, such as spelling ".show()" with a lower-case "s", you
will see a message similar to this: "There were build errors. Would you like to continue
and run the last successful build?" – Always click No. You do not want to see the last
run – you want to see your current bugs.

Chapter 10 - Form Controls and Events Page: 595


To fix this message permanently, make the following one-time change:

a. (Click "No" to dismiss the dialog)


b. Select top-menu "Tools, Options"
c. Scroll to Projects and Solutions; open "Build and Run"
d. Change "On run, when build or deployment errors" to "Do not launch"

Compiler Errors when Deleting old Example Code:

As you practice the examples in this chapter, changing objects from one type to the next,
the editor can get confused and may generate background code errors. For example, when
changing from checkboxes to radio buttons, the editor may display this unexpected
message: "ExampleProcess.frmProcess' does not contain a definition for
'frmProcess_X'".

Chapter 10 - Form Controls and Events Page: 596


Double-click the compiler-error (text) to jump the editor to the form's auto-generated
code; this will be a section of code you did not write. Carefully, but without fear, delete
the highlighted line and re-test the program.

Chapter 10 - Form Controls and Events Page: 597


Starting and Naming a New Application

By default, Visual Studio created generically-named projects, classes, and forms.


Previous examples in this book made no effort to name things differently than their
default Windows Application1 or Form1. In real life, greater care is needed.

Steps for a new Project:

1. Close any previously opened Visual Studio projects (File, Close Solution); no need to
save.

2. From the Visual Studio Start Page, "More project templates"

• On the tree-side, illustrated below, select Visual C#; select "Windows Classic
Desktop", then "Windows Form App (.NET Framework)"

• In the Name field, type: "ExampleProgram" (no quotes; this will become both the
project name and the subdirectory name)

• Browse or type location: "C:\Data\Source" (this is the parent directory)

Chapter 10 - Form Controls and Events Page: 598


I do not recommend using Visual Studio's default location C:\Documents and Settings
(or Windows 8 C:\Documents), especially if you are on a corporate network using
roaming profiles. The default folder has risks should your profile be deleted and re-
constructed. By storing development code on a server (or C:\Data), you are
insulating your code from profile changes.

• A directory called "ExampleProgram" will be created beneath C:\data\Source. Do not


click "Create Directory for Solution". If you do, you will get a "subdirectory within a
subdirectory." I have no idea why Visual Studio works in this fashion.

• Click Ok to create the project

Results: C:\Data\Source\ExampleProgram is built and a new blank Form1 is constructed.

3. Confirm the "Solution Explorer" and "Properties" panes are open on the right-side of the
screen (they may be tabbed). If not, follow these steps.

• From the top menu: View, Solution Explorer


• Top menu: View, Properties Window (not View, Property Pages)

• Note "Form1.cs [Design] is active and "Form1" is displayed. If not, double-click the
"form1.cs" detail-line found in Solution Explorer's tree. Form1 has "selection
handles" – indicating this is the active object. Once the form is selected, the
Properties window automatically switches to show the active object.

4. On the bottom panes, close the Output Tab, if displayed. The Error List tab is vastly more
interesting while coding:

Chapter 10 - Form Controls and Events Page: 599


Always Rename the Form:

When starting a new project, rename the form before working on the code.
There are three places to rename:

The internal ".cs" name (the filename)


The (Name) Property, illustrated below
The Text property(Title bar)

5a. In Solution Explorer, locate Form1.cs (as an aside, this is the physical file's name, as
stored on the disk).

'Other-mouse-click' the name, choose Rename.


Rename to a name, such as "FrmProcess.cs" (or FrmA100Main.cs, etc.)
The ".cs" extension is required and must be typed. The form name must begin with a
capital letter (new requirement to VS2017).

When prompted: "You are renaming a file. Would you also like to perform a rename in
this project and all reverences...", click Yes.

5b. On the design pane, click Form1 to activate (select). In the editor's lower right, Properties
pane, click the Properties Toolbar button (Properties button, illustrated below)

Chapter 10 - Form Controls and Events Page: 600


Near the top of the list, confirm the (Name) is now "FrmProcess" (version 2015 and older,
you had to rename this manually)on. This is the name, as it will be known in your
program code. By this stage, you have renamed both the physical file and the form's
internal name.

If this were a Payroll application you might use a name like "FrmPayrollMain" and if this
were part of a large, complicated application, consider naming the form with a numeric
prefix, as in "A100_FrmPayrollMain", "A220_FrmW2", etc. Large projects could have a
B and C-series of forms. Do not use spaces in the name.

5c. In addition to the Form's underlying name, now FrmProcess, also change the form's
"Text" property – which is the form's Title-bar text.

With Form1 still active, scroll near the bottom of the Properties list.
Change the "Text" property from "Form1" to "FrmProcess" (or "Main Process"). This is
a cosmetic name and can include spaces and this is the name your users will know the
screen by.

The "Text" property is the title-bar text (visible to the end-user)


while the "Name" property is the behind-the-scenes-name. In this
example, the form might be called a friendly name "Process
Main" but the underlying code refers to the form as "FrmProcess"

Chapter 10 - Form Controls and Events Page: 601


You may find it surprising, but a more friendly title-bar name, especially in a large
program, might be "A100Main Payroll" or "PAY100" You will find the users will call
the form "A100" when they call the Helpdesk.

Recommended Form Properties

Once the form is named, I almost always do these same steps:

1. In the same properties list, set these values:

ControlBox = true
Form Border Style = Fixed Single
MaximizeBox = False
MinimizeBox = True
Window State = Normal

The net effect makes the program-window un-sizable, but it can be still minimized to
the task bar. This will probably be a typical window setup for most programs.

2. Place a 'Close' button on the form by dragging a Button object onto the form (see Toolbox
flyout, (CommonControls), on the left-side). In the button's properties, rename (Name)
the button from "button1" to "BtnClose"

Using a button prefix, 'btn' is recommended because you can


glance at the code and understand what the object is. Some think
this is a waste of time because you can hover the mouse over the

Chapter 10 - Form Controls and Events Page: 602


code and tell what the item is. Although true, I prefer a more
hands-off approach.

In the button's TEXT field, change the button's cosmetic text from "button1" to
"&Close" (note the ampersand). Notice the button's on-screen caption now reads
"Close", with an underline, indicating the user can press "alt-C" to close. Data-entry
clerks like this.

3. Once the button is in place, and named, double-click and type these two statements in the
BtnClose_Click event:

Standard BtnClose, completed


private void BtnClose_Click (object sender, EventArgs e)
{
//Simple Close event; see the next section for
//details on an auto-close and prompted close events

this.Close(); //more on "this" later


Application.Exit();
}

where:

• The empty parenthesis this.Close() and Application.Exit() are required by both


commands, as well as a semi-colon. The statements are case-sensitive.

• this.Close() and Application.Exit() can be thought of as a suggestion for the


operating system to close the program. If the program has other threads or timer
loops, or other background tasks are running, the program may delay being closed.

A harsher and not recommended way to exit is to use: Environment.Exit(99);. This


returns an error code "99" to the operating system and brutally ends the program.

See below for a prompted Form-Closing routine.

Chapter 10 - Form Controls and Events Page: 603


4. Add a "Form Load" event.

In the form's design view, click the form's title bar to select.
In the "Properties" window (lower left of editor), click the lightning bolt-icon, taking you
to the "Events" window. (Events are things that happen to objects. Buttons have Click
events, forms have Open and Close events, etc.)

• In the list, locate the "Load" event.

Chapter 10 - Form Controls and Events Page: 604


• Double-click the blank field next to the event's name, taking you to code-view. A
default event is automatically stubbed-in. Add this diagnostic code, where the first
line is commented with "//", for later testing:

private void FrmProcess_Load(object sender, EventArgs e)


{
//this.Show(); //Disable this statement with a comment
MessageBox.Show("Form is loaded");
}

FrmProcess_Load is an "event" that runs when the form is "loaded" into memory and the
event fires before the form appears on the display. Almost all forms have tasks that need
to run as the form is opened, making this a common event.

Testing the Load Event with and without "this.Show":

A. Press F5 to run the program.

Results: The MessageBox with the text "Form is loaded" displays before the main form
appears. Once OK is clicked, the main form appears.

B. Close the running program and un-comment the Load-event's "this.Show();" line by
removing the double-slash comment characters. Press F5 to run the program again.

Standard Form_Load Event, completed


private void FrmProcess_Load(object sender, EventArgs e)
{
this.Show(); //Uncomment this line
MessageBox.Show("Form is loaded");
}

Results: This time the MessageBox displays over the top (or at least near) the main form
and both are displayed at the same time. The "Show" command forces the panel to
display before it would normally and is especially useful when displaying startup error
messages. This is recommended.

"this." prefix:

Commands, such as " .Trim()", ".Substring" and others, are "methods" that run against
a variable or object. Objects defined in a form, the buttons, textboxes and the like, all
belong to the form's "class" and each has an optional prefix, "this.", which literally means
"this object", "this form". This is analogous to the "util.IsBlank( )" prefix from
previous chapters. VBA programmers may have seen a similar prefix, "me!". When
dealing with objects on a form, the "this." prefix is optional, but some programmers
recommend using it, making the location of the object abundantly clear. If you have the
chance to wander around the various class libraries Visual Studio automatically created
in the background, you will find they are prefixed with "this.". In your own code, I
believe this is redundant.

Chapter 10 - Form Controls and Events Page: 605


5. Link the CL800_Util Library:

The utility library, written in Chapter 6 and 8 are handy in almost every program you will
write.22 For most of the example programs in this book, particularly in later chapters,
continue with these steps from Chapter 8: "Add the (CL800_Util) class library to an
existing program." Since these methods are used with every form for the remainder of
this book, link them now:

As a reminder, follow these steps (from Chapter 8)

Detailed steps:

1. In Solution Explorer, highlight "ExampleProgram" (the second name in the Solution


Explorer tree).

Other-mouse-click, Add, Existing Item

Tunnel to "C:\Data\Source\CommonVS\NS800_Util" (or the location you saved the


utility libraries)
Highlight Cl800_Util.cs"

Click the pull-down menu on the "Add" button and select "Add as Link".
Note the new CL800 shortcut item in Solution Explorer.

Return to FrmProcess.cs (code view) by double-clicking anywhere in the form or by


"other-mouse-clicking" anywhere in the form and select "View Code" (shortcut key:
F7).

22
If you did not create the CL800_Util library (Chapters 6 and 8), ignore this step. But be aware,
later chapters expect these routines to be in place.

Chapter 10 - Form Controls and Events Page: 606


2. Near the top of the code, add:

using NS800_Util;

3. At "public partial class FrmProcess : Form", declare the new utility class with

CL800_Util util;

4. In the constructor, instantiate with:

util = new CL800_Util();

Standard CL800_Utility Class, from Chapter 8, recommended


using NS800_Util;

public partial class FrmProcess : Form


{
* CL800_Util util; //declare new util class

public FrmProcess()
{
InitializeComponent ();
* util = new CL800_Util(); //instantiate the class

Initial Form Test:

Press F5 to run and test the form. Note the Minimize and Maximize buttons and sizing.
Click Close to return to the editor.

Chapter 10 - Form Controls and Events Page: 607


Next Steps:

This completes the basic form-setup I use with most programs. The remainder of this
chapter deals with form controls, objects and events – the basic building blocks of a
Windows program.

The controls described in this chapter use their default names (button1, textBox1, etc) but
in your programming I recommend changing the object's name to something more useful.
Give the control a real name because it is a sin to leave a control named "textBox1"; The
six-seconds it takes to do this right will save grief when debugging.

Chapter 10 - Form Controls and Events Page: 608


AutoClose Events

Most programs are closed (ended) when the user clicks BtnClose or selects another
control, such as File-Exit. The logic written in the previous section, and repeated here, is
adequate for most programs. However, there are times when a program may need to auto-
close itself and this entails a bit more logic and is documented here for reference.

Skip this section if you do not have a need for this logic. See the
previous section for prompted close events.

Standard btnClose:

Here is close-logic that works with almost all programs – provided the end-user is the one
initiating the program's close event:

Standard btnClose Event, repeated


private void BtnClose_Click (object sender, EventArgs e)
{
//Simple Close event; see the next section for
//details on an auto-close event

this.Close();
Application.Exit():
}

AutoClose: A Failed Example:

Some programs may need to run unattended and to close automatically. If you were
writing a "Console Application" (a DOS-like C# program, described in Chapter 24), this
is not an issue because when the program ends, it ends. However, if the program has a
Windows graphical front-end, it remains operational and waits for user action to close.
To close automatically, additional code is required.

Consider this example which launches, initializes a form, calls a Form_Load event, and
then calls the BtnClose event, ending the program. You will find the program does not
close. For example, Form1_Load:

:
private void Form1_Load(object sender, EventArgs e)
{
this.Show();
//Do other stuff here

//Auto-Close:
MessageBox.Show("Diagnostics: Program will auto-close next...");
* BtnClose_Click(null, null); //does not work
}

Chapter 10 - Form Controls and Events Page: 609


Standard AutoClose Solution:

To make this work, add a "this.Dispose();" method immediately after the call to btnClose.
This will become the last executable statement in the main (Form Load) event and this
idea only works when there is a Form_Load event.

Successful AutoClose called from a Form_Load event


:
public Form1()
{
//The Form's 'Constructor'
InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e)


{
//Do stuff here
MessageBox.Show("Diagnostics: Program will auto-close next");

* BtnClose_Click(null, null)
* this.Dispose():
}

where:

• "this.Dispose();" tells the compiler to free up memory and release all resources.
This is more dramatic than a Close(); and it must be the last executable statement
in the Form1_Load event.

• this.Dispose() only works (at least for an auto-close) at the end of a Form_Load
event. But this will not work if you have a simpler program that does all of its work
in the form's Constructor (see example, below). Console apps use a different
technique, see Chapter 24.

• The author recommends using a (Form1)_Load event in all Windows programs.

Optional Prompted Form_Closing:

You may have the need interrupt the form-close event, perhaps because a file was not
saved or other reasons or you may simply want to prompt the user "are you sure?".

Users can close the form using the BtnClose, as described above, or they can also close
the form with the title-bar's "X", the title-bar's left-icon (control menu), or with an Alt-F4
– all are valid ways to close a form. All four actions call an event,"Form_Closing". This
event is called just before the form closes and it can be interrupted or cancelled.

A. In the form's design view, highlight the form's background or title-bar, selecting the entire
form. In the Form's Event button, scroll down, locating the "FormClosing" event (notice
this is not the "FormClosed" event).

Chapter 10 - Form Controls and Events Page: 610


B. Double-click the event's blank field to stub-in a new event.

C. Use this code in the FormClosing event:

Confirm FormClosing with End-User, completed


private void frmProcess_FormClosing
(object sender, FormClosingEventArgs e)
{
//This event runs just before the form closes
//Form can close with "X", ALT-F4, or be called by btnClose...
//Prompt the user for confirmation:

DialogResult myAnswer = MessageBox.Show


("Close the program?", "Closing Title",
MessageBoxButtons.YesNo)

if (myAnswer == DialogResult.No)
{
//Cancel the close event
e.Cancel = true;
return;
}
else
{
//Application.Exit(); //Can't use this or it will call this
//routine a second time

Environment.Exit(0);
}

//btnClose (if present) also needs a change...


}

Chapter 10 - Form Controls and Events Page: 611


Modified BtnClose_Click event, completed
private void BtnClose_Click (object sender, EventArgs e)
{
//Use this design in conjunction with the prompted FormClosing
//event from above...

this.Close();
//Application.Exit(); //See the FormClosing event
}

where:

• BtnClose calls the form's Close event. That event is intercepted by a FormClosing
event, where the user is prompted for permission.

• If permission is denied, an 'e.Cancel' (notice the signature) is passed back to the


operating system.

Testing:

• Run the program and close with "X", BtnCLose, or Alt-F4.

Related Topic: Auto-Minimize:

You may have need for a form to auto-minimize (rather than closing).

Use this code:


this.WindowState = FormWindowState.Minimized;

Also, as demonstrated in earlier routines,


this.Show(); forces the form to display
this.Hide(); hides the form

Chapter 10 - Form Controls and Events Page: 612


textBoxes

textBoxes are the data-entry fields that users will type text and numbers into a program.
Although they were used in prior chapters, their capabilities were glossed-over until now.
This section shows the most commonly needed properties and events.

textBoxes, like most other controls, have properties (max-length, font size, color, etc.)
that can be set and they can also trigger events, such as "on cursor-exit", "text changed."

Some Properties, such as "PasswordChar" or "MaxLength" are usually set at design time
while others are often set as the program runs.

Chapter 10 - Form Controls and Events Page: 613


textBox Summary
Prefix: pnl, txt

Use if (textBox1.Text.ToLower() == "smith")

(Property) textBox1.BorderStyle = BorderStyle.FixedSingle;


textBox1.Font = new Font(textBox1.Font, FontStyle.Bold);
textBox1.Enabled = false;
textBox1.Visible = false;

(Property) MaxLength = n
PasswordChar = *
ReadOnly = false
Multiline = true | false

Event(s) Click
Enter
Leave
TextChanged

I am constantly surprised at how easy it is to set some textBox


attributes programmatically with C# and other times at how
complex it is, with some of the commands almost defying
description. Often, there are multiple ways to make the settings.
Where possible, I use the simpler techniques.

Setup:

The example program described at the front of this chapter will be used throughout this
chapter. The following textBox properties are probably the most useful.

• BorderStyle = FixedSingle (Flat)


• MaxLength = n
• Text Property
• ReadOnly
• Enabled
• PasswordChar
• Invisible
• MultiLine

1. Using the Toolbox, click-and-drag a textBox to the panel. Drop two textBox fields.
(Alternately, double-click the tool; the boxes will stack on top of each other; then click
and drag to appropriate locations on the screen.)

Confirm Object Names and Other Properties:

On the form's design grid, "other mouse-click" the first textBox and choose "Properties
from the context menu – this is an alternate way to open the Properties pane on the right-
side of the editor. In the Properties window is a row of buttons, two of which are of
particular interest: The Properties and the Events (lightning bolt) icons. Select the
Properties button.

Chapter 10 - Form Controls and Events Page: 614


Confirm the object you highlighted is named "textBox1"; see "(Name)" near the top of the
scrollable list.

2. In the Properties list, change "BorderStyle" from "Fixed3D" to "FixedSingle".

This gives the textBox a simple boxed-line marking the field. In


Visual Studio 2010 and older, the 3D box contained architectural
details that cluttered the screen. It is probably wise to chose
FixedSingle incase Microsoft changes their mind again.

If you need to move two textBoxes directly next to each other, be sure to uncheck the
editor's default "Snap-to-Grid" option (see the front of the chapter) or move the objects
one pixel at a time by highlighting the box and pressing the keyboard's arrow-keys.

Experimenting with textBox Fields at Design Time:

The next sections explore a different textBox property. After making each Property
change, press F5 to run and test the program. When you are done, close the running
program before going to the next example.

MaxLength:

Returning to TextBox1's field Property, change "MaxLength" to 10, which limits typed
data to 10 characters.

Run the program and type a lengthy phrase in the field. As a side note, this field may
ultimately end up in a database, which also may have length restrictions; the two should
be matched. Zero indicates an unlimited length.

Having troubles finding the MaxLength Property?


Click the Alphabetical Property button:

Chapter 10 - Form Controls and Events Page: 615


Default Text Value:

The "Text" property pre-populates the field with a default value when the form first loads.
Near the bottom of the properties list, change the Text property to "Smith" (no quotes)
and this acts as a design-time default value for the field. The field can also be set
programmatically; see later sections in this chapter.

Read Only:

For the experience, set textBox1's "ReadOnly" property to "true" and leave the default
"Smith" text value. With this, the user can select, highlight and copy from the field but
cannot change the data. This is marked by a gray background and black text.

Enabled:

"Enabled = false" is similar to ReadOnly, with one important difference: if "Enabled" is


false, the field is visible but the user cannot select or copy data (contrast this behavior

Chapter 10 - Form Controls and Events Page: 616


with ReadOnly). In this mode, both the text and the background are shades of gray,
making the distinction between Enabled and ReadOnly subtle. The default gray
background can be changed with the "BackColor" property.

Password Fields:

The "PasswordChar" property converts the text field into a password field. Text typed in
the field is displayed as asterisks (or any other character). Be aware the password field is
not encrypted.

In the properties screen on textBox1, set the PasswordChar property to '*' (asterisk).
Before trying this property, be sure to undo the previous settings – set ReadOnly = false
and Enabled = true.

Hiding Fields (Visible):

Hide the field by setting the "Visible" property to false.

The field is visible at Design time but hides at runtime. Typically a developer hides a field
when the form loads but later exposes it as events warrant. For example, you might hide
an email-address field until the user selects a checkbox, "yes, I have an email address" or
you might not show the password field until they have typed a valid application login ID.
Certain fields might also be hidden from standard users but are visible to power users.

Multiple Lines:

Text fields can be set to handle multiple lines (with carriage returns and word-wrapping).
With textBox2, do the following:

Chapter 10 - Form Controls and Events Page: 617


• Click the black arrow on the upper-right of the textBox
• Choose Multiline
• Stretch the textBox handles down to make the box taller, as illustrated.
• In Properties, set "ScrollBars" to Vertical

Press F5 to run the program. Type a few lines of text in the second textBox.

Multi-lined textboxes can handle 64K of data, far more than anyone would be willing to
type. Be aware there is another type of textBox, "RichTextBox", which has additional
features, not discussed here.

Chapter 10 - Form Controls and Events Page: 618


Setting Field Properties at Runtime

The properties above were set in the Form's design and were active as soon as the
program (or Form) loads. Sometimes a program needs to make these changes at runtime,
in response to an event or a data-entry field.

A. Before trying the following examples, undo the previous changes for textBox1 by making
these changes to textBox1:

Set ReadOnly = false


Enabled = true
PasswordChar = (nothing)
Visible = true
textBox1's ".Text" property = "Smith-Jones" (no quotes).

The textBox examples below require some thought. Oddly,


although textBoxes are one of the simplest controls, they have the
most complicated parameters and are often frustrating to
program correctly.

B. With the ToolBox flyout, add a new button to the form, accepting the default name,
"button1".

Each of the following examples will be triggered using the button1 click event. As you
work through the examples, double-clicking button1 (while in design view) will place the
editor into Code View, exposing the button1_Click event. With each of these examples,
remove the previously-typed commands. As usual, to run, press F5. Then click button1 to
test the new code.

Disable / Enable a textBox:

Use button1 to Disable (Enable) a field.


To the end-user, disabled fields are grey; the user can see the text but they can't change or
copy the data. Add this logic to button1's Click event by double-clicking button1 in
design view.

Example: Disable / Enable textBox Fields


Private void button1_Click (object sender, EventArgs e)
{
textbox1.Enabled = false;
}

Run the program (F5). Notice you can see and change textBox1 ("Smith"). Click button1
and attempt to change the same text again.

Chapter 10 - Form Controls and Events Page: 619


Results: Once disabled, you cannot select or edit in textBox1's data-entry field; the field
is greyed-out.

Close the program and return to the editor.

Hide Fields (Visible):

Make the field invisible when button1 is clicked. Modify button1's logic with this new
statement. Comment or remove the previous example's Enabled statement:

Example: textBox Invisible / Visible Fields


private void button1_Click (object sender, EventArgs e)
{
// textBox1.Enabled = false;
textBox1.Visible = false;
}

Invisible fields are oddly not visible to the end-user at run time nor can they be clicked on,
selected or tabbed into. However, the program can still query and change the value within
the textBox. Sometimes programmers use these types of fields to hide information on the
screen for later processing. Setting both Visible=False and Enabled=False is superfluous.

Border Style Changes:

Programmatically change the border style from "Fixed3D" to "FixedSingle". Admittedly,


this type of change is almost always done at design time, and with Visual Studio 2015 and
newer, this is less of an issue because both Fixed3D and FixedSingle look the same – but
the technique requires a slightly more complicated syntax. Add this code to button1's
Click event:

Chapter 10 - Form Controls and Events Page: 620


Example: textBox BorderStyle
private void button1_Click (object sender, EventArgs e)
{
textBox1.BorderStyle = BorderStyle.FixedSingle;
}

Technically, the line should read:


textBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;

but since the top of the program has a "using System.Windows.Forms" statement, a
shorter, non-prefixed name can be used. The syntax is counter-intuitive when compared
to previous commands. For example:

//This statement fails with invalid syntax


textBox1.BorderStyle = FixedSingle;

fails with an error: "The name 'FixedSingle' does not exist in the current context." As you
type the incorrect command in the editor, pop-up help will not offer a "fixedSingle"
choice. This is an indication the syntax is wrong. See the next section for more details.

Font Color Changes:

Programmatically change the font color with either of these two statements. The first uses
a system-wide color variable and with the second you can specify precise Red-Green-Blue
color numbers. Use one or the other:

Example: textBox Foreground Color (FontColor)


private void button1_Click (object sender, EventArgs e)
{
//Changing a Font Color: two methods displayed:

//Using a text color (Recommended):


textBox1.ForeColor = Color.Red;

//Using a Red-Green-Blue (RGB) Numeric value:


textBox1.ForeColor = Color.FromArgb (255,0,0); //Red
}

The system-defined color scheme is expansive:

Chapter 10 - Form Controls and Events Page: 621


Font Bold and Font Changes:

Programmatically setting a font (Bold) is messier. This syntax fails:

//This fails...
textBox1.Font = Font.Bold;

As you typed "Font dot Bold", the popup help (as well as the ensuing error message)
implied this is a boolean (true/false) setting. The Properties design view screen offers a
bit of help in this area. Notice how "Font" has a down-triangle (a "plus" sign in older
versions). Look at what is hidden within the Font tree – most of the formatting
parameters you might care about are true|false:

This leads one to believe a double-dotted syntax would work:

textBox1.Font.Bold = true; //but this also fails

Sadly, this error message appears: "Property or Indexer 'System.Drawing.Font.Bold'


cannot be assigned to – it is read only."

Chapter 10 - Form Controls and Events Page: 622


The correct syntax for this command requires an instantiated new font before it is usable:

Example: textBox FontBold


private void button1_Click (object sender, EventArgs e)
{
textBox1.Font = new Font(textBox1.Font, FontStyle.Bold);
}

Note the details of the command: it sets the New font to the existing 'textBox1.Font', and
then changes the FontStyle to Bold. As an aside, here is the command to change the font
to Arial, regular:

Example: textBox FontStyle


private void button1_Click (object sender, EventArgs e)
{
textBox1.Font = new Font("Ariel", FontStyle.Regular);
}

Why the Complexity? You started with a simple "Enable=true" property and then moved
to progressively more complicated statements, with the Font statement being the most
complicated. How can you tell which syntax to use? As you type the commands, the pop-
up help immediately shows if there is a problem. Basically, if it is not in the list as you
are typing, you have the wrong syntax.

Chapter 10 - Form Controls and Events Page: 623


See also the Formatting Chapter.

Chapter 10 - Form Controls and Events Page: 624


textBox Events

You have seen how buttons have events, such as button1_Click. textBoxes also have
events – over 50 – but only three or four are of general interest and they are
straightforward when compared with their Properties. This section looks at the events
which can be tied to a textBox.

Enter and Leave Events:

An "Enter" event detects when the object gets 'focus." If the user tabs into the field,
clicks with the mouse, or another command sends control to this object, then the control's
Enter event triggers. Within the event any logic can be written. For example, a context-
sensitive help could open or a text-label could bold, showing this is the active field. A
"Leave" event detects the opposite: when the focus leaves the object (before another
object gets focus). To build and test the event, do these steps:

1. From the Toolbox, place a label (label1) on the form.


The label is cosmetic and will be used in lieu of a MessageBox.

2. Add a second textBox below the first (or change the previous example's textBox2 to a
non-multi-lined textBox); see illustration.

Chapter 10 - Form Controls and Events Page: 625


3. Single-click (highlight) textBox1.
In the Properties window, along the small tool-bar, click the Events button, which looks
like a lightning bolt.

• Scroll down the Events list, locating the "Enter" event.

• Double-Click in the blank field next to the "Enter" label; this takes you to the editor
where the initial routine is automatically stubbed into place.

4. Modify the Enter event's code so it changes the label's text. Note how it uses
"label1.Text" – not just "label1":

Example: textBox Enter Event


private void textBox1_Enter (object sender, EventArgs e)
{
//Enter events trigger when the field gets "focus"
label1.Text = "You are in textBox1";
}

5. Return to the panel's design view, and still on the highlighted textBox1, scroll down the
Events list, looking for a "Leave" event. Double-click the blank field next to the "Leave"
event property. Code these statements, which will take what was typed in the field and
shift it to upper-case.

Chapter 10 - Form Controls and Events Page: 626


Example: textbox Leave Events
private void textBox1_Leave (object sender, EventArgs e)
{
//Leave events trigger when focus leaves this field (before
//another fields gets focus)

textBox1.Text = textBox1.Text.ToUpper();
label1.Text = ""; //You are no longer in textbox1...
}

Testing the Enter and Leave Events:

A. Press F5 to run the program. Note, textBox1 is not the active control; the form has
"focus."

B. Click textBox1. Type some text.


Results: label1 changes to "You are in textBox1".

C. Tab or click into textBox2. The contents of textBox1 shift to upper-case and label1 is
cleared. Shift-Tab (back-tab) or click to return to textBox1.

Leave events are useful for auditing data-entry fields. On the Leave event, the value
"textBox1.Text" can be compared against a database or other auditing rules and if there is
a problem, the user can be notified with a MessageBox or a label-change. See also the
"TextChanged" and KeyPress events.

TextChanged Event:

The "TextChanged" event does what you would expect, with a twist. The event fires any
time the text is changed – firing on individual keystrokes. If "Smith" is highlighted in
textBox1 and the user presses backspace, the event triggers. Then, if the user types
J-o-n-e-s the event fires five more times, once for each character.

The event is commonly used to set a flag, such as "boolLastNameModified = true",


marking when a field was changed, and this can help control how a record is saved.
Although it seems redundant to set the flag (5 times, J-o-n-e-s), it takes less processing
power than doing a full-field compare against the original field at the "Leave" event. For
example:

Example: Tagging a field as Modified


private void textBox1_TextChanged (object sender, EventArgs e)
{
//Mark this field as changed by the end-user
// (where boolLastNameModified is a variable that needs
// to be declared at the top of the form)

boolLastNameModified = true
}

Chapter 10 - Form Controls and Events Page: 627


The TextChanged event can also aid in data-entry with side-lookups: Imagine your
program accepts a last-name in the field. As a new name is typed (Jones), a side-panel
could show all of the available "J*" names. As "Jo" is typed, the list could change to all
the "Jo*" names, etc. To give a flavor on how this event operates, simulate a lookup by
adding , add this logic to textBox1's TextChanged event.

private void textBox1_TextChanged (object sender, EventArgs e)


{
label1.Text = "looking up " + textBox1.Text;
}

Press F5 to run. Type new text in textBox1. Note label1, which simulates a lookup
routine.

Chapter 10 - Form Controls and Events Page: 628


KeyPress Events (Intercepting Keystrokes)

KeyPress, much like TextChanged, examines each keystroke but it does it at the keyboard
level, giving control over the keystroke before the rest of the program processes it. You
have probably never thought about this, but when the letter "J" is typed, there must be
some processing that goes on before the letter "J" is placed on the screen. Here is your
chance to leap in and do other things. This event is fun and mischievous.

Removing Previous Events:

Important: To keep the previously-built TextChanged event from cluttering this


example, make these changes from the Events window:

Remove the TextChanged event in this order:


Highlight field textbox1
Select Events (lightning-bolt icon, not Properties)
Backspace over the text within "textBox1_TextChanged" property.

This removes the event but does not delete the underlying code – leaving the code
"orphaned." Except for clutter, there is no harm in leaving unused code in the program
but it should be deleted. Always remove the event before deleting the called method.

If 'TextChanged' code is deleted before the event is removed from the event list, the
compiler will have a run-time complaint, "...does not contain a definition for
'textBox1_TextChanged (or other event name) and no extension method...". To fix,

Chapter 10 - Form Controls and Events Page: 629


double-click the compiler error and remove the highlighted line in
frmProcess_designer.cs. Continue the example with these steps:

Keypress Events: Discard all numeric Keystrokes:

1. In textBox1's Event Properties (Lightning Bolt - event, not property)


Scroll to the "KeyPress" event.
Double-click the blank field next to the label, building a new event.

Add this code, which looks at each keystroke and discards all numeric entries (see later
for code that only accepts numeric values):

Discarding Numeric Keystrokes, as typed


private void textBox1_KeyPress (object sender, KeyPressEventArgs e)
{
//Discard all numeric keystrokes:
// MessageBox.Show (e.KeyChar.ToString() );

if (util.IsNumbers (e.KeyChar))
e.Handled = true;

//Optionally: if (char.IsNumber(e.KeyChar))
}

where:

• This example assumes the CL800_Util libraries are linked into the program.
util.IsNumbers examines the passed data, in this case, an individual character, to see
if it is numeric.

If you do not have the CL800_Util libraries built or written, see Chapter 6 to
manually write this routine.

Testing:

• Start the program, click into textBox1. Type "Boeing 747". Note the digits are not
accepted.

• Try backspacing. See next section.

KeyPress Description:

The KeyPress signature-line, illustrated on the private-void line, accepts a value called
'KeyPressEventArgs'. This contains the end-user's keystroke, as it was pressed. The
signature-line assigns it to a variable traditionally called "e".

Chapter 10 - Form Controls and Events Page: 630


"e." is another variable that has its own methods and properties. These can be explored in
the event's code by typing "e." (e-dot) and looking at the popup. From the list, select
e.KeyChar. The property "gets or sets the corresponding character of the key pressed."

With e.KeyChar, all keystrokes can be intercepted and examined before they appear on
the screen. This gives the program the opportunity to toss or modify keystrokes it does
not like. For example, assume numeric data is not allowed in the LastName field; if
numbers are typed, they can be discarded as they are typed.

Study the textBox1_KeyPress event code, from above. The if-statement detects numeric
values using if (util.IsNumbers (e.KeyChar)) and if found, the statement
"e.handled = true" executes. The e.handled property tells the computer to ignore the
keystroke, saying it has already been "handled" by the program (setting the property to
"true") – you are telling the computer, "don't bother, I'll take care of this character." As
e.handled is typed, notice it is a property and not a method (see popup illustration above);
because of this, it does not use parenthesis ().

Detecting an Enter-key Event:

If a user typed a value in a text box and pressed ENTER, you could detect the Enter-key
and act on this event. For example, advance the cursor to a different field or call a button
event.

To enable the event on a particular textBox, highlight textBox1 in design view, then click
the Events in the Properties window. Locate the KeyPress event; double-click the blank
field to stub-in code. Type this logic, replacing the code from the previous example:

Detecting an ENTER Key event on a particular textBox field


private void txtBox1_KeyPress(object sender, KeyPressEventArgs e)
{
//Detecting an ENTER Key event on a text field
if (e.KeyChar == 13)
{
textBox2.Focus();
label1.Text = "textBox2 is now active!";
}
}

Chapter 10 - Form Controls and Events Page: 631


where:

• ASCII code 13 is an enter-key value. Of interest, this is the second-half of a CRLF


(\r\n).

• "textBox2.Focus();" tells the cursor to advance to textBox2; you could advance or


skip over any other field. This example is simplistic and would be more dramatic if
there were other fields to focus on.

• Re-launch the program and tab into and out-of textBox1, without pressing Enter.
Notice the event did not fire – a tab is not an Enter.

Allowing only Numeric Values:

Fields might only allow numeric values. But you have to be careful in your testing.
Although you may only want numeric values, consider how frustrating it would be if the
user could not press backspace or a decimal. In this example, the routine allows numeric
values 0-9, plus backspace and decimal points – but does not allow plus or minus signs.
(See above for "allow only character data").

1. In the textBox1's Event list (not properties), double-click the KeyPress event to return to
the event's code. Replace with this example:

Allowing only Numeric.decimal Values


private void textBox1_KeyPress (object sender, KeyPressEventArgs e)
{
//Only allow numeric values in this field but do allow
//backspaces and periods, but not minus signs (ascii 45)

if (char.IsNumber(e.KeyChar))
{
//Do nothing, allow the keystroke to pass
}
else
{
char c = e.KeyChar; //Convert keystroke to a character value
int ic = (int)c; //Convert via "cast" to integer

if (ic == 8 || ic == 46)
{
//A backspace8 or period was typed
//Let it live
}
else
{
e.Handled = true; //Discard
}
}
}

where:

Chapter 10 - Form Controls and Events Page: 632


• ascii 45 is a minus sign; ascii 8 is a backspace.

• Ascii tables are widely available on the Internet.

UpperCasing as Typed:

The KeyPress event is also useful for changing the case of the typed character. If data-
entry fields need to be stored as upper-case, the program could wait for the
textBox1_Leave event and shift the typed results to uppercase using this command:

textBox1.Text = textBox1.Text.ToUpper();

Or this could be handled as a KeyPress event and each character can be upper-cased as
typed. While this takes a bit more CPU processing, users see instant feedback.

This command is more complicated than most because the KeyPress event only works
with 'character' data and ToUpper only works with "string" data. Read this statement
from the inside-out:

Shift Each Character to UpperCase, as typed


private void textBox1_KeyPress (object sender, KeyPressEventArgs e)
{
//Shift each letter to upper as typed:
e.KeyChar = Convert.ToChar(Convert.ToString(e.KeyChar).ToUpper());
}

From the inside, the current 'e.KeyChar' is converted to a string, then it is shifted to upper-
case; then it is converted back to a character and re-assigned back upon itself. Since an
e.handled was not specified, the manipulated keystroke is sent on its way, as if nothing
happened.

More Fun with KeyPress Events (Replace Characters):

For no other reason than to be obnoxious, use the KeyPress event to monitor for all "J"'s
and if found, replace with "Q". This will only be active in textBox1.

private void textBox1_KeyPress (object sender, KeyPressEventArgs e)


{
if (e.KeyChar == 'J' || e.KeyChar == 'j')
e.KeyChar = 'Q';
}

Comments:

• KeyPress events only deal with character data; never string data. For this reason, tic-
mark delimiters are used.

Chapter 10 - Form Controls and Events Page: 633


• From the pop-up help, "e.KeyChar" can get or set the KeyPress object. Here, 'Q' was
set as the replacement character with an assignment = 'Q'.

• Other related events are "KeyDown" and "KeyUp", where KeyDown is triggered as
the keyboard key is pressed down – before the KeyPress event. And KeyUp triggers
after the key is released. These two events can detect things like shift and alt-keys
being held down.

• A more practical use for this event would be to replace underscores with dashes or
backslashes with slashes, correcting for user-mistakes as they are typing (for example,
if your database did not allow underscores in a particular field or if you want to
correct web-addresses as the user types).

Test this event by launching the program and typing "Jones" in textBox1. I'm sure you
can see no end to the mirth.

Intercepting Shift, Alt and Ctrl Keystrokes:

Keystrokes such as "Control-C" can be intercepted by your program. For this example,
take the values in textBox1 and textBox2 and copy them to the Windows Clipboard –
even if the text is not highlighted. This example ties the event to the Form (rather than a
particular field or button), this way, as long as the form is in the foreground, the control-C
keystroke can be detected. Naturally, keystrokes other than control-C could be used.

Capturing Control-C Keystrokes:

1. In design view, click on the Form's background or click the title-bar to highlight the entire
form.

2. In the form's Properties window (not the lightning bolt icon)

Set the KeyPreview (property) to True.

Chapter 10 - Form Controls and Events Page: 634


Without this, the form does not listen to key-events. textBox fields do not require this
setting.

3. With the form still active, switch to the form's Event list (Lightning Bolt). Locate the
"KeyDown" event and double-click the empty field to stub-in the code.

Some subtleties here: Be sure the form is active (not a button or textBox) and the form
uses a KeyDown event, not a KeyPress event.

4. Add these statements to the Form1_KeyDown event (note: this is the KeyDown event and
not the KeyPress event):

Form1_KeyDown (Control-C) and Clipboard


private void Form1_KeyDown (object sender, KeyEventArgs e)
{
//Intercept keystrokes, such as Ctrl-C (for copy)

if (e.Control == true)
{
if (e.KeyCode == Keys.C)
{
//Copy fields textBox1 and textBox2 to the clipboard

System.Windows.Forms.Clipboard.SetDataObject
(textBox1.Text + "\r\n" +
textBox2.Text,
true);
label1.Text = "Text copied to clipboard";
}
}
}

Chapter 10 - Form Controls and Events Page: 635


where:

• Notice the signature line's "KeyEventArgs e"; this is the currently-pressed keystroke.

• e.Control = Control-Key is being held down.


e.Alt
e.Shift

(Fkeys are also supported)

• Use if(e.Control == true & e.Shift == true) to detect keystrokes, such as


Shift-control.

• Clipboard.SetDataObject("text", true) copies the text to the clipboard and the


"true" tells the program to leave the data in the clipboard, even if the program ends.
You can also copy image data (SetImage) and other events, which are beyond the
scope of this chapter.

Note: If you were to write a diagnostic "MessageBox.Show" statement in the keystroke-


logic, the MessageBox itself will show in the clipboard because the event happens too fast
for the clipboard. (a util.Wait(1) will help you work around this as you play with the
code. See Chapter 19 for wait-states.)

Testing:

Run the program and populate data in textBox1 and 2.


While the form, or any object inside, is active, press Ctrl-C.
Open Notepad (Start, Run, "Notepad.exe"), select Edit-Paste.

Results: Values in TextBox1 and 2 should paste into the new document, with a carriage-
return between them. Objects other than the Form's background can still be active (for
example, textBox1, and the keystroke will still work). If a textbox's text is manually
highlighted, and you press Control-C, the highlighted text is copied instead of the two
concatenated fields – in other words this works as you would hope.

Problems?

If the Control-C copy event does not run, check these two places. First, be sure the
Form's Property (highlight the entire form by clicking the title bar), then make sure
"KeyPreview" is true. Secondly, confirm the event's code is a "KeyDown" event - not a
KeyPress event.

Attaching Events to Fields:

The control-C example was attached to the form and not to an individual field so no
matter which object was active, only textBox1 and textBox2 are copied to the clipboard.
Instead of placing the event on Form1_KeyDown, each field could have its own
KeyDown event and these would take precedence over the Form-level events; those
Microsoft folks were thinking when they designed this.

Chapter 10 - Form Controls and Events Page: 636


Intercepting Shift, Control, Alt-Key events on a button or other object:

The same type of logic can be attached to a button or other object. As before, set the
form-level's KeyPreview by moving to the form's Design View, highlight the form's
titlebar (or the form's background) and set the 'KeyPreview' property to True. Then, in the
button's Click event, add code similar to this near the top of the routine:

bool boolshiftKeyPressed = false; //Can use this later in module

if ((Control.ModifierKeys & Keys.Shift) == Keys.Shift)


{
//A shift-key was pressed while the button was clicked
boolshiftKeyPressed = true;

//Consider: Enable eye-candy or some other visual clue:


pnlYellowFlagGraphic.Visible = true;
}

Chapter 10 - Form Controls and Events Page: 637


comboBox and listBox

comboBoxes and listBoxes are similar controls, both offering a list for the end-user to
choose from. The list can be populated at form-design, programmatically, or from a
database. Values can be selected using the mouse or keyboard.

Many comboBox techniques can also be used with listBoxes.

comboBoxes occupy one line on the panel. When clicked, they expand over the top of
other fields as a popup list. listBoxes are larger and appear as a pre-expanded list, always
occupying the full vertical space. In both, vertical scroll bars are used if the number of
options exceed the list's on-screen height. As long as screen real estate allows, users
prefer listboxes because a click is not required to activate, but designers always like
comboBoxes because they take less space.

Types of Combo/List boxes:

comboBoxes can be considered static, semi-static, or dynamic.

If the list is static, unlikely to change, and the list is small, they are usually hard-coded in
the form or loaded at the form's Load event. Examples would be lists, such as, "January,
February...", or "Mr., Mrs., Ms. Dr.".

Lists with occasionally-updated data (such as Employee ID's, Product Numbers, Building
Locations, Area-Code lists) are usually populated one-time during Form-load, and remain

Chapter 10 - Form Controls and Events Page: 638


static until refreshed, usually by closing and re-opening the program or some other
manual action.

Dynamic data such as current inventory levels, pricing, stock information, registered
guests, and the like, are usually populated at the moment needed, and are populated from
an external database. Building a list on-the-fly from external data is often necessary but
it comes at CPU and back-end data-processing expense. As these features are developed,
you may need to balance current data with performance.

comboBox Summary
[Recommended Prefix: cbx, lbx]

Use on comboBox1.SelectedIndex numeric value


simple comboBox1.SelectedItem Selected value’s text
lists
comboBox1.Text Alternate text
Use on comboBox1.SelectedIndex
data-
source

(Property) Sorted = true not for data-bound


MaxDropDownItems = n

DropDownStyle = DropDown For AutoComplete


Sorted = True
AutoCompleteSource = ListItems
AutoCompleteMode = SuggestAppend

Use comboBox1.Items.Add("Tokyo"); adds to bottom


comboBox1.Items.Insert(2, "Boise"); adds at position 2,
shifting others down

Use comboBox1.Items.Remove("Tokyo"); removes Tokyo


comboBox1.Items.RemoveAt(0); removes item at
position zero
comboBox1
.Items.Remove(comboBox1.SelectedItem); removes highlighted
item

comboBox1.Items.Clear(); remove all items

comboBox1.SelectedIndex = 3 Programmatically
highlight the 4th
item, base-0

Programmatically
listBox1.SetSelected(0, true);
highlight the first
item in a listBox.

Loops foreach
(object detailLine in comboBox1.Items)
if(detailLine.ToString() == "ABC")
searches as a "starts
int itemFound =
comboBox1.FindString("Portla") with"

int itemFound = search with exact


comboBox1.FindStringExact("Portland") match; -1 if not
found.

Chapter 10 - Form Controls and Events Page: 639


Event(s) comboBox1_SelectedIndexChanged
comboBox1_ValueChanged
comboBox1_ChangeCommitted

if (listBox1.SelectedIndex == -1)
//A blank line was selected

Cautions:
SelectedIndexChanged may fire multiple times if the
keyboard is used, rather than the mouse.

Click and Leave events are not recommended because


the user can activate the box but not select an entry.

Static (Hard-Coded) ComboBoxes:

Loading a comboBox with default values are usually demonstrated with a simple
"Items.Add" - essentially hard-coding the choices. This technique is usually
underwhelming and more advanced ideas are required. Never-the-less, it is a good place
to start.

Start from the form's design view.

1. From the Toolbox flyout (+Common) menu, place a comboBox anywhere on the form.
For these explanations, keep the default name, "comboBox1".

2. Double-click the form's title bar, opening the form's Form Load event. Loading
comboBoxes are usually done during the form load event.

Add these statements:

Chapter 10 - Form Controls and Events Page: 640


Example: Hard-coded comboBox Data
private void FrmProcess_Load(object sender, EventArgs e)
{
// In the Form-Load event, populate the screen's comboBox:
// Recommend also setting these design-properties:
// AutoCompleteSource = ListItems
// AutoCompleteMode = Suggest

this.Show();

comboBox1.Items.Clear(); //It is always wise to clear first

comboBox1.Items.Add("San Francisco");
comboBox1.Items.Add("Portland");
comboBox1.Items.Add("Boise");
comboBox1.Items.Add("Salt Lake City");
comboBox1.Items.Add("San Jose");
comboBox1.Items.Add("Austin");
}

where:

• The initial comboBox1.Items.Clear(); is recommended, especially if the load


statements are somewhere other than the Form Load event.

• Items are added in the order typed, unless later sorted. These city names, in a non-
sorted order are needed for later examples.

3. Press F5 to run the program.

Results: The form loads and the list is populated by the Load event.
Click the pull-down list. The records display unsorted.

AutoComplete / Type-Ahead:

Watch the behavior of the comboBox with the following examples. While inside the
comboBox, begin typing "San..." as in San Francisco, or San Jose. Notice the control
does not help select a value from the list. Data-entry clerks, who universally hate mice,
will not be happy. This will be fixed in a moment.

4. Close the running program and return to design view and change the comboBox's
".Sorted" property to "true".
Re-run the test, noting the sorted records.
Again, notice how typing "San" does not help select the records.

Chapter 10 - Form Controls and Events Page: 641


5. Return to design view again:
Assuming the comboBox is sorted from the previous step, make these two property
changes, in this order:

1. AutoCompleteSource = ListItems
2. AutoCompleteMode = SuggestAppend

j These settings are not available in listBoxes.

6. Run the program again and retest, typing "San...."


The list condenses to show the available options (San Francisco, San Jose). This is what
your users will want.

Chapter 10 - Form Controls and Events Page: 642


Query the Selected Value:

The comboBoxes demonstrated above are not linked to an external data-source. When
built in this fashion, there are two properties that query the selected value.

• comboBox1.SelectedIndex (Returns a numeric, base-0 value)


• comboBox1.SelectedItem (Returns the text value, as selected)

".SelectedIndex" returns a numeric, base-zero count, showing the position of the


selected value. The first item in the list is zero, the second is 1. If no item was selected
(the comboBox is blank; the user took no action and no value was set as a default), then
.SelectedIndex returns a -1. A negative one is C#’s way of saying the item was not found
or not identified.

Sorted comboBoxes report the index count, as sorted. In the example above, “San Jose”
was at position 4 when loaded, but after sorting, it reports as position 5.

".SelectedItem" returns the text value (e.g. "Portland," "Boise").

1. Assuming the comboBox was populated in the form Load event, add the following logic
to button1:

Example: Displaying a comboBox's Selected Index (number) and Name


private void button1_Click (object sender, EventArgs e)
{
//Display which item was selected and which name.
//The event "comboBox1_SelectedIndexChanged" is commonly used

MessageBox.Show
(Convert.ToString(comboBox1.SelectedIndex) + "\r\n" +
comboBox1.SelectedItem);
}

To test, press F5, select a city, then click button1.

Chapter 10 - Form Controls and Events Page: 643


where:

• "comboBox1.SelectedIndex" returns the "4th" item in a zero-based list and the


number is converted to a string as it is handed off to the MessageBox function. The
returned number depends on the sort-order in the list. For example, if un-sorted, San
Francisco returns 0; if sorted, the index returns a 4.

• "\r\n" inserts a pretty carriage-return, line-feed (a line break).

• "comboBox1.SelectedItem", or "comboBox1.Text" displays the text of the item


selected and this works for simple comboBoxes.

When the comboBox is connected to an external table or database, the index


may not be accurate. New items could be added to the list from other users or
other processes during the middle of this transaction.

"No Selection"?

It is possible for the user to make "no selection". The form loads and populates the
comboBox, but if the user makes no selection, and button1 is clicked, it returns a classic
C# "-1" - meaning nothing was selected. Your program must test for this possibility.

This can be tested with:

if (comboBox1.SelectedIndex == -1)
MsgBox.Show ("No choice was made")

Using the SelectedIndexChanged Event:

Instead of a button1_Click event, a more common method for detecting comboBoxes is


the "SelectedIndexChanged" event, which fires each time a value is selected. This has
uses and drawbacks.

Build a test demonstrating the event with these steps:

Open the form in design view. Using the toobox flyout, add a label, label1, moving it to
a location above the comboBox, so it is not occluded by the drop-down menu.

Then, highlighting comboBox1, open the Events Property window (the lightning-bolt
icon). Double-click the "SelectedIndexChanged" event. Add this statement (ignoring
button1):

Chapter 10 - Form Controls and Events Page: 644


comboBox1.SelectedIndexChanged
private void comboBox1_SelectedIndexChanged
(object sender, EventArgs e)
{
label1.Text = comboBox1.Text + " is selected";
}

where:

• combBox1.Text is being used rather than .comboBox1.SelectedText because .Text


shows the highlighted value, before a value is "selected" with an "Enter" or Tab out of
the field. A subtle difference.

A. Launch the program and expand the list by clicking on comboBox1.

B. Move down the list by pressing the down-arrow key on the keyboard.

The .SelectedIndexChanged event fires at each keystroke, updating the label. In other
words, the event fires one time with a mouse-click but may fire multiple times if a
keyboard is used. This behavior can surprise a developer but a program can take
advantage of this by displaying a database lookup on each value, perhaps showing the
end-user demographic data on a selected name.

listBoxes have additional concerns in this area. See the next section.

Avoid Click and Leave Events with combo and listBoxes:

The "Click" event is not recommended. Click events fire when the comboBox
is activated (selected) but this does not mean the user chose a value. For
similar reasons, Leave events are not useful because the user can activate the
box, making no selection, then leave the control for another part of the screen.

Dumping the Contents of a combo or listBox:

Use this routine to dump the contents of a comboBox:

Chapter 10 - Form Controls and Events Page: 645


Displaying comboBox Contents, diagnostic code
private void button1_Click (object sender, EventArgs e)
{
//Dump the contents of a comboBox into a messageBox:
//(Please test with short lists when testing)

string strMsg = "";

foreach (object detailLine in comboBox1.Items)


{
strMsg = strMsg + detailLine.ToString() + "\r\n";
}

MessageBox.Show(strMsg);

Programmatically Searching combo and listBoxes for Delete:

C# provides two methods for programmatically finding values in a list box. This is useful
when the list needs to be updated while the program is running. For example, using either
.FindString or .FindStringExact, have the comboBox search the list, and if the
value is found, delete it. To test, attach this test routine to button2:

Example: comboBox / listBox .FindString Method + Delete


private void button2_Click (object sender, EventArgs e)
{
//Example routine that searches for the name "Boise"
//in a previously-populated combo or listbox and then
//deletes the item from the list:

int iitemFoundPos = comboBox1.FindString("Boise");

if (iitemFoundPos >= 0)
{
MessageBox.Show("Found at base-0 line number: " +iitemFoundPos);
comboBox1.Items.RemoveAt(iitemFoundPos);
this.Refresh();
}
else
{
MessageBox.Show("Value was not found");
}
}

Chapter 10 - Form Controls and Events Page: 646


ComboBoxes linked to an External Database:

In the form design window, comboBox items can be added directly in the Properties
window. See comboBox1, Properties, "Items (Collection)", where you can hard-code the
values in a popup list.

I do not recommend using this design. If the select-list is going to be hard-coded, use
program code to populate the box rather than hiding them in a field's properties – for no
other reason than it is easier to find in the editor.

But if the list is subject to changes, populate it from an external file or database so the
program does not need to be re-compiled just because of a lookup change. This is
covered in Chapter 17 (Excel and Access) and in Chapter 26 (Microsoft SQL).

For smaller, less volatile lists, read default values from an ASCII file, an INI file or an
XML table. See chapter 12.

Chapter 10 - Form Controls and Events Page: 647


listBox

listBoxes are similar to comboBoxes with one major cosmetic difference. While
comboBoxes occupy one line on the screen and expand into a list when the drop-down is
clicked, listboxes continually occupy their vertical space.

Choosing one over the other is a matter of preference and screen real estate. For short
lists, users prefer the listBox because it does not require a click to activate and they can
see all of the choices at a glance. This benefit is lost when the number of items exceeds
the size of the box.

Example: Populating a listBox with Filenames:

This example populates a listBox with filenames found in a disk directory. From the list,
allow the users to select a filename to view the date and time the file was last changed.
Then, expand the program by allowing the user to select multiple filenames. The
directory-listing uses file-logic described in Chapter 23.

Building the Example:

A. From a new project (or re-work previous examples), create a form with these items from
the toolbox flyout menu:

• listBox,
• two labels
• button1

Size the listBox as illustrated. Values within will be populated in a moment.

Chapter 10 - Form Controls and Events Page: 648


B. Open the form in code view. Near the top, in code view, add a "using System.IO;"
statement, which is required by the disk directory commands. System.IO is case-
sensitive. Below it, create a variable, strSourcePath, which holds the a directory path:

Example: listBox - Create a required variable, "strSourcePath"


:
:
using System.IO;

public partial class frmProcess : Form //Yours may be named Form1


{
string strSourcePath; //form-level variable

public frmProcess()
{
InitializeComponent();
}
:

C. Double-click the form's background, taking you to the form's Load event.
Add these statements, setting the file-directory name and calling the module that actually
gets the file listing:

Example: Using Form_Load to call listBox Populate


:
private void frmProcess_Load (object sender, EventArgs e)
{
this.Show();

strSourcePath = "C:\\Data\\Graphics"; //any dir of your choosing


label1.Text = strSourcePath; //Cosmetic

//Call the routine to populate the list:


A200_GetGraphicFiles();
}

Chapter 10 - Form Controls and Events Page: 649


where:

• strSourcePath should point to a directory on your system with graphic files or


substitute a directory with .XLS, .DOC, etc. Note the double-backslashes in the path
name. If you do not have a data-graphics directory, use
"C:\\Windows\\web\\wallpaper" or "C:\\users\\<your userid>\\Pictures"

After typing "A200_GetGraphicFiles( );", click the small lightbulb do-dad and allow C#
to stub-in the basic outline of the new routine.

Look directly below the form_Load method's closing brace for the new module. Within
A200, remove the "throw new NotImplemented" line.

Now write the code to populate the listBox.

1. The module A200_GetGraphicFiles( ) fetches each filename and places their names into
an array (a vertical list of names) using the GetFiles method, covered in detail in
Chapter 23. The routine examines each found file to see if it is an acceptable graphic file,
and if-so, adds it to the listbox. This example's requirements are to display only graphic
files. Each file (*.*) is retrieved, then filtered with an if-statement:

Chapter 10 - Form Controls and Events Page: 650


Example: listBox - Retrieving Filenames and Populating the box
private void A200_GetGraphicFiles()
{
//Always clear the (previous) list before appending new names
listBox1.Items.Clear();

//Load each file's "File Information" into an array called myFiles:


//Note square [brackets]
DirectoryInfo myDirectory = new DirectoryInfo(strSourcePath);
FileInfo[] myFiles = myDirectory.GetFiles("*.*");

//Retrieve each filename from the array and place in the listbox:
foreach (FileInfo myfileinfo in myFiles)
{
//Loop through each file, processing only graphic files:

if (myfileinfo.Name.EndsWith(".bmp", true, null) ||


myfileinfo.Name.EndsWith(".jpg", true, null) ||
myfileinfo.Name.EndsWith(".gif", true, null) ||
myfileinfo.Name.EndsWith(".png", true, null) ||
myfileinfo.Name.EndsWith(".tif", true, null) )
{
listBox1.Items.Add(myfileinfo.Name);
}
}
}

where:

• Directory and File manipulation commands are covered in detail in Chapter 23.

• The first line in the routine wisely clears previous lists.

• myDirectory is an invented name 'of type DirectoryInfo'. This is not a string or an


integer; it is a 'DirectoryInfo'. The form-variable, strSourcePath, is passed into this
object. Again, see Chapter 23.

• myFiles is an array [ ] 'of type FileInfo' and this contains all of the filenames in the
directory. Of interest, the FileInfo's are objects - things with a bunch of properties
and methods – one of which is the file's name. The quoted-string "*.*" is a wildcard
that retrieves every file in the directory, regardless of its extension.

Arrays are a vertical list of related information and you have dabbled with them
previously. The [square brackets] are a dead-giveaway that you are dealing with an
array. Chapter 22 covers arrays in detail.

• The "foreach" statement loops through each file-information object and assigns it a
temporary name called 'myfileinfo' - again, an invented name. From this, it can look
at the object's filename. If the name ends in a graphic extension (".bmp", etc.), add it
to the listBox; otherwise, discard. The .EndsWith searches with a "true, null", which
means the search is not case-sensitive.

Chapter 10 - Form Controls and Events Page: 651


Testing the listBox:

The listbox is testable now, but does not do much. In any case, press F5 to run. Because
of the code in the FormLoad event, the listBox populates with filenames, provided it
found any in the directory you passed. Click a filename to highlight. Try control-clicking
a second filename – you will not succeed.

If your compile fails, you may see these errors:

"The type or namespace name 'DirectoryInfo' could not be found (are you missing a using
directive or assembly reference?" Solution: do you have a "using System.IO;"
statement?

The list is empty. Solution: Are there graphic files in the directory? If not, for testing,
choose another file-extension type, such as .XLSX, .DOCX, .WPD, etc.

Directory-not-found errors. Solution: Be sure the strSourcePath contains double-


backslashes in the path-name and make sure the directory actually exists. A try-catch is
advisable in a routine like this, but is not illustrated.

Processing the Selected Record:

Once the list is populated, program logic usually needs to do something with the choices
the end-user makes. comboBox and listBoxes have a "SelectedIndexChanged" which can
query what was selected.

Close the running program and return to the editor's design view. Highlight listBox1. In
the Properites window, Events, create a "SelectedIndexChanged" event by double-
clicking the event's name, then add these statements:

listBox - SelectedIndexChanged Event, test


private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
{
//Display which item was selected and which name.

MessageBox.Show (Convert.ToString(listBox1.SelectedItem));
}

Test the routine by starting the program and single-clicking a filename in the populated
list. As with the comboBox example, traverse the list using the down-arrow-key and this
triggers the event as each item is passed over.

Blank-line Testing:

Often missed by developers, users can amazingly select "blank" lines in the listBox.
Assuming you have followed the examples from above, continue with this test:

Chapter 10 - Form Controls and Events Page: 652


A. In code view, make this change to the A200_GetGraphicFiles routine:

:
myfileinfo.Name.EndsWith (".tif", true, null) ||
myfileinfo.Name.EndsWith (".png", true, null) )
{
listBox1.Items.Add(myfileinfo.Name)'
}
}

//After foreach's closing brace, add these dummy blank records


//for testing:

listBox1.Items.Add(""); //Sneak two empty lines as a simulation


listBox1.Items.Add("");
}

(A more accurate simulation would be to resize the listbox, so it is taller than the number
of records populating the list – imagine the test program only returned three or four
filenames. This would leave 'blank lines' at the bottom of the list.)

B. Run the program again. This time, use the mouse and click an empty-line at the bottom of
the listBox. Results: Non-populated lines can be selected and empty values are returned.
This can cause problems for downstream routines.

C. Modify the event with this new logic, where a listBox1.SelectedIndex == -1 is tested,
using a double-equals and a minus-one:

Chapter 10 - Form Controls and Events Page: 653


listBox - SelectedIndexChanged Event, completed
private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
{
//Display which item was selected and which name.

//Test for blank selections:


if (listBox1.SelectedIndex == -1)
{
//Do nothing
}
else
{
MessageBox.Show (comboBox1.SelectedItem);
}
}

listBox Multiple Selection Enhancements:

A. Listboxes (but not comboBoxes) can be modified so users can select more than one item.
In design view, change these listBox1 properties (click the Properties, not the Events
button):

ScrollAlwaysVisible = True
SelectionMode = MultiExtended
Sorted = True

B. From the previous example, if you added code for two blank lines, remove that logic now.

C. Modify the listBox1.SelectedIndexChanged event, commenting-out the


MessageBox.Show statement. (The MessageBox would be annoying and the statement
would only show the first file selected, regardless of how many were highlighted.)

Chapter 10 - Form Controls and Events Page: 654


To test, run the program then click the first line plus shift-click the last line (which
highlights all items). Control-click intermediate entries to un-highlight. These are
standard Windows highlighting techniques. Your list will be different.

Processing the Selected Record


Processing Multiple Selected Records:

With standard combinations of click, shift-click and control-click, users can select a
multiple records in a list (provided the selection mode is set to Multi-extended).
Detecting which item or items were highlighted is relatively easy.

1. Begin by displaying basic file information about the first-selected file.


In design view, double-click listBox1, opening the default "SelectedIndexChanged" event.
Add this code:

Example: listBox Showing Basic File Information on a Highlighted Item


private void listBox1_SelectedIndexChange (object sender, EventArgs e)
{
//Display fileinformation about the first-highlighted file:

//Setup the Directory/File query mechanisim:


DirectoryInfo myDirectory = new DirectoryInfo(strSourcePath);

//Load the selected file's information into the DirectoryInfo


//object. Even though only one file was highlighted, the results
//must still drop into an array: FileInfo[].

FileInfo[] myFiles =
myDirectory.GetFiles(listBox1.SelectedItem.ToString() );

//Show the results in an on-screen label:


//e.g.: myfile.txt: 12/01/2009 10:33am

label2.Text = listBox1.SelectedItem.ToString() + ": " +


myFiles[0].CreationTime.ToString();
}

where:

• With each selected item, load the file's information into an array, then take the first
file's information and display it in a label. The next example shows how to process
multiple files.

2. In design view, double-click button1.

3. Add this code to the button1_Click event:

Chapter 10 - Form Controls and Events Page: 655


Example: listBox - Process All Highlighted Records
private void button1_Click (object sender, EventArgs e)
{
foreach (string strselected in listBox1.SelectedItems)
{
MessageBox.Show(strselected);
}
}

where:

• listBox1.SelectedItems ('SelectedItems' with an 's') processes one or more records,


depending on how many were highlighted.

• "strselected" is the temporary string used by the loop.

Looping through Lists:

Imagine for some particular reason you needed to remove a filename from the previously-
found listBox. In my example, target the filename "3-box.png", where your filenames
will be different.

Or alternately, change this example so it loads City-names, from the comboBox examples,
and you might want to delete "Portland".

Items in the list (collection) are in a base-zero array, where the first item is in position [0]
zero, the second in [1], and so on. There are several ways to remove a known value from
the list, starting with the "hard-way" – a sequential search.

Chapter 10 - Form Controls and Events Page: 656


Example: Deleting a value from the list using the "hard way", completed
private void button1_Click (object sender, EventArgs e)
{
//Search for a hard-coded filename, such as "3-box.png"
//and remove if found. This is the "hard" sequential way

int ifileCount = -1;


int ifileFound = -1

foreach (object detailFile in listBox1.Items)


{
ifileCount++; //Count current file-number, with [0] first
if (detailFile.ToString() == "3-box.png")
{
//It would be nice to delete the item now, with
//listBox1.Items.Remove(detailFile)
//but you can't remove an item while in the middle of a
//foreach loop. Instead, do the following:

ifileFound = ifileCount; //Mark the current file-number


break; //Bail out of the loop
}
}

//If found (ifileFound >-1, then delete the item, this is


//done outside of the loop

if (ifileFound >= 0)
{
listBox1.Items.RemoveAt(ifileFound); //Remove by number
this.Refresh(); //update the screen
}
}

where:

• This is the hard-way to delete an item in a list or combo box but the code
demonstrates how to sequentially loop through the items in the list.

• This loop only deletes the first occurrence of the search-string.

• An item cannot be deleted while inside the foreach loop; you will get a runtime error
if you try. Basically, you can't remove an item from the list while the list is being a
looped. Because of this, store the found-file's index number and then, outside of the
loop, issue the delete.

• "RemoveAt(index number)" deletes by number. Remove("3-box.png") removes by


exact text. See the next example.

The Easy Way to Identify and Delete a listBox Item:

Chapter 10 - Form Controls and Events Page: 657


Example: Removing an Item from a listBox, the easy way
private void button1_Click (object sender, EventArgs e)
{
//From a listbox:
//Search for the File "3-box.png" and remove if found.
//This is the "easy way":

int ifileFound = listBox1.FindString("3-box.png");

if (ifileFound >= 0)
listBox1.Items.RemoveAt(ifileFound);
}

where:

• ".FindString" finds using a "starts with"


".FindStringExact" finds an exact match; case sensitive.

If upper/lower case is a problem, loop manually, using the "hard way"

foreach (object detailFile in listBox1)


{
if (detailFile.ToString().ToUpper() == "3-box.png")
{

• Also consider ".ItemsContains", not demonstrated here.

Other uses for listBoxes:

ListBoxes can display small reports where the details are read from another location. For
example, a database may have a list of date and sales figures. The list could be populated
with a statement similar to this:

listBox1.Items.Add(strSalesDate + " " + totalSales);

Date Total Sales


2008-10 4,567
2008-12 3,547
2009-01 2,123
2009-02 3,450

Clicking on a date, the program could open the report's details. With lists like this, there
are obvious benefits to sorting. Historical lists can get quite long and often it would be
nice to truncate the list, showing only the most recent 13-months of activity.

Consider this clean-up loop, which only leaves the 13 most current items:

Chapter 10 - Form Controls and Events Page: 658


Example: listBox - Delete all but the most recent 13 items
private void btnDeleteHistory_Click (object sender, EventArgs e)
{
//In a theoretical list, where all date-records are listed,
//one per month, delete all but the last 13 months of activity
//from a listbox...

//This assumes sorted values are loaded

DialogResult drResult;
drResult = MessageBox.Show("Delete old history?", "Delete?",
MessageBoxButtons.YesNo);

if (drResult = DialogResult.Yes)
{
int iendCount;
iendCount = (listBox1.Items.Count -13) -1;

if (iendCount > 0)
{
for (int i=0; i<iendCount; I++)
{
//Delete the item in the list
listBox1.Items.RemoveAt(i);
}
MessageBox.Show("History deleted");
}
else
MessageBox.Show("No old history to delete");
}
else
{
//Delete cancelled when user clicked NO
}
}

Chapter 17 shows how to use combo and listBoxes with Microsoft Access or SQL tables.

Chapter 10 - Form Controls and Events Page: 659


checkBoxes

checkboxes allow a user to mark an entry as either Yes/No, True/False, or sometimes can
be set to a 'indeterminate state,' where the user has not indicated any choice - a grey area,
if you will. checkBoxed panels should almost always allow for intermediate states. You
will find checkBoxes require odd logic to detect if the box is checked or not.

Forms can have multiple checkboxes and each is independent of the other. Contrast this
with Radio-buttons, which like to travel in groups.

Chapter 10 - Form Controls and Events Page: 660


checkBox Summary
[Recommended Prefix: cbx or cb]
Use if (cbxcheckBox1.Checked == true) boolean T/F

(Property) ThreeState == false Simple boolean T/F


ThreeState == true Requires nested-if
Checked == true to test properly; see
CheckState == Indeterminate
below

Use if (cbxcheckBox1.CheckState == .unchecked


CheckState.indeterminate .checked
.indeterminate
Set: cbxCheckBox1.CheckState =
CheckState.Checked

Event(s) cbxcheckBox1_CheckChanged
cbxcheckBox1_CheckStateChanged
cbxcheckBox1_Clicked

checkBox1 Example:

This example uses a standard checkBox and then uses button1 to query the status of the
box. For the experience, have a second button, button2, uncheck the box. The example
uses the default fieldname "checkBox1" but in your code, a "cbx" or "cb" prefix, along
with a real variable name, is recommended.

1. On a new Form1, place these objects by using the flyout toolbox menu:
button1
button2
checkbox1

2. In form design, double-click checkBox1 to build the default "CheckChanged" event. This
event fires when the box is clicked or un-clicked. In the event, add a diagnostic
MessageBox statement, which will be fairly annoying:

private void checkBox1_CheckChanged(object sender, EventArgs e)


{
MessageBox.Show("CheckBox is being changed");
}

This event fires when ever the checkBox is touched by an end-user. Try this now by
running the program.

3. Return to design view and double-click button1.


In button1's event code, query and report on the status of the checkBox with this logic:

Chapter 10 - Form Controls and Events Page: 661


Example: checkBox.CheckState Status
private void button1_Click(object sender, EventArgs e)
{
//Examine checkBox state:

if (checkBox1.CheckState == CheckState.Checked)
MessageBox.Show("It is checked, but I'll uncheck with button2");
else
MessageBox.Show("It is unchecked");
}

As usual, note the double-equals in the if-statement.

Alternately, test with these statements; both are synonymous:

Alternate: Testing Two-State checkboxes


//Testing for Checkstate

if (checkBox1.CheckState == CheckState.Checked)
if (checkBox1.Checked == true)

4. In design view, double-click button2, adding this logic:

Example: Changing a checkBox State


private void button2_Click (object sender, EventArgs e)
{
//Force checkbox1 to unchecked:
//Notice this still triggers a "CheckChanged" event!

checkbox1.CheckState = CheckState.Unchecked;
}

The CheckState is changed with a single-equals sign, not a double-equals – you are
setting a value, not checking it's status.

5. Press F5 to run the program.

Each time checkbox1 is either checked or unchecked, whether by you or by


button2, the CheckChanged event fires! In other words, when button2 sets the
checkbox's state and the event fired, even though you did not explicitly call it.
Note: The CheckChanged event fires before the button2's actual un-checking
of the box.

Chapter 10 - Form Controls and Events Page: 662


6. Close the program and return to the editor.

Comment (//) the checkBox1_CheckChanged MessageBox command for the remainder of


the tests.

private void checkBox1_CheckChanged(object sender, EventArgs e)


{
// MessageBox.Show("CheckBox is being changed");
}

ThreeState:

checkBoxes can be checked, unchecked, or optionally set to a third state called


"Indeterminate" – meaning the user had not made a selection either way. You may have
seen these represented in other programs as a "grey" checkbox.

Consider a form with a checkBox text-caption, "Do you Agree?" If the form loads with
an initial default "checked," the program would have to assume the end-user agrees when
in-fact the user had not made a selection. Conversely, defaulting to unchecked means
they did not agree but on the initial form-load, they may not have selected that option
either. This is where the indeterminate choice can be helpful (radio-buttons would also be
a good choice in this situation; see the next section).

To demonstrate indeterminate values, make the following changes:

7. In Form1's design view, highlight "checkBox1".

Scroll down the Properties list and locate the "Text" property.
Change from the default text from "checkBox1" to "Do you &Agree?" (note the
ampersand-A and do not type the quotes)

While still on the checkBox's properties, continue with these property changes:

• Change "ThreeState" to "True"


• Scroll to "Checked", set the default value "True"
• Set "CheckState" to "Indeterminate"

Run the program.

Chapter 10 - Form Controls and Events Page: 663


Users can click the box multiple times, toggling between the three choices.

As the form first loads, the indeterminate box looks like it is "filled" but it is
not considered "Checked" and logic will show it "unchecked." Manu users
will have to be educated to teach them what this means.

To demonstrate, remove the logic from button1_Click and make these new changes. It
will require two independent if-statements for the test. The first checks to see if no-
choice has been made (indeterminate) and the second shows what choice is detected (even
if none):

Indeterminate CheckStates, preliminary and incorrect


private void button1_Click (object sender, EventArgs e)
{
if (checkBox1.CheckState = CheckState.Indeterminate)
MessageBox.Show("Preliminary: No selection made...");

//But this test will show the same box as "Unchecked"


//– I suppose that is somewhat accurate....

if (checkBox1.CheckState == CheckState.Checked)
MessageBox.Show("It is checked");
else
MessageBox.Show("Ah, it still shows as Unchecked");

Testing ThreeState:

With the button1 logic in place, press F5 to run the program.

• Leave the "Do you Agree" checkBox unmolested.

Chapter 10 - Form Controls and Events Page: 664


• Click button1. The first MessageBox confirms no selection has been made...

• The second MessageBox reports the box is unchecked even though it appears "filled".
The visual-issue is this: The box appears filled (but not checked) and this fools end-
users and shows the test above is flawed.

When coding for a ThreeState-checkBox, nest the if-statement in this fashion:

Check for three-state checkBox, recommended


private void button1_Click (object sender, EventArgs e)
{
if (checkBox1.CheckState == CheckState.Indeterminate)
{
MessageBox.Show("No selection...");
}
else
{
if (checkBox1.CheckState == CheckState.Checked)
MessageBox.Show("Checked");
else
MessageBox.Show("Unchecked");
}
}

In a real program a selected checkbox might write a field to a database. Your


program likely needs to convert the results to a simple True /False because
most databases do not allow a third state.

SQL Servers (See Chapter 25) do not accept tri-state values for checkBoxes
and the values must be converted to an integer 1 or zero.

Use this logic:

Converting Tri-States for SQL Server


//SQL Servers cannot see the tri-state checkbox;
//It can only store an integer value 1 or zero.
//Use this logic to convert from a tri-state to a two-state value:

int icbxState; //Declare an integer

if (checkBox1.CheckState == CheckState.Checked)
icbxState = 1;
else
icbxState=0;

//...Store icbxState into the SQL table

Chapter 10 - Form Controls and Events Page: 665


checkBox Events and Event Order:

When the checkBox control was double-clicked in design view, Visual Studio took you to
the "CheckChanged" event and this is their recommended event. A few pages earlier, you
played with the routine using this logic:

private void checkBox1_CheckChanged(object sender, EventArgs e)


{
MessageBox.Show("CheckBox is being changed");
}

There are an amazing number of other events tied to a checkBox, but only three are likely
useful in day-to-day programming:

CheckedChanged (Just before state "changed" happens) Microsoft's default


CheckStateChanged (Just after state is changed – a more useful event)
Click (On Click Event – the control was touched)

If you were to write a MessageBox for all three events, you would find each triggers, one
after another, with the Click event being last. The differences between the three events
are subtle.

"CheckChanged"
This event runs the millisecond after the user clicks the control – but before the change is
allowed by the program. This gives the program a chance to intercept the change and
disallow. This makes other logic counter-intuitive because during the "CheckChanged"
event, the control might be "checked" while other events would think the control was
unchecked.

Chapter 10 - Form Controls and Events Page: 666


"CheckStateChanged"
This event runs immediately after the computer accepts the change from the user and
triggers as a boolean value saying the control was modified, one way or the other. More
on this idea in the next example.

"Click" The Click event runs after the first two events and this event basically says the
user touched the control and their entry was accepted. Logic could be placed here or in
the CheckStateChanged event with little difference in how a program operates.

In all three cases, keep in mind these are events and have nothing to do
with the three-state setting of the actual checkBox. The events are
triggered on any change and do not directly note how it was changed;
use the CheckedState properties to determine the set value.

Microsoft has struggled with tri-state checkBoxes for the past


several versions of Visual Studio. Depending on the .DotNet
version you are compiling for, the third-state has appeared as a
light-grey box, a light-grey-check-mark and as dark-gray
smaller-box.

Try this: Change the "FlatStyle" property to "Flat", run the program, and cycle through
the three states. Even now Microsoft seems uncertain about how to handle this.

The user sees this visual indication:

Chapter 10 - Form Controls and Events Page: 667


Chapter 10 - Form Controls and Events Page: 668
Radio Buttons

Radio Buttons behave much like checkboxes with two major differences: they prefer to
travel in groups and only one of them likes to be selected at a time. When one button in
the group is selected, other de-select.

A single-radio button is illogical because once clicked it can't be un-clicked; in other


words, they can not be used as a single-checkbox. To help users understand what they are
clicking, radio buttons are round; checkboxes square.

radioButton1
[Recommended Prefix: rbx, rb, rbtn]
Use if (rbxRadioButton1.Checked == true) boolean T/F

Event(s) rbxRadioButton1_CheckChanged Separate events are


rbxRadioButton1_Clicked needed for each button so
these are seldom used and
the events are very
"chatty"

Events *do not* fire on


default values!

Use rbxButton1.Checked = true; Set a radio-button on.

Chapter 10 - Form Controls and Events Page: 669


In your test program, do the following:

• Drop three Radio Buttons, radio buttons 1, 2, and 3


• In the properties screen, rename (Name) buttons as: rbxRed, rbxGreen, and rbxBlue
• Change the "Text" property to "&Red", "&Green", "&Blue"
• From the Toolbox flyout, add a Label; leaving with the default name and text,
"label1"
• Delete all previous logic inside the button1 and button2's _Click event

The resulting screen, along with a button1, should look similar to the illustration below.

By default, none of the radio buttons are selected when the form loads. Change this
behavior by setting the rbxRed button's "Checked" property to true.
Testing Radio Buttons:

Press F5 to run the program, then click Red, Green and Blue. Notice how only one color
can be selected at a time. Close the program and continue with these steps.

Chapter 10 - Form Controls and Events Page: 670


1. Use button1 to query the status of the selected Radio Buttons. In design view, double-
click button1 and add this event logic:

Example: Testing which Radio Button was clicked


private void button1_Click (object sender, EventArgs e)
{
//Set a cosmetic label when a radiobutton is clicked

if (rbxRed.Checked == true)
label1.Text = "Red was checked";
else if (rbxGreen.Checked == true)
label1.Text = "Green was checked";
else if (rbxBlue.Checked == true)
label1.Text = "Blue was checked";
else
label1.Text = "Neither was checked";
}

Testing radio buttons is a nuisance. This is one of the few times I like using else-if-
statements instead of nested-ifs. You will find traditional nested-ifs too cumbersome
when there are three or more Radio Buttons.

"Switch" statements can't be used because a different radio button is being


tested each time. This is what makes them a pain

With radio buttons there is a possibility none were selected.

Using the Radio Button:

Typically, radio button logic sets another variable and a later step, often much later,
checks what happened. Consider this snippet:

if (rbxRed.Checked == true)
myFavoriteColor = "Red";
:

where the string variable "myFavoriteColor" was declared elsewhere, in a higher-scope.


The variable might be used to write to a database record or control some other aspect of
the program. Once the selection is converted to a (string), a switch can be used to test the
results. For example:

if (rbxRed.Checked == true)
myFavoriteColor = "Red";
else if (rbxGreen.Checked == true)
myFavoriteColor = "Green";
:
: (etc) with other colors
:
else
myFavoriteColor = ""; //None selected

Chapter 10 - Form Controls and Events Page: 671


:
://Later in code...

switch (myFavoriteColor)
{
//Now do something with the set myFavoriteColor
case "Red":
// <do Red stuff>
break;
case "Green":
// <do Green stuff>
break;
case "Blue":
// <do Blue stuff>
break;
default:
// <do when nothing was selected>
break;
}

Radio Button Events:

Radio buttons have two main events: "CheckChanged" and "Click", but as you will see,
they are seldom used. They fire twice as often as expected and different events are
needed for each button (red, green, blue). Try this example:

1. While in design view, double-click the "Red" radio-button to open the "CheckChanged"
event. Add this logic to the Red object. Notice it only applies to the red button. Write
similar events for Green and Blue – this means three total methods which makes for
chatty code.

Example: radioButton CheckChanged Event, not recommended


private void rbxRed_CheckChanged (object sender, EventArgs e)
{
if (rbxRed.Checked == true)
MessageBox.Show("Red was checked");
else
MessageBox.Show
("Red was unchecked and someone else was checked");
}

2. Press F5 to run the program.

If rbxRed button's "Checked" property was set to a default = true, Red is


already checked as the form loads. Because it was not "clicked," the
"CheckChanged" event does not fire. If your database depends on this to write
or set a value, it will be missed.

3. Click Green.

Chapter 10 - Form Controls and Events Page: 672


Two events trigger: Red gets un-checked --firing the event; then Green's event
fires. The "CheckChanged" event means any change to the (Red) object –
which includes an automatic de-selection! This is less than useful.

Instead, I recommend setting an intermediate variable, such as 'myFavoriteColor', as seen


on the previous page, and let that variable do all the work.

Because Radio Buttons travel in groups (in this case, they are
grouped by the form itself), writing an event for each button is
almost pointless. Each radio triggers two events – one for the
uncheck and another for the check and default values are hard to
capture. For this reason, the data stored by the radio buttons are
not usually acted upon until later in the program, typically at a
record-save event.

Grouping Radio Buttons:

The radio buttons on the form bind to the form's internal group. As other radio buttons
are added, no matter where they are placed on the form, no matter what they are called,
they join the form's group.

What happens if two more buttons: say Male and Female are needed and they need to
behave separately from the color-buttons? To separate the color-buttons from the
background, group them with a container tool. While in design view, do the following:

1. From the Toolbox flyout menu, open the Containers list and select "GroupBox".
Click and drag a box, surrounding the Red, Green and Blue radio buttons, as illustrated:

Start dragging a few millimeters above the first radio-button, so the "groupBox1" label
has room to land (see illustration below). The selection box must encompass the entire

Chapter 10 - Form Controls and Events Page: 673


button and its text labels before it is accepted into the group. If a radio button is missed
and falls outside of the group, open the "Common Controls" toolbox, choosing the
"Pointer" option. Then drag the missed button on top of the group, in any sloppy position.
Then re-arrange as needed.

2. Build a new set of Radio Buttons:

Using the Toolbox, drop two more Radio buttons on the form by clicking and dragging
them from the "Common Controls" flyout directly to the form. Drop them in a location
that does not intersect groupBox1.

Once the two buttons are dropped, name them rbxMale and rbxFemale. Change their
.Text property to "&Male" and "&Female". The end results should look similar to this:

Chapter 10 - Form Controls and Events Page: 674


The Male and Female buttons are grouped automatically by the form (no container is
needed unless you want to separate it from other groups of buttons). Any new buttons
dropped on the form (anywhere), will bind with Male/Female, unless grouped separately.

3. Run the program by pressing F5.


Click Blue
Click Male

The two new buttons, male and female, are linked together and do not interact with the
original group. Close the program and return to design view.

4. Highlight groupBox1 and set its ".Text" property to nothing (empty-string) by


backspacing over "groupBox1". Once a set of radio buttons are grouped, I do not know of
a way to hide the container-box, nor is there an obvious way to ungroup them short of
dragging them outside of the box.

Chapter 10 - Form Controls and Events Page: 675


ProgressBar

Progress bars are one of the simpler controls. They grow from 0% to 100% and are
almost always used in loops. For example, imagine an input file with 10,000 records.
The progressBar's minium could be set to zero and the Maximum could be set 10,000.
With each iteration in the loop, the ".Value" property could be increased by one. The
progress bar would inch along until it reached 100%.

progressBar
[No recommended prefix]
Use progressBar1.Value = 65 integer

(Property) Maximum 100, 1000,


loopCounter, etc.

(Property) Style Block


Continuous

Event(s) None recommended

Logic Recommend an Insurance statement to keep from exceeding


the maximum limit:

if (icurrentValue > progressBar1.Maximum)


icurrentValue = progressBar1.Maximum;

Chapter 10 - Form Controls and Events Page: 676


ProgressBars are easily simulated in code. Instead of using a loop, this example uses
multiple clicks of button1 to grow the progressBar by 10, this way you can watch the
growth in slow-motion. Do the following in your example program:

1. From the Toolbox flyout menu, drop a Progress Bar on the form.
Size the bar to any width and thickness desired.

2. Drop two "labels", which will be a numeric indicator on progress.


Label1's name = "lblfrontCounter"; default Text Value = "0" (zero)
Label2's name = "lblbackCounter"; default Text Value = "0"

Label1 will hold the "starting value" (zero) and label2 will be the current numeric value.

3. Set these additional properties:

progressBar1 Visible = true (showing at startup)


lblfrontCounter Visible = false (hiding until needed)
lblbackCounter Visible = false

When the program runs, the numbers remain hidden until sparked to life by another event,
in this case, button1. This makes for a nice touch in the program's design.

4. In design view, highlight the ProgressBar, then Properties.


Confirm the "Maximum" (not MaximumSize) is set to 100. This can be set to any upper
number, such as the number of records in a file, an array count, etc.. For this example,
use a simple 0 to 100 percent, setting the maximum to 100.

5. button1 will trigger changes in the ProgressBar's value by growing 10% with each click.

In design view double-click button1.


Between the opening and closing braces, add these statements:

Chapter 10 - Form Controls and Events Page: 677


Example: progressBar Simulation, preliminary
private void button1_Click (object sender, EventArgs e)
{
//Simulate Progressbar movement with every mouse-click.
//This routine will fail past 100
//Note: int iCurrentValue needs to be defined higher-up, above
//this routine.

lblfrontCounter.Visible = true;
lblbackCounter.Visible = true;

iCurrentValue = iCurrentValue + 10;

lblbackCounter.Text = Convert.ToString(iCurrentValue);
ProgressBar1.Value = iCurrentValue;
}

6. The variable, "iCurrentValue" has not been declared yet. The intention is to click
button1 multiple times, growing from 0 to 10, 20, 30, etc. But the counter cannot be
declared within button1 because it is destroyed each time the routine ends (this is the
"scope" of the variable).

Near the top of the program, after the Form class statements, declare and initialize
"iCurrentValue" as a class-level (form) variable.

7. Press F5 to run the program.


Press button1 multiple times until the program crashes.

Ultimately, "iCurrentValue" grows beyond the Maximum setting of 100 (100, 110, 120)
and the program fails with a run-time error. Stop the running program by clicking the red
"stop debugging" box on the editor's toolbar.

progressBars need insurance to keep from exploding.

Chapter 10 - Form Controls and Events Page: 678


8. A simple if-statement fixes the problem. Add the bolded-logic below, which prevents the
value from growing beyond the maximum. If it does, it simply sets/reset to the maximum,
keeping the program from crashing:

Example: progressBar Simulation, completed


private void button1_Click (object sender, EventArgs e)
{
//Simulate Progressbar movement with every mouse-click.
//This routine will fail past 100
//Note: int iCurrentValue needs to be defined higher-up, above
//this routine.

lblfrontCounter.Visible = true;
lblbackCounter.Visible = true;

iCurrentValue = iCurrentValue + 10;


if (iCurrentValue > 100)
{
iCurrentValue = 100; //Force back to 100%
}

lblbackCounter.Text = Convert.ToString(iCurrentValue);
ProgressBar1.Value = iCurrentValue;
}

Run the program again, clicking button1 11 times.

comments:

• With each click of button1, the variable "icurrentValue" increments by 10. Since the
variable is declared outside of button1's scope, it survives each button click and does
not fall out of "scope" (Chapter 9).

• Labels lblfrontCounter and lblbackCounter are made "Visible" each time the button is
clicked. This is slightly inefficient but an acceptable practice because there is no
other place to do this work. If this were a real program, button1 would be clicked one
time, exposing the controls, then it would call another routine that would do the
actually looping [through a set of records], resolving this minor inefficiency.

• ProgressBars must be prevented from exceeding their maximum value. In the code
above, when iCurrentValue exceeds the upper-limit, force the current value back to
the upper limit. Thus, when icurrentValue calculates to "110", set it to "100". Do
this before touching the bar's value.

If your program triggers this routine, there is a flaw in your logic. For example, a
10,000 record loop should not run 10,001 times. In this example, when button1
reached 100, it should disable button1 - making the button ineligible to click. In
either case, the if-statement is good insurance.

Chapter 10 - Form Controls and Events Page: 679


• Progress Bars are useless in fast-running loops. For example, displaying a progress
bar for any of the short loops in Chapter 3 would be a waste of time because the bar
would grow to 100% in a zillionth of a second. Users would only see a solid-bar on
the screen.

• It is good design to hide the progressBar (Design View, set Visible = false). Expose
only when an event warrants.

Other uses for Progress Bars:

Assume your program has a ten-step process to go through and each step takes a
noticeable but undetermined amount of time to complete. For example, the program may
need to authenticate to a remote server, open and write a few hundred files, print a long
report, upload data to a server, etc.

The amount of time cannot be predicated, but you still want to give users a progress
report. At each step you could bump the progress bar by some small amount just to show
the user something is happening. In other words, even though you don't know when the
process completes, lie about it. This is exactly what most installation programs do.

Consider this pseudo-code:

progressBar1.Value = 5;
A110_LoginToServer();

progressBar1.Value = 18;
A120_WriteNewFiles();

progressBar1.Value = 42;
A200_PrintReport();
etc.

Chapter 10 - Form Controls and Events Page: 680


monthCalendar and DateTimePicker

The Month Calendar allows users to select a date using either the keyboard or the mouse.
The selected date is stored in a DateTime variable of your choosing. On the panel, this
control appears as a full calendar. A sister control, the DateTimePicker, is a less-
complicated object, with less capabilities, but it takes up less real estate on the screen.

monthCalendar1 Summary
[No recommended prefix]
Use dateVariable = date value
monthCalendar1.SelectionStart

Convert.ToString
(monthCalendar1.SelectionStart)

(Property) monthCalendar1.MaxDate = Set the latest and


Convert.ToDateTime("01/31/2010"); earliest dates
monthCalendar1.MinDate = DateTime.Now;

monthCalendar1.MaxSelectionCount = 1

Event(s) monthCalendar1_Leave This is an ok event.

monthCalendar1_DateChanged
monthCalendar1_DateSelected

See also Chapter 5 and 21 on date formatting and use.

Chapter 10 - Form Controls and Events Page: 681


The monthCalendar's controls are numerous.

• Each date is a clickable


• Dates can optionally be highlighted as a range
• The two arrows on the top cycle previous and next years
• Clicking the month (e.g. "January"), displays a comboBox pull-down list
• Clicking the year (e.g. 2014) displays a list-box

Some users will not know how to use the controls

The datePicker control takes less real estate but most users will be unfamiliar with how to
actually use it.

Chapter 10 - Form Controls and Events Page: 682


Building the MonthCalendar:

From the ToolBox flyout menu, click and drag MonthCalendar to the form. This is a
large object that occupies a lot of space on the screen. Later examples show how to tame
this.

Highlight the Calendar and note these Properties:

.MaxDate "mm/dd/yyyy" This is the furthest in the future you'll allow the calendar
to go. You could, for example, restrict the user to only
one month in the future. This value should be set
programmatically, not at design time.

.MinDate "mm/dd/yyyy" This restricts the user to the earliest date they can select
and it is often set to today's date, or perhaps tomorrow.
Again, set programmatically. For example, a "Departure
Date" might always be set to "tomorrow", based on a
workstation or server's current time.

.MaxSelectionCount
1 (one) Limit the user to select one date by setting this to 1 and
this is the most common setting. Use larger numbers for
a range (7 for 7 days). Multiple dates are stored in an
array.

Warning on Min and Max Dates:

Min and Max dates are often set in the form's Load event, illustrated below,
but this can introduce flaws. What if the user launches the program on
Monday and doesn't close the program until Friday, leaving it open and
running all week. The Min and Max dates will be wrong. If this is a
possibility, set these values in another event that fires more often.

Form Load Event:

1. Place a MonthCalendar on the form.

2. Double-click the main form's title bar, taking you to the form's Load event.
In the event add logic allowing the user to select any date, starting with today's date and
for the next two weeks – limiting dates to no more than 14 days in the future.

Chapter 10 - Form Controls and Events Page: 683


Example: Setting a Calendar with min/max Dates and Times
private void frmProcess_Load (object sender, EventArgs e)
{
//Programmatically setting the calendar's date-time ranges
//Use caution here. Consider what would happen if the user left
//the program running for several days

this.show();

monthCalendar1.MinDate = DateTime.Now;
monthCalendar1.MaxDate = DateTime.Now.AddDays(14);
monthCalendar1.MaxSelectionCount = 1;
}

Many Excel users make the mistake of using ".Now() – with parenthesis", as
in, = DateTime.Now(); The compiler generates this error message:
"System.DateTime.Now' is a 'property' but is used like a 'method'." Remove
the parenthesis.

Dates can be limited to .AddMonths(xx) and .AddYears(1).

3. In button1's click's event, report on the date selected with a MessageBox.

Chapter 10 - Form Controls and Events Page: 684


monthCalendar, Show selected date, completed
private void button1_Click (object sender, EventArgs e)
{
MessageBox.Show ("The chosen date was: " + "\r\n" +
monthCalendar1.SelectionStart.ToShortDateString();
}

where:

• If "MaxSelectionCount = 1", the "SelectionStart" and "SelectionEnd" (not used in the


example) are the same date and the user is not allowed to select a date-range. If a
date-range is allowed, using "MaxSelectionCount = 5", the user could click and drag
or control-click within the calendar, selecting up-to 5 multiple dates.

• "SelectionStart" is a "Date" variable. Keep in mind there are strings, integers,


booleans, and dates. By default, if no "time" is set, all dates get 12:00:00 AM – but if
this control is hooked to ".Now", it absorbs the time the control was clicked.

• If MinDate and MaxDate are set, dates outside of those ranges are not accepted. See
the illustration above, which limits the user to a two-week block.

• In the Message Box, the "\r\n" are carriage-return-linefeeds that add a cosmetic line
break in the dialog. "\r\n" are ASCII-text strings and can be included as part of
another string or concatenated as separate strings.

• monthCalendar1.SelectionStartDate.ToShortDateString() gives a mm/dd/yyyy


result. Using .ToLongDateString gives "Sunday, January 15, 2017". You could also
include times with other options. See Chapter 21 Formatting.

Setting Different Start Dates:

From the examples above, the calendar's starting date can be set to a week prior, + 1 day
(Jan 8th), and extended two weeks beyond today, giving slightly more than three weeks.
Highlight the date 7 days ago as the recommended default (solid blue square "9th").

If the user took no action, button1_Click would report January 9th was the
selected (default) date.

This example uses date-time math, described next:

Chapter 10 - Form Controls and Events Page: 685


Force any date variable, or the calendar object, to a specific date by setting the
"SelectionStart" property using this syntax. Notice the word "new":

Manually Setting a Date


monthCalendar1.SelectionStart = new DateTime(2017, 01, 01);

Date-Time Math:

Similar to the example above, calendar math can be performed against any calendar or
date-variable.

Date-Time Math
.AddDays(n)
.AddHours(n)
.AddMinutes(n)
.AddMonths(n)
.AddYears(n)

To Subtract (Days), use


.AddDays(-n)

Chapter 10 - Form Controls and Events Page: 686


Other Date Methods:

Rather than referring to the calendar's selected date, "monthCalendar1.SelectionStart",


convert the date to a standard DateTime variable and then use that variable throughout the
remainder of the routine.

DateTime myChosenDate = monthCalendar1.SelectionStart;


MessageBox.Show(Convert.ToString(myChosenDate.DayOfWeek));

or

MessageBox.Show(myChosenDate.DayOfWeek.ToString());

which returns "Monday". Notice the variable is declared as a type "DateTime", which is
similar to integer, string, boolean. From here, it is easy to examine these other DateTime
properties and methods:

myChosenDate.Date; DateTime: 1/22/2014 12:00:00 AM


myChosenDate.DayOfWeek; Monday, Tuesday, etc.
myChosenDate.Month; integer: 1 = January
myChosenDate.Day; integer: 22
myChosenDate.Year; integer: 2014

myChosenDate.ToLongDateString(); Monday, January 22


myChosenDate.ToShortDateString(); 1/22/2014
myChosenDate.ToLongTimeString(); 12:00:00 AM
myChosenDate.ToShortTimeString(); 12:00 AM

You can find all these properties and methods after you type the variable-name.dot. If
monthCalendar1 is used directly, suffix the name with ".SelectionStart" – an actual date is
required before it can be converted to a string. For example:

string selectedDate =
monthCalendar1.SelectionStart.ToShortDateString();

See Chapter 21 for additional formatting information.

Chapter 10 - Form Controls and Events Page: 687


Chapter 10 - Form Controls and Events Page: 688
MenuStrip

A menu can be constructed below the application's title bar using the "MenuStrip" tool.
What you place in the menu is completely under your control but ideally each major
function (typically buttons) are represented. This gives users the choice between the
mouse or the keyboard. Menus are a good place to tuck seldom-used functions (such as
month-end processing) without occupying valuable screen real estate.

Building a MenuStrip:

The sample "Process" program you have been working with does not have a lot to do and
this means a menu-strip would be boring. Simulate new menus with a series of message
boxes and other useless functions.

1. In the form's design ToolBox, drag a MenuStrip to any location on the form. Notice this
is in the "Menus and Toolbars" section, not in Common Controls. Once dropped, it puts a
place-holder/token at the bottom of the design screen, "MenuStrip1", illustrated below.

2. Along the top of the form, click the black arrow in the upper-right of the new menu.
Click "Insert Standard Items. This brings up the menu editor with normal File, Edit,
Tools and Help. These is a useful starting point.

Chapter 10 - Form Controls and Events Page: 689


3. Click inside of the Tools menu, then click the "Type Here" box just below "Options".

Type "C&alendar" in the first box; ignore the other two boxes that appear (note the
ampersand). The ampersand sets a keyboard shortcut "alt-a". "C" is unavailable because
it is occupied by the "Customize" menu.

4. Highlight the "Calendar" menu choice. Double-click the word "C&alendar" to open the
Click-event in code view. Optionally, highlight the menu-item and in the Event Property
(Lightning Bolt icon), double-click the "Click" event.

In the new routine, write a call to the already-existing "button1_Click" event from the
previous sections (for lack of a better place). button1_Click's signature requires two items

Chapter 10 - Form Controls and Events Page: 690


in the parenthesis, but since you have nothing important to pass, fill with nulls -
"button1_Click (null, null)":

private void calendarToolStripMenuItem_Click (object sender, EventArgs e)


{
//Call the Button1_Click event, passing two dummy values through
//the signature:

button1_Click(null, null);
// (Or substitute a simple MessageBox.Show command here)
}

Notice the event's name: "calendarToolStripMenuItem_Click", which is fairly long. All


the menus will get similar names. The first word, "calendar" is significant.

Menu events should seldom run their own code; instead, they
should call other routines, which were probably written earlier.
If building a new menu choice for something not-yet-written, such
as "Month End Processing", call a separate function rather than
clutter the menu-event with logic. In this case, make up the
routine's name with something like
"A330_MonthEndProcessing( );"

5. Return to design view and open the "File" menu.

Highlight "Exit"
Add a "Click" event that calls the same "bClose" event written when the form was first
built. Again, pass dummy or empty values through the signature:

private void exitToolStripMenuItem_Click (object sender, EventArgs e)


{
//Call the previously written button Close event:
bClose_Click ("", null);
}

6. For the experience, delete the "Options" menu. Highlight "Options"; other-mouse-click
and choose delete.

Test the Menu:

Press F5 to run the program.


Choose Menu Tools, Calendar.
Choose Menu File Exit.

Deleting Menu Strips:

To delete a menu strip, delete the menuStrip1 placeholder at the bottom of the form's
design. This will not delete code called by the menus and those routines become
orphaned and should be deleted separately.

Chapter 10 - Form Controls and Events Page: 691


Horizontal and Vertical Lines

Visual Basic 6 was able to draw a decorative horizontal and vertical lines in form design.
These can help visually separate one section of the panel from the next and I typically use
them to divide the form's buttons and Message-lines from the main part of the program.
Oddly, newer Visual Studio editions still cannot draw lines (including vs2017).

This section shows how to simulate the lines with a graphics box. This is less-than-
graceful, especially if you later decide to change the width of the form.

Drawing a Horizontal Line:

1. In design view, select the "PictureBox" control from the Common Controls ToolBox
menu.

2. Drag a horizontal box on the form, using any height. Size the box's width to roughly the
width of the form. Make sure the width is as wide as needed; this is hard to adjust once
drawn. For now, ignore the height of the box. The default name, pictureBox1, is
adequate.

Chapter 10 - Form Controls and Events Page: 692


3. With the line still highlighted in design view, set these properties:

BorderStyle = FixedSingle
Enabled = False

Inside of the +Size property (click the plus)


Height = 1

4. Drag the line (really a thin box) into the desired position or better yet, highlight the line
and press up and down arrow keys to move vertically in the form.

Adjusting the Width:

Because the graphic is so thin, it is often easier to delete and rebuild rather than adjusting
the width. Optionally, highlight the form's background and note the form's (Property,
+Size) width. Then, highlight the picture box and manually type a new width, 10 pixels
or so smaller than the screen; illustrated above with "473".

Chapter 10 - Form Controls and Events Page: 693


ToolTips

ToolTips are small, floating help boxes that show when a mouse hovers over a control.
Each tooltip is built by hand from the form's design view and before they can be used, the
feature must be enabled via a ToolBox control

Tooltips are recommended in most programs because they give the users to give users
unobtrusive help on what a particular button or icon does.

Enabling ToolTips:

1. In the form's design view, click the form's Titlebar to select the entire form (or click the
form's background).

2. From the ToolBox Flyout menu, select "+Common Controls" and drag the ToolTip tool
anywhere on the form. "toolTip1" appears as a place-holder near the bottom of the design
screen; the object does not show on the actual form.

3. The toolTip object enables a new property on each of the form object's (illustrated below).
The toolTip control also has its own properties; highlight the toolTip1 control, near the
bottom of the design-view's editing screen. Notice various properties which can be set:

Active = True
BackGroundColor = "Info"
Use Fading, etc.

Chapter 10 - Form Controls and Events Page: 694


Using ToolTips:

toolTip text is set manually on each field, button, or other object, as needed. To avoid
clutter, not all items deserve a toolTip.

4. In design view, highlight any text field, label or graphic, etc.. In the Properties window,
near the bottom of the list, locate the (field's) "ToolTip on toolTip1" property-line (this
property is only visible when the ToolTip1 control is added to the form).

5. Type a short line of text describing the control. This text appears when the mouse hovers
over the field while the program is running. It is best to keep the phrases to a few words.

Testing toolTips:

In any test program, enable the toolTips and liberally add toolTips to a variety of objects
on the form.

Press F5 to run the program.


Hover the mouse over any field with a toolTip.

Programmatically Setting toolTip Text:

Use this code to set or change an objects ToolTip text at run-time, placing in any location
in the code where the tip needs to change:

Chapter 10 - Form Controls and Events Page: 695


toolTip1.SetToolTip(button1, "New ToolTip Text goes here");

where:

• Note you are not setting this on button1. The class is "toolTip1.SetToolTip". You
are passing parameters to another object – and not to the current form.

Programmatically Building ToolTip1:

As an aside, the entire toolTip features could be enabled in code, bypassing the toolTip
object which was previously dragged on the form. If you use this method, all toolTips
must be set with code and none will be visible in the form's design view. This is not
necessarily recommended:

1. Declare the toolTip object at the top of the form's form-level variables, keeping the
toolTips visible to all methods within the Form:

Declare toolTips in Code (instead of using a toolbox object)


public partial class form1 : Form
{
ToolTip toolTip1; //Set params below in Form Constructor...

: etc

2. Then, within any other method, add the following logic. I recommend placing the code in
the form's constructor logic:

Enabling ToolTips
public form1()
{
InitializeComponent();

//Create the ToolTip and associate with the Form container.


//(Declare ToolTip toolTip1; at the FormLevel)
ToolTip toolTip1 = new ToolTip();
toolTip1.AutoPopDelay = 2000;
toolTip1.InitialDelay = 1000;
toolTip1.ReshowDelay = 500;

Chapter 10 - Form Controls and Events Page: 696


ToolBox: "PictureBox" Icons and Buttons

Buttons can be labeled with text or you can attach an image using the "BackgroundImage"
property. However, instead of that, I like to place images directly on the form, bypassing
the button control, then adding a click_event to the image – treating it as-if it were a
button.

Adding a PictureBox Element:

1. With your favorite graphic editor (Photoshop®, PaintShopPro®, or even MSPaint, create
a small graphic (in the illustration above, a Notepad and Help Icon were used).

Crop the graphic tightly and save as a .PNG, or .JPG (JPEG). Save with no compression
(see your photo-editor for details). The image background should be transparent (if PNG)
or, if .JPG, set the background to the same color as the form – usually, but not always
white.

Organize your work:

Chapter 10 - Form Controls and Events Page: 697


Using Windows File Explorer, find the Solution's directory (where the Visual Studio
Project is being stored. In the Solution's root directory, next to the *.sln file, create a sub-
directory called "images". Save the graphic here. Once saved, continue with these steps:

2. Using the PictureBox tool, Click and drag a rectangle approximately the same size as the
graphic; it can be resized later. The new object is named "PictureBox1".

3. In the Properties pane, locate the "Image" property. Click the ellipsis...
Choose "Import" and locate the saved PNG/JPGfile

4. Give the graphic object an internal name other than "PictureBox1", for example,
BtnLaunchNotepad.

5. Set the pictureBox.BorderStyle = none.

where:

When using PictureBox graphic elements, keep these thoughts in mind:

Chapter 10 - Form Controls and Events Page: 698


• Imported graphics generally need the same background color as the main Form. If the
form's background is not white, this invariably requires some work with a photo
editor. If you can use PNG's, use a transparent background on the image.

• Use PNG or JPG files (BMP if you must) and always size the graphic in the photo
editor, not in Visual Studio. Crop the graphic tightly.

• Graphics can be "layered" in front of or behind other objects. See Format, Order,
"Bring to Front" and "Send to Back". If they disappear unexpectedly while editing
the form, select them from the object-pull-down list in the upper-right corner of the
design view editor, then bring them to the Front. Visual Studio does not overlap
graphics very well.

Events:

To create a click-event, follow these ideas.

Start by giving the pictureBox a meaningful name (e.g. BtnLaunchNotepad).

Highlight the graphic. In the object's Properties screen, switch to the Events list
(Lightning Bolt icon) and scroll to the "Click" event. Double-click the empty field to
stub-in the event. The event can open other forms or do any type of logic you would
normally associate with any other button event.

It is common to have the Form_Load event to hide the graphics (which essentially
disables them) and only exposes them if a proper user-id is used or some other event.
Use:

PictureBox1.Visible = false;
btnLaunchNotepad.Visible = false;

Launching Other Applications From the Graphic:

For example, this code can be used to launch another application from a graphic_Click
event. See Chapter 18 for more details.

Summary: Launching Notepad as a Separate, Independent Thread


private void btnLaunchNotepad_Click (object sener, EventArgs e)
{
System.Diagnostics.Process proc = new System.Diagnostics.Process();
proc.EnableRaisingEvents = false;
proc.StartInfo.FileName = "notepad.exe";
proc.StartInfo.Arguments = "testfile.txt"; //from current directory
proc.Start();
}

Chapter 10 - Form Controls and Events Page: 699


Summary: Launching Notepad as a Dependent Thread (WaitFor)
private void btnLaunchNotepad_Click (object sender, EventArgs e)
{
//Launching as a dependent thread - WaitForExit

System.Diagnostics.Process procNotepad =
new System.Diagnostics.Process();

procNotepad.EnableRaisingEvents = false;

procNotepad.StartInfo.WindowStyle =
System.Diagnostics.ProcessWindowStyle.Maximized;

procNotepad.StartInfo.FileName = "notepad.exe";
procNotepad.StartInfo.Arguments = "";
procNotepad.Start();

procNotepad.WaitForExit(); //Order dependent after Start

MessageBox.Show("Returned from Notepad");


}

Chapter 10 - Form Controls and Events Page: 700


Link Labels

Link Labels are hyperlink labels that launch the workstation's default browser, to an
address of your choosing. The labels act much like standard labels, except two sections of
code are required to enable the webpage hyperlink.

Building a Link Label:

1. In form design, use the toolbox to drag a "LinkLabel" from the Common Control section.

2. In the label's properties, change the name from "linkLabel1" to a name of your choosing.
These examples remain named as linkLabel1.

Change the label's Text property from "linkLabel1" to a cosmetic name of your choosing,
such as "www.keyliner.com"

3. In the label's Events Properties (see Properties, Lightning Bolt icon), double-click the
empty LinkClicked box to stub-in a new method

Chapter 10 - Form Controls and Events Page: 701


4. In the newly-constructed method, add this line of code:

linkLabel Event Method, complete


private void linkLabel1_LinkClicked
(object sender, LinkLabelLinkClickedEventArgs e)
{
System.Diagnostics.Process.Start(e.Link.LinkData as string);

//Additional code is required in the Form1_Load module


}

5. The link's destination is defined at a higher level in the program, typically in the
Form1_Load event. If your program does not have a Form_Load event, open the form in
design view and double-click on the form's background to create the method.

Add this required code anywhere near the top of the event.

Associating a Link to a linkLabel, complete


private void Form1_Load(object sender, EventArgs e)
{

//Associate the linkLabel with a website of your choosing.


//Be careful of upper and lower-cased "Links".

LinkLabel.Link mylink = new LinkLabel.Link();


mylink.LinkData = "http://keyliner.blogspot.com";
linkLabel1.Links.Add(mylink);

//Other code in the load event...


}

Chapter 10 - Form Controls and Events Page: 702


where:

• Use care when typing the logic for this event. There are mixtures of upper and lower-
cased "links".

• The association is attached to the form, not to the actual link label. Multiple links
require different names, such as mylink, mylink2, etc.

• Note the webaddress slashes are slashes, not backslashes.

Testing:

Testing is easy. Press F5 to launch the program. Click the link. The workstation's default
browser should open in a separate window, arriving at the defined site.

Chapter 10 - Form Controls and Events Page: 703


Label Tricks

When Forms have wallpaper or background graphic or when a Form has an unusual
background color, standard textBoxes look unprofessional. If the textBoxes are
"informational-only" and the user does not need to type in the fields, Labels are the better
choice because they give more control on the formatting.

Labels (toolbox, Common Controls, Labels) can be made with transparent backgrounds
(so the wallpaper or form-color shows through) and the labels can be right, left or center
justified.

Transparent Labels:

On any form with either an unusual background color (such as "White Smoke" instead of
white) or with a graphic-background, as illustrated below, place a label and make these
property changes (using design view's Properties window):

AutoSize = False
BackColor = (Choose the Web tab, then Transparent; even if not a Web program)
ForeColor = White or Black; depending on your background image.

This makes the label blend into the background. Sadly, you cannot format textBoxes as
transparent, even though the menu choice is available (tested in all versions of Visual
Studio, including 2017).

Chapter 10 - Form Controls and Events Page: 704


j For variable labels (where the text changes during run-time), control the maximum
width of the label by setting AutoSize = False and dragging to the width you want to
allow.

Using Labels in Your Program:

Treat a Label exactly as a textBox. To populate a field, note the dot-Text.

label1.Text = "Some data in the field";

When to Use Labels:

Labels are useful for "display-only" fields. Good examples would be for demographic
information, message fields, record numbers, etc. They can also have Click Events -
acting as a button or on-screen menu.

Chapter 10 - Form Controls and Events Page: 705


Tab-Order

No form is complete without spending time on the field tab order. Tab-order specifies
how the cursor hops from field-to-field when the user presses Tab on a data-entry screen.
A logical tab-order gives your program a professional feel and it makes "heads-down"
data-entry clerks happy.

By default, the tab-order is determined by the order fields, buttons and other controls were
placed on the form when designed and the order is invariably wrong.

Setting a Form's Tab-Order:

Setting tab order is tricky. Study the panel and have a mental picture how you want the
tabs to move. Consider all data-entry fields and buttons. Ignore labels and graphics.
Steps:

1. In form's design view, choose View, "Tab Order" from the top menu.
This is a toggle and it remains on until re-selected. All fields and objects in the form are
assigned a number (see illustration).

2. Click each data-entry field and button, in the order you want the tabs to progress. If you
do not want a field to be in the tab-order, do not click it. Ignore labels and their assigned
tab-number.

j Important: To exit the Tab-Order design, re-select View, Tab-Order.

Chapter 10 - Form Controls and Events Page: 706


3. For reasons only known to the Microsoft gods, labels get a tab order. This bugs me to no-
end. Fix with these steps:

a. Exit Tab Order


b. Highlight each label
c. In Properties, set the Tab Index to 0 (zero). All labels can share the same tab index.

To remove a field from the tab-list, set "Tab Index" to zero, and set "Tab Stop" to false.

This concludes the Forms chapter.

Chapter 10 - Form Controls and Events Page: 707


Chapter 11 - Calling Multiple Forms Page: 708
An Absolute Beginners Guide to C# - Volume 1
Visual Studio C# 2017
Intro -Through Forms
by Tim R. Wolf
2017.06 Version 1.04
Table of Contents

9 Chapter 1 - Introduction to the Editor 3


Your First Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Variables and Scope. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Working with Text Boxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Naming Fields
Concatenation
Default Text Values

9 Chapter 2 - Introduction to Loops 45


"while" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
loopCounter
Appending a String
Incrementing a Counter
Incrementing with "++"
Concatenating to Self with "+="
MessageBoxes
Infinite Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Breaking into the Loop
"while" Loop - Printing Numbers 1-10. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
"do" Loops.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
for-next Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Carriage-Return/LineFeed (CRLF)
Variations on "for-next" Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Controlling Loops with Variable textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Interrupting Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
continue;
break Statements
"while-loops and 'continue'
Nested Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
foreach Loops. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Loop Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110

9 Chapter 3 - Conditional Branching 117


Numeric and String Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Basic if-statement Construction
Numeric "if" Statements
Brace Style
"else"
Semicolon Rules
&& (AND) || (OR) Booleans. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
"|" and "||" (OR) Boolean
"^" (XOR – Exclusive OR) Boolean
Nested if-statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
"else if"
Math.Min - Numeric Testing for Smaller Value.. . . . . . . . . . . . . . . . . . . . . . . . . . 139
Compounding if-Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
switch Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Case "Value":
default Clause
Required breaks
Compound case Statements
Case-sensitive switches - .ToLower()
goto Statements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
Ternary Operator. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

9 Chapter 4 - Strings 163


Declaring Strings.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Special Character Strings (Escape Codes) \t, \r\n. . . . . . . . . . . . . . . . . . . . . . . . . . 167
Reserved Backslash
Carriage-Return-Line-Feeds CRLF
Embedded Quotes
Verbatim Text Strings - @
ASCII Codes
String Concatenation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
string.Concat ( ) and "+"
Floating Point Numbers
string.Compare( )
<string>.EndsWith. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
Detecting .XLS file Extensions
<string>.Length. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
<string>.ToUpper( ), .ToLower( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
<string>.PadLeft(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
<string>.PadLeft(int, '*');
Date Padding with Leading Zeros
<string>.Trim( );. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
<string>.TrimStart()
<string>.TrimEnd()
Trimming with other Characters
char.IsNumber ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
<string>.Replace( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Character to Numeric Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Conversions with Casting
null and Empty Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
String.IsNullOrEmpty( )
Null-Conditional Operator
Testing for "blank". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
if ((strtest + "").TrimStart().Length == 0)
Finding Strings - IndexOf. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Reading an Overload
<string>.LastIndexOf
<string>.Contains
Parsing and Substrings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
<string>.Substring. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Classic Left-strings .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Classic Right-string. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
Mid-Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
"Mid-String" for any Length Delimiter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253

9 Chapter 5 - Numbers and Dates 265


Integers Defined. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Floating Point Numbers Defined.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Casting and Conversions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
Implicit Conversions
Explicit Conversions
try-catch Error Trapping. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
Converting Strings to Numeric. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
TryParse
Rounding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Math.Round
Truncating Decimals. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
Math.Truncate
Basic Math Functions (Division). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
DivRem (Divide Remainder) / Mod. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
Math.DivRem
Mod "%"
Other Math Functions (SQRT, etc.). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298
Sqrt (Square Root)
Random Numbers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
Dates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
DateTime.Now
String.Format
Date Parts
Date Format Pictures
DateTime.TryParse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
UTC (Zulu) Time
Empty Dates - Nullable Dates
DateTime.Compare. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
Date.Compare: Two Files
Less Exact Date Comparisons
Rounding Dates
Optional Project: MTWRFSU.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
Optional Project: AllowByHour. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327

9 Chapter 6 - Utility Functions - Methods 333


IsBlank( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
IsFilled( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
LeftStr, RightStr and MidStr string Functions. . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
Classic LeftStr ( ) "Left-string".. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
Left-string with String Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Classic Right-String with Numeric Parameters.. . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Right-string using Delimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Mid-string with Numeric Parameters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Mid-string Numeric Start Position but No Length (Overload). . . . . . . . . . . . . . . . 382
Mid-string with string Delimiters and NumberOfCharacters. . . . . . . . . . . . . . . . . 383
Mid-string with Two String Delimiters.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388

9 Chapter 7 - Advanced Utility Functions 395


StripSlashes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
StripTrailingComments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404
StripLastCharacter.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
StripNonNumerics ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
StripNonNumerics, Preserving Decimals and Signs. . . . . . . . . . . . . . . . . . . . . . . . 424
Overloading
ParseBetweenDelimiters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Overloading ParseKeyValue. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
ParseKeyName: Master Function. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
IsNumeric ( ). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463
IsNumbers ( ).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
StripDuplicateCharacters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475
VerifyYN.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Passing Variables by Reference. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481

9 Chapter 8 - Class Libraries 493


Building an External Class Library from Existing Code. . . . . . . . . . . . . . . . . . . . 496
Linking an Existing External Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
Using the util. Class Library (CL800 Util). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
Link vs Copy
Inline "Program" Class Libraries (PayrollTools). . . . . . . . . . . . . . . . . . . . . . . . . . 513
Using CL800_Util within the new Class.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 520
Constructors
Manually Building a Class Constructor
Creating an "External" Class Library from Scratch. . . . . . . . . . . . . . . . . . . . . . . . 525
Compiling and Using DLLs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526
Using the DLL
"Modularizing" with Program-Control Functions.. . . . . . . . . . . . . . . . . . . . . . . . . 532
"void" Functions
Passing Variables to Program-Functions
Returning Values from a Program-Function
Naming Standards for Functions and Class Libraries.. . . . . . . . . . . . . . . . . . . . . . 535
Object Prefixes

9 Chapter 9 - Variable Scopes 543


Variable Scope, Demonstrated. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
Scope within Loops
Form-Level (Class) Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 551
"Global" Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554
"Quick and Dirty" Global Variables
"static" Modifier
Form1 to Open Form2
Using a Global Class to Pass Values.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564
Building an External Global Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 568
Getting and Setting Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 577
Building Get/Set Properties
Passing Variables by Ref.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 582

9 Chapter 10 - Form Controls and Events 589


Default Editor Settings - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594
Snap To Grid
Compiler Errors
Starting and Naming a New Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 598
Rename the Form
Recommended Form Properties
"Form Load" event
"this.Show"
Link the CL800_Util Library
AutoClose Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 609
textBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 613
MaxLength
Default Text Value
Enabled
Password Fields
Multiple Lines
Setting Field Properties at Runtime. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 619
textBox Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 625
Enter and Leave Events
TextChanged
KeyPress Events (Intercepting Keystrokes). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 629
Allowing only Numeric Values
UpperCasing as Typed
Intercepting Shift, Alt and Ctrl Keystrokes
Alt-Key events on a button or other object
comboBox and listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 638
Static (Hard-Coded) ComboBoxes
AutoComplete / Type-Ahead
Query the Selected Value
"No Selection"
Using the SelectedIndexChanged
ComboBoxes linked to an External Data
listBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 648
Blank-line Testing
listBox Multiple Selection
Processing Multiple Selected Records
checkBoxes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 660
ThreeState
CheckChanged
CheckStateChanged
Click
Radio Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 669
Radio Button Events
Grouping
ProgressBar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 676
monthCalendar and DateTimePicker. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 681
.MaxDate
.MinDate
Form Load Event
Date-Time Math
MenuStrip. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 689
Horizontal and Vertical Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 692
ToolTips. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 694
ToolBox: "PictureBox" Icons and Buttons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
Launching Other Applications From the Graphic
Link Labels. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Label Tricks.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 704
Transparent Labels
Using Labels in Your Program
Tab-Order. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 706

9 Chapter 11 - Calling Secondary Forms 711


Opening Secondary Forms - Simple. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 716
Issues with a Simple Form Call
Using a Global Variable to Re-Open Form1.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 724
Using a FormReference to Open Form2 - Recommended. . . . . . . . . . . . . . . . . . . 727
"FormRef" properties
Modal and Modeless Forms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 735
Transferring Data Between Forms using Global Variables. . . . . . . . . . . . . . . . . . 737
Using Quick-and-Dirty Global Variables
Using a Global Class (Global Variables)
Retrieve the Stored Value in Form2
Passing Data with a Signature and a Constructor. . . . . . . . . . . . . . . . . . . . . . . . . . 741
The Parent's Call
The Child (Form2) Constructor
Passing Values using get-set Properties - Recommended. . . . . . . . . . . . . . . . . . . . 745
Form Starting Positions - Global Class. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 753
MessageBox Overloads. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 756
Custom Dialog Boxes: NS810_Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 759
Returning Results to the Parent via Properties
Custom InputBoxes: NS815_InputBoxDialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . 777
Detecting a KeyPress ENTER Key Event.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 785
Using NS815 to Pass Arrays instead of Strings. . . . . . . . . . . . . . . . . . . . . . . . . . . 787
Multiple Input Screens in the Same Program. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 789

9 Appendix A - Compiler Error Messages 3

9 Appendix B - Compile and Distribution 38


Cheap and Easy EXE Distribution:.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Virus Risks
EXE Icons. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Using Visual Studio to Edit Icons
Attaching Icon Files to your Project
Creating Publishing / Distribution Packages.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Generating a Creator Tag
Introduction

Audience:

This book is volume one of a three volume set and is intended for beginning Microsoft
Visual Studio C# (C Sharp) programmers, versions 2015 through VS 2017. This is a
hands-on book, with actual programs, starting in Chapter 1 and it talks about the
practical day-to-day, nuts-and-bolts programming that real people need to know.

It assumes no prior programming experience.


Little time is spent on theory.

Each topic has step-by-step instructions with numerous code examples and over 900
cropped and annotated illustrations. It explains the "why" of a program.

Prior Experience

If you already have moderate programming experience, especially in Visual Basic, this
book will expand your skills, giving confidence in the new language.

The goal is to have as much time on the keyboard, working with common business
problems. After studying this book and working through the examples, you will be a
proficient programmer – able to write real programs that do real work. You will be able
to read files, parse data, write to database, and build data input screens.

Why this book?

This book is different than most publications. Little time is spent on theories and
technical side-trips are rare. Starting in Chapter 1, you will immediately begin working
with loops, if-statements and string-manipulation. This means some topics, such as
conversions, numeric types, and other such concepts, are glossed over until they are more
germane to the concepts being taught.

Where other publications might spend a page or two on a topic, this book dives into the
most common and most useful ways to solve a problem. For example, over 80 pages are
devoted to opening multiple forms and how to pass data between them. The parsing
chapter devotes 70 pages to this subject, covering delimiters, CSV, Tab, Excel, and other
techniques. This is not over-kill. You will find these address real-world data-processing
problems. I cover the tips and tricks you will need to know.

Chapters often show different techniques for the same problem and the benefits and
drawbacks of each are explained. If there is a chance of making a mistake in
punctuation, style, or logic, the examples show how to resolve them. Compiler errors are
scattered throughout the book and there is a comprehensive alphabetic error reference in
the appendix, showing likely causes and recommendations.

A side-effect of this first volume will be a library of utility modules that can be used in
all of your programs. These utilities can automate mundane tasks, such as parsing
delimited files, punctuating phone numbers, street-addresses, and capitalizing proper
names. These libraries will save boat-loads of time and will be literally useful in all your
programs.

You will also notice none of the examples use Console-applications (DOS-like
programs) – all are Windows forms. Besides being more visually interesting, they allow
greater feedback while developing and the programs more accurately reflect what
happens in the business world.

What this book does not cover:

I do not cover some of the more theoretical underpinnings of modern programming.


Things such as polymorphism and object-inheritance are not discussed directly, but the
techniques are used throughout the book. Advanced programmers may take exception,
but I favor a more procedural background and I always approach problems from a
business-oriented perspective. You have a job to do, files to process, data-entry-screens
to write. These are the things this book is concerned about.

These volumes do not cover web-development but the programming skills taught are
100% transferrable. SQL databases are introduced, but four lengthy chapters only
scratch the surface. However, if you fear treading in this area, these four chapters will
get you started and will show relatively advanced techniques.

Why C#?:

Microsoft has built a wonderful programming environment which verges on magical.


The language is mature and consistent and can be applied in any conceivable situation.
It is a powerful tool that can solve complex problems.

The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.

How to Use This Book:

These three volumes are teaching books and because of that, it makes a poor reference
guide. To get the most utility, start at page one and work your way through chapters.
Each chapter builds on the previous. It takes time and effort to learn programming. As
you work through the chapters you must sit in front of the compiler and write the code.

This series was divided into three volumes, partly to aid in printing, and partly to make
the chapters more accessible.

Volume 1:
1 Introduction to the Editor
2 Introduction to Loops
3 Conditional Branching
4 Strings
5 Numbers and Dates
6 Utility Functions
7 Advanced Utility Functions
8 Class Libraries
9 Variable Scope
10 Form Controls and Events
11 Calling Multiple Forms
A Compiler Error Messages
B Compile and Distribute

Volume 2:
12 ASCII Files
13 Parsing Tab and CSV Files
14 INI Files
15 XML and App.config Files
16 Windows Registry
17 Reading Excel and Access
18 External Programs (Shell)
19 Wait, Delays, Pauses
20 Printing
21 Formatting

Volume 3:
22 Arrays
23 File Manipulation
24 Console Applications
25 SQL Databases
26 SQL Record Edits
27 SQL Data Grids
28 Data Grid Cell Editing
C Installing SQL Server Express
D Routines (of Interest)

Thank you

Thank you for purchasing this book. I hope you enjoy it as much as I have had writing it.
Comments and suggestions are welcome.

traywolf at keyliner com


www.keyliner.com

This book was written with Visual Studio 2013 through 2017's Community Edition, with
sections referencing Microsoft Office and Microsoft SQL Server 2016 Express.
© 2017 by Tim R.Wolf. All rights reserved.

Original text written with WordPerfect version X7. Illustrations created with Corel's
PaintShop Pro, version X8.
The Compiler and Other Tools:

As of mid 2017, Microsoft allows you to freely download a fully-capable version of


Visual Studio. The software is free and this book was written with these versions. See
this link:

http://www.visualstudio.com/en-us/products/visual-studio-community-vs

This book is applicable to VS 2010 and newer, with an emphasis in version 2017.

For the database chapters you will need a copy of Microsoft MS-SQL database or a copy
of Microsoft's free downloadable "MS-SQL Express". Details can be found in the
Appendixes.

Everything in this book was designed to run on a stand-alone (home) workstation,


including the SQL chapters. You will not need a server, external SQL database or
Active Directory to complete the chapters. However, when useful, references to these
other resources are made.
Absolute Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 11 - Calling Secondary Forms
Chapter 11 - Calling Secondary Forms

On the surface, having a program open a second form is relatively easy; Instantiate a new
form and you are on your way, but the net's most commonly recommended way to do this
is flawed and better techniques are required. This chapter explores several ideas and
discusses the benefits and drawbacks of each.

Using techniques from this chapter, you will construct a handy custom input dialog box,
which prompts for information and return results to your program. This is a feature that
should have been included in the MessageBox overloads, but wasn't.

Topics:

• Opening a second form using a simple instantiation (independent forms)


• Re-opening Form1 using a Global pointer
• Intercepting a form-close "X" event
• Returning to Form1 using an "FormRef" - Recommended
• Modal and Modeless Forms
• TopMost (Floating Forms)

• Transferring data between forms using global variables


• Passing data between forms using a Signature and Constructors
• Passing data between forms using Properties - Recommended
• Form2's Starting Position (form XY positions with Global Class)

• MessageBox overloads (Dialog Result Forms)


• NS810_Dialog and NS815_Input Boxes / Custom Dialog boxes
e.g. Form Properties / Class Properties - passing values the proper way

• Multiple, different InputBoxes / Custom Dialog boxes in the same program


• Using "Enter" (Enter-key) as a default action
• Modifying other controls in a multi-form program

(See also Chapter 9, Variable-Scope, for information on sharing data between the forms.
See Chapter 14 "INI Files" for another example of Class Properties and passing values
back to the parent form.)

Why?

When do you need to open a secondary window? It is surprisingly useful in a variety of


programs. For example, a server login prompt might be required, or a preference-section,
where you do not want these settings to clutter the main program. Secondary forms are
often used for more advanced or esoteric functions that need to be hidden until needed.

Overview:

Chapter 11 - Calling Multiple Forms Page: 711


Opening a second form using an "FormRef" is the recommended design (some document
this as an 'InstanceRef'). This opens a second form (panel) and can return to the original.

The Parent Form's (Form1) Call to the Child Form, summary


private void BtnOpenForm2_Click (object sender, EventArgs e)
{
//Recommended logic to open Form2, using FormRef logic.
//Work also needs to be done in Form2

Form2 myForm2 = new Form2();


myForm2.Public_FormRef = this; //get-sets are needed

myForm2.Show();
//myForm2.ShowDialog(); //to launch modal

this.Hide(); //used by this example; not recommended otherwise


//Consider other actions to protect from launching
//multiple copies of Form2

where:

• The Child-Form is called with a FormRef


• Use either myForm2.Show( ) or myForm2.ShowDialog( )

Chapter 11 - Calling Multiple Forms Page: 712


The Child Form (Form2) uses these statements to keep track of the Parent Form:

The Child Form (Form2): Receiving Logic, summary


//Form2's receiving call (FormRef):

private Form private_FormRef = null;

public Form Public_FormRef


{
get => private_FormRef;
set => private_FormRef = value;
}

//Form2's Closing Events:

private void BtnCloseForm2_Click (object sender, EventArgs e)


{
//Return to Form1 using the FormRef setup previously:
Public_FormRef.Show();
this.Close();
}

private void Form2_Closing (object sender, FormClosingEventArgs e)


{
//Return to Form1 when they click "X"
Public_FormRef.Show();
}

where:

• Near the top of the Child Form, build a Private and Public Property
• The Close Event require two routines

Overview: Calling NS810CustomDialog:

This chapter also discusses a custom-built inputbox dialog. This form accepts data-entry
from the end user and returns values back to the parent program, using "Form Properties".

Step 1: Declare

In Solution Explorer, add existing item "NS810_FrmDialog.cs" (as link), and continue
with this code:

Chapter 11 - Calling Multiple Forms Page: 713


Custom Dialog Boxes: Declare the Dialog, overview
using NS800_Util;
using NS810_Dialog;

namespace ExampleProgram
{
public partial class Form1: Form
{
CL800_Util util; //You may or may not have this...
FrmDialog myCustomDialog;

public Form1()
{
InitializeComponent ();
util = new CL800_Util(); //You may not have this one
}
:

Step 2: Call:

Call NS810_Custom Dialog, overview


private void button3_Click (object sender, EventArgs e)
{
//Declare the variable higher up in the program or here:
// FrmDialog myCustomDialog

//Instantiate the new custom dialog at the moment needed


//in order to save memory:

myCustomDialog = new FrmDialog


("This is the main prompt",
"A subprompt can go here and it can be much longer...",
"This is the titlebar text",
"&Cancel",
"&Process",
"&Rework",
"DESKTOP",
null,
0);

myCustomDialog.Public_FormRef = this;

myCustomDialog.ShowDialog();

//Diagnostic code: Remove when done testing:


//MessageBox.Show("Button was: "
+ myCustomDialog.Public_ibuttonPushed);
}

See the chapter for details on how to query the results. See also, a related form,
NS815InputForm.cs.

Chapter 11 - Calling Multiple Forms Page: 714


Overview: Using Class (Form) Properties:

Use Form (Class) Properties to pass values down into and back from a child-class. For
example, from Chapter 14, the downstream Class, can declare properties (variables):

Setting Class Properties in the Downstream Class, overview


//Form/Class-level Variables:

private string private_strProgramVersion = "";


public string Public_strProgramVersion
{
get => private_strProgramVersion;
set => private_strProgramVersion = value;
}

private bool private_boolStartupError = false;


public bool Public_boolStartupError
{
get => private_boolStartupError;
set => private_boolStartupError = value;
}

The calling routine (Form1) would instantiate and call a public method (e.g.
B000_ReadINI). When done, Form1 could retrieve the values stored in the class with
syntax similar to this:

Calling the Class in Form1 and Retrieving Properties, overview


using NS860_INIRead;

private void button1_Click (object sender, EventArgs e)


{
//Instantiate the class
readINI = new CL860_BasicINIRead();

//Call the class public method:


readINI.B000_ReadINI();

//Query the found values


if (readINI.Public_boolStartupError = false)
{
MessageBox.Show(readINI.Public_strProgramVersion);
}
}

Chapter 11 - Calling Multiple Forms Page: 715


Opening Secondary Forms - Simple

Secondary forms can be added to a project and then called using a variety of techniques.
The next several pages discuss incorrect ways to open secondary forms – at least they
would be incorrect in all but the simplest programs.

A later technique, called FormRef/InstanceRef, is the


recommended way to open a secondary form, but these simpler
methods are worth exploring.

Simple Call: Parent Opening Child as an Independent Form:

The net widely demonstrates and recommends opening a second form using a simple form
instantiation. This opens a second form independently of the first -- but the design is
hazardous and fraught with concerns.

The newly-opened forms can behave in a several different ways.

• The second form (sometimes called a child-form) can be independent of the first; the
user can switch between them
• The child-form can be opened, locking access to the first
• The parent-form can be hidden or closed as the child opens

Features of a simple form instantiation:

• Users can switch back and forth between each form and the form's title bars move
independently of each other.

• If the parent (Form1) closes, the child (Form2) also closes because Form1 "owns"
Form2. Form1 is the Master Entry Point (the main procedure) and if Form1 closes
while Form2 is active, Form2's data is lost.

• Users can launch multiple child forms, by design or accident.

• This method is only used in the simplest programs and lacks re-enterability. If Form2
is closed or hidden, you cannot get back to the same "copy" of the form and all data in
Form2 is effectively lost.

Chapter 11 - Calling Multiple Forms Page: 716


Program 11.1: Basic Instantiation; Two independent Forms

1. Starting with a new project:


In the main (parent) Form, accept the default Form1

• Add a button with these properties:


Name: btnOpenForm2
Text: Open Form2

• Add a standard textBox: textBox1, anywhere on the Form.

2. Add a second Form to the project, following these steps:

In Solution Explorer, "other-mouse-click" the application name


(WindowsApplication1/Example Program, etc.)
Select Add
Choose "Windows Form"

Chapter 11 - Calling Multiple Forms Page: 717


For these examples, accept the default name, "Form2.cs," but in real life the Form should
have a meaningful name such as FrmLogin.cs, where the ".cs" extension is required. The
"Frm" prefix is optional, but recommended, making the code more readable.

Once the new form is created, note the new tab along the top row of the Designer
(Form2.cs [Design]).

Chapter 11 - Calling Multiple Forms Page: 718


3. On the editor's top row of tabs, click "Form1.cs [Design]", returning to the original form.

Double-click btnOpenForm2 and add this code.


For now, leave the "this.Hide" statement commented-out.

Program 11.1: Simple Form1 Logic to Open Form2, completed


private void btnOpenForm2_Click (object sender, EventArgs e)
{
//Less than stellar logic to open Form2.cs from within Form1
//Not recommended but often cited on the Web

Form2 myForm2 = new Form2();


myForm2.Show();

//Optionally Hide Form1


//(You can't Close form1 because it is the entry point to your
//program). If you Hide Form1; Form2 has to unhide it or you
//will be stuck in limbo.

//this.Hide();
}

where:

• When Form2 was added to the project, it became a new class within the project:
"public partial class Form2"

Chapter 11 - Calling Multiple Forms Page: 719


• From the code above, notice how the new form is being instantiated as a "new" Form2
class (a copy of the original class). In other words, when button "Open Form2" is
clicked, it spawns a new instance of the other form.

• During the instantiation, Form2 is given a new (variablized) name, "myForm2". This
is similar to "util = new CL800_Util" from previous chapters.

• myForm2.Show( ); displays the form using the class's "Show" method. In other
words, the object (a new instantiation of the Form2 class) has a procedure that 'knows
how to show itself.'

Continue with these changes:

4. Using the editor's top row of tabs, click "Form2.cs [Design]" and add these new
components (illustrated below):

• Add a textBox (which will be used with later examples)


• Add button, btnCloseForm2 (text: "Close Form2")
• Add button, btnHideForm2 (text: "Hide Form2")

Double-click each button, adding these statements:

for btnCloseForm2:
Form2 Logic: Close thyself
this.Close();

for btnHideForm2
Form2 Logic: Hide thyself
this.Hide();

Chapter 11 - Calling Multiple Forms Page: 720


The final results should look like this (Program 11.1):

As a reminder; this method for opening a second form has


numerous flaws even though it is widely recommended on the
web.

Testing Independent Forms:

Launch the program and do the following tests.

A. From Form1, click "Open Form2"


Click between Form1 and Form2; each form becomes active, as clicked.

B. In Form2, type any non-sense text in the textBox


Close Form2
Re-open Form2

Results: Typed-text is missing. "btnOpenForm2" spawned a new instance of Form2.

C. Close Form2 and re-open Form2


Again, type in the textBox
This time, click "Hide Form2"; which returns you to Form1.
Open Form2 again

Results: Typed-text is still missing. Again, "btnOpenForm2" opens a new instance of


Form2. The original Form2 (with your typed text) is still in memory but is un-retrievable
with the current logic.

Chapter 11 - Calling Multiple Forms Page: 721


D. From Form1, click btnOpenForm2 multiple times.

Results: Multiple instances of Form2; this is probably not what you want.

E. Finally, run this test, demonstrating that Form1 is the Master Form:
While Form2 (one or more copies) is open, close Form1 using the "X"

Results: All Forms close and the program ends.

Issues with a Simple Form Call:

Opening Form2 as a new instance with a simple myForm2.Show( ) is widely suggested


but it has numerous problems:

• Users can launch multiple copies of the second Form


• Each copy is unaware of the other; data does not carry-forward
• Form1 is still active (which can be good and bad)
• Data in Form2 can be lost if Form1 is closed

Opening Form2; Hiding Form1:

Continuing with a poor design, in Form1's btnOpenForm2, un-comment the "this.hide"


statement and run the program (see code in previous section).

• With this, Form1 hides itself immediately after opening Form2, keeping the end-user
from clicking the "Open Form2" button a second time (the form is hidden and the user
can't click the button). The issue is how to get back to Form1 once it is hidden. You
will find that a simple "Form1.Show" does not work because Form1 is out of scope.

Chapter 11 - Calling Multiple Forms Page: 722


• If Form2 is closed with the "X" (instead of using btnCloseForm2), Form1 remains
hidden, leaving the program in limbo. In other words, there is a risk when the child-
form is closed, leaving no way to return the previously-hidden Form1. In this state
the program is still running but the user cannot get to the controls.

Form2's Close button could re-instantiate a copy of Form1, using:


Form1 myMainForm = new Form1();

This would certainly re-open Form1, but like the earlier examples, this would be a new
copy of the Form. More importantly, underneath all this would be an unseen mess of
orphaned forms, occupying memory and resources.

There are two ways to work around this. The first uses a global variable and a recorded
pointer back to Form1. This is the easier-to-understand, but less than elegant solution to
the problem. The second solution is slightly more technical and produces better results.
Both are demonstrated here because you may see either method when working with other
people's code.

Chapter 11 - Calling Multiple Forms Page: 723


Using a Global Variable to Re-Open Form1

In order for Form2 to return to Form1 (while preserving previously entered data), declare
a new variable, of type "Form"; this holds a pointer back to Form1. This solution is
flawed because it uses a global variable, but demonstrated because it is often proposed as
a solution to the simple form's shortcomings.

The next section demonstrates a second method for opening a


new form by using a Global Variable to hold the original form’s
address. That method is OK, but not the best.

Program 11.2: Opening Two Forms with Global Variables

1. At the top of Form1, declare a quick-and-dirty (global) variable to hold this name (the
variable could also live in a Program Class library, Chapter 8)

Multiple Forms using a Global Pointer; Variable Setup, completed


public partial class Form1 : Form
{
//In Form1's Form-global area,
//Declare a variable of type "Form1" to hold a pointer.
//Not recommended

public static Form1 Form1Anchor = null;

public Form1 ()
{
etc...

2. In Form1, modify the "Open Form2" button (btnOpenForm2). Just before showing
Form2, take a snapshot of Form1's "address" and store in the newly-created variable:

Multiple Forms using a Global Pointer; The 'Call', completed


private void BtnOpenForm2_Click (object sender, EventArgs e)
{
//Open Form2 with the ability to return to Form1

Form1Anchor = this; //Mark the form's reference in a global var.

Form2 myForm2 = new Form2();


myForm2.Show();
this.Hide();
}

Chapter 11 - Calling Multiple Forms Page: 724


3. In Form2's "BtnCloseForm2" event, add logic that returns the program back to Form1's
original address. This is testable now:

Multiple Forms using a Global Pointer; The 'Return', completed


private void BtnCloseForm2_Click (object sender, EventArgs e)
{
//Close Form2 and return to Form1 using a global variable

this.Close();
Form1.Form1Anchor.Show(); //Restore the pointer
}

Intercepting Form2's Close "X" Event:

This leaves a slight problem: If the user clicks the "X" in Form2, they bypass the
BtnCloseForm2 event and loose the ability to re-open the original Form1. To work
around this, intercept the Close event and manually call the Form Closing event.

4. In Form2's design view, highlight the form's title-bar

• Click on the Properties, Events icon (Lightning bolt)


• Locate the FormClosing event (not the FormClosed event)
• Double-click the event's empty field to stub-in the function and type the following
code

Multiple Forms using a Global Pointer; Intercepting 'Close', completed


private void Form2_FormClosing (object sender, FormClosingEventArgs e)
{
//On FormClosing, pull up the previously-saved pointer to Form1
//Can't call 'btnCloseForm2_Click(null, null)' because it is
//somewhat recursive. For simplicity, repeat the logic
//here

Form1.Form1Anchor.Show();
}

Chapter 11 - Calling Multiple Forms Page: 725


See Chapter 19 "Wait Events" for other ideas on Close logic.

Testing Form1/Form2 (Program 11.2):

Press F5 to run the program. Type something in Form1's textBox, then click
BtnOpenForm2. Close Form2 using the "X" and return to Form1.

Results: Control is returned to the original Form1, complete with its own previous data.

The drawback to this design? It uses a Global Variable and the two forms are linked
permanently by code, noting the reference to Form1.FormAnchor.Show();.

The next section takes this basic idea and uses proper object-oriented techniques to open
the form.

Chapter 11 - Calling Multiple Forms Page: 726


Using a FormReference to Open Form2 - Recommended

The previous solution works well but the global variable builds a dependency between the
two forms. Because of this, Form2's code could not be re-used in other projects. The
variable could be moved to a different class but a dependency would still exist.

A better solution is to pass the original (Parent) form's address into the secondary form
and let that form hold onto its parent, holding the value in a "Class Property." When it is
time to return to the calling program, use that stored address to return to the original.

Using Form Properties (get / set) is the recommended


design for opening secondary forms.

The address will be written into something called a Form Property and you can think of
this as a variable. Forms have properties such as width, background colors, and the like.
They can be extended with new properties, From Form1 you can set values into Form2's
properties and vis-a-versa. Data can travel both directions without resorting to global
variables.

This works with other class libraries and is not restricted


to forms.

The benefit of this design, any program can access the variable and the called-form can be
independent of this project. This same technique can pass any number of values from the
parent to the child form, and most importantly, it allows you to retrieve values on return.

Program 11.3: Opening Two Forms with a FormRef

This example assumes you have removed the previous logic from earlier in this chapter or
start a new project two forms, Form1 and Form2.

1. Setup the call to Form2

In Form1's BtnOpenForm2_Click event, set up the call to Form2.


As before, the statements instantiate a new Form2 – but just as Form2 is opened, pass
Form1's address ("this") to a Form2-entity called "Public_FormRef":

Add this code to OpenForm2_Click event:

Chapter 11 - Calling Multiple Forms Page: 727


Multiple Forms using FormRef; Form1's 'Call', completed
private void BtnOpenForm2_Click (object sender, EventArgs e)
{
//This module is in Form1 and is the logic that opens Form2
//It "passes" its own 'address' into Form2.cs for
//safe-keeping

Form2 myForm2 = new Form2();


myForm2.Public_FormRef = this; //get-sets are needed

myForm2.Show();
//myForm2.ShowDialog(); //to launch modal

this.Hide(); //used by this example; not recommended otherwise


//Consider other actions to protect from launching
//multiple copies of Form2
}

The official-sounding variable name, "Public_FormRef", is an invented name


with no particular significance beyond holding a reference to the current form's
address. Because the variable does not yet exist (it has to be written into Form2),
the editor cannot help with the command and it will gripe, "does not contain a
definition for 'FormRef'...".

where:

• Form2 is the official "named" form, as seen in Solution Explorer's tree view. In this
example, Form2.cs

• "myForm2" is a friendly, reference name, assigned for the duration of this module.

• Notice how from Form1, you are reaching into Form2's variable "Public_FormRef".
You will be "setting" its value equal to something. Public_FormRef does not yet exist
in Form1.

Real-world Example:

In these examples, the Form names, Form1 and Form2, were purposel y demonstrated
with their default names. A more real-world example might read like this:

frmA031AddCategory addCat = new frmA031AddCategory();


addCat.Public_FormRef = this;
addCat.ShowDialog (); //called as a Modal Form

Discussion:

The called-form (e.g. the child-form) needs a modification so it can accept the address of
the parent-form.

Chapter 11 - Calling Multiple Forms Page: 728


Visual Studio 2017 notes:

Starting with VS 2017, Microsoft displays a compiler Note, saying the "Object
initialization can be simplified, changing. Note the lightbulb icon on left margin:

Form2 myForm2 = new Form2();


myForm2.Public_FormRef = this; //get-sets are needed

to

Form2 myForm2 = new Form2()


{
Public_FormRef = this
};

Illustrated in the editor this way:

Clicking "Object initialization can be simplified with change the code to the new design.
I am hard-pressed to see the difference.

2. Open Form2's code view and build the "FormRef" properties.

Two new form properties will be built - one Public and the other private. Where they are
placed is not important but I typically write them near the top of Form2's class, just above
the "public Form2" constructor. Manually type the properties as you would any other
standard method. Locate a blank line between two other methods and begin coding.

Do this work in Form2.cs:

Chapter 11 - Calling Multiple Forms Page: 729


Program 11.3: Multiple Forms using FormRef; 'get-set', completed
public partial class Form2 : Form
{

//Setup controls so calling-forms can find their way home.


//Note the private and Public views
//Other changes: FormClosing: FormRef.Show();
//Calling Form: myForm2.FormRef = this;

private Form private_FormRef = null;

public Form Public_FormRef


{
get => private_FormRef;

set => private_FormRef = value;


}

public Form2()
{
InitializeComponent();
}

where:

• Out of convention, most developers type the get's and sets, with their braces, on one
line. All get-sets follow this same pattern.

• private Form private_FormRef = null;

The statement has a cosmetic "private_" prefix, and is being used to help explain how
the variables work. The variable/property's name's prefix, "private_", is not required
and is not a keyword. But the declaration "private Form" is required. In my own
practice, I still use this nomenclature in my production work because it makes the
variables easy to understand.

Notice the statement ends with a semi-colon and does not have a set of open-and-
close parenthesis because it is not a method.

Being "private", it cannot be manipulated by any other form or class – only by the
methods in this form. This protects it from being called or changed by outside
entities.

private_FormRef is not a string or an integer; it is of type "Form". This is similar to


the global variable built in Solution-1 – but this is a generic Form (variable) and does
not specify either Form1 or Form2. For this reason alone, this is better than the global
variable techniques.

Chapter 11 - Calling Multiple Forms Page: 730


• public Form Public_FormRef

A second variable/property is needed. To aid in the description, the name is prefaced


with the word "Public_" and again this is a matter of style and helps explain the
nature of the variable.

This is the "publicly visible" property and it contains two methods: get and set and
these are from the calling routine's point of view. When another Form wants to "set"
the address-value, it ultimately uses this statement (see BtnOpenForm2's logic).

For example, in Form1:


myForm2.Public_FormRef = this; //This is a 'set'

In other words, the equal-sign (which means "assign this value to...") is a "set." It
turns out that all variables have a "get" and "set" methods. (You could allow a
variable to "get" but not "set" by deleting the "set" statements. This makes it a Read-
Only from the calling routine's point of view.)

• Public_FormRef is an arbitrary name for the property.

• And more interestingly, you can create other get-set variables in this same fashion.
As you will see, Form1 can "set" a value, essentially passing a default, and later,
Form1 can read ("get") the value to see if the value changed. These are called "Form
Properties", or more precisely, "Class Properties".

See Chapter 14 INI Files for other examples of this technique. In


that chapter, a generic INI-file read routine (a new class) will
find numerous values and return them to the calling form using
get-sets.

Keep in mind these newly-invented constructs are "properties" of the current form and are
not "methods." They do not have signature-lines () and do not accept parameters. More
importantly, the properties do not return values to the calling routine; in other words,
there is no void, int, or string returned. They are true properties, with values, and those
values can be queried.

The "public" statement gives Form1 a path to reach into Form2 and 'deposit' a new value,
by using myForm2.Public_FormRef = this. The "private" setting holds the value
safely within Form2 so it can't be modified by any other method – except through the
"get" and "set" routines.

The two properties are named like this:

private Form private_FormRef


public Form Public_FormRef

Notice the variable's initial capitalization. Starting in Visual Studio 2017, the
capitalization difference is enforced with a compiler warning message:

Chapter 11 - Calling Multiple Forms Page: 731


The unusual sytax ("=>") is new to Visual Studio 2017.

Prior version would code a FormRef / InstanceRef like this:

public Form Public_FormRef


{
get { return private_FormRef; }
set { private_FormRef = value; }
}

3. Modify Form2's button-close event so control can return to the calling program:

Still within Form2, locate the BtnCloseForm2_Click event. Add the statements that
close Form2 (itself) and return to the original Form1 address.

Important: As before, two routines are needed: One for the button
"BtnCloseForm2" and a second for the "X" closing event.

Program 11.3: Multiple Forms using FormRef; Button Close event, completed
private void BtnCloseForm2_Click (object sender, EventArgs e)
{
//Return to Form1 (or any other calling form), using FormRef
//setup by the Form1 call

Public_FormRef.Show();
this.Close();
}

4. Similarly, create a Form Closing event.

Chapter 11 - Calling Multiple Forms Page: 732


In Form2's design view, click on the form's background or title-bar to activate the object.
In the Properties Window, open the Events list (the lightning bolt icon). Locate the "Form
Closing" event (not the Form Closed event) and double-click the empty-field to stub in
the new module. Complete with this code:

Program 11.3: Multiple Forms using FormRef; Form Closing event, completed

private void Form2_FormClosing (object sender, FormClosingEventArgs e)


{
//Return to Form1 using FormRef setup by the Form1 call
Public_FormRef.Show();
}

Read the statement, Public_FormRef.Show( ); as if it said "Form1's Address.Show".

where:

• When defining the two new properties in Form2: 'private_FormRef' and


'Public_FormRef,' many developers follow an old Microsoft naming standard where
m_InstanceRef is used for the private-variable's name. This naming scheme is no-
longer recommended. My technique prefacing with either Public or private makes it
clear which variable you are referencing.

• Public_FormRef holds a reference to the "instance" of a form class (what I like to


call an 'address'). This is not a direct link to "Form1;" instead, it is populated with
Form1's "this" property.

To prove it is a property, return to Form2.Public_FormRef = this; statement in Form1.


Backspace and re-type the line. Now the Properties are constructed in Form2, the
editor gives a visual clue:

Testing:

Chapter 11 - Calling Multiple Forms Page: 733


This code is testable now and solves the shortcomings found with the first solutions.
Launch the program, open Form2, close Form2.

Continue reading for additional information.

Chapter 11 - Calling Multiple Forms Page: 734


Modal and Modeless Forms

Forms and other dialogue boxes can be opened in a Modal or Modeless format. A Modal
form keeps focus and does not allow the user to return to the original calling form until
the newly-opened form is closed. This is the most common way to call a secondary form
and an example would be a File-Open dialog in Microsoft Excel and other programs.
Standard MessageBoxes are also an example of a Modal form. In each case, you must
close (OK) the window before doing anything else.

Forms can also be opened as Modeless, where they can remain active while the user
switches to other parts of the program or to other programs. A modeless form acts as a
standalone, floating window. Examples of these are toolbars and property windows.
These forms can be opened and the user can go into other parts of the program to do other
work. The first example in this chapter (Programs 11.1 and 11.3) demonstrated Modeless
forms – which allowed jumping from Form1 to Form2, with each remaining active, even
though it was Form1 that called Form2.

In summary:

Modal - Locked use "myForm2.ShowDialog()"


Modeless - Independent form use "myForm2.Show()"

Modal Forms:

Using the previous section's example programs (Program 11.3), in BtnOpenForm2_Click,


open Form2 as a Modal (dialog) form with these steps:

Using Form1 in the FormRef example program as a starting point:


(Delete the Hide-statement at line 7, if still present)
Modify line 5 to read "myForm2.ShowDialog( );"

Multiple Forms using Modal (ShowDialog), completed


1 private void btnOpenForm2_Click (object sender, EventArgs e)
2 {
3 Form2 myForm2 = new Form2();
4 myForm2.Public_FormRef = this; //Prep for a return with
//get-sets
5 myForm2.ShowDialog(); //open as a Modal form
6
7 // this.hide();
8 }

Leave the rest of the program, including Form2, as before.

Chapter 11 - Calling Multiple Forms Page: 735


Testing a Modal Form:

Launch Form1; then click btnOpenForm2.


Once Form2 is opened, try to click within Form1.

Results: Form1 is locked and un-selectable until Form2 is closed. This is a Modal Form.
Use this type of Form when you want the user's undivided attention.

Chapter 11 - Calling Multiple Forms Page: 736


Transferring Data Between Forms using Global Variables

C# does not have a direct way for one form to see another's text fields and values, but it
does have several indirect methods. Fields and variables can be sent "one-way," from the
parent to the child, or "two-way," from the parent to the child and back again. Naturally,
there are right ways and wrong ways to do this.

This section demonstrates four ways to pass values into a downstream form, starting with
poor techniques to better. All examples assume the two-form program from the previous
sections:

• Use quick-and-dirty Global variables (public-static)


• Use a Global Class (Chapter 8, Method 2 Internal Program-specific Class)
• Pass a short-list of variables through the Form's Constructor class
• Passing variables as Properties

If a particular form can reach directly into another's values, the two forms become
dependent and that relationship is not conducive to re-useable code. The first form has a
hard-coded link to the other. The global variable method is a slightly better design but is
still flawed. Other techniques become less direct, making for better code. Passing
variables as properties is the best way to transfer data between forms. This makes for re-
useable, object-oriented code, but requires a more involved setup.

Using Quick-and-Dirty Global Variables:

A variable can be passed from one form to the next using a "quick-and-dirty" global
variable (Chapter 9). Although easy to setup, it is not proper and most programmers
would call this obscene. Never-the-less, here are the steps:

1. In Form1 (the calling program), declare a quick-and-dirty global variable, near the top of
the program:

public static string strTest = "This works";

2. In Form2 (the called program), add this logic to the Form_Load event:

private void Form2_Load (object sender, EventArgs e)


{
MessageBox.Show("The value from Form1 is: " + Form1.strTest);
}

Notice how Form1 is explicitly mentioned in a Form2 event. This does not bode well for
re-useable and shareable code. How would you use Form2 in the payroll system when
that program does not have a Form1?

Chapter 11 - Calling Multiple Forms Page: 737


Using a Global Class (Global Variables)

This method is slightly better than the previous. Instead of having a dependency on a
form-name, build the dependency in a separate class. As useless as this sounds, in a large
development project, a central class is might be handy for common, company-wide
variables and you could improve the design even further by using "properties" (with get
and set routines). The discussion is a repeat of Chapter 9's Global Variables.

General Program Design:

Once again, using a global variable, store a value in an (Internal) class, separate from
either Form1 or Form2. For this example, have Form1 detect a change in textBox1, write
the value to the global variable, then launch Form2. Form2 can retrieve the value.

For this example, the global value will be defined in an "Internal Program-specific"
class. An External class could also be used.

Program 11.4: Using Global Class Variables

Steps:

1. Create or use a two-Form program, following the examples previously described in this
chapter (Program 11.3: "Using an FormRef to Return to Form1"). For this example,
confirm both Form1 and Form2 have a "textBox1."

Chapter 11 - Calling Multiple Forms Page: 738


2. Create a "Program Specific" class (ProgramGlobals) to hold the global variables. As
described in Chapter 8, this creates an "Internal Program Class Library" that houses the
program's global variables:

In Solution Explorer,

• "other-mouse-click" the application's name, select Add, New Item


• Choose "Class"
• Type "ProgramGlobal.cs" for the new class name (.cs extension required. This is an
invented, arbitrary name)

3. Open the new class in Code View and create a quick-and-dirty global variable using
"internal static":

Program 11.4: "ProgramGlobal.cs": Building a Global Variable


namespace ExampleProgram
{
class ProgramGlobal
{
//Create "Global" variables here

internal static string strMyCompanyName = "ACME";


}
}

where:

• "internal static" creates a "quick and dirty" variable. "internal" is a slightly better
choice than "public" – limiting the variable to only this program.

• By convention, use initial-caps, signifying a "Global" variable. Starting in VS2017,


Microsoft enforces the initial cap with a compiler warning message.

4. In Form1's design view, double-click textBox1.


This stubs-in a "textBox1.TextChanged" event. Add this code:

Program 11.4: Form1 textBox1.TextChanged event; Move Value to Global


private void textBox1_TextChanged (object sender, EventArgs e)
{
//As the value changes, write the results to a
//Global variable in the ProgramGlobal class

ProgramGlobal.strMyCompanyName = textBox1.Text;

Chapter 11 - Calling Multiple Forms Page: 739


where:

• The variable "strMyCompanyName" cannot be reached without its class-prefix


"ProgramGlobals.strMyCompanyName"

• As each character is typed, the event fires and writes to the global variable
"strMyCompanyName". Admittedly, a "TextChanged" event is somewhat inefficient.
Other events, such as a Leave event, should have been used.

Retrieve the Stored Value in Form2:

Within Form2's logic, retrieve the saved global variable and display in Form2's
"textBox1". It could have been used in any other calculation without the Form_Load
event.

5. Build Form2's "Form_Load" event by double-clicking the Form's title-bar while in design
view. Add this logic to the newly-created event:

Program 11.4: Form2 Retrieving the Global Value


private void Form2_Load (object sender, EventArgs e)
{
textBox1.Text = ProgramGlobal.strMyCompanyName;
}

Testing the Global Variable:

Launch the program. In Form1, type any value in textBox1 (e.g. "Acme, Inc."). Click
button "OpenForm2".

Results: Form2's textBox1 displays the same company name.

Other Comments:

Both Form1 and Form2 have the ability to change the global value. This is one of the
'features' that makes this method somewhat dangerous in the real world; any module,
method or class can see and change variable, including other programs unrelated to yours.
This makes debugging difficult. One way to solve this is to place logic around the SET
command, preventing all but authorized programs from making changes.

This design gets weird when two or more solutions use the same class and one updates the
variables, but the other does not.

Chapter 11 - Calling Multiple Forms Page: 740


Passing Data with a Signature and a Constructor

Variables can be passed from one form to another using the signature line and the
secondary form's Constructor. This works well if there is a relatively short list of
variables and the pass-through is one-way. The idea is similar to passing values to a
downstream function through its parenthesis.

This is not a horrible way to pass data from parent to child. But
it is limited to relatively short lists of variables.

When calling the new form, place variables within the call's open-and-closing parenthesis.
Then, within the new form's constructor, declare variable types for each of the passed
values, giving temporary access.

With this method, data moves from the parent to child, but only in
a one-way, downstream relationship. Updated or changed values
cannot be returned to the parent.

A minor inconvenience of passing data through the Constructor is the variables live for
about a millisecond before they are discarded and fall out of scope. Because of this, they
must be moved to a new local variable before the Constructor ends. This is best
understood with an example:

The Parent's Call:

You can use the FormRef program 11.3 as a base for this example. Disregard the global
variables used previously.

In the parent Form1, when instantiating the call to Form2, pass the variable through the
open-and-close parenthesis. For example, as Form2 is instantiated, pass a city-name,
strPassedCityName:

From Form1: Passing a Variable to Form2's Constructor


private void BtnOpenForm2_Click (object sender, EventArgs e)
{
//Passing a variable to Form2 at the "call"
//Form2 also requires modification; see below

string strPassedCityName = "Memphis";

Form2 myForm2 = new Form2(strPassedCityName);


:

where:

• Any in-scope variable previously built in (Form1) can be passed into the new form.

• The variables must be pre-initialized in Form1 before passing - even if initialized with
an (empty string "").

Chapter 11 - Calling Multiple Forms Page: 741


• Multiple variables, delimited with commas, can be passed. For example, two strings
and an integer could be passed with:

Form2 myForm2 = new Form2 (strVar1, strVar2, intVar3)

Again, all values must be initialized with something; you cannot pass a declared
variable that has not had an assignment.

• The target form (child Form2) requires modifications in its Constructor.

The Child (Form2) Constructor:

Form2's Constructor accepts the passed variable(s) by declaring the variable's type and
assigning it a new, temporary name. This is the same concept when passing variables to
other functions and methods:

Passing Variables to Form2 via a Constructor, required changes


1 public partial class Form2 : Form
2 {
3 //Build a variable to hold the "real" value at a higher scope
4 string strStoredCityName = "";
5
6 //Modify Form2's Constructor, with a passed variable...
7 public Form2 (string strpassedVariable)
8 {
9 InitializeComponent;
10
1 //The passed variable must be moved to a safer location
2 //before the constructor closes. Typically it is moved
3 //to a form-level variable
4
5 strStoredCityName = strpassedVariable;
6
7 //strStoredCityName can be used anywhere in Form1
8 }

where:

• Line 7 is Form2's Constructor event. Normally this line would have a simple – and
empty – open-and-close parenthesis, but now has a passed parameter via the signature
line:

public Form2 () //Original design


public Form2 (string strpassedVariable)

• The incoming (string) variable is assigned an arbitrary name, which is used for the
short duration of the Constructor's event. This name can be the same or different than
original variable's name. The passed value is a copy of the original; this is why it is a
"one-way" variable.

Chapter 11 - Calling Multiple Forms Page: 742


• If multiple variables are passed, the same number of variables are declared in the
Constructor. Separate each with commas and declare their type. Again, the names
assigned here in the "catch" can be different than the names passed:

public partial class Form2 : Form


{
private Form private_FormRef = null;
public Form Public_FormRef
{
get => private_FormRef;
set => private_FormRef = value;
}

//The Constructor for the Form:


public Form2 (string strpassedVar1, string strpassedVar2, int intVar3)
{
InitializeComponent();
}

: etc.

• This newly-minted variable, string strpassedVariable falls out of scope


as soon as the Constructor ends – and this is long before other routines in Form2 can
work with them. Invariably, the variable(s) needs to be moved to a new location.
Line 4 declares a new form-level "real" value; line 15 moves the temporary-passed
variable to the "real" location. You will have to do something like this with all passed
variables. This is a nuisance.

Comments on the Signature and Constructor Design:

The jump from the passed-variable to "real" variable is clumsy. If the constructor has a
long and distinguished list of variables, a complete second list also has to be built at the
class-level to hold the final values. Some would argue the previous global class method
was more tedious, but at least that class could be used with multiple forms. Consider
passing an array if the list is long and all are of the same type.

Passed values are sent on a one-way journey. You cannot


return values back to the calling form with the
Constructor technique.

Non-Form Classes - the Constructor:

This technique of passing values through the signature line can be used in any type of
class library; it is not restricted to form-classes.

When building a standard Class Library, manually build the Constructor. For example, if
you built a new class called "CL860_MyClass", open the module in code view and near
the top of the program, just after the class definition, build the "Constructor method" like
this:

Chapter 11 - Calling Multiple Forms Page: 743


:
class CL860_MyClass
{
//Class-level variables might live here
//Other class definitions here
//CL800_util util;

public CL860_MyClass()
{
//This is this class's constructor, manually typed like
//any other method.
//Variables might be passed through the signature line.

//You might declare other classes, such as:


//util = new CL800_Util();

//But remember, and variables declared here, or in the


//signature, will fall immediately out of scope and must
//be moved to a better location before the closing brace.
}

//Other methods, public and private, below here


:

"Constructors" are recognizable because they do not have a return-value and


do not have a 'type'.

The next section shows a better design for passing values into the form and you can
control if the variables travel on a one-way or two-way journey.

Chapter 11 - Calling Multiple Forms Page: 744


Passing Values using get-set Properties - Recommended

In the example above, the child form retrieved a passed value through its constructor and
had to immediately move it to a Form-level variable for safe keeping (see line 15
strStoredCityName). This is an acceptable design, but data can only flow one-way, into
the child-form or child-class; updated value(s) cannot be returned to the parent.

Earlier in this chapter, you saw how FormRef variables could house the calling-form's
"address" and how the calling form could shove its own address into the downstream
module. The technique is called a form property (or more accurately, a class property).

This same design can pass any number of variables into a downstream class and you can
control whether the journey is a one-way or two-way trip. More importantly, it does not
rely on global variables nor does it create a dependency between the two classes. This is
the recommended design for sharing data between forms and classes.

Passing Values to Form2:

Program 11.5: Passing Values Through the Call

This example uses a secondary pop-up form to prompt the user for three fields: A
company name, city and a numeric inventory. Results are returned to the first form.

Instead of using global variables or signatures, the values will pass through object-
oriented-friendly properties – using the same technique as the FormRef variables.

Re-create the example program from Example Program 11.3. This has two forms with a
FormRef call, opening Form2. If you are using the previous examples, remove the global
variables and constructor steps, from both Form1 and Form2.

Here were the initial build steps from previous sections:

A. In a new Project, add button1 to Form1.


Name the button, "BtnOpenForm2"
Change the Text property to read "Open Form2"

Double-click the button, and write this routine:

Form1 call to Open Form2, completed


private void BtnOpenForm2_Click(object sender, EventArgs e)
{
Form2 myForm2 = new Form2();
myForm2.Public_FormRef = this;
myForm2.ShowDialog(); //Modal forms recommended
}

B. Add "Form2" to the project (Solution Explorer, "Add", "Windows Form".

Chapter 11 - Calling Multiple Forms Page: 745


Double-click Form2's background to open the editor.

Create get-set properties (Form Properties) just after the class definition, again, as
described in previous sections:

Form2 get-set Properties, completed


public partial class Form2 : Form
{
private Form private_FormRef = null;
public Form Public_FormRef
{
get => private_FormRef;
set => private_FormRef = value;
}

C. In Form2, create a button, "BtnCloseForm2".


Double-click the button to stub-in this event:

BtnCloseForm2_Click, returning to Form1, completed


private void BtnCloseForm2_Click (object sender, EventArgs e)
{
Public_FormRef.Show();
this.Close();
}

D. Higlight Form2's background, and in the Events panel, create a FormClosing event (not
FormClosed) by double-clicking the blank field near the event's name

Form2 - FormClosing event, completed


private void Form2_FormClosing (object sender, FormClosingEventArgs e)
{
Public_FormRef.Show();
}

Continue with the Example Program:

With the basic form's open and close events completed, continue with these steps:

1. In Form2, create three textBoxes and three cosmetic labels, named:

PnlCompanyName
PnlCompanyCity

Chapter 11 - Calling Multiple Forms Page: 746


PnlInventoryNumber (where 'Pnl' means panel 23)

2. Build Form properties to hold the values in Form2.

In Form2's code view, create the form properties, supporting the new fields. For
consistency, place these after the FormRef variables. These properties live in the same
area as any other class-level variable. For example, here is the CompanyName field:

23
Pnl prefix is an invented phrase and is not required. A txb (textbox) prefix is used by a lot of
developers. And many developers hate prefixes, finding them unnecessary.

Chapter 11 - Calling Multiple Forms Page: 747


Program 11.5: Setting Property Variables in Form2, completed
public partial class Form2 : Form
{
//Setup FormRef variables so control can be returned to
//the calling form, Form1 (or any other form that calls
//this module)

private Form private_FormRef = null;


public Form Public_FormRef
{
get => private_FormRef;
set => private_FormRef = value;
}

//Create Properties that other forms can use:

private string private_strCompanyName = "Acme";


public string Public_strCompanyName
{
get => private_strCompanyName;
set => private_strCompanyName = value;
}

private string private_strCompanyCity = "Memphis";


public string Public_strCompanyCity
{
get => private_strCompanyCity;
set => private_strCompanyCity = value;
}

private int private_iInventoryNumber = 0;


public int Public_iInventoryNumber
{
get => private_iInventoryNumber;
set => private_iInventoryNumber = value;
}

public Form2()
{
//The constructor lives here
:

where:

• Three variables with three sets of private/public.

• Two of the variables are strings; the last is an integer (int). Each have default values,
which can be overridden by Form1 when it loads.

• These properties are similar to the FormRef variables that help open and close the
forms.

3. Still in Form2, double-click the form's background to stub-in the Form_Load event. Then
add this code to pre-populate the panel-fields:

Chapter 11 - Calling Multiple Forms Page: 748


Program 11.5: Form2 Load Event, setting defaults, completed
private void Form2_Load (object sender, EventArgs e)
{
//Populate default values (or values as passed into the form)

PnlCompanyName.Text = private_strCompanyName;
PnlCompanyCity.Text = private_strCompanyCity;
PnlInventoryNumber.Text = private_iInventoryNumber.ToString();

where:

• The child-form always works with the private variables – never public. Private_
variables are visible only to the current class, the current form.

If you are typing this, and cannot find (PnlCompanyCity), for example, it means the
variable was not declared with a get-set property; see step 2, above.

Allow User Changes:

If the user types new values in Form2's panel, update the form properties to reflect the
new values. To do this, build a Leave event for each field using these steps:

4. For each data-entry field in Form2:

a. From Form2's design view, highlight PnlCompanyName.

b. In the Properties screen, change from Properties to Events (the lightning bolt icon).
Double-click the Leave event to stub-in the basic code.

Chapter 11 - Calling Multiple Forms Page: 749


c. Make these changes to the event:

Program 11.5: Detecting Field Changes, CompanyName, completed


private void PnlCompanyName_Leave (object sender, EventArgs e)
{
//Update the Form Property when ever the user makes a change
//Or you could more sloppily use the TextChanged event

//Notice you are updating the private Property.

private_strCompanyName = PnlCompanyName.Text
}

Make similar events for the other two fields.

Testing Form2's Load Event:

The program is initially testable now. Press F5 to run.

When Form1 displays, click "Open Form2".


Results: Form2's textbox fields are populated with default values. Note the default
company name is "Acme".

Close the program and return to the editor

Chapter 11 - Calling Multiple Forms Page: 750


Allow Form1 to Set new Defaults for Form2:

Form2 has a company name default of "Acme", in the "Memphis". Although the values
were hard-coded into the form, it might have retrieved these values from a database or
other means. For demonstration, allow Form1 to push new default values.

1. Return to Form1's code view and make these changes to btnOpenForm2:

a. Pass new values into Form2's three field properties, changing the defaults. Do this
after the instantiation, before the Show:

Program 11.5: Form1 calls Form2 with Default Field changes, tentative
private void BtnOpenForm2_Click (object sender, EventArgs e)
{
Form2 myForm2 = new Form2();
myForm2.Public_FormRef = this;

//Set new default values


myForm2.Public_strCompanyName = "ABC Corp";
myForm2.Public_strCompanyCity = "Portland";
myForm2.Public_iInventoryNumber = 42;

myForm2.ShowDialog(); //Modal form!

//Diagnostics:
//Display CompanyName, as typed in Form2
MessageBox.Show
("Company Name: " + myForm2.Public_strCompanyName);
}

where:

• iInventoryNumber is passed as a number, not a string because Form2 declared it as an


integer.

If the Public_ variables do not appear in the intellisense popup menus, Form2
may not have the form-property values defined with get-sets – or you are
spelling "Public" with a lower-cased p.

With these statements, Form1 is forcing new default values into Form2, overriding
Form2's hard-coded settings. Notice how it reaches into "publicly" exposed settings.
The public values are the ones with a SET clause and you are definitely setting a new
value.

Chapter 11 - Calling Multiple Forms Page: 751


Testing 1:

Launch the program and open Form2.


Results: Default CompanyName changed from Form2's default "ACME" to Form1's new
preference, "ABC Corp", etc.

Testing 2:

Launch the program and open Form2. Make a change to the CompanyName ("Boise")
and close the form.
Results: Form1's MessageBox detects the change.

Exercise 1:

At the end of Form1's btnOpenForm2 event, add a MessageBox showing all three fields.
Then, re-launch Form2 and make changes to all three fields. If you have followed these
examples precisely, only the CompanyName detects the change. Why? Fix this issue.

Exercise 2:

Change the .ShowDialog() to .Show() and add a Form1.Hide() after the .Show. Form2
will now launch as a modeless (free-floating) form and form1 will stay active, but will
hide itself as soon as Form2 is launched. Run the previous test again.

Results: Form1's logic will immediately fall to the diagnostic MessageBox -- before the
user has a chance to make changes in Form2. You have to do something to slow-down
the OpenForm2 routine.... a modal form works well. Force-stop the program and return
to .ShowDialog and comment out the .hide statement.

Exercise 3:

In Form2's Code View, comment the private_strCompanyName's SET clause:


//set => private_strCompanyName = value;

Results: Editor error: Property or indexer 'Public_strCompanyName' cannot be assigned


to – it is read only. The field is now unchangeable, making this a one-way variable pass.

This completes how to pass and update multiple variables using Form or Class properties.
These same techniques can be used in any class. For another example, see Chapter 14 INI
Files, class CL860.

Chapter 11 - Calling Multiple Forms Page: 752


Form Starting Positions - Global Class

Form2 opens in a default screen position and is not dependent on Form1's location. If you
have a dual-monitor PC, move Form1 to monitor #2 and call Form2; notice Form2 opens
on the wrong monitor.

Using a Global Variable Class, Form1's XY positions can be stored and Form2 can use
this to decide where to display itself.

Build a Global Variable Class - Onetime setup per Project/Solution

In the existing project begin by building a new class to store positioning values. By
making a "Global Variable Class", all forms in the project can share these same variables.
See Chapter 9 for details on building a project-level Global Variable Class.

A. In Solution Explorer, other-mouse-click the project's name.


Select Add New Item; choose "Class"
Use this recommended class name: "ProgramGlobal.cs" (including the .cs extension)

B. Add this code, which is a short list of variables that can store a form's size and position
for later use. Default values are set incase a calling routine forgets to populate:

ProgramGlobal.cs, form position variables, completed


namespace Address
{
class ProgramGlobal
{
//Global Variables here:
internal static int IntForm1LeftPos = 10; //Position
internal static int IntForm1TopPos = 10;
internal static int IntForm1Height = 10;
}
}

C. Click "X" to close and save the new Class.

Setting Form1's Starting Position in the New Class

When Form2 is opened, it looks to Form1 to see its position and adds cosmetic offsets
along both the X and Y axis to overlap the windows in some arbitrary and pleasing
position. The math is simple.

Chapter 11 - Calling Multiple Forms Page: 753


1. Return to the main, calling form, Form1. Locate the button "Open Form2". Double-click
to go into the method's code view.

2. Before launching Form2, add this logic:

Form1's Call to Form2, Setting Starting Positions, completed


private void btnOpenForm2_Click (object sender, EventArgs e)
{
//Button to open Form2

//Record Form1's starting postion, so Form2 opens near by


ProgramGlobal.IntForm1LeftPos = this.Left;
ProgramGlobal.IntForm1TopPos = this.Top;
ProgramGlobal.IntForm1Height = this.Height;

//Other logic to open form2 follows here...


:

3. Open Form2's code view by double-click Form2's designer title-bar (or from Solution
Explorer, other-mouse-click Form2, choosing Code View). Locate the Form2_Load
method. Add logic to calculate this form's opening position:

Chapter 11 - Calling Multiple Forms Page: 754


Form2's Load Event, Setting Starting Positions, completed
private void Form2_Load (object sender, EventArgs e)
{
//Retrieve the calling form's starting positions and open nearby
this.Top = ProgramGlobal.IntForm1TopPos +
ProgramGlobal.IntForm1Height -
this.Height + 40;

this.Left = ProgramGlobal.IntForm1LeftPos + 20;

//It is possible for the upper-left corner to be above or off


//the edge of the screen when the calling form was too high.
//Adjust by checking for negative or close-to-edge numbers
if (this.Top < 10)
this.Top = 10;
if (this.Left < 0)
this.Left = 10;

//Other form2 logic goes here...


:
:
}

Testing:

Launch the program. Position Form1 near the upper-left of the screen. Click button
OpenForm2.

Results: Form2 should open near Form1.

Chapter 11 - Calling Multiple Forms Page: 755


MessageBox Overloads

Throughout this book, MessageBoxes have been used to display diagnostic text.
Although they were not instantiated as a second form, they certainly look like one. And,
to-date, the MessageBoxes have been one-way – they accept data but do not return values
to the calling program. This section explores standard MessageBoxes and expands the
idea by building and calling more sophisticated routines, using the multi-form concepts
described above.

Standard MessageBoxes:

The .Show method has an overload that is more capable than a simple message box and
with this extension, you can prompt the user with a "yes/no" question using three basic
parameters:

MessageBox.Show ("The message to display",


"The Title bar",
MessageBoxButtons.YesNo);

which looks like this on screen:

The prompt can be attached to any event or it can be placed anywhere within a block of
code. MessageBoxes return a result of type "DialogResult" and are usually tested with a
switch statement:

Chapter 11 - Calling Multiple Forms Page: 756


Classic Yes/No Dialog, completed
private void Button1_Click (object sender, EventArgs e)
{
//Classic Yes/No Dialog
DialogResult myAnswer;
myAnswer = MessageBox.Show("Proceed with the program",
"Authorization to continue?",
MessageBoxButtons.YesNo);

switch (myAnswer)
{
case (DialogResult.Yes):
MessageBox.Show ("you clicked Yes");
break;

case (DialogResult.No):
MessageBox.Show ("you clicked no");
break;
}
}

where:

• A variable, arbitrarily named "myAnswer," is declared as a type "DialogResult" (it is


not a string, integer or boolean – it is a DialogResult)

• The MessageBox.Show method was passed two strings. One populates the main
prompt and the second is the title-bar text.

• The third parameter sets the "Yes/No" buttons, with other minor variations including
"OK/Cancel", and "Yes/No/Cancel"

• When the MessageBox is dismissed (by the end-user), it returns a value to myAnswer,
which is tested in the switch-statement

• Most programmers would declare the variable on the same line as the MessageBox, as
in

DialogResult myAnswer = MessageBox.Show(....

Icon Fluff:

MessageBoxes can be augmented with small, pre-determined icons, using a


MessageBoxIcon variation as a fourth parameter:

DialogResult myAnswer;
myAnswer = MessageBox.Show ("Proceed with the program",
"Authorization to continue?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Stop);

Chapter 11 - Calling Multiple Forms Page: 757


These boxes are simple and easy, but lack flexibility and cannot prompt beyond a Yes/No
question. Standard MessageBoxes can not ask for a password or prompt for other data
entry. And they are ugly.

The next sections show how to build reusable custom dialog boxes with many features
above and beyond a simple MessageBox.

Chapter 11 - Calling Multiple Forms Page: 758


Custom Dialog Boxes: NS810_Dialog

Standard, out-of-the-box MessageBoxes are easy to use but lack flexibility and are only
able to deal with Yes/No questions. Sometimes a program needs to prompt users with a
more sophisticated list of choices; for example, "Rework", "Process", "Cancel". In this
section, build a custom dialog, which is driven by passed-variables. The final result will
be more attractive than a standard MessageBox.

This form will be useful in a variety of other projects. Because of


this, create the form in a new class, similar to the
NS800/CL800_Util Class libraries built in Chapter 8. Once
written, it can be linked when needed into any program.

This is a fun, challenging, and an interesting project that uses all


of the skills learned to this point in the book.

This custom dialog is a secondary form with up to four passed buttons, all with variable
text. The user's response is returned to the calling program where it can be tested. The
form is called using standard "FormRef" logic. The initial construction follows the same
steps described earlier and is repeated here.

Summary of Steps to Use this Dialog:

This is a summary on how to call the finished form; these are not the build steps.

A. In Solution Explorer, Add Existing Item:


C:\Data\Source\CommonVS\NS810_Dialog;
Choose "FrmDialog.cs". Click "Add as Link" on the pull-down option.

B. Add this statement: using NS810_Dialog;

Chapter 11 - Calling Multiple Forms Page: 759


C. In the button event where you need the dialog:
FrmDialog myCustomDialog; //myCustomDialog is an invented name

D. In the same event, continue with this syntax to call the form:

myCustomDialog = new FrmDialog


("This is the main prompt",
"This is the sub prompt with longer text",
"Title Bar Text",
"&Cancel", // Button 1 of 4"
"&Process", // Button 2 of 4"
null,
null,
"DESKTOP",
0);

myCustomDialog.Public_FormRef = this;
myCustomDialog.ShowDialog();

//Retrieve the pushed button:


MessageBox.Show ("Button was: " +
myCustomDialog.iPubbuttonPushed);

where:

• Two prompts, a major and minor, can be passed. The first (main prompt) will display
in larger, bold letters; the sub-prompt will be quieter and more subdued.

• Four buttons can be defined with the text. In this example, two of the buttons are
"null" and will not display on the dialog.

Each button returns (via Public_ibuttonPushed) as a numeric 1, 2, 3, or 4, depending


on which button the user clicked.

• The "DESKTOP" parameter is an attitude prompt that controls how aggressive the
dialog box displays. "DESKTOP" appears as a separate window on the task bar,
displays on top of all other programs and positions itself in the center of the main
screen. "APPLICATION" is less intrusive and the window displays within the
running application and does not appear as a separate icon on the task bar; it is very-
much tied to the running program.

Build Steps to Create a Custom Dialog:

This form will be useful in a variety of other projects. Because of this, place it in a new
Class library, "NS810_Dialog," similar to CL800_Util class. Once written, you will never
have to write this routine again.

The new dialogue must be in a different class library than CL800


because it contains Windows Form Controls, giving it the ability
to display buttons and other form objects. You cannot put the
logic within CL800 because it does not (and cannot) have these
controls.

Chapter 11 - Calling Multiple Forms Page: 760


A complete code list can be found at the end of this chapter.

1. Create a new Class Library by starting a second copy of Visual Studio (the Editor),
leaving your original program still active in its own editor.

• Launch Visual Studio; create a new Project

• Choose "Windows Form Application" template (rather than "Class Library").


Chapter 8 selected Class Library, but in this case you must select the Windows
Form because the program needs access to the Form classes and methods.

• Use this suggested name and location:

Name NS810_Dialog
Location C:\Data\Source\CommonVS

Save in a location for common code modules; do not save in the same directory as
the current project you are working on. This Class will be useful in a variety of
other programs.

Click OK to create the class.

2. In Solution Explorer (right-nav), change the form name from Form1.cs to


"FrmDialog.cs" (the .cs extension is required; use a capital "Frm").

Chapter 11 - Calling Multiple Forms Page: 761


3. In FrmDialog's design view, lock-down the form with these property changes:

Name = FrmDialog (from rename step, above)


ControlBox = true
FormBorderStyle = FixedSingle
MaximizeBox = false
MinimizeBox = false
ShowIcon = false
ShowInTaskbar = false
StartPosition = CenterParent
Text = "Dialog"

4. Add these objects to the Form. See the illustration, below, for placement:

• label1 Name = PnlMainPrompt


• label2 Name = PnlSubPrompt

Set PnlMainPrompt's (label1) Font to a slightly larger size (MS SanSerif 14)
Set PnlSubPrompt's (label2) ForeColor to a dark-gray from the "Custom" tab.
Set PnlSubPrompt Autosize = False (be sure to highlight the label first)
Resize PnlSubPrompt, as illustrated below.

5. Place 4 buttons, starting with button1 in the lower-right corner, as illustrated.

• Rename from their default name: "button1" to "Button1" -- through Button4. Reason:
Starting in VS2017, Microsoft displays an editor warning that these objects must
begin with a capital letter. This is an informational warning, but it clutters the editor's
Error List,

• Leave each button with its default text, "button1"


• Size each button a little wider than normal: +Size, Width = 95

• Set each button Visible = false (Important)


• Set each of the four button's tab-order, starting with button1 as the first button,
working backwards to button4. The two labels do not need a tab-order and will not
have a tab-stop.

6. Add an optional, cosmetic Horizontal Line


(using Picturebox: Height = 1, BorderStyle = FixedSingle), as described in Chapter 10.

Chapter 11 - Calling Multiple Forms Page: 762


7. From the beginning of this chapter, add the FormRef logic to the Custom Dialog's opening
routines. As illustrated below, this code goes near the Constructor "public FrmDialog( )".
This is the "private Form private_FormRef" and "public Form FormRef" statements.

While you are in the neighborhood:

• Link the CL800_Utility libraries with an "Add Existing Item", choosing


CL800_Util.cs and mark the library as "Linked" (See Chapter 8)
• Add the other required CL800_Util statements
• Add the two FormRef get-set properties. See code, below.

FrmDialog: Standard Setup using FormRef for Returning to the main form
using NS800_Util;

public partial class FrmDialog : Form


{
CL800_Util util;

public FrmDialog( <later, passed parameters go here> )


{
InitializeComponent();
util = new CL800_Util();

//Assemble the passed parameters here:


}

private Form private_FormRef = null;

public Form Public_FormRef


{
get => private_FormRef;
set => private_FormRef = value;
}

Chapter 11 - Calling Multiple Forms Page: 763


8. Modify the constructor [ public FrmDialog ( ) ] so it accepts a passed-parameter-list
from the calling program, marked above with "<later, passed parameters go here>".

The form needs to accept incoming text for PnlMain and PnlSubPrompts, title-bar text, a
button-list 1 through 4, and a default-button number. This is not as complicated as it
sounds but it is a long list of string parameters – all placed withing the Constructor's
parenthesis. The variables are invented names and each is separated by a comma. Modify
the constructor now:

FrmDialog: Accepting Passed Parameters from the Calling Program, partial


:
public FrmDialog
(string strMainPrompt,
string strSubPrompt,
string strTitleBarText,
string strButton1Caption,
string strButton2Caption,
string strButton3Caption,
string strButton4Caption,
string strAttitude,
int idefaultButton)
{
InitializeComponent();
util = new CL800_Util();

//Assemble the passed parameters here:


}

where:

• You are building a list of expected passed parameters. Each variable is given a type
(string) and an invented name within the list. Notice the comma after each item,
except the last. All are within the Constructor's opening and closing parenthesis.

• The variables act like any other variable you have passed into a function. In this case,
the variables are populated when the new form is called by the parent form.

• Stylistically, I like to type each variable on its own line.

• The values, once passed, must be moved to a more permanent location during the
//Assemble step. More on this next.

9. While still in the newly-built form, locate the step marked with "//Assemble".

Populate the final dialog by taking each of the passed variables and moving
them to a more permanent location. This must happen in the FrmDialog
constructor, after InitializeComponent, or the parameters fall out of scope and

Chapter 11 - Calling Multiple Forms Page: 764


vanish. In other words, strMainPrompt, strSubPrompt and their friends, must be
moved to different locations before the Constructor's closing-brace is reached.

The values can be moved to panel-fields or other variables, as needed by your program's
logic. In this case, they are moved directly onto the form's main prompts and buttons.

The calling program passes button details through four button parameters,
strButton1Caption, strButton2Caption, etc..

If your program needs less than four buttons, the calling program still must
pass four values because that is what the signature requires. Unneeded
buttons are padded with nulls.

The logic below looks at each passed button's text and, if a real value was passed, it
changes the default button's text to the passed value. At the same time, it un-hides the
button (from above, in design view they were marked Visible = False):

FrmDialog: Build the Prompts and Expose the Buttons, completed

:
using NS800_Util;

namespace NS810_Dialog
{
public partial class FrmDialog : Form
{
CL800_Util util;

public FrmDialog
( <...long parameter list here; see above> )
{
InitializeComponent();
util = new CL800_Util();

//Assemble the passed parameters:


PnlMainPrompt.Text = strMainPrompt;
PnlSubPrompt.Text = strSubPrompt;
this.Text = strTitleBarText;

//Set Startup Attitude: - How aggressive to display


//Attitude "DESKTOP" or "APPLICATION"
//where DESKTOP appears on task bar and center of screen
//where APPLICATION appears centered in current app
if (strAttitude.ToUpper() == "DESKTOP")
{
StartPosition = FormStartPosition.CenterParent;
ShowInTaskbar = true;
TopMost = true;
}
else
{
//Default if not specified
StartPosition = FormStartPosition.CenterProgram;
ShowInTaskbar = false;

Chapter 11 - Calling Multiple Forms Page: 765


TopMost = true;
}

//Expose the buttons


//If text was passed in the button list, Expose the buttons:

if (util.IsFilled(strButton1Caption))
{
Button1.Visible = true;
Button1.Text = strButton1Caption;
}

if (util.IsFilled(strButton2Caption))
{
Button2.Visible = true;
Button2.Text = strButton2Caption;
}

if (util.IsFilled(strButton3Caption))
{
Button3.Visible = true;
Button3.Text = strButton3Caption;
}

if (util.IsFilled(strButton4Caption))
{
Button4.Visible = true;
Button4.Text = strButton4Caption;
}
}

where:

• In the assembly, the panel MainPrompt and SubPrompt are populated with the first-
two passed values. This is the text the end user sees in the dialogue.

• All four buttons are initially marked Visible = false in design view.

• The "Expose" section examines each button for values greater than null
(strButton1caption). If text is present, make the button visible and put the passed-text
into the button's text property.

• All four buttons must be processed, even if you intend on only using two. The
program needs to process all four choices. When less are passed, as nulls, it keeps the
unused buttons hidden.

Side Notes:

In this example, the variables are moved directly to an on-screen object (text boxes,
buttons, etc.). The data could also have been moved to other variables or to a series
of "get-set" properties.

Chapter 11 - Calling Multiple Forms Page: 766


Although not required for this example, if you were expecting the passed values to
change after the child-form loads, consider using a Property. For example, the
MainPrompt could be built similarly to the FormRef, using "string" for the property-
value:

private string private_MainPrompt = null;


public string Public_MainPrompt
{
get => private_MainPrompt;
set
{
private_MainPrompt = value;

//Other things can happen when ever the value is set,


//such as populating the actual titlebar:
this.Text = TitleBarText + " was set";
}
}

10. Add a Closing routine with these steps:

In FrmDialog's design view, highlight the form's background (selecting the Form)
Choose the Events button (Lightning Bolt icon)
Double-click the blank field next to the FormClosing Event; and add this code:

FrmDialog: FormClosing Event, complete


private void FrmDialog_FormClosing
(object sender, FormClosingEventArgs e)
{
//Return to the calling form using the FormRef passed by
//that form...

Public_FormRef.Show();
}

This is the module that returns to the Parent program.

Typo: When coding the Public_FormRef.Show() statement, it


is easy to forget the open-close parenthesis. The compiler will
show this error: "Only assignment, call, increment, decrement,
and new object expressions can be used as a statement"

11. Save with a File, Save All.


Close this copy of the Visual Studio Editor and return to your original program.

Even though there is no real logic in the form, it contains enough information for basic
testing. Once this new Form is linked into your program, additional changes can be made
from within the Parent program.

From the parent calling program, the call to this new Child Form might look like this:

Chapter 11 - Calling Multiple Forms Page: 767


Linking the New Class:

Return to your original (calling) program (ExampleProgram or frmProcess, from earlier


examples). Link the new dialog class into your project. Use the same steps as in the
CL800_Util class. Attempt this yourself now before reading the steps:

A. In Form1's Solution Explorer, highlight the project's name


'Other-mouse-click' and choose "Add Existing"
Browse to C:\Data\Source\CommonVS\NS810_Dialog
Choose FrmDialog.cs

On the Add button, click "Add as Link'

As before, if you make a mistake and 'add' instead of 'link', delete


FrmDialog.cs from Solution Explorer and try again.

Note: the NS800_Util library is also required by these routines – it uses the "IsFilled"
method. Steps to link this library are covered in earlier chapters.

B. In the calling program's code view, near the top, add this statement:
using NS810_Dialog;

C. Just below the class-declaration, declare the new form with a

"FrmDialog myCustomDialog;"

This makes it visible to all methods and it can be used multiple times in the program:

Chapter 11 - Calling Multiple Forms Page: 768


Custom Dialog Boxes: Declare the Dialog, completed
using NS800_Util;
using NS810_Dialog;

namespace ExampleProgram
{
public partial class Form1: Form
{
CL800_Util util; //You may or may not have this...
FrmDialog myCustomDialog;

public Form1()
{
InitializeComponent ();
util = new CL800_Util(); //You may not have this one
}
:

The form is declared as you would any other form. By declaring the form near the top of
the program, it can be called from multiple locations, but in this case, do not instantiate in
the form's constructor because you do not want to wake up the form at that point.
Contrast this with the CL800_Utility class, which was declared and instantiated so it
could be used anywhere, at any time. In this case, the custom dialog will only be
instantiated when needed, freeing memory.

D. In the parent-form (ExampleProgram/Form1), create a new button. For this example,


button3 was used. In the routine instantiate the new class and pass the list of parameters
to the new Dialog form:

Chapter 11 - Calling Multiple Forms Page: 769


ExampleProgram (using button3) calling the Custom Dialog, completed
private void button3_Click (object sender, EventArgs e)
{
//Object FrmDialog myCustomDialog
// was declared in the form-variable / class-variable section

//Declare the new custom dialog at the moment needed in order to


//save a little-bit of memory:

myCustomDialog = new FrmDialog


("This is the main prompt",
"A subprompt can go here and it can be much longer...",
"This is the titlebar text",
"&Cancel",
"&Process",
"&Rework",
null,
"DESKTOP",
0);

myCustomDialog.Public_FormRef = this;
myCustomDialog.ShowDialog(); //Must be a modal call

//Diagnostic code: Remove when done testing:


//MessageBox.Show("Button was: "
// + myCustomDialog.ipubbuttonPushed);
}

As you type new FrmDialog(..., Visual Studio's intellisense pop-up help will guide
you.

Cosmetic Testing:

The new class is ready for a cosmetic test. In its current state, it accepts passed
parameters, including a custom button list, and it can return to the calling form – but it
does not yet return what the user clicked.

a. Return to the Main (calling) Form (Form1 / FrmProcess, etc.).


In design view, confirm that (button3) is built and has the code documented from above.

b. Press F5 to run the program.

Click on (Button3).
The custom dialog should appear. Buttons "Cancel, Process, and Rework" are not
functional yet.
Click "X" to close the Form.

Results:

Chapter 11 - Calling Multiple Forms Page: 770


Returning Results to the Parent via Properties

The last remaining step is to detect which button (Cancel, Process, Rework) was pressed
and return the user's selection to the parent. As you have seen, values are easily passed
from the Parent to Child, but returning results up-stream are not as obvious. Using Global
variables would work, but then this new form would be tied to the calling program and it
could not be re-used in other programs. Passing ByRef fails because of scope-concerns.
The remaining choice is to use form properties, with get-sets, to store the user's selection.

In past examples, 'FormRef Properties' stored the parent form's 'address' in the child-form.
For this form, a similar technique will use a FrmDialog property, private_ibuttonPushed,
to store the user's selected value, returning a 1-4. Then, in a slight twist, the parent
(calling form) will ask what the value is.

See this same technique used in Chapter 14 (INI Files), where


multiple string variables are passed back into the main program
from a different class.

Build a Property to Store the Selected Button:

In your test program's Solution Explorer, "other-mouse-click" FrmDialog.cs and choose


view code, opening the editor.

Chapter 11 - Calling Multiple Forms Page: 771


In FrmDialog.cs's code, below the previously-written FormRef properties, create two new
buttonPushed variables/properties – one private and the other Public. In this example, the
variables will be integers – representing the button's number:

Chapter 11 - Calling Multiple Forms Page: 772


FrmDialog: Defining buttonPushed Properties, completed
:
private Form private_FormRef = null;
public Form Public_FormRef
{
get => private_FormRef;
set => private_FormRef = value;
}

//Create two new properties for the button pushed..

//Use this property in this form:


private int private_ibuttonPushed = 0;

//Use this property in the Parent (calling) form:


public int Public_IbuttonPushed
{
get => private_ibuttonPushed;
set => private_ibuttonPushed = value;
}

Create an artificial btnClose Event:

When one of the four buttons is clicked, store the button-count in "private_ibuttonPushed"
(this class's private variable). Then call a button-close event. Since each of the default
buttons acts as a close-event, have each of them call a separate Close routine, avoiding
duplicate code.

In FrmDialog's view code, locate a place to write a new method "BtnClose"; this will not
be attached to any object or event on the form, but will be called by other events. Out of
habit, I still call this with a "Btn" prefix because it will act as if the user clicked "Close."
This is in addition to the previously-written "FrmDialog_FormClosing" event.

Although this is only two lines, it is called from four button locations. From a program
design's point-of-view, other logic could be added and all others will benefit. Manually
type this code in an appropriate location:

In FrmDialog, Manually Create BtnClose, completed


private void BtnClose()
{
//Simulate a button-close event - even though there are no
//button-closes on this form:

Public_FormRef.Show();
this.Close();
}

Chapter 11 - Calling Multiple Forms Page: 773


Populate private_ibuttonPushed when Clicked:

As a last step, record the user's clicked button into the holding variable – the
private_ibuttonPushed property. A numeric counter, 1-4, representing the button, is what
is stored:

1. Using Solution Explorer, "other-mouse-click" FrmDialog.cs and choose "view designer."


Double-click each of the four buttons along the bottom row. This stubs-in the click-
events.

2. For each of the button events, add these same two statements, changing
"private_ibuttonPushed = 1, to "= 2", etc. Illustrated are two of the four events:

FrmDialog ButtonClick Events x4, completed


private void Button1_Click (object sender, EventArgs e)
{
private_ibuttonPushed = 1;
BtnClose();
}

// : Do this for all four buttons,


// : (buttons 2 and 3 and 4 (not listed here)

private void Button4_Click (object sender, EventArgs e)


{
private_ibuttonPushed = 4;
BtnClose();
}

Testing the new Custom Dialog:

Returning to the parent form (ExampleProgram / FrmProcess / Form1), confirm the


previously-written button3_Click call-logic looks like this:

Chapter 11 - Calling Multiple Forms Page: 774


Example Program (using button3) Calling the Custom Dialog, previously written
private void button3_Click (object sender, EventArgs e)
{
//Declare the form up higher in the program or here
FrmDialog myCustomDialog;

//Instantiate the new custom dialog at the moment needed


//in order to save memory:

myCustomDialog = new FrmDialog


("This is the main prompt",
"A subprompt can go here and it can be much longer...",
"This is the titlebar text",
"&Cancel",
"&Process",
"&Rework",
null,
"DESKTOP",
0);

myCustomDialog.Public_FormRef = this;
myCustomDialog.ShowDialog();

//Diagnostic code: Remove when done testing; returns 1,2,3 or 4:


MessageBox.Show
("Button was: " + myCustomDialog.Public_IpubbuttonPushed);
}

where:

• From the calling program, the diagnostic


MessageBox.Show(myCustomDialog.Public_IbuttonPushed)

retrieves the public value via the "get", and displays it in the parent form, or it could
be tested with logic such as this:

if (myCustomDialog.Public_IbuttonPushed == 1) // button-1 (usually Cancel)


if (myCustomDialog.Public_IbuttonPushed == 2) etc...

To test:
if (myCustomDialog.Public_IbuttonPushed == 1)
{
//They pressed "Cancel"; note, button numbers are base-1
}

Chapter 11 - Calling Multiple Forms Page: 775


or test with a switch statement:

switch (myCustomDialog.Public_IbuttonPushed)
{
case 1:
//They clicked Cancel;
break;
case 2:
//Clicked Process
break;
case 3:
//Clicked Rework
break;
}

To test, Press F5 to run the program.


Choose any of the three-displayed buttons (button4 was null/hidden).

Results: The parent program displays "Button was x".

This completes the custom dialog box. The final result can be linked into any of your
development projects and it can be called with one statement. The box is flexible and can
accommodate a variety of prompts and buttons without having to re-write the code and
vastly improves a standard MessageBox. The next section demonstrates a nearly-identical
"InputText" box.

Chapter 11 - Calling Multiple Forms Page: 776


Custom InputBoxes: NS815_InputBoxDialog

C# does not have a simple "Input Box" dialog where you can prompt the user to type a
line of text in a separate input box (Visual Basic 6 users will recall inputBox =
"Message"). Besides, even if it did, it would not be as attractive as the forms offered
here.

This is a simple, yet attractive InputBox form that is missing in


C#. Once written, this class can be linked into your programs
with just a few steps.

Using nearly identical steps from the Custom Dialog routine, this section builds a generic
InputBox routine. The results look similar to this:

Because the steps are similar to the NS810_Dialog routine, they are summarized here.
See the previous section for specific steps.

These same techniques can be used in other class libraries.


Create private and public variables within the child-class. Then,
from the parent-form (or parent class), reach in and query the
values set.

This box will return a user-typed result to the calling program or null if "Cancel" is
clicked. A default input-string can be passed into the routine. Unlike the previous
section, this box only allows two buttons - OK or Cancel.

These steps are one-time setup steps.

Chapter 11 - Calling Multiple Forms Page: 777


A. Begin by starting a new Visual Studio project (opening a second copy of Visual Studio):

Name: NS815_InputBoxDialog
Location: C:\Data\Source\CommonVS
Form Name: FrmInputBox

B. Create a form, similar to NS810 (FrmDialog), with a main and sub-prompt; with 2 buttons
instead of 4. As before, change the buttons from default names of "button1" and
"button2" to "Button1", "Button2" (initial upper-case). Set the button's Visible property
to False.

Set the Form Border styles; hide control boxes, etc., all following the design on the
previous sections.

C. Add a standard textBox to the form (see "John Smith", illustrated above); name this field
"PnlInput".

Set the field and button tab-order as indicated on the illustration:

D. Link CL800_Util library code as you have in previous programs. (If you do not have the
util library, write your own "IsFilled" logic, seen later in the program.)

Add "FrmInputBox_FormClosing" logic by first stubbing-in the event from the form
designer's EVENT pane.

Standard FormClosing Event; tied to the Form's Event, completed


private void FrmInputBox_FormClosing
(object sender, FormClosingEventArgs e)
{
Public_FormRef.Show(); //Test by clicking "X"
}

Chapter 11 - Calling Multiple Forms Page: 778


The Public_FormRef.Show() value will be written in a moment. The editor
will temporarily flag as an error.

Write a simple "BtnClose" function. This is not tied to an event but will be called by the
button routines.

BtnClose routine, completed


private void BtnClose ()
{
//Simulate a button-close event;
//This is called by other button routines and is not
//linked to a true event

Public_FormRef.Show();
this.Close();
}

Again, see the previous section for construction details. This form is nearly identical to
NS810.

E. In the top Class-variable section, create Form properties to hold the private and Public
Form References, and to house the inputBox values.

Create these in the Form's Class-level variable section, near the CL800_Util util line.:

FrmInputBox: Properties, completed


public partial class FrmInputBox : Form
{
CL800_Util util;

private Form private_FormRef = null;


public Form Public_FormRef
{
get => private_FormRef;
set => private_FormRef = value;
}

//This is where you store the value that will be "returned"


//to the calling program (actually, it is the calling program
//that 'asks' for the value:

private string private_strInputBox = null;

public string Public_strInputBox //This is the exposed variable


{
get => private_strInputBox;
set => private_strInputBox = value;
}

:
:

Chapter 11 - Calling Multiple Forms Page: 779


where:

• For this example, the property "Public_strInputBox" is a string value. It will be


populated when the user clicks button2 (OK) and this is the only variable that is
visible to the calling program.

• The variable "private_strInputBox" is declared as a private variable and is only visible


to this routine. When the calling program attempts to retrieve the value, it will pass
through the 'get' routine. All values stay private (scoped) to the input form and are
never exposed, except through the get.

F. Extend FrmInputBox's Constructor so it accepts the following passed parameters; the


Constructor is the line that reads "public FrmInputBox()" and has the
InitializeComponent line. Type this lengthy comma-separated list inside the
parenthesisis. This is not the same parameter list as the 810 DialogBox:

FrmInputBox: Accepting Passed Parameters, completed


:
using NS800_Util;

namespace NS815_InputBoxDialog
{
public partial class frmInputBox : Form
{
CL800_Util util;

private Form private_FormRef = null;


:
:<other declarations here>
:

//This is the Constructor [ public FrmInputBox () ]


public FrmInputBox
(string strMainPrompt,
string strSubPrompt,
string strTitleBarText,
string strButton1Caption,
string strButton2Caption,
string strDefaultInput,
string strAttitude,
int defaultButton)
{
InitializeComponent();
util = new CL800_Util();

//Assemble the passed parameters:


PnlMainPrompt.Text = strMainPrompt;
PnlSubPrompt.Text = strSubPrompt;
this.Text = strTitleBarText;

//Set Startup Attitude


//Expose the buttons
//: (more code, below in next section)

Chapter 11 - Calling Multiple Forms Page: 780


G. Still inside of FrmInputBox.cs, continue in the same routine by setting the attitude and
exposing the passed buttons.

Pre-populate the inputBox field with the PnlInputText.Text statement...:

FrmInputBox; Exposing Buttons in the Constructor, completed


:

//Set the startup attitude: How aggressive to display


//Attitude = "DESKTOP" or "APPLICATION"
//where DESKTOP appears on the task bar and the center of
// the screen.
//where APPLICATION appears centered in the current app
if (strAttitude.ToUpper() == "DESKTOP")
{
StartPosition = FormStartPosition.CenterScreen;
ShowInTaskbar = true;
TopMost = true;
}
else
{
StartPosition = FormStartPosition.CenterParent;
ShowInTaskbar = false;
TopMost = true;
}

//Expose the buttons, if passed:


if (util.IsFilled(strButton1Caption))
{
Button1.Visible = true;
Button1.Text = strButton1Caption;
}
if (util.IsFilled(strButton2Caption))
{
Button2.Visible = true;
Button2.Text = strButton2Caption;
}

//Populate default inputbox, if passed


if (util.IsFilled(strDefaultInput))
{
PnlInputText.Text = strDefaultInput;
}

} //End of constructor

H. In FrnInputBox's design view, create the button-click logic by double-clicking Button1


and Button2. Add these statements to each event. Be sure to double-click the button so
the event is tied to the code.:

Chapter 11 - Calling Multiple Forms Page: 781


frmInputBox: Button Logic, completed
private void Button1_Click (object sender, EventArgs e)
{
private_strInputBox = null; //They clicked Cancel!
BtnClose();
}

private void Button2_Click (object sender, EventArgs e)


{
private_strInputBox = PnlInput.Text; //Move what was typed...
BtnClose();
}

where:

• As usual, take care with proper casing. "Button1" begins with a capital "B" (as
renamed in design view). "PnlInput.Text" begins with a capital "P".

I. Close the Visual Studio Editor (this is the second copy of Visual Studio), saving all
changes.

Return to your original source code in the ExampleProgram and make the call to the new
InputBox routines.

Using the Custom InputBox Dialog:

In your test program (ExampleProgram/frmProcess - Form1), follow these steps to bring


in the library and make the call:

1. Using Solution Explorer, "Add Existing Item,"


C:\Data\Source\CommonVS\NS815_FrmInputBox.cs
choosing "Add" (as opposed to "Add as Link").

Input panels often have customized or additional logic. Because


of this, you may not want to link the name space into your
program. In these instances, a copy is better – this way you can
make code changes without impacting other programs that may
share the same library.

2. In your main program (FrmProcess / Form1), add this statement near the top of the
program:

using NS815_InputBoxDialog;

3. Declare the new class, similarly as you have with CL800 and the 810_Dialog.
Previous examples also shown:

Chapter 11 - Calling Multiple Forms Page: 782


ExampleProgram: Declare the InputBox, completed
using NS810_Dialog; //From previous examples
using NS815_InputBoxDialog;

namespace ExampleProgram
{
public partial class Form1 : Form
{
CL800_Util util;
FrmDialg myCustomDialog; //From previous example

FrmInputBox myCustomInputBox;

public Form1()
{
InitializeComponent();
util = new CL800_Util;
}

4. Drop another button (button4) in your test program.


Attach this code to the button-event:

ExampleProgram: button4 calling the InputBox, completed


private void button4_Click (object sender, EventArgs e)
{
//Call the Input Box, passing a default value:

myCustomInputBox = new FrmInputBox


("This is the main prompt",
"Here is some additional prompting and stuff",
"TitleBar text here",
"&Cancel",
"&OK",
"John Smith or null",
"APPLICATION",
0);

myCustomInputBox.Public_FormRef = this;
myCustomInputBox.ShowDialog();

//Diagnostic code:
//Note: this is the "Publicly-declared variable" from within
//the input form:
MessageBox.Show(myCustomInputBox.Public_strInputBox);
}

Testing with Minor Flaws:

Press F5 to run your program; click on Button4.


Type any text in the Input Box; then Click "OK"

Results: FrmProcess reports back what was typed. If Cancel is clicked, null is returned.

Chapter 11 - Calling Multiple Forms Page: 783


Concerns:
• After typing text in the input-field, Enter cannot be pressed (It should assume OK).
This will be fixed shortly.

• There are no restrictions on the length of the text field or other audits.

This completes the Custom InputBox form.

You may have noticed two minor issues. First, for all intents, button1 has been hard-
coded as a 'Cancel' button even though the button-text is variable. If clicked, a null is
returned regardless of the button's text. This is a relatively minor issue and you may wish
to write other code to work around this. Secondly, the last parameter is meant to change
the default button; this field is not being used at this time.

Applying Form Properties to other Classes:

The technique of using a get/set to retrieve values from a sub-form also works well in
other classes and you are not restricted to a single field.

In Chapter 14 INI Files (CL860_BasicINIRead), the main form, Form1, calls a class that
reads the contents of an INI file. The entire class is composed of several dozen functions
and it retrieves multiple values. When the class closes and control returns to Form1,
Form1 can retrieve all of the found values through these class properties (exactly like the
input-box properties built in the example above).

For example, from Chapter 14:

The called class might declare several Properties, as in:

private string private_strProgramVersion = "";


public string Public_strProgramVersion
{
get => private_strServerName;
set => private_strServerName = value;
}

private bool private_boolStartupError = false;


public bool Public_boolStartupError
{
get => private_boolStartupError;
set => private_boolStartupError = value;
}

The calling routine (Form1) would call some public method (such as B000_ReadINI) and
when it returned, Form1 could cherry-pick values stored in the class, as in:

readINI.B000_ReadINI();
MessageBox.Show(readINI.Public_strProgramVersion + "\r\n" +
readINI.Public_boolStartupError.ToString());

where: readINI is the instantiated class. More details in Chapter 14.

Chapter 11 - Calling Multiple Forms Page: 784


Detecting a KeyPress ENTER Key Event

When text is entered in the input-field, most users would prefer to press Enter to accept
the entry. In this section, write an event which listens for the ENTER key and then
automatically launches button2_Click (OK). This example uses the InputBox screen built
in the previous section but will work on any panel text field.

1. In frmInputBox's design view, highlight field PnlInput.

2. In Properties, click the Events button


Locate the KeyPress event
Double-click the blank field to the right; this stubs-in the code

3. In Code View, in the new stubbed-in section, add this if-statement

frmInputBox: ENTER KeyPress Event, completed


private void PnlInput_KeyPress (object sender, KeyPressEventArgs e)
{
if (e.KeyChar == 13)
{
button2_Click(null, null);
}
}

where:

• Notice how the KeyPress event is attached to the textBox object (pnlInput) and not to
the Form itself. Multiple fields can have KeyPress Events.

• The signature-line has a KeyPressEventArgs, assigned to a variable called "e" out of


convention and tradition.

Chapter 11 - Calling Multiple Forms Page: 785


• Within the routine, e.KeyChar == 13 tests for an ascii Carriage-Return. If found, it
presses the form's own button 2, which is the "&OK" button. Other characters can be
tested with additional if-statements within this same event.

Testing:

Launch the main Program (F5, frmProcess) and click (button4).


If you passed a default name, e.g., "John Smith", it displays in the inputBox.
Type changes to the name and press Enter

Results: Control is returned via button2 ("OK"). frmProcess's Diagnostic messageBox


shows the entered name.

This completes the InputBox routines and this library can be easily inserted into any of
your projects.

Chapter 11 - Calling Multiple Forms Page: 786


Using NS815 to Pass Arrays instead of Strings

A program is not restricted to integers and strings. For another example, a call to the
NS815_Input Dialog could return an array.

This section is presented as a discussion and is not a complete


example.

Consider the following code-snippets, which use NS815 for the base-design. Because this
was originally designed to return a string, you would not want to modify the original
module for fear of breaking other programs that link-in the same routines. To work
around this, when "Adding an Existing Item (NS815)" in Solution Explorer, you would
"Add" (instead of "Add as Link") – giving you a copy of the routine.

Within the copied 815-module's "frmInputBox.cs", declare an ArrayList with these lines
of code scattered appropriately in the module:

using System.Collections;
ArrayList alMyWorkingValues; //Do all your manipulations here
alMyWorkingValues = new ArrayList();

Below the FormRef, add the properties to support the array. As before, the variable is
declared as a private and then as a public value. Note each are declared as an 'ArrayList'
and would replace all "String" variables that lived here previously:

private ArrayList private_alTheReturnedArray = null;

public ArrayList Public_alTheReturnedArray


{
get
{
//Copy the working values to the private variable and let the
//'get' return what it found...
private_alTheReturnedArray = alMyWorkingValues;
return private_alTheReturnedArray
}
}

Within the main, calling program, declare an array to receive the results and call the
NS815_Input form – or any other form:

private void btnCallTheInputRoutine()


{
ArrayList alreturnedArray;
alreturnedArray = new ArrayList();

//NS815 or some other form previously added to Solution


FrmInputBox getStuffFromOtherForm;

//Call the form:


getStuffFromOtherForm = new FrmInputBox
("Big prompt text",

Chapter 11 - Calling Multiple Forms Page: 787


"small text also used by the NS815 routine",
"Cancel",
"Accept",
"", "APPLICATION", 0);

getStuffFromOtherForm.Public_FormRef = this;
getStuffFromOtherForm.ShowDialog();

//Reach into and grab the public array


//Use this returned value in the main program
alreturnedArray = getStuffFromOtherForm.Public_alTheReturnedArray;
}

Chapter 11 - Calling Multiple Forms Page: 788


Multiple Input Screens in the Same Program

NS815_InputBox prompts the user for a single textBox data-entry field but invariably
your program may need multiple textboxes on that same panel or perhaps one of the
textboxes needs to accept only numeric data or it may have auditing routines that vary. A
single 815-routine may not be adequate.

In these cases, you should "Add (as a copy)" rather than "Adding as a link" when bringing
in the NS815 name-spaces24. With this, you can make unique changes to the copy – with
a problem. With multiple 815-routines, you will find two copies of the 815 libraries will
share the same form design – and this will not allow you to make custom changes to one
without affecting the other.

This section is presented as a discussion and is not a complete


example.

Follow these steps to add the new (copy) of an NS815 Input form:

A. For the first custom InputBox, Add as a normal linked (or copied) module, using the steps
described in the previous section.

B. In Solution Explorer, rename from "frmInputBox.cs" to "frmInputBoxVersion1.cs" (the .cs


extension required).

C. "Other-mouse-click" the newly-renamed frmInputBoxVersion1 and chose "view designer",


opening the form designer. In the Form's main properties window, rename the form from
"frmInputBox" to "frmInputBoxVersion1" (this way, this form name will not collide with
the soon-to-be-added second input form).

D. In Solution Explorer, expand the submenu next to "frmInputBoxVersion1"; this opens up


two new documents:

frmInputBoxVersion1.designer.cs
frmInputBoxVersion1.resx

Double-click on the interior "frmInputBoxVersion1.designer.cs".

In the opened code, change the top-line


from "namespace NS815_InputBoxDialog" to
to "NS815a_InputBoxDialogVersion1"

As the name is changed, click the small under-score-red-line and allow the change to
propagate through the program. Close this window.

24
There are ways to inherit and make changes in the new copy of the Form, but these steps are
beyond the scope of this text.

Chapter 11 - Calling Multiple Forms Page: 789


In your original (main) program, confirm the using NS815_InputBoxDialog" changed to
the new "using NS815a_InputBoxDialogVersion1".

E. In Solution explorer, re-add/import a second copy of the original "NS815_InputBox."


Once it arrives, consider renaming it, as described above, to "...Version2" (or some other
meaningful name).

F. Add "using NS815_InputBoxVersion2" to the top of your program.

G. In the other logic in the program (usually a button_Click event), instantiate and use the
form as before:

Calling the second copy of InputBox, sample


FrmInputBoxVersion2 myNewInputBox;

myNewInputBox = new FrmInputBoxVersion2


("Main prompt text goes here",
"subprompt text goes here",
"Titlebar text here",
"&Cancel",
"&OK",
"default input text or use null",
"APPLICATION",
0)

myNewInputBox.Public_FormRef = this;
myNewInputBox.ShowDialog();
strreturnedValue = myNewInputBox.Public_strInputBox;

This completes the discussion of having multiple inputBoxes in the same program.

Chapter 11 - Calling Multiple Forms Page: 790


Final program listing for NS810_Dialog. See this chapter for instructions on how to add
this code to its own class library. Required objects (buttons, etc.) are shown earlier in the
chapter.

For NS815_Input's complete code, contact the author for a download.

FrmDialog.cs (NS810_Dialog), Complete code listing

using System;
using System.Windows.Forms;
using NS800_Util;

namespace NS810_Dialog
{
public partial class FrmDialog : Form
{
CL800_Util util;

public FrmDialog
(string strMainPrompt,
string strSubPrompt,
string strTitleBarText,
string strButton1Caption,
string strButton2Caption,
string strButton3Caption,
string strButton4Caption,
string strAttitude,
int idefaultButton)
{

InitializeComponent();
util = new CL800_Util();

//Assemble the passed parameters here:


PnlMainPrompt.Text = strMainPrompt;
PnlSubPrompt.Text = strSubPrompt;
this.Text = strTitleBarText;

//set the Startup Attitude - does the prompt stay attached to the
//running program or is it front-and-center
//StartAttitude = "APPLICATION" or "DESKTOP"
// Application = start position center parent, show in taskbar=F
// Desktop = start position, center screen, show in taskbar=T
if (strAttitude.ToUpper() == "DESKTOP")
{
StartPosition = FormStartPosition.CenterScreen;
ShowInTaskbar = true;
TopMost = true;
}
else
{
//Default more conservatively to the Application
StartPosition = FormStartPosition.CenterParent;
ShowInTaskbar = false;
TopMost = true;
}

//Expose buttons
//If text was passed in the button list, expose the buttons:

Chapter 11 - Calling Multiple Forms Page: 791


if (util.IsFilled(strButton1.Caption))
{
Button1.Visible = true;
Button1.Text = strButton1Caption;
}
if (util.IsFilled(strButton2.Caption))
{
Button2.Visible = true;
Button2.Text = strButton2Caption;
}
if (util.IsFilled(strButton3.Caption))
{
Button3.Visible = true;
Button3.Text = strButton3Caption;
}
if (util.IsFilled(strButton4.Caption))
{
Button4.Visible = true;
Button4.Text = strButton4Caption;
}

private Form private_FormRef = null;

public Form Public_FormRef


{
get => private_FormRef;
set => private_FormRef = value;
}

//Use *this* property in this form:


private int private_ibuttonPushed = 0;

//Use this property in the parent (calling) form:


public int Public_ibuttonPushed
{
get => private_ibuttonPushed;
set => private_ibuttonPushed = value;
}

private void FrmDialog_FormClosing


(object sender, FormClosingEventArgs e)
{
Public_FormRef.Show();
//The cancel button, if passed) has similar logic...
}

private void Button1_Click (object sender, EventArgs e)


{
private_ibuttonPushed = 1;
BtnClose();
}

private void Button2_Click (object sender, EventArgs e)


{
private_ibuttonPushed = 2;
BtnClose();
}

private void Button3_Click (object sender, EventArgs e)


{
private_ibuttonPushed = 3;
BtnClose();
}

Chapter 11 - Calling Multiple Forms Page: 792


private void Button4_Click (object sender, EventArgs e)
{
private_ibuttonPushed = 4;
BtnClose();
}

private void BtnClose()


{
Public_FormRef.Show();
this.Close();
}

}
}

FrmDialog - End

This completes the multi-form chapter and C# Programming, Volume 1.

Volume2:
Reading and Writing ASCII Files
Reading INI Files
Parsing Tab Files
Parsing CSV
Reading XML Files
Windows Registry
Wait States
Printing
Advanced Formatting

Volume 3
Arrays and Lists
File Manipulation
Console Applications
SQL

Chapter 11 - Calling Multiple Forms Page: 793


Exercises

A. Write a program where Form1 calls Form2.


Then, allow Form2 to call Form3.
Provide a mechanism to directly return from Form3 to either Form1 or Form2.

B. Modify Program 11.4 so Form2 can also update the same Global variable
"ProgramGlobal.Form1textBoxValue" and have that newly-updated value appear
correctly in Form1 when Form1 regains focus.

Hint: This requires a new Form-level event in Form1.

C. From a test program, use the NS810 dialog box to display an attractive message with
these three buttons:

Cancel
Order Lunch
Order Dinner

Have the main program display a message box showing which was returned.

D. Similar to C, call NS815 and return to the calling program the text they typed in the input
box.

Chapter 11 - Calling Multiple Forms Page: 794


Chapter 11 - Calling Multiple Forms Page: 795
Appendixes
Appendix A - Compiler Error Messages

Alphabetic Listing

This is an alphabetic listing of various compiler messages with likely solutions. These are
from Visual Studio 2005, SP1 through VS 2014.

Errors and warnings are sorted alphabetically. Search by the first non <variable> word.
e.g. "Argument '2': cannot convert from 'double' to 'float' will be found under "cannot..."
Messages such as "The type arguments..." will be under "The"; Messages that begin with
punctuation ("; expected") are listed first.

Additional Comments on Error Messages:


If you have repaired a mis-typed line in your program but the same error message
continues to show, try the following from the editor window: Select menu choice: "Build,
Rebuild Solution".

"; expected" (semi-colon expected)


Also: Invalid Expression Term "."

Symptoms:
The compiler normally shows exactly where a semi-colon is expected and when you get this
error it is normally flagged at the very end of a line. If the compiler shows it in the middle
of a line, it can get confusing.

Problem:
This incorrectly typed command would show an expected missing semi-colon at the
Convert.ToString phrase.:
MessageBox.Show Convert.ToString(loopCounter); //missing paren

Solution:
In this MessageBox example, note that the MessageBox.Show phrase was incorrectly typed;
it is missing a set of parenthesis. This confuses the compiler like something awful. The
correct syntax is:

MessageBox.Show (Convert.ToString(loopCounter));

Problem
In this incorrectly typed command, the word "if" is 'misspelled' with a capital "I" instead of
a lower-cased "if":
If (IsBlank(testString)) //Capital "If" is wrong

A110: Database problems, various

See "A Network-related or instance-specific error occurred while establishing a connection


to SQL Server"

A constant value is expected

Possible Solution:

Common Error Messages and Solutions Appendix A: 3


In a 'switch' statement, a 'case' is using a variable instead of a hard-coded literal or a
constant. Replace the case <variableName> with case "quoted-string" or number.

A list that is this enumerator is bound to has been modified. An Enumerator can only be used if the list
does not change. (Sic)

Symptoms:
Attempting to delete an item from an array, comboBox, listBox, etc, while in the middle of
a foreach loop.

Issue:
You cannot delete an array-item while in the midst of a foreach loop.
Mark the item's position (counter) – typically in another temporary array and use a separate
loop to remove them, after the first loop completes.

A local variable named 'e' cannot be declared in this scope because it would give a different meaning to
'e', which is already used in a 'parent or current' scope...

Symptoms:
The top of the module, typically button1_Click, already has an 'e', as in "EventArgs e" and
you probably have a try-catch that also uses "(Exception e)"

Recommendations:
See button1_Click's signature line and compare it with the catch statement's signature lines

Change the "(Exception e)" to "(Exception e2)" with corresponding changes to e2.Message.
Or consider moving (most) of the logic from button1_Click to its own routine: e.g.
A100_Process(); which won't have an 'e' in its declaration.

A Namespace does not directly contain members such as fields or methods.

Possible Solution:
Do not define variables or methods (functions) above the form level; form-class level.
If you are trying to make a "global" variable, see Chapter 7.

A Network-related or instance-specific error occurred while establishing a connection to SQL Server

Symptoms:
While opening a form that uses SQL server resources.

Solution:
Confirm that the SQL Server is running and you have rights to the database.
If the SQL Server is running locally, on the LocalHost, confirm the Microsoft SQL Server
Services are started. From Windows, Start-Run, "Services.msc"; Confirm SQL Server
(SQLExpress) is started

An Error has occurred while establishing a connection to the server.


When connecting to SQL Server 2005/2008, ... (this failure may indicate) ...
SQL Sever does not allow remote connections.
Could not open a connection to SQL Server.

Common Error Messages and Solutions Appendix A: 4


See SQL: An Error has occurred while establishing a connection to the server....

An object of a type convertible to 'string' is required.

Issue:
"return" is not returning the correct 'type'.

Solution:
Examine the method's signature line to see if it returns a string, integer or other type of
object. The corresponding return statement(s) within the module must also return that same
'type'.

For example, a string function needs to return a <string> value.


It cannot return (nothing)

example code:
private string myFunction()
{
if (util.IsBlank(mystring))
return mystring;

Meanwhile, a void function can only return (nothing):

private void myFunction2()


{
stuff
return;

An object reference is required for the nonstatic field....

This is a generic error that generally means the compiler cannot find the variable or an
associated class was not instantiated.

Possible Solution:
If the variable or method in question is in a different Class, do one of the following:
a) Declare the variable as "public" or "internal" and instantiate the class within your
Form/Class using the "new" keyword. See Chapter 6, External Class Libraries, for
details.

b) Declare the variable as "public static" <string> or

c) If the method is in error, consider declaring the method as "public static...." or better
yet, "internal static" as in

internal static string B000_INILoad.B021_DiscoverINIFileName();


//returns string

Possible Solution:
If calling a class.method within another 'sub-Class' (see Chapter 6; where cl800_Util was
placed within the PayrollTools.cs Class), there may not be a "constructor" in the new
(payroll) class and because of this, the (cl800_Util) declaration may not have a place to run.

Move the declaration into another method, directly above the Instantiation.
In simpler terms, move "cl800_Util util;" just above the line "util = new cl800_Util();"

Common Error Messages and Solutions Appendix A: 5


Possible Solution:
If you have just switched a variable from a local variable to a "public static" variable, re-
compile the program using menu Build, Rebuild Solution.

Possible Solution:
Misspelled or wrong case variable name.

Possible Solution:
Especially when using a (Form's) properties. Do not use the current Form's name (it was not
instantiated within itself); instead, use "this."

ProgramGlobal.IformLeftPos = frmA000Form.Left;
ProgramGlobal.IformLeftPos = this.Left;

Note: You could also simply use "... = Left;", which is considered too vague for
most people even though the code would work.

ArgumentoutOfRangeException was unhandled

Argument out of range exception (s) are always due to an array being unalloacted, un-
available or a value [x] within square-brackets was using a larger number than the size of
the array. This always indicates a logic or counting problem and often the problem happens
at the end of a loop, where you over-shoot by one position. Remember, arrays are base-0; a
ten-item array's last position is [9].

See also "Index out of range"

If manipulating SQL data, statements, such as:


dataGridView1.Columsn[0].xxxxx = "yyyy"
may indicate the SQL Server service has not started on your development machine or the
remote SQL server is not available.

In any case, array arithmetic should be protected with a try-catch (if using a for-next loop or
are addressing [addresses] directly. Consider using a for-each loop, if logic is appropriate.

Argument '1': cannot convert from 'object' to 'string'


Also: The best overload method match for '<form(parameter)>' has some invalid arguments.

Issue:
The parameter you are trying to send is something other than a <string>; often the results
are an object-type or a "collection".

example:
frmA031CategoryAdd addCat = new frmA031CategoryAdd
(dataGridView1.SelectedRows[0].Cells[0].Value);

...SelectedRows[0].Cells[0].Value is not necessarily a string. You can test this


by adding a .ToString() method and placing a debugging breakpoint at the statement. While
debugging, hover the mouse before the ".ToString()" method to see the value is missing
quotes – indicating it is not a string.

Possible Solution:
Convert it to a string using one of these two techniques:
... (dataGridView1.SelectedRows[0].Cells[0].Value.ToString());

Common Error Messages and Solutions Appendix A: 6


... ("" + dataGridView1.SelectedRows[0].Cells[0].Value);

Argument '1' must be passed with the 'ref' keyword


Also: The best overloaded method match for '<class>(ref string, string)' has some invalid
arguments.

Solution:
When using "By Reference" (ref), both the calling and the called functions need the 'ref'
keyword. C# requires this for documentation purposes.

Example:
appendDefaultAreaCode (ref myPhoneNumber, locationDefaultAreaCode)

private void appendDefaultAreaCode


(ref myPhoneNumber, locationDefaultAreaCode)
{

Array creation must have array size or array initializer

Issue:
Sometimes you can declare an open-ended array with a simple statement, such as:
string [] afoundFields;
but if the array is used inside of a loop (while-statement), C# often requires that the array be
initialized with a starting value or by declaring a fixed array size. This is incase the while
statement never runs and downstream commands may panic.

Solution:
Initialize the array with an item count. Consider over-allocating.
string [] aFoundFields = new string [100];

Array does not have that many dimensions

Possible Solution:
Assuming a single-dimension array (a linear array),
aArrayName.GetUpperBound(0);

where (integer 0) is the first dimension of the array, "aArrayName".

Presumably you used aArrayName[x,x], when the array only had one dimension,
aArrayName[x].

'<btnClose>' is a 'field' but is used like a 'method'


<btnClose> is a field but is used like a method

Summary:
Typed as btnClose()
Should be typed as an Event: btnClose_Click(null, null);

See also: "is a field but is used...."

Common Error Messages and Solutions Appendix A: 7


Build Failed - with no compiler error messages

Likely solutions - Do all:


Close Visual Studio
Using Windows Explorer, open the Project's folder; delete all "*.suo" files
Re-Launch VS; select top-menu View, Output Window
Rebuild solution.

If still an error, look in the output Window. (See top-menu, View, Output)

Related: See "The type or namespace name 'Tasks'....

Cannot access a closed registry key

Symptoms:
Usually while performing a .GetValue(stringName)

Solution:
Move the RegKey.Close command below the GetValue statements. If the GetValues are in
a loop, be sure the Close is after the loop.

Cannot Assign to '<string name>' because it is a 'foreach iteration variable'

Symptoms:
Attempting to manipulate an array-element from within a foreach loop.

Issue:
Within a foreach loop, you cannot modify or change the values used by the foreach loop.
More to the point, you cannot transform or change the array's internal elements with a
foreach loop.

Solutions:
If you are merely trying to change the value of the array's element, move the value to a
secondary (intermediate) temp-string. Consider this example, with particular attention on
strtempString:

foreach (string strtempString in aTestArray)


{
//Note: strtempString cannot be manipulated directly
//within the loop

//Use an intermediate value to manipulate


string tstring = strtempString;

tstring = tstring.ToUpper();
}

If your intent is to actually change the value(s) of the items in the array, you cannot use a
foreach loop. Instead, use a for-next loop.

for (int ii = 0; ii <= aTestArray.Length - 1; ii++)


{
//This actually modifies the values in the array...
aTestArray[ii] = aTestArray[ii].ToUpper();
}

Common Error Messages and Solutions Appendix A: 8


Note the loop runs to the Array's length, minus-1 – a base-0 calculation
See the Array Chapter, "Transforming Array Elements" for more details.

Cannot connect to <Server> (SQL Server Management Studio)

Symptoms:
When attempting to launch SQL Server Management Studio

Possible Solution:
Are the services (Start, Run, Services.msc) "SQL Server" started?

<argument>: cannot convert from 'double' to 'float'

Solution:
A numeric parameter must be specified as a floating point number "F"
e.g.
Pen myPen = new Pen (Color.Black, 0.3) should be
Pen myPen = new Pen (Color.Black, 0.3F)

Cannot convert method group '<various: GetLength, etc>' to non-delegate type 'int'. Did you intend to
invoke this method?

Possible solution:
ilastHighlighted = myFiles.GetLength();

Did you remember the closing parenthesis?

Cannot Convert method group '<name>' to non-delegate type 'bool'. Did you intent to invoke this
method?

Possible Solution:
if using an implied comparison in an if-statement:

if (A100_SomeMethod_ThatReturns_Bool)
{
//Incorrect, missing ()
}

you forgot the parenthesis after the method name. Instead:

if (A100_SomeMethod_ThatReturns_Bool() )
{
//corrected with ()
}

if (A100_SomeMethod_ThatReturns_Bool() == true)
{
//optional
}

Cannot currently modify this text in the editor. It is read-only

Solution:

Common Error Messages and Solutions Appendix A: 9


Close the running program before attempting to modify either the code or the design-view.
You cannot edit while the program is running.

Either close the running VS program (your program) or in the Visual Studio Editor (ISE),
click ribbon-bar "Red Square" icon to abruptly close your program.

Cannot convert null to 'System.DateTime' because it is a non-nullable value type

Issue:
A DateTime method is attempting to return a null value to the calling module when only
"DateTimes" are allowed. This often happens in a try-catch error condition.

Solution:

Change the signature line from


private DateTime A100_MethodName()

to a "nullable" DateTime, where the <brackets> are required.

private Nullable <DateTime> A100_MethodName()


{
try
{
//Do stuff here
}
catch
{
return null;
}
}

Test in the calling routine using


if (returnedVariable.HasValue)

where the HasValue method only operates on items with a nullable data-type. See below for
more information on this.

Optionally, in the case of this examle's DateTime value, you could also use this command,
bypassing the Nullable solution: return DateTime.MinValue;

Solution:

Change the original DateTime value to a "nullable" variable by using a question-mark in the
declaration.

:
DateTime? dtValue = (some date/time or null if not available);

With this, the downsteam function can return a null, if it has the need to do so.

:
if (dtValue.HasValue)
return dtValue.Value;
else
return null;

Common Error Messages and Solutions Appendix A: 10


<procedureName> cannot declare instance members in a static class

Symptoms:
Usually when building a new method or function near the "static class program" / "static
void Main" class – the main driving procedure for your program. You have tried to use a
"private void <functionName>" within a "static" class.

Solution:
Consider changing
private void <functionName> to
private static void <functionName>

Cannot implicitly convert type 'int' to 'string'

Symptoms:
Code is trying to display a text message, a MessageBox, assign a text label, or assign a text
field with both text (string) data and numeric data. The numeric data refuses to cooperate.

Solution:
Use Convert.ToString on any numeric fields (or other non-string data-types) before moving
them or concatenating them to another string [field].

MessageBox.Show ("variable 'i' is set to this value: " + Convet.ToString(I));


textBox1.Text = "The number is equal to " + Convert.ToString(valueA));

also: <variableName>.ToString();

Cannot implicitly convert type 'long' to 'int' (are you missing a cast?)

Possible Solution:
Examine the return values of the command you are using. It likely is returning a 'long'
value, not an integer. The error will be flagged deep within the code, but it is the function's
(method's) signature line where you may need to make the fix.

For example:
private int A630_ReturnFileLength (string strpassedFileName)

and: return fi.Length; <error complains here

but the fix may be changing the "int" to "long" on the Signature line.
Change "private int ..." to "private long ..."

CS0029
Cannot implicitly convert type 'string' to 'System.Windows.Forms.Label'

Solution:
Be sure to use a ".Text" when populating a label.
For example, assigning a blank string to a label:

Incorrect: lblmyField = "";


Correct: lblmyField.Text = "";

Common Error Messages and Solutions Appendix A: 11


Cannot implicitly convert type 'System.Data.CommandType' to 'SystemLdata.Sqlclient.SqlCommand'

Issue:
Missing method name ".CommandType"

example code:
SqlCommand refCategoryCMD = new SqlCommand("RecordCategoryDelete");
refCategoryCMD = CommandType.StoredProcedure; //In error

Solution:
refCategoryCMD.CommandType = CommandType.StoredProcedure;

Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a
cast?)

Symptoms:
You are using a string array and attempting to assign a value to another text field.

For example:
lblDisplay.Text = aNames[1]; //fails
MessageBox.Show(aNames[1]); //fails

Solution:
Convert to String prior to assigning. This can be done explicitly or implicitly:

lblDisplay.Text = aNames[1].ToString();
lblDisplay.Text = (string)aNames[1];
MessageBox.Show("" + aNames[1]);

CS0029
Cannot implicitly convert type 'string' to 'bool'
Cannot implicitly convert type 'int' to 'bool'

Symptoms:
In an "if" or other conditional.

Likely solution:
Did you use a required double-equal in the conditional?
if (testString = "Smith") vs
if (testString == "Smith")

Cannot implicitly convert type 'string[*,*]' to 'string[]'

Likely explanation: A multi-dimensioned (dynamically-sized) string array was declared in a


higher scope using and later dimensioned with actual sizes:

string [,] aCollectionNames; //Declared at a higher scope

and then later, in a different method, initialize with a fixed size, as in:

aCollectionNames = new string [15,4]; //Dimensioned

Common Error Messages and Solutions Appendix A: 12


The author had this error after several mistyped array definitions. But once the array was
declared, as described above, the error persisted. Finally, after selecting menu "Build,
Rebuild Solution"; the problem went away.

Cannot implicitly convert type 'string' to 'System.Windows.Forms.TextBox'

Likely Solution:
You neglected the ".Text" appendage.

For example:
Incorrect:
textBox1 = textBox1 + Convert.ToString(<variable>);

Correct:
textBox1.Text = textBox1.Text +
Convert.ToString(<variable>);

For example:
MessageBox.Show("'" + pnlCategoryCode + "'");
vs
MessageBox.Show("'" + pnlCategoryCode.Text + "'");

Cannot implicitly convert type 'System.DateTime?' to 'System.DateTime'. An explicit conversion exists


(are you missing a cast?)

Issue: You are using a Nullable <DateTime> and since a Null is allowed, you must re-
convert to the same type. This seems redundant in code because the called function may
already be returning a Date Time. Re-cast the returned value:

Example Syntax:

DateTime dtfileDate;
dtfileDate = (DateTime)A700_ReturnFileCreateDate(textBox1.Text);

See also "Cannot convert null to 'System.DateTime' because it is a non-nullable value


type" and consider a "nullable" declaration or cast (e.g. DateTime? dtValue;)

See Chapter 24 for further discussions.

Cannot Insert an explicit value into a timestamp column (SQL)

Issue:
SQL data field was defined as 'Timestamp' but C# code is trying to insert a Date. Change
the SQL field definition to a date-time or date format.

Cannot use local variable '<variable>' before it is declared

Likely Solution:
Declare (and possibly initialize) the variable before using:
string myString = "";
if (myString = "House")

Common Error Messages and Solutions Appendix A: 13


Also, you can see this message if an if-statement, either above or below the first error has a
mis-spelled "Else" (vs "else") or missing parenthesis. This may take a while to locate in
large modules.

Possible (and likely) Solution:


In the function or method, is the last "return" statement mis-spelled, as in capital-R-Return?
or is the "return" clause otherwise mal-formed.

Changes are not allowed while code is running...

Solution:
Your program is still running from your last compile (F5 / Run). Locate the program on the
task bar and close before attempting to run it again. Alternately, from the Editor, press
Shift-F5 to force-close the program.

Command Line Arguments not parsed; Command Line Arguments ignored

By default, Visual Studio will not allow passed command line arguments, even
though the Start Options are set in the Project's properties.

Symptoms:
The program will behave as if no command-line arguments were passed, especially
if you compile a Release version of the program. Make this additional change in
the program:

In Project Properties, Security, click [x] Enable ClickOnce Security Settings.


Then click "This is a partial trust application".
Click the Advanced button
Unclick "Debug this application with the selected permission set"
Click OK
Click "This is a full trust application"

Alternately: In Project Properties, left-nav, click Security.


Uncheck "Enable ClickOnce Security Settings".

Control cannot fall through from one case label ('case "<label>":') to another

Solution:
In a 'switch' statement, a "case" statement is missing a break; command, as in

case "Green":
<do stuff here>
break;
case "Red":
<do other stuff here>
break;

<formanme> does not contain a constructor that takes 1 arguments

Solution:
The call, typically on btnFormName_Click, instantiates a new form, as in:
frmA031CategoryMaint catMaint = new frmA031CategoryMaint("");

Common Error Messages and Solutions Appendix A: 14


catMaint.InstanceRef = this;
catMaint.ShowDialog();

where it is passing one parameter, in this case, null, typed as ("").

But in frmA031's constructor, at

public frmA031CategoryMaint()
{

(this example) does not show any parameters.

The method's signature line must match the calling statement's (values). The two must
match the same count of parameters.

<DataGridView> does not contain a definition for Cells and no extension method Cells accepting a first
argument....

See below: "does not contain a definition for 'Cells'....

CS1061
'<Classname>' does not contain a definition for "<MethodName>' and no extension method
'<MethodName>' accepting a first argument of type '<ClassName>' could be found
(are you missing a using directive or assembly reference)

For example: 'MainProgram' does not contain a definition for "A000_Base' and no extnsion
method 'A000_Base' accepting a first argument of type 'MainProgram' could be found (are
you missing a using directive or assembly reference)

Likely solution:
In another class (e.g. MainProgram.cs), you have not yet created or have misspelled a
method called "A000_Base".

<formname> does not contain a definition for <event such as 'checkBox1_CheckChanged'>


<formname> does not contain a definition for <'textBox1_TextChanged'>

Symptoms:
This is an Event problem where the original Event's code was either deleted or renamed in
Code View, but the pointer to the event was not changed in the Event Properties.

Solution(s):
There are two ways to correct this error. Either is acceptable.

1. Double-click the error and the editor will take you to the [Form1.Designer.cs] class;
and as scary as this may look, delete the entire highlighted line.

2. Or, open the <event> properties (Lightning Bolt) for the control in question and delete
the event information from the property screen. For example, if this were a
textBox1_TextChanged event, delete the detail-text after the (lightning-bolt) event.
Doing so still leaves the "textChanged" code, orphaned, in the program. It should be
deleted by hand.

Common Error Messages and Solutions Appendix A: 15


<System.Windows.Forms.DataGridView> does not contain a definition for Cells and no extension
method Cells accepting a first argument....

Solution:
In the foreach clause, did you use "DataGridViewRows" (and not just "DataGridView")?

foreach (DataGridView currentRow in dataGridView1.SelectedRows)


MessageBox.Show("Selected: " + currentRow.Cells[0].Value;

Entering Break Mode failed for the following reasons: Source file <server-drive....form.cs> does not
belong to the product being debugged.

Cause:
A previous project was moved from a server-drive to a local disk.
Reference paths still point to the (old) server location.

Solution:
With Visual Studio 2005 or above, select menu Build, Clean Solution followed by Build,
Rebuild Solution.

With Visual Studio Express, these menu choices may not be present. Do the following:
a. Close the Visual Studio Project
b. Using Windows Explorer, locate the solution; delete the "bin" and "obj" sub-
directories. Re-open the Solution and the problem should be fixed.

error CS0234: The type or namespace name 'Tasks' does not exist in the namespace
'System.Threading'

Error visible in the View, Output pane.


See error message "The Type or namespace name 'Tasks'

ExecuteNonQuery: Connection Property has not been initialized.

When using a Stored Procedure and attempting a SAVE or INSERT (ADD) operation.
Missing a connection clause with the SqlCommand. For example:

Common Error Messages and Solutions Appendix A: 16


Incorrect:
SqlCommand refCategoryCMD =
new SqlCommand("RecordCategoryUpdate");

Corrected:
SqlCommand refCategoryCMD =
new SqlCommand("RecordCategoryUpdate", refCategoryConn);

where "refCategoryConn" was the connection defined earlier in the routine, as in:
string strConnection = "Data Source = <servername\\SQLExpress;" +
"Initial Catalog = <database name>;" +
"User ID=<sa>; Password = <password>";
refCategoryConn = new SqlConnection(strConnection);

<method name> hides inherited member 'SystemWindows.Forms.<object>' Use the new keyword if
hiding was intended.

example message: Form1.left(string, char)' hides inherited member


'system.windows.forms.control.left'.

Cause: The name of your function/procedure/method is the same as a built-in name. e.g., if
you built a function called "Left". This is a new warning, starting with Visual Studio 2010.

Solution:
This error can be ignored. But consider renaming your function. For example, instead of
"Left", use "LeftStr". In general, single-word functions, such as Left, Mid, Right should not
be used.

Field '<name>' is never assigned to, and will always have its default value null (warning)

Possible Solution:
A variable was declared but was never set equal to anything. The 'variable' does not need to
be a normal variable, it could be a class name. Consider this example when declaring an
external class library with the "new" statement either commented or not typed in the proper
location:

clSiteGlobals SiteGlobals;
//SiteGlobals = new clSiteGlobals();

IDE1006 Naming rule violation: These words must begin with upper case characters: <button1_Click>

This is an informational message. Rename the procedure or method, shifting the first
character to upper-case. This is to follow recommended naming standards for cross-
platform programs.

Identifier Expected

Possible Solution:
When declaring a function, are all the parameters in the parameter list prefixed with a data-
type? Missing "string", "int", etc.

Incorrect: static bool IsNumeric(passedString)


Correct: static bool IsNumeric(string passedString)

Common Error Messages and Solutions Appendix A: 17


'<btnClose>' is a 'field' but is used like a 'method'
<btnClose> is a field but is used like a method

Likely Solution:
You are calling a button-event from another location but forgot or mis-typed the event-
name.
btnClose ("", null); //Incorrect – not just the btn name
btnClose_Click ("", null); //Corrected: _Click was missing

Index Out of Range (DataGridView)

Symptoms:
Commands that use a column index position, such as

dataGridView1.Columns[0].HeaderText = "SEQ";
dataGridView1.Columns[1].Width = 55;

must be written after the statement that populates the actual grid. See Chapter 27 for
examples.
GetMyData(strSelectString)

Confirm the SQL Server (SQLExpress) services are running (Services.msc) or the remote
server is available.

See also: ArgumentoutOfRangeException was unhandled

Index was outside the bounds of the array

Symptoms:
A generated error, usually from a try-catch, where array operation attempted to access a
point not in the array – usually one position beyond the end of the array [max n + 1].

If you are not looping through the array and are directly accessing the array (e.g. variable
[n]), then likely the array was not populated with data; especially with a previous .Split
command.

Possible Diagnostics:
If using a foreach loop, place a debug break point at the top of the loop and monitor the
loop. If you suspect the error is (1000 records) into the loop, add this diagnostic logic to the
program and break within the if-statement:

foreach ....
{
if (recordCount > 999)
MessageBox.Show
("Reached suspected error; put break point here");
// <regular processing here>>
}

If using a ".Split" and a subsequent command accesses a variable-field [n] directly, likely
the split found an empty record and had nothing to split into the array. After the split, check
for blank records before executing the (parsing) logic within the loop.

Symptoms:

Common Error Messages and Solutions Appendix A: 18


If you are processing a CommandLine (arguments list), what happens when no command-
line arguments are passed? If you reference aargs[1], the program would abend. Consider
this statement:

if (aargs.Length >= 2 && aargs[1].ToUpper() == "/DIAG")

where the double-ampersand is absolutely required in the test.

Index was outside the bounds of the Array (SQL)


Also: Index was out of Range

Symptoms:
After executing a SQL statement, such as a ExecuteReader.

Possible Solutions:
Confirm the SQL Service is running (Start, Run, Services.msc; look for MSSQL/SQL
Server).

Confirm you have the proper "strConnection" (Data Source=<Franken8>)

Confirm the SQL SELECT statement (an assembled string) includes the field-name you
need and it is punctuated with appropriate commas and spaces, especially within the
assembled string.

Are you trying to reference a [column] position before a DataGridView was populated?

Invalid Expression Term ',' (plus "; expected") when using a picture clause

Likely symptoms:
You are using a picture clause (with a String.Format).

Solution:
Did you forget the words "String.Format ("?

Invalid Expression Term '{' (when using a picture clause)

Solution:
String.Format requires a string, even if a single numeric variable is being formatted.
Encompass the phrase with quotes:
textBox1.Text = String.Format ({0:dddd}, dtValue); //Incorrect
textBox1.Text = String.Format ("0:dddd}", dtValue); //Correct

Invalid Expression Term "."


See "; expected".

Common Error Messages and Solutions Appendix A: 19


Invalid Expression term 'else' and ";expected"

Possible Solutions:
The "if" clause above the errored line requires braces for the "then" section. Sections with
more than one command require braces to group them.

if (valueA == valueB)
{
<stuff>
<more stuff>
}

Possible Solutions:
Does the if-statement-clause have an unneeded semicolon on the if-clause itself? Remove
the semicolon.

InvalidCastException was unhandled


See Error: Unable to cast object of type 'System.Windows.Forms.TextBox' to type
'System.IConvertible'.

Invalid Column Name '<field>' (SQL Read)

Symptoms:
An Invalid Column Name <field name> during a SQL Read or SQL ExecuteReader and the
field name is obviously right, when examined in SQLServer Management Studio.

Possible Solution:
The assembled strSQLstmt (the SELECT statement) is mal-formed, usually a space is
missing in a quoted string, especially on the last field-name, just before the FROM clause.
Set a breakpoint at the ExecuteReader and examine the IntelliTrace. For example, in this
illustration, notice how the "FROM" is crammed next to the field "Comment":

Common Error Messages and Solutions Appendix A: 20


Invalid token '{' in class, struct, or interface member declaration

This message generally means the compiler is confused about an opening or closing brace
or there is a mis-placed semi-colon that confuses where the compiler expects a brace.

Possible Solution:
You have a semicolon at the end of an if-statement; while-loop or for-next-loop or remove
an unnecessary semi-colon from the end of a statement:

private void xxxxx (object sender, EventArgs e); (bad semicolon)


while (loop stuff); (bad semicolon)
if (condition); (bad semicolon)

Possible Solution:
There is a statement or group of statements typed after the module's closing brace. Make
sure all your code is above the closing brace (e.g. above button1_Click's closing brace).

Possible Solution:
Check to make sure that all opening braces have a closing brace and all braces are lined-up
properly. Especially near the end of the program/namespace.

Invalid token 'string' in class, struct, or interface member declaration

Common Error Messages and Solutions Appendix A: 21


Likely solution:
In a statement, such as:

public string SomeMethodNameHere()

where "string" is flagged as an error.


Be sure the word "public" (private, etc.) is lower-case. Not "Public".

Login Failed for user <xxxx>. Reason: Server is in script upgrade mode. Only the administrator can
connect at this time. Error 18401.

Solution:

The SQL Server service just started and the engine is updating tables. Wait a few minutes
and try the SQL connection again.

<method> is inaccessible due to its protection level

Solution:
The method in the Class Library are "private".
Set to either:
"public" if the Class is instantiated, or if the class is in the same namespace as the calling
routine.

Set to "public static" if the Class is not instantiated and it is in another namespace.

MessageBox is a 'type' but is used like a 'variable'

Solution:
You forgot to use a dot-method with the command.
For example: MessageBox.Show (...)
where the .Show was missing

Must declare the scalar variable "@<variable name".

Symptoms:
During a btnSave event, when using replaceable parameters

Solution:
A field was used in the UPDATE/INSERT statement but it was not defined with an
"AddWithValue" clause. For example:

refCategoryCMD.Parameters.AddWithValue
("@NonRequiredField",
util.StripSQLinjections(pnlNonRequiredField.Text));

Also check the SQLstmt in two places (once for INSERT and once for EDIT), making sure
the field-name is listed, and within the parenthesis of the field list:

strSQLstmt = "INSERT INTO refCategory " +


"(RecordCategoryCode, RecordCategoryDesc, DeleteInhibit, NonRequiredField) " +

Common Error Messages and Solutions Appendix A: 22


"Values ( @CategoryCode, " +
"@CategoryDesc, " +
"@CheckBox, " +
"@NonRequiredField )";

strSQLstmt = "UPDATE refCategory SET " +


"RecordCategoryCode = @CategoryCode, " +
"RecordCategoryDesc = @CategoryDesc, " +
"DeleteInhibit = @CheckBox, " +
"NonRequiredField = @NonRequiredField " +
"WHERE (RecordCategorySeq = '" + strEditPassedRecordSeq + "')";

Newline in constant

Likely Solution:
You are appending a "\" backslash character in a string, probably to build a directory-path.
Backslash is a reserved character. Use double-backslashes to represent a single backslash.

serverName + "\" + directoryName //error


serverName + "\\" + directoryName //corrected

No overload for method '<method name>' takes 0 arguments

Likely solution:
Typically with a button or other on-screen event, such as a button, notice the signature line
of the button; there are two parameters. For example, btnSomething_Click(object
sender, EventArgs e). When calling an event like this, make your call in this fashion:

btnSomething_Click(null, null);
Passing a null value for each item in the signature line.

No overload for method '<method name>' takes '1' arguments

Solution:
You are attempting to pass <1> parameter via the signature line to a function that either
needs more than one value or no values. In other words, the two signature lines need to
match, parameter-to-parameter. Double-click the error to locate the invalid call statement.
Then locate the routine it is calling; once found, look at its signature line (the items in
parenthesis; they must match).

Non-invocable member 'System.IO.FileInfo.Length' cannot be used like a method


Non-invocable member 'System.Windows.Forms.Control.Text' cannot be used like a method

Solution:
You are attempting to use a 'property' as-if it were a method. In other words, remove the
trailing parenthesis. For example:
fi.Length ( ); //is incorrect; use instead:
fi.Length;

or: pnlMsg.Text ("some text in quotes here"); //incorrect; use instead:


pnlMsg.Text = "some text here";

Common Error Messages and Solutions Appendix A: 23


Object reference not set to an instance of an object
Use the "new" keyword to create an object instance.

This is a generic error that can be hard to resolve.

Solution:
Generally it means something is mis-spelled.

For example: RegKey.GetValue("ApplicationzzzName").ToString()


has a mis-spelled parameter.

Solution:
You are calling another method, in another library, without having first instantiating the
object. For example, when using the cl710_Formatting.cs library's "ProperNames"
function, you may need to declare the library either at the top (Class level) or within the
current function (e.g. button1_Click):

formatting = new cl710_Formatting();


then: formatting.ProperNames(<stuff>);

Solution:
You have declared a variable, such as a string, an array, a number, but have not initialized it
to a value; the variable still contains nulls.

For instance, a string declared but not initialized:


string myName;

if (myName.Length == 5) ... Generates this error

For instance:
string [] aMyArray;

with: aMyArray[1] = "Dog" will generate the error.

In each case, initialize the variable with a non-null value before using it in any equation or
comparison. With an array, instantiate the value with the "new" keyword:

Only assignment, call, increment, decrement, and new object expressions can be used as a statement.

Symptom 1:
In a for-next statement you have mis-keyed one of the three required phrases. For example,
this statement has an error in the first phrase:
for(i; i <= 10; ++i)

Possible Solution:
you can't use a simple variable in the first part of the phrase; it must have an assignment
clause. The statement correctly typed is:
for(i=1; i <= 10; ++i)

Possible Solution:
A method, such as
sr.Close(); or
A180_ClearDateEntryFields();

Common Error Messages and Solutions Appendix A: 24


was typed without opening and closing parenthesis.

Operator '&' cannot be applied to operands of type 'string' and 'string'

Solution:
Use the "+" symbol to concatenate strings. You used to be a Visual Basic programmer,
weren't you?

Operator '&&' cannot be applied to operands of type 'bool' and 'string'

Possible solution:
In a complex if-statement or while-loop clause, would an extra set of parenthesis help?

while (lineCount < linesPerPage &&


( strReadLine = myAsciiFile.ReadLine() ) != null)

Operator '==' cannot be applied to operands of type 'string' and 'method group'
Operator '==" cannot be applied to operands of type 'method group'
Operator '+' cannot be applied to operands of type 'string' and 'method group'

Likely Solution:
You forgot a "( )" after a function name.
"ToString" vs "ToString()" is commonly missed.

For example:
If using a method, such as .ToLower; as in ...textB.ToLower
did you remember the required parenthesis, as in: textB.ToLower()
if (textB.ToLower == "dog") //incorrect
if(textB.ToLower() == "dog") //correct

For example, also in a written method call:


if (A027_CheckPreviousCount == 15) //incorrect
if (A027_CheckPreviousCount() == 15) //correct

Operator '>=' cannot be applied to operands of type 'string' and 'string'

Symptoms:
You are using a > or < conditional when comparing two strings, as in:
if (testString >= "Brown")

Solution:
You can't use >, < operators against two strings. This is different than (VB). Instead, see
string.Compare(string1, string2, T|F);
string.CompareOrdinal(string1, string2);

Operator "||" cannot be applied to operands of type 'int' and 'int'

Symptoms:
You are using an equal sign in an if-statement; you need double-equals for the comparison.
e.g.
if (passedPhoneNumber.Length = 7 || passedPhoneNumber.Length = 8)

should be:

Common Error Messages and Solutions Appendix A: 25


if (passedPhoneNumber.Length == 7 || passedPhoneNumber.Length == 8)

CS0019
Operator '+' Cannot be applied to operands of type 'TextBox' and 'TextBox'
Operator '+' cannot be applied to operands of type 'System.Windows. Forms.TextBox'

Solution:
You forgot to include the object's (field) dot-property after the object's name.

Consider this code:


label1.Text = textBox1 + textBox2;
label1.Text = textBox1.Text + textBox2.Text;

CS0642
Possible mistaken empty statement

Symptom:
On an if-statement, while, or for-next statement

Likely Solution:
Although this is a warning, it is most likely a true error. Do you have a superfluous
semicolon after an if-clause, for-next, or other loop statement?
Remove the semicolon and let the next line (or the next set of braces) act as the end-of-line.

if (stringA == stringB); // <- Remove this semicolon


{

Warning: Possible unintended reference comparison; to get a value comparison, cast the left hand side
to type string

Symptom:
When comparing a string array to a string:
if (alSomeArray[iposition] == "some fixed string")
The ~ warning appears after runtime, not during editing

Solution:
Do one of the following by casting explicitly or implicitly:

if (alSomeArray[iposition].ToString() == "some fixed string")


if ((string)alSomeArray[iposition] == "some fixed string")

Note: The error will only clear after run-time; it will not clear during the editing session
(VS2010).

Property of indexer '<class.variable>' cannot be assigned to – it is read only.


Property or indexer '<class variable>' cannot be assigned to - it is read only.

Solution:
If this is in an if-statement, did you remember to use double-equals (==)?

Solution:

Common Error Messages and Solutions Appendix A: 26


In the "get/set" routines, typically in a Global External Class Library, there is not any logic
for the "set". If your intention is to make a read-only variable, either remove the logic in
your program that is trying to set the variable's value (e.g. myName = "Smith") or add an
empty-set routine, which ignores the myName = Value statement. Fixing the error is
preferable.

Property Value Not Valid (Dialog box)


Property Not Valid

Solution:
Your program is still running while trying to edit the source code. Close your running
program before changing the code or an object's property (e.g. Click the editor's ribbon
icon: "Red Square").

Send Error Report / Don't Send "Please tell Microsoft about this problem"

Symptom:
When you ctrl-alt-Break your program and your program may be in an infinite loop or
otherwise crashed. Microsoft sees this as a problem and offers to send a diagnostic error
report to Redmond. This message is annoying and should be disabled.

Solutions:
Click "Don't Send," then make this registry key change to your workstation.

Start/Run/Regedit
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PCHealth\ErrorReporting. Dword
Value: DoReport, 0 = Don't Send.

SQL: An Error has occurred while establishing a connection to the server. When connecting to SQL
Server 2005/2008, ... (this failure may indicate) ... SQL Sever does not allow remote
connections. Could not open a connection to SQL Server.

Possible Solutions:
1) If you are connecting from a remote computer: In Microsoft's Surface Area
Configuration tool (SQL 2005), confirm that "Local and remote connections" is
selected. Choose TCP/IP

See: "C:\Program Files\Microsoft SQL Server\90\Shared\SqlSAC.exe"

2) If you are connecting from a remote computer: Consider starting the Windows Service:
SQL BROWSER

3) Does the SQL Express Server have a software Firewall installed? For example, if using
Windows XP SP2 with Windows Firewall, open the Firewall's control panel; click
Exceptions: Add this program: sqlserver.exe. Also add SQLbrowser (udp port 1434).

Other Solutions:

4) You are using the wrong server name in the connection string.

With SQL Server 2008:


string strConnection =

Common Error Messages and Solutions Appendix A: 27


"Data Source = Franken8;" +
"Initial Catalog=Address;" +
"User ID=sa;Password=<yourpassword>";

With SQL Server 2005:


string strConnection =
"User ID=sa;Initial Catalog=Address;Data Source=FRANKEN8\\SQLEXPRESS";

or use ...Data Source=LOCALHOST\SQLEXPRESS If a local database

This can be especially true if you have moved your application from one computer to
another (when in development) and your Development SQL Server also moved. The
author had this problem when moving from a Desktop to a Laptop.

5) You are using the wrong Catalog (database name). e.g. from Chapter 16 "Address"

6) And of course, the wrong username and or password. If the password is encrypted; did
you decrypt it prior to executing the command?

SQL: Login Failed for user <xxxx>. Reason: Server is in script upgrade mode. Only the administrator
can connect at this time. Error 18401.

Solution:

The SQL Server service just started and the engine is updating tables. Wait a few minutes
and try the SQL connection again.

Static member '<namespace.class.variablename>' cannot be accessed with an instance reference;


qualify it with a type name instead.

Solution:
Was the External Class Instantiated (with a "new" keyword)?
If so, remove the "static" modifier from the variable's declaration and use the "new"
variable's name as a variable prefix.

e.g. SiteGlobals = new clSiteGlobals ();


then: MessageBox.Show (SiteGlobals.CompanyName)

If the External Class was not Instantiated (using Quick and Dirty Global Variables), prefix
the variable name using the physical Class Name, as seen in Solution Explorer. Do not use
the "new" keyword.

For example:

Use:
MessageBox.Show(<namespace name.> clSiteGlobals.CompanyName);

'String' does not contain a definition for 'length' and no extension method 'length' accepting a first
argument of type 'string' could be found (are you missing a directive or an assembly
reference?)

Solution:

Common Error Messages and Solutions Appendix A: 28


Capitalize the .Length property, as in:

switch (strfoundString.Length)
{
:
}

'System.Configuration.ConfigurationSettings.AppSettings' is obsolete: 'This method is obsolete, it has


been replaced by System.Configuration!
System.Configuration.ConfigurationManager.AppSettings (depricated)

Symptoms:
When attempting to use an app.config file and
MessageBox.Show (ConfigurationSettings.AppSettings ["<variable name>"]);
And yet, the statement still works properly, except for a compiler warning.

Solution:
In Solution Explorer, References, add a Reference to .NET, System.Configuration.dll
Then change the call statement to
MessageBox.Show (ConfigurationManager.AppSettings ["<variable name>"]);
See the App.Config chapter for full details.

'System.DateTime.Now' is a 'property' but is used like a 'method'

Solution:
Remove the parenthesis from the .Now. This is not Excel.
= DateTime.Now; //Not DateTime.Now()

System.FormatException: 'Input string was not in the correct format.'

Likely solution:
You are attempting to Convert.ToInt32(textBox1.Text) and the textBox was empty or
contained non-numeric values, such as a hyphen or other character.

This is a run-time error that should have a try-catch clause.

System.InvalidCastException: 'Unable to cast object of type 'System.Windows.Forms.TextBox' to type


'System.IConvertible'.'

You will see this message if a textBox or other string field is blank and is trying to be
converted to a numeric value - Visual Studio 2012 and older.

You will also see this if Convert.ToInt32(textBox1.Text) - where the .Text property is
missing.

Note: This is a run-time error (an unhandled exception) and your program has crashed.
Provided this is not a syntax error, consider a try-catch block.

System.Windows.Forms.TextBox - various:

Issue: Likely missing ".Text" appendage.


See:

Common Error Messages and Solutions Appendix A: 29


Cannot implicitly convert type 'string' to 'System.Windows.Forms.TextBox'

CS1061
'System.Windows.Forms.<field>' does not contain a definition for 'text'

Possible Solution:
Usually this means you mis-typed or more likely, mis-capitalized a field's property value.

Consider:
Field1.text (with a lower-cased .text) vs
Field1.Text

Other properties exhibit similar type messages when mis-spelled.

CS1061
'MainProgram' does not contain a definition for "A000_Base' and no extnsion method 'A000_Base' accepting
a first argument of type 'MainProgram' could be found (are you missing a using directive or
assembly reference)

Likely solution:
In another class (e.g. MainProgram.cs), you have not yet created or have misspelled a
method called "A000_Base".

'System.Windows.Forms.MessageBox' is a 'type' but is used like a 'variable'.

Solution:
You forgot to use a method: MessageBox.Show (
e.g., you forgot the ".Show"

This is incorrect: MessageBox("Hello World");


Corrected: MessageBox.Show("Hello World");

The best overload method match for '<form(parameter)>' has some invalid arguments

See "Argument '1': cannot convert from 'object' to 'string'.

The best overloaded method match for 'string.PadRight(int,char)' has some invalid arguments

Solution:
When PadLeft or PadRight, the pad-fill is a character, not a string. Delimit a single
character with tic marks, not quotes.
e.g. strtestValue.PadRight(ipadLength, '*');

The best overload method for 'System.Windows.Forms MessageBox.Show(string)' has some invalid
arguments.

Possible Solution:
MessageBox.Show must have a string as the first item in the "show list". For example:

MessageBox.Show (comboBox1.SelectedItem); //errors; not really a string.


MessageBox.Show (Convert.ToString(comboBox1.SelectedItem)); //works

Common Error Messages and Solutions Appendix A: 30


You can also trick the method by appending an empty-string before the first object being
displayed. Often, the object is converted to a string automatically, but the MessageBox
doesn't know this. Force it to get past the compiler by putting an empty-string in the front:
MessageBox.Show ("" + comboBox1.SelectedItem);

Another way around this problem is to use a ".ToString()" method. For example, with this
RegistryKey example (snippet):

MessageBox.Show (RegKey.GetValue("ApplicationName").ToString());

The class name '?' is not a valid identifier for this language

Likely Solution:
Close all frm (forms), then close and re-open the solution. It appears the development
environment can get confused, especially if you have been deleting methods.

The current project settings specify that the project will be debugged with specific security settings

Symptoms:
When using File-IO functions or Command-Line arguments (and others)

Select Project, Properties, Security


Uncheck the "Enable ClickOnce Security Settings"

The Insert Statement conflicted with the Foreign Key constraint <key name>... (SQL)

Symptoms:
When attempting a SQL Insert where the main table has a relationship with a sub-table. For
example, adding a new NAMES record, pointing to Record Category = 1, when using:
tblNamesCMD.Parameters.AddWithValue("@RecordCategorySeq", 1);

Problem:
@RecordCategorySeq = 1
Looking in the RecordCategory table, there is not a record with a value "1"

The left-hand side of an assignment must be a variable, property or indexer.

Example Problem Statement:


if (String.Compare (strReadLine, null) = 0)

Possible Solution:
With if-statements, use a double-equal signs (not single) when comparing values; as in:
if (String.Compare (strReadLine, null) == 0)

The name 'ConfigurationManager' does not exist in the current context.

Solution:
See "System.Configuration.ConfigurationSettings.AppSettings' is obsolete: "

Common Error Messages and Solutions Appendix A: 31


CS0103
The name '<variable>' does not exist in the current context.

Symptom 1:
You have not declared the variable using "string", "int", "float", etc, as in:

string myString;
int aNumber;

Or you have declared the value as "myString" but used the variable later as "mystring"
(case-sensitive).

Or you declared the variable in another (routine or module) and that declaration is outside
the scope of your current routine/method/module. A variable was declared in another
construct (such as within a for-next loop) and that construct has ended.

Consider the integer i, which is declared as part of the for-next loop but was used in a
MessageBox outside of the loop; in this case, variable "i" was out of scope and cannot be
used.

for (int i=1; i <= 10; ++i)


{
<stuff to do>
}
MessageBox.Show ("Variable i = " + Convert.ToString(i));

Solutions vary. Check variable declarations.

Symptom 2:
Error: The name '<FixedSingle>' does not exist in the current context.
You are setting a Property incorrectly, such as
textBox1.BorderStyle = FixedSingle

Possible Solution:
The item on the Right-side of the equal sign may need to be prefixed with a property name,
as in:
textBox1.BorderStyle = BorderStyle.FixedSingle

Note: BorderStyle requires a "using System.Windows.Forms;" statement or you can fully-


qualify the name, as in:
textBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle

Possible Solution:
If the 'name' is a keyword-like name:
The name 'IsNumeric' does not exist.... do you need a Class Library prefix, such as:
util.IsNumeric?

The type arguments for method 'System.Array.Resize<T>(ref T[], int)' cannot be inferred from the
usage. Try specifying the Type Arguments explicitly

Issue:
Array.Resize only works with single-dimensioned arrays. You cannot resize multi-
dimensioned arrays; you cannot resize List<T> arrays.

Common Error Messages and Solutions Appendix A: 32


Solution:
Manually copy existing array to a new, larger array -- but you will have problems that the
new array will have a different name. There does not seem to be a good solution to this
problem.

The type or namespace name 'boolean' could not be found (are you missing a using directive or an
assembly reference?)

Consider this called function, which returns a boolean:


static boolean IsBlank(string passedString)

Should be typed as:


static bool IsBlank(string passedString)

C# is inconsistent in how one should spell boolean. When used in a function, use "bool".

The type or namespace name 'CurrentUser' | 'Local Machine' does not exist in the namespace
'Registry' (are you missing an assembly reference?)

Symptoms:
When attempting to read a specific registry key from the Windows Registry

Solution:
Confirm you have a "using Microsoft.Win32;" at the top of the program.
Then use this prefix in the RegistryKey command:

RegistryKey RegKey =
Microsoft.Win32.Registry.LocalMachine.OpenSubKey
(@"Software\Test");

The author is unsure why the "Microsoft.Win32.Registry" prefix is required when a "using"
statement is in place.

More generally:
You are missing a 'using' statement (e.g. using System.Management;).
if this does not resolve the problem, often you can add a new "Reference" (in Solution
Explorer). The name will usually be the same ("System.Management").

The type or namespace name 'DllImport' could not be found (are you missing a using directive or an
assembly reference?)

Possible Solution:
Add these two statements at the top of the (DllImport) class:

using System.Collections;
using System.Runtime.InteropServices;

Note "DllImport" is spelled with lower-cased -el's Dll's

The type or namespace name 'Return' could not be found...

Solution:

Common Error Messages and Solutions Appendix A: 33


Spell "return" with a lower-case 'r'.

If a Void function:
return;

If a non-Void function:
return (some-variable);

The type or namespace name 'single' could not be found (are you missing a using directive or an
assembly reference?)

Solution:
With floating point numbers,
Use Single (with a capital S) or "float" instead.

Unlike "int", Single does not have a shorter alias. Many programmers prefer "float".

The type or namespace name 'StreamWriter' / 'StreamReader' / 'WriteLine' could not be found (are
you missing a using directive or an assembly reference?)

Likely solutions:
Confirm "using System.IO;" near the top of the program.

Confirm you are using the variable name on the WriteLine method.

For example, this is incorrect:


System.IO.WriteLine("my text to write");

Use this:
myreviewFile.WriteLine...

The type or namespace name 'Tasks' does not exist in the namespace 'System.Threading'

You are likely using one of the Wait methods and System.Threading.Tasks is only available
in dot net 4.0 and higher.

Solution:
In the project, select top-menu "Project, Project Properties".
Change the target framework from .NET Framework (3.5) to version 4.0 or newer.
Re-compile.

The type or namespace 'Windows' does not exist in the namespace 'System' (are you missing an
assembly reference?) File: cl800_Util.cs

Likely solution:
You are writing a Console application and have imported the cl800_Util library. cl800's
"Wait" routines call a System.Windows.Form module -- which Console applications do not
allow.

Recommended Solution:
Delete cl800_Util from Solution Explorer and re-add as a "Copy" (not as a link). Once
added, locate the WAIT routines and remove them from the cl800_Util library. Because
cl800 is copied, you are only damaging this program's local version. If you write a lot of

Common Error Messages and Solutions Appendix A: 34


console applications and wish to continue using cl800, move the WAIT logic into its own
library.

Note: The text was changed to reflect this need. All Wait routines were moved into their
own class library. You would see this message if you tried to combine them contrary to
what the book recommends.

Related Solutions:
Console applications cannot call any "Windows-like" method. For example,
MessageBox.Show will not work in a console application. Adding a "using
System.Windows.Forms" defeats the purpose of a console application.

There were build errors. Would you like to continue and run the last successful build?

Symptoms:
When you compile (F5) your newly-written program.

Solutions:
Select checkbox "Do not show again" and click No. In other words, you would never want
to run the previous version of your code (before newly introduced bugs; you really want to
see the current bugs).

If you had already checked yes, see Tools, Options, "Projects and Solutions", "Build and
Run". Set "On Run, when build or deployment errors occur" to "Do not Launch".

Unable to cast object of type 'System.Int32' to type 'System.String' (SQL ExecuteRead)


Unable to read record (SQL)

Symptoms:
When attempting to read a SQL record.

Solution:
When assembling the SELECT statement (strSQLstmt), the "WHERE" clause's record
number (e.g. usually a SEQuence number), must be enclosed in tic-marks. For example:

Incorrect:
:
"WHERE NameSeq = " +
strEditPassedNameSeq;

Corrected:
:
"WHERE NameSeq = " +
"'" + strEditPassedNameSeq + "'";

See also:
Invalid Column Name (SQL)

Common Error Messages and Solutions Appendix A: 35


Unable to cast object of type 'System.Windows.Forms.TextBox' to type 'System.IConvertible'. When
casting from a number, the value must be a number less than infinity.

Likely Solution:
A Convert.To phrase is missing a dot-property

Consider this flawed for-next loop fragment:


for (int loopCounter = 1;
loopCounter <= Convert.ToInt32(textBox2); ....

The "Convert.ToInt32( )" does not point to a particular property.

It should read
Convert.ToInt32(textBox2.Text)

I bet you used to be a VB programmer.

Unable to Read Record (SQL)


See Invalid Column Name (SQL)
See Unable to cast object of type 'System.Int32' to type 'System.String' (SQL)

Unrecognized escape sequence

A string was found with a "\" (backslash) character. This is a reserved character needed for
"escape sequences." If you need a backslash character in a string (typically for a file-
name\path), double-up the backslashes, as in: "C:\\data\\filename.ext"

\t = tab
\r = carriage return
\n = newline
\r\n = crlf
\\ = backslash
\' = tic
\" = quote

See Appendix Special Characters for details.

Use of unassigned local variable <"myInteger"> | <"myString">, etc

Possible Solution:
In your declarations, usually at the top of your routine, a variable, such as

int <myInteger>; or string <myString>

was declared but not initialized with an explicit value. Or you attempted to use a variable
on the right-side of an (equals) statement when it has not yet been populated by another
statement earlier in the code.

Or, the variable was declared, but because of logic, was never assigned a value before being
used in another statement or calculation.

The variable needs an initial value, either explicitly or programmatically. Remember,


declaring a variable does not initialize it..

Common Error Messages and Solutions Appendix A: 36


Another likely scenario is the variable was not initialized and a "while" loop was going to
set the value but the loop never ran (or more likely, the compiler thought the loop had a
possibility of never running).

Recommendations:
Consider using this type of syntax:

int myInteger;
myInteger = 0

or declare and initialize on the same line, as in:


int myInteger = 0;

Possible Solution:
The variable was declared in another module and has fallen out of scope.

Visual Studio cannot start debugging because the debug target <your project name\bin\debug> is
missing. Please build the project and retry, or set the OutputPath and AssemblyName
properties appropriately to point at the correct location for the target assembly.

Solution:
The Program must compile at least one time without errors or you will see this message.
Delete or comment-out the line causing a compiler error.
Run the program again (even if the program does nothing but display the form)
Close the running program and re-introduce the errors. This error should go away.

Solution:
Immediately after starting any new project, press F5 to compile the first empty-screen.
Then immediately close the running program and begin your coding work.

Solution (untested):
Select Menu: Project, Properties.
Go to "Build"; check the "Output" section at the bottom
Browse to your project's main directory/path, choosing Bin\debug"; this is where the actual
exe/dll lives.

When casting a number, the value must be a number less than infinity...

See
Error: Unable to cast object of type 'System.Windows.Forms.TextBox' to type
'System.IConvertible'.

Common Error Messages and Solutions Appendix A: 37


Appendix B - Compile and Distribution

This section discusses how to compile and distribute an .EXE.

Background:

When developing and testing a program, pressing F5 (top menu Debug, Start
Debugging) compiles the program, writes a temporary executable, and then
launches that .EXE as a separate task on the Windows task bar.

On the disk, Visual Studio builds a Debug folder in the Project's directory and in
there you will find a compiled .EXE and other support files – but only the .EXE is
needed for distribution. If you compile for "Release" (described below), a new
directory, "Release" is populated similarly.

The debug version (the .exe) contains code overhead that helps you test and
develop and this version is about 10 to 15% larger than a release version.
Although you can distribute the debug version to end-users, it is not recommended.

How to Compile and Distribute EXEs Appendix B: 38


Cheap and Easy EXE Distribution:

Follow these steps to compile your program as a stand-alone executable and to


give it a formal version number and release date. The resulting .EXE is not a full-
fledged, installable program, but it can be manually distributed (without a setup
routine) and the executable can be run from a server or from a thumb-drive, etc.
This method works well in corporate environments.

1. Open your Visual Studio solution as you would normally.

2. Select top-menu Project, (project name) Properties.

a. In the Project Properties screen, click the left-nav "Application" tab.


Change the "Startup object" from "not set" to your program's main routine,
often <ProgramName.Program>.

b. Click button, "Assembly Information".

Type a short description for the program and fill out the company, copyright,
etc.
Manually set an assembly version (version number) and a file version. The
GUID is a random number, which you should leave as-is.

c. On the left-nav, select "Build".


Change the top Configuration menu from "Debug" to "Active (Release)"
Recommend leaving the platform at "Active (Any CPU)".

3. Close the Properties tab and return to the editor.


On the top ribbon, change from "Debug" to "Release".

How to Compile and Distribute EXEs Appendix B: 39


4. Build the final code by choosing top-menu "Build", then "Build <your project's
name>"

5. Once built, use Windows File Explorer to open the project's "bin\Release" folder
(for example: C:\data\Proj\VS\FileManipulation\bin\Release)

The file (e.g.) FileManipulation.exe is distributable to end users or can be


positioned on a server. The file version is visible from Windows File Explorer.

How to Compile and Distribute EXEs Appendix B: 40


Virus Risks:

EXE files placed on a file server / file share, like all executables, are susceptible to
being infected by viruses. Be sure the EXE is in a read-only directory and your
development staff does not have write-access to the EXE or any DLL's in this
directory. This includes you. Use a service account, from a secured workstation,
when updating shared executables.

Protecting executables from viruses is a real-world


problem experienced by the author. A helpdesk employee's
machine was infected and they in-turn infected a variety of
executables on a main login-script server. As each
workstation logged in, they all were infected. It wasn't
until the next day the company's virus signatures were
updated. The Author now believes it is safer to distribute
executables locally, on each workstation. This makes
house-wide infections less-likely but is more problematic
when updating.

How to Compile and Distribute EXEs Appendix B: 41


EXE Icons

The taskbar and shortcut icon will be a default Visual Studio icon and your
program deserves better. Unfortunately, you will have to create, buy or steal your
own icon. Of the three techniques, one of them requires a bit of artistry and it is,
of course the most fun.

Obviously, thieving an icon is reprehensible and can get confusing if your program
shares the same icon as another. To help, Microsoft provides a free library of
icons, which can be found in the Microsoft Visual Studio Image Library,
downloadable at this link:

http://msdn.microsoft.com/en-us/library/ms246582.aspx

As of 2015.01, this is a downloadable .zip file. Extract all


files in the archive, then tunnel to
ImageLibrary\Actions\ICO. Other ICO libraries are near-
by.

The number of (Application) icons is limited, but the number of toolbar icons is
expansive. Regardless, it provides a good starting point, especially if you want to
draw a complete set of icons, with more on this in a moment.

Icon Files:

Icon files (.ico) are peculiar because they contain multiple images, at different
resolutions and different color depths. A fully-populated icon has these images
embedded:

16 x 16, 4-bit color


16 x 16, 8-bit color
16 x 16, 32-bit color

32 x 32, 4-bit color


32 x 32, 8-bit color
32 x 32, 32-bit color

How to Compile and Distribute EXEs Appendix B: 42


48 x 48, 32-bit color

256 x 256, 32-bit

To do an icon properly, you need a full-fidelity version at 256 x 256 pixels and
another at 48 x 48 pixels, followed by progressively smaller and less-detailed
versions. You will have poor results if you take a full-sized version and attempt to
scale it down to the smaller sizes; color shading and pixellation will occur and it
is beyond the scope of this book to describe the intricacies. As you will learn,
there is an art to creating icons.

Editing Icon .ICO files

Contrary to popular belief, you cannot create icons with most photo editors and
you can't edit them properly with MSPaint (it only sees one icon within the file).
There are ways to draw an icon locally, saved as a PNG, and then upload to a
website for ico conversion, but these are generally limited to one size, one icon.

As a free solution, Microsoft recommends this web-based editor. It will not build
the larger Windows-8 style tiles, but it is generally workable:

www.xiconeditor.com:
http://msdn.microsoft.com/en-us/library/gg491740%28v=vs.85%29.aspx

Using Visual Studio to Edit Icons

Amazingly, Visual Studio, the editor, can also edit ico (icon) files, but it comes
with infuriating limitations, only editing the 16 and 32 pixel icons. And it does not
seem to give full control over color pallets.

With the editor, you can view, but not modify 48 and 256-pixel icons. Also, it
does not appear capable of building a new icon file – it only works against existing
ones, but this restriction is easily worked around.

Even with these limitations, it is worth a moment to explore. Do the following:

A. Because you cannot create a new ico file with Visual studio, you must begin your
work with an existing icon. Locate a larger, full-fidelity icon, one with multiple
icons within the file. For example, from the downloaded ImageLibrary (see
above):

ImageLibrary\Objects\ico\ActiveServerPage(asp)_11272.ico

Or search C:\Windows for any *.ico files.

Protect the original file by copying the .ico to a temporary location before editing.
I recommend creating an "Images" folder within your project and storing icon and
clipart files there.

How to Compile and Distribute EXEs Appendix B: 43


B. From any Visual Studio Project, select File, Open. Tunnel to and open the .ico file
and it will open in a tabbed-window, next to your code and form designs. The left-
nav shows each of the different sizes. Note the editing ribbon bar is only available
on 16 and 32-pixel icons.

Example Icon in Visual Studio, showing multiple sizes

Once edited, close the tab and save the changes.

Attaching Icon Files to your Project:

The icon needs to be attached in two locations: One for the file system and a
second for the running program.

1. Open the top-menu, Project, (your project's name) Properties. Select left-nav
"Application". In the Resources section, set the icon file by browsing to the icon
file. Illustrated "ActiveServerPage(asp)_11272.ico". Once selected, File Explorer
will show this as the EXE's default icon and it will automatically pick the
correctly-sized icon.

How to Compile and Distribute EXEs Appendix B: 44


2. Return to the Form Editor (Form1, Design View). In the form's properties, locate
the "Icon" setting. Browse to the same .ico file and select. Again, the editor will
choose the properly-sized icon from within the file.

How to Compile and Distribute EXEs Appendix B: 45


3. Re-compile the program for the changes using F5 or F6-re-build. The icon will
show in File Explorer, on the program's (form's) title bar, on the Task bar, and on
any desktop shortcuts created. The icon file appears as a resource in Solution
Explorer.

How to Compile and Distribute EXEs Appendix B: 46


Creating Publishing / Distribution Packages

The "cheap and easy EXE" distribution method described above is my favorite
way of distributing a compiled program, but you can build a setup.exe that
automatically installs the software and builds an un-install routine. The benefits of
this design are:

• Setup.exe installs your program in a location of your choosing


• Adds an un-install to the Control Panel's "Add Remove Software" (Programs
and Features).
• It builds a desktop icon for the current user (In Windows 8, adds a tile to the
All Apps menu.

For example, from Windows 8's Control Panel, Programs and Features:

and from the Tile screen:

Building a Distribution Package:

1. Create a Release version of your application, as described earlier in this chapter,


then close the project.

2. If this is the first time building an MSI package, you must install a Microsoft-
recommended InstallShield ("InstallShield Limited Edition for Visual Studio" - a
free product from a third-party).

How to Compile and Distribute EXEs Appendix B: 47


Flexera Software
http://learn.flexerasoftware.com/content/IS-EVAL-InstallShield-Limited-Edition-V
isual-Studio

From the web site, download and follow the instructions. Once downloaded and
installed, close and restart Visual Studio.

3. Re-launch Visual Studio, selecting


New Project, Other Project Types, "Setup and Deployment"
Choose the "InstallShield Limited Edition Project" template

For the Location, type a path that is near but separate from your original Visual
Studio Solution. For example: C:\data\Source\FileManipulation\ (Deployment),
where "Deployment" is the recommended name. As usual, I recommend leaving
Create Directory for the solution and letting the "Name" become the actual
directory.

The new solution opens into an Install Shield Wizard with a row of buttons/icons
showing each step, starting with "Application Information." This is called the
Project Assistant and if you get lost, look in Solution Explorer and double-click the
Project Assistant

How to Compile and Distribute EXEs Appendix B: 48


4. In the InstallShield wizard's first step, "Application Information", fill out your
company name, web-address, and version number (not illustrated).

5. In Step 2/Icon 2 – "Installation Requirements", choose any restrictions you may


have, such as only installable on Windows 7 or newer and choose any required
software, typically Microsoft.Net Framework version 4.x. The screen is self-
explanatory.

6. In "Application Files", rename the default [ProgramFilesFolder] from


"InstallShield" to "MyCompany" or "MyApplication". This becomes the default
installation folder.

In the right-hand Name section, "other-mouse-click" and browse to your Release


version and add your final compiled EXE to the list. Add any additional INI files,
Readme.txt, etc, in this same location.

7. In the "Application Shortcuts" section, choose where you want icons built,
typically the Start Menu (In Windows 8 this is the All Programs tile screen).

How to Compile and Distribute EXEs Appendix B: 49


8. In the "Installation Interview" section, choose options as needed. Typically:

No - Do not display license agreement


No - Do not make users type their company or username
Yes - Allow users to modify the Installation Location
Yes - Allow the user to launch the application after install

9. Finally, select top-menu "Build", "Build Solution". Note that this is not part of the
Wizard steps. This completes the MSI build.

10. In Windows Explorer, tunnel to ...\Express\CD_ROM\DiskImages\Disk1

This is your Deployment directory – not your original program solution!

Results:
Note the Setup.exe, Setup.INI and .MSI file.

This entire directory can be positioned on a Share, CD, thumb-drive, etc. and is
ready for use.

Testing:

Launch Setup.exe and allow the program to install.

Results:
• In this example, installation arrives at "C:\Program Files
(x86)\MyCompany\*.exe"
• Note desktop icon (if selected)
• If Windows 8, note the All Programs Tile installed as "Launch (your program
name)"
• In Control Panel, Programs and Features (Add Remove), you can un-install.

How to Compile and Distribute EXEs Appendix B: 50


Possible Warning:

Warning: -7235: InstallShield could not create the software identification tag
because the Tag Creator ID Setting in General Information View is empty.
ISEXP: Warning.

Solution:

This is a warning and can be ignored with no harm. It suggests files required for
automatic inventory scanning are not in place and implies a corporate install. As
of 2014.03, this design is not in wide-spread use.

The warning can be resolved in one of two ways.

1. Disable the Inventory feature: In the Deployment Package's solution, Solution


Explorer. Tunnel to "Orgainize your Setup", "General Information". Scroll
down to the Use Software Identification Tag. Set Use Tag = no

2. Or Enable the Software Inventory by filling out the fields in the "Software
Identification Tag" section.

In Solution Explorer, tunnel to "General Information"

Complete these fields:


Tag Creator Name: Your business name
Tag Creator ID: (See below to generate)
– example: regid.2009-04.com.yourBusinessName

How to Compile and Distribute EXEs Appendix B: 51


Generating a Creator Tag:

Go to this site:
Magnicomp Software Tag Maker (free)
http://www.magnicomp.com/cgi-bin/mcswtagmaker.cgi

Complete the online form and generate an XML tag file.


Download and store the Tag file in your deployment's root directory.

My tag file was named this way:


2009-04.com.keyliner\regid.2009-4.com.keyliner.examplefilemanipulation_13
96226547.swidtag

a. In your Package's "Application Files" section, add the tag file, so it installs
at the same level as your .EXE program.

b. A second copy of the tag file must also be copied, using "Application
Files": (example file name):

%PROGRAMDATA%\2009-04.com.keyliner\regid.2009-4.com.keyliner.e
xamplefilemanipulation_1396226547.swidtag

Note: The Vendor's Generated Tag webpage will have the exact link and
name you should use – cut and paste.

Your MSI package must also build the Program Data directory:
"%PROGRAMDATA%\2009-04.com.keyliner"

where %PROGRAMDATA% value is a Windows system environment


variable. On Windows Vista and later the value is usually
C:\ProgramData

Recompiling:

If your original program is pulled for maintenance or enhancements, you must


rebuild the Release version *and* rebuild the Deployment version. Remember,
your source code and the deployment solution are two different Visual Studio
projects.

There are other features, such as automatic updates when version numbers change.
This is beyond the scope of this chapter.

This completes the Compile and Distribution chapter.

How to Compile and Distribute EXEs Appendix B: 52


How to Compile and Distribute EXEs Appendix B: 53
How to Compile and Distribute EXEs Appendix B: 54
Version History:

1.01 2015.04.10
Initial Release. Submitted to Wrox Publishers; declined due to length and
competition with other titles.
Advanced copy to D.Parks for review

1.02
Chapter 19 Wait States
Expanded Auto-launch example, 19.5

Chapter 20 Printing
Added reference to "Add Reference" for System.Printing on Console
apps

Chapter 23. Files


Added "Directory.Create" exception note dealing with
C:\Program Files (x86)
1.03
Split into three volumes
CH0 - 11 + Appendixes A,B (Through Multiple Forms)
CH12 - 21 ASCII - Formatting + Appendix C
CH22 - 27 Arrays - SQL + Appendix D

How to Compile and Distribute EXEs Appendix B: 55


A Beginners Guide to C-Sharp - Volume 2
Visual Studio C# 2017
ASCII through Advanced Formatting
by Tim R. Wolf
© 2017.06.01 1.02
Table of Contents

9 Chapter 12 - ASCII Files 17


Reading ASCII (Text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
StreamReader
Priming Read
Ending the Loop and Closing Files
Using an EndOfStream Read
The "Using" Clause
ASCII File Reads with try-catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Issues with try/catch and Close
Chaining catch-statements
Completed ASCII ReadFile - Program 12.5. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Writing ASCII (text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Appending ASCII (text) Files - Advanced Open Methods. . . . . . . . . . . . . . . . . . . . 54
Preventing Append from Running Twice

9 Chapter 13 - Parsing Tab and CSV Files 65


Automatic Parsing by Delimiter (Split).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Declaring the Destination Array
foreach
Alternate Loops
.Split Limitations
CSV Files
Split Example: Phone-Numbers
Manually Parsing Comma-Delimited Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
First Field Logic
for-next Logic
Substring
Last-field Logic
Tab-Delimited Files
Parsing a Variable Number of Columns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Parsing ASCII CSV Files with Embedded Commas.. . . . . . . . . . . . . . . . . . . . . . . 105
Error Processing in CSV Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Moving ParseCSVLine to the Utility Library

9 Chapter 14 - INI Files 147


INI File Structure and Design. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
A015: Loop Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A017: Parse INI Detail Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A028: Finding the Application's Default INI Location.. . . . . . . . . . . . . . . . . . . . . 178
Command-Line Override.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Simulate a Command Line Options
Embedded Spaces in the Parameters
Command-line ini=
A029: Write Default INI when Missing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Basic INI File Read: Complete Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Using a Program Class - CL860 INI File Read. . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
CL860 Basic INI Read Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Using CL860_BasicINIRead

9 Chapter 15 - xml and App.config Files 223


xml File Structure
app.config xml.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Using (Reading) app.config
Building a Manual xml File.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Reading xml File Sequentially. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

9 Chapter 16 - Windows Registry 249


Organization of the Registry
Reading a Specific Registry Key.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Opening the Registry
Closing
Reading a Specific Registry Value
Reading Mulitple Keys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Reading All Values within a SubKey. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Multi-Line String Registry Values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Creating/Modifying Name-Value Pairs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Opening the Registry in 'Writeable' Mode
Creating Sub-SubKeys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Deleting Values and Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Deleting Entire SubTrees
Deleting the Current Key
Enumerating SubKey (folders). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

9 Chapter 17 - Reading Excel and Access 289


Reading an Excel File using a COM object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Open the Workbook
parameter list
Reading a Row
comboBoxes based on Access Database Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Attaching Microsoft Access Data to a comboBox. . . . . . . . . . . . . . . . . . . . . . . . . 317
ODBC Call
Connecting to the comboBox
Refreshing the List
Performance Considerations

9 Chapter 18 - External Programs (Shell) 333


Starting a New Process. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Launching Notepad with a Specific Filename
Building an Exit Event
Using Code to Start a New Process.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
proc.WaitForExit.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
DOS Output.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Capturing DOS output into an Array
Multiple Instances of an Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Simple Instance Test
Mutex: A Better Solution for Multiple Instances. . . . . . . . . . . . . . . . . . . . . . . . . . 355

9 Chapter 19 - Waits, Delays and Pauses 361


Poor Wait States – Not Recommended
Empty loops
current time and loop until xx seconds
Sleep events
System.Threading.Thread.Sleep (milliseconds). . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Completed wait Simulation - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Wait as a Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Using cs805_Wait
Calling Wait
Timer Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Basic Timer Code
Event Horizons
Countdown and Timer Example
Disabling the Close "X" Mid-Transaction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Intercepting a Close Event
Countdown and Timer Program - Completed. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Simple Startup Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Splash Screens.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

9 Chapter 20 - Printing 415


Printing Simple Text. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Print Layout / Print Rendering
Pixel Positions in Millimeters (x,y)
OverShooting
Printing Horizontal Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Printing Rectangles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Filling
Printing Graphics.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Printer Dialogs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Adding a Custom Printer Dialog
Print Preview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Printer Setup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Printing Text Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Font Height Calculations - Lines per Page
Page-Break Logic
GraphicsUnit.Millimeters
Setting New Margins, Landscape. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
Current Margin
Setting New (Printable) Margins
Begin and End Print Events.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

9 Chapter 21 - Formatting 469


Font Color, Font Bold. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Font Bold
Font Style Ariel
Basic String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
String.Format with Alignment.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Proportional Fonts
Numeric Formatting with String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Standard Decimals
Formatting with Alignment
Commas and Currency
Numeric Picture Clauses. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Padding Zero with Leading and Trailing Decimals
Place Holder (#)
Thousands Separator ( , )
Variable Picture Clauses (Group Separators)
Date and Time Pictures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Converting from Strings
Converting Dates
Other Date Time Properties and Methods.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
DateTime Formatting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Pictures
Display Dates, Year first
Format Class Library - cl710_Formatting.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Phone Number Formatting
Proper Names
Proper Addresses
PhoneNumberFormat Method - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
PhoneNumberFormat Module - Coding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Extension Extract
Strip Normal Punctuation
Strip "1-" Prefixes
Punctuate
Punctuating the AreaCode
"ProperNames" Formatting - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
ProperNamesFormat - Coding.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
Introduction to Volume 2

This is the second volume of a three volume set and it assumes you have completed the
studies in Volume 1. The topics here are more advanced, but are still targeted towards
beginning students. See Volume 1 for a more complete introduction to this series.

How to Use This Book:

This is a teaching book and because of that, it makes a poor reference guide. To get the
most utility, start at page one and work your way through chapters. Each chapter builds
on the previous and you will learn the most if read in order. It takes time and effort to
learn programming. As you work through the chapters you must sit in front of the
compiler and write the code.

Other Volumes (Chapters) in this Series include:

Volume 1:
1 Introduction to the Editor
2 Introduction to Loops
3 Conditional Branching
4 Strings
5 Numbers and Dates
6 Utility Functions
7 Advanced Utility Functions
8 Class Libraries
9 Variable Scope
10 Form Controls and Events
11 Calling Multiple Forms
A Compiler Error Messages
B Compile and Distribute

Volume 2:
12 ASCII Files
13 Parsing Tab and CSV Files
14 INI Files
15 XML and App.config Files
16 Windows Registry
17 Reading Excel and Access
18 External Programs (Shell)
19 Wait, Delays, Pauses
20 Printing
21 Formatting

Volume 3:
22 Arrays
23 File Manipulation
24 Console Applications
25 SQL Databases
26 SQL Record Edits
27 SQL Data Grids
28 Data Grid Cell Editing
C Installing SQL Server Express
D Routines (of Interest)

Thank you

Thank you for purchasing this book. I hope you enjoy it as much as I have had writing it.
Comments and suggestions are welcomed.

This book was written with Visual Studio 2013 through 2017, with sections referencing
Microsoft Office 2010 and Microsoft SQL Server 2014 Express.

Original text written with WordPerfect version X7. Illustrations created with Corel's
PaintShop Pro, versions X5 and X6.

The Compiler and Other Tools:

As of mid 2017, Microsoft allows you to freely download "Visual Studio Express"
Then, in November, 2104, a new version, Visual Studio Community was released and it
is also free. See this link:

http://www.visualstudio.com/en-us/products/visual-studio-community-vs

This book was written using Visual Studio Professional 2013 and 2017 Community
Edition. The techniques described here are applicable to VS 2010 and newer. Where
there are differences, they are noted.

For the database chapters you will need a copy of Microsoft MS-SQL database or a copy
of Microsoft's free downloadable "MS-SQL Express". Details can be found in the
Appendixes.

Everything in this book was designed to run on a stand-alone (home) workstation,


including the SQL chapters. You will not need a server, external SQL database or
Active Directory to complete the chapters. However, when useful, references to these
other resources are made.

Conventions

Conventions used in this book are different than normal and are designed so you can find
pertinent code quickly. At the beginning of each chapter is a summary or outline of the
most common commands or coding techniques. These act as a reference for the details
that follow.

Code examples are show in plain text, white text boxes or shaded text boxes.
The boxing and shading gives you a clue about how "solid" or complete the code is. If
you are skimming through the chapters looking for something, the formatting style tells
you how far along the text is. The formatting styles move from discussions to
preliminary to final code.

Discussion Code Blocks:

Code that is in-line with the text, and without a text-box border, is a discussion or
theoretical block and you are not expected to type or test. For example, this is a
discussion code block:

//Consider showing which number was selected


MessageBox.Show("Selected value: " + Convert.ToString(inumericValue));

Code is always typed in a fixed-width, non-proportional Courier font.

Preliminary Code Blocks:

White-text boxes are preliminary or incomplete. Often the code may have ellipsis,
indicating some code has not been developed or was discussed earlier in the chapter and
removed in the interest of space. The code block always has a caption, with a status,
such as "preliminary," "flawed," or "tentative".
Example Code, not complete or preliminary
private void button1_Click (object sender, EventArgs e)
{
int iloopCounter = 1;

while (iloopCounter <= 10)


{
textBox1.Text = textBox1.Text + "Hello";
iloopCounter = iloopCounter + 1;
}

:
: other code goes here
}

These types of code boxes are meant to be instructional, and set the stage for later, more
complicated techniques. Although these may be incomplete or flawed, you should key
them into your editor and try them. Later examples will always show a completed or
final version.

Completed Code:

Completed or final code is displayed in a grey-shaded text-box, along with the words
"complete" in the caption.

Production or Complete Code, complete


private void button1_Click (object sender, EventArgs e)
{
int iloopCounter = 1;

while (iloopCounter <= 10)


{
textBox1.Text = textBox1.Text + "Hello";
iloopCounter = iloopCounter + 1;
}

MessageBox.Show(textBox1.Text);
}

A grey textbox indicates solid, reliable code that meet the main goals of the section being
discussed. If you are flipping through a chapter, skimming for the "answer," look for the
grey-boxes.

Some code blocks span several pages and may be illustrated in this fashion, with a
shaded or white textbox above and below the code block:

Long Example Code, complete


private void button1_Click (object sender, EventArgs e)
{
//A long code block that might extend across several pages
//will have a box above the code block and below

int iloopCounter = 1;

while (iloopCounter <= 10)


{
//Write each item in a textbox
//Include a carriage-return/linefeed
textBox1.Text = textBox1.Text + "Hello" + "\r\n";
iloopCounter = iloopCounter + 1;
}

textBox1.Visible = true;
textBox1.Focus();
}

Example Code Block, end

At the end of the code blocks are detailed "where" or "comment" descriptions, which
summarize what was said in the previous text. These are always in a bullet-list. For
example:

where:

• Padding strString1.PadLeft(10) assumes a space as the pad-character.

• Padding strString2.PadLeft(10, ' ') explicitly used a space-character. Note


the tic-marks, which indicates "character data" – characters are single-position
strings and are delineated with apostrophes.

Author Comments:

If I have an opinion on a programming technique, it is typed as an indented, italicized


block, such as this:

In real life, no programmer would define a "part-number" as a


floating-point number, even if it has a decimal point. This
should really be declared as a string. Only declare numeric
values if you intend to perform mathematical operations.

Paragraph Numbering:

There may be numerous steps to solve a programming goal and you will find I am a fan
of numbering them. Within each section, you will find numbered steps, 1, 2, 3 or
A, B, C.

Setup or pre-requisite steps, as well as testing, are numbered "A, B, C". Actual
programming steps (devoted to the section you are reading), are numbered "1, 2, 3".
Legal Stuff:
Visual Studio, Excel, Access, Visual Basic, Microsoft SQL, SQL Express, are Microsoft
Products with all of their copy-rights and trademarks

- TRW, 2017.06
Chapter 12 - Reading and W riting ASCII Files Page: 14
A Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 12 - ASCII Files
Chapter 12 - ASCII Files

This chapter deals with external "flat" ASCII text, Tab Delimited, and CSV files
(comma-separated values). ASCII files are simple text files, editable in Notepad, and
contain only characters A-Z, numeric digits, commas, tabs, etc. – basically anything that
can be typed on a keyboard. Even though flat files are somewhat old-fashioned, they are
still commonly used to transfer data between different systems and in some instances,
storing data in an ASCII format is easier and cheaper because of licensing and other
issues.

While the goal is to read and parse ASCII files, these next two
chapter's real aim is to teach parsing techniques and how to
sequentially process data files. These are valuable skills and
considerable time will be spent in this area.

Topics:

• Opening ASCII (flat) files with StreamReader class


• Using a while-loop
• Priming Reads; Reading Records with sr.ReadLine
• try-catch problems; nested try-catch
• Chained try-catch
• Advanced Open Methods
• Writing ASCII files with sw.WriteLine
• Define the StreamWriter in on place, instantiate in another (complicated reports)
• Appending to existing files
• Opening multiple ASCII Files

See Chapter 13 for CSV and TAB Parsing routines.


See Chapter 14 for instructions on processing INI files.
See Chapter 21 for recursive calls to an ASCII read routine.

Chapter 12 - Reading and W riting ASCII Files Page: 17


Overview:

Statements for opening and reading ASCII files are displayed here. Program 12.5 shows
the complete code in context of a program.

ASCII File Read, simple overview


using System.IO;

StreamReader myInputFile; //Declare above try


string strInputFileName = "C:\\temp\\test.txt";
string strReadLine;

try
{
//Open 'try'
myInputFile = new StreamReader(strInputFileName); //Open the file

strReadLine = myInputFile.ReadLine(); //priming read

try
{
//The process 'try'
while (strReadLine != null)
{
// <loop details>
strReadLine = myInputFile.ReadLine(); //bottom of loop
}
}

catch (Exception edetail)


{
MessageBox.Show("Error Reading Line: " + "\r\n"
+ edetail.Message);
}

myInputFile.Close(); //Close here, if declared above the try...


myInputFlie.Dispose();
}

catch (Exception e2)


{
MessageBox.Show("File Open error: " + "\r\n" + e2.Message);
}

Program 12.5: ASCII ReadFile, final version

using System.IO;

private void A110_ReadTextFile();


{
//Open an ASCII file; read all non-blank records; write what was
//found into textBox1.Text.

Chapter 12 - Reading and W riting ASCII Files Page: 18


//Call this routine from from (button1_click)

string strReadLine; //variable for current record


string strInputFileName = "C:\\Data\\test.txt";
int iRecordCount = 0;
textBox1.Clear();

try //file-open-try
{
//Open the file
StreamReader myTest = new StreamReader(strInputFileName);

//Read the first record from the file (Priming):


strReadLine = myTest.ReadLine();

try //detail-try
{
while (strReadLine != null)
{
iRecordCount++;

//Skip blank records but continue reading:


if (util.IsFilled(strReadLine))
{
//process record details here...
//append the record into textBox1:
textBox1.Text += strReadLine + "\r\n";
}

//Read the next line as the last step in the loop:


strReadLine = myTest.ReadLine();
}
}
catch (Exception e)
{
MessageBox.Show("Detail problem in loop: \r\n" +
"Record: " + Convert.ToString(iRecordCount) +
strReadLine + "\r\n" + e.Message);
}

myTest.Close(); //Close the file after the loop and the


//detail-try
myTest.Dispose();
}
catch (DirectoryNotFoundException e2)
{
MessageBox.Show("Directory not found for " +
strInputFileName +
"\r\n" +
e2.Message);
}
catch (FileNotFoundException e3)
{
MessageBox.Show("File not found for " +
strInputFileName +
"\r\n" +
e3.Message);
}
catch (Exception e4)
{
//This is the catch for file-level problems:
MessageBox.Show("File level problem: \r\n" + e4.Message);
}

Chapter 12 - Reading and W riting ASCII Files Page: 19


End: Program 12.5: ASCII ReadFile, final version

Alternate Method for Reading ASCII Files


//A more concise way to read a file.
//With this, do not use a priming read or a next-record read
//(a drawback is you can't use a multi-layered try-catch)

string strInputFileName = "C:\\temp\\test.txt";


string strReadLine;

try
{
StreamReader myInputFile;
myInputFile = new StreamReader(strInputFileName); //Open file

while (strReadLine = myInputFile.ReadLine() != null)


{
// <loop details>
}
}
(Exception e)
{
MessageBox.Show("File Problem: " + "\r\n" + e.Message);
}

Chapter 12 - Reading and W riting ASCII Files Page: 20


ASCII File Write, overview
using System.IO;

:
//Declare StreamWriter higher-up if you need to write from
//several different routines...

string strOutputFileName = "C:\\data\\test2.txt";

try
{
//Declare here if all the writes happen in this area...
StreamWriter myNewFile;
myNewFile = new StreamWriter(strOutputFileName);

myNewFile.WriteLine("Hello World");

myNewFile.Close();
myNewFile.Dispose();
}

catch (Exception e)
{
MessageBox.Show("Error writing file" + "\r\n" + e.Message);
}

ASCII File Read, without a Priming Read


For reference:
Although not needed in this chapter, a loop sometimes needs to be called recursively
and this construct is sometimes used (See Chapter 21 Printing for an example. This is
an advanced topic:

//No Priming Read


while ((strReadLine = myAsciiFile.ReadLine()) != null)
{
//Other statements

//No read-next statement


}

Chapter 12 - Reading and W riting ASCII Files Page: 21


ASCII Files:

ASCII files, sometimes called "flat files," are text files that do not have formatting or
other compiled codes and these files are usually edited by programs such as Notepad,
DOS Edit, TED and other such editors. The files often have these common filename-
extensions: ".LOG", ".INI", ".TXT", "CSV", and ".TAB".

In a production system, they are typically generated by other computers; downloaded


from mainframes or exported from databases. ASCII files have the benefit of being an
understandable, non-proprietary format and are typically human-readable. Often these
types of files are used for configuration "INI" files.

Setting up the Example Program:

For the test program, build a new Project, following the steps at the beginning of
Chapter 8.

Add the following to the main form:

• multi-lined textBox1
• Add a vertical scroll bar ("Scrollbar" Property) to the text box.
• label1. Set Autosize = false and size to width
• button1 (renamed to "BtnProcess" with a text label of "&Process")
• BtnClose

• Link-in/Add the CL800_Util Library, described in Chapter 8. Summary of steps:

Chapter 12 - Reading and W riting ASCII Files Page: 22


In Solution Explorer, Add Existing Item, Link to CL800_Util.cs
Add statement: using NS800_Util;
In "public partial class Form1": CL800_Util util;
After "Initialize Components": util = new CL800_Util();

If you do not have the CL800_Util libraries, some functions, such as "IsBlank" will
have to be written by hand.

Building Test Input Files:

Using Windows Notepad, build a test input file:

a. From Windows, click Start, Run (or Windows-R), "Notepad.exe".

Notepad is an ASCII text editor, which is somewhat like a word processor but it is the
most featureless word processor you have ever seen. There are no bol ds, underlines or
other such amenities; it only likes to work with text – ASCII text. Developers often use
this program to view computer-generated data files.

b. In the editor, type a few short lines of text, any text. For future examples, type an equal-
sign in at least the first two records. Press Enter after each line.

A defining characteristic of an ASCII text file is a carriage-return line feed after each
line (CRLF). In the file each line is treated as a "record" and the CRLF is the record
delimiter.

In Notepad, select File, Save.


Type this name, including quotes: "C:\Data\Test.txt"
(Save the file to a local disk for these examples and these examples assume a C:\Data
directory is already built.)
Close the Editor

Chapter 12 - Reading and W riting ASCII Files Page: 23


c. Do the following from a DOS prompt to demonstrate the nature of the file:

Start, Run, "CMD", click OK.


At the DOS C:> prompt, type this command, using the keyword "Type":
TYPE C:\DATA\Test.txt

The ASCII text displays on the screen.


Type the command "EXIT" to close the command processor.

If you were to TYPE other files, such as "C:\Data\MySpreadsheet.xls", you would find
unreadable text – with lots of smiley faces and other nonsensical characters. Data files
such as Word or Excel documents, or .exe files are not meant to be human readable.

Chapter 12 - Reading and W riting ASCII Files Page: 24


Reading ASCII (Text) Files

In the following exercises, read the test ASCII text file line-by-line and write the results
in a multi-lined textBox by appending each line to textBox1. Later, as the examples
progress, the file will be parsed and only certain data will be retrieved. These are
common data-processing tasks.

The "StreamReader" class is used for the file-read. This class specializes in ASCII data
and is sensitive to carriage-return/line-feeds (CRLF).

Using StreamReader to Read ASCII Files:

Using the example program illustrated at the start of this chapter, begin coding by
enabling ASCII file reads with a using System.IO; statement.

1. Before the StreamReader and StreamWriter classes can be used, include the libraries in
the program. Do this by scrolling to the top of the program and add this case-sensitive
statement:

Required using System.IO (input/output) statement


using System.IO;

This command is similar to the "using cl800_Util" command typed earlier and it allows
you to use the C# I/O libraries without prefixing every command with the words
"System.IO". I like to think this "links" the library, but technically, C# already knows
about the System.IO routines (it comes as a default with every new project). Because of
this, do not add System.IO to the Solution Explorer's list of modules.

2. From Form1's design view


Change button1's Name property from button1 to "BtnProcess"
Modify (button1 – now BtnProcess) .Text property to read "&Process" (ampersand).

3. Double-click BtnProcess, opening code view.


In the BtnProcess_Click" event, add this statement:

btnProcess Calls A110_ReadTextFile


private void BtnProcess_Click (object sender, EventArgs e)
{
// Generally, a button should never do real work; instead, it
// should call a separate function:

A110_ReadTextFile();
}

Chapter 12 - Reading and W riting ASCII Files Page: 25


4. As you finish typing the statement "A110_ReadTextFile();", hover the mouse over the
line and notice the lightbulb icon. Click the icon and chose "Generate method". Scroll
down in the code, directly below the current button routine, looking for the new A110
module. Delete the "throw-error" statement. You will return to this new routine in a
moment.

A110_ReadTextFile is an invented name for a new function about to be written. The


read and other file logic will take a dozen lines of code, cluttering the button's routine.
For this reason, and because it is good programming style, divide major functions into
their own routines.

From previous chapters, a module-numbering scheme, such as


A110, is recommended but not required.

5. Scroll to the new A110_ReadTextFile routine.

Delete the default "throw-error" logic and replace with the code on the following pages.
Here are some definitions of the variables:

strReadLine A temporary variable that holds the contents of the current, active
line from the ascii file. For example, on the first-read, it will hold
"Test Line = 1".

strInputFileName
Is the Notepad filename saved earlier: "C:\\Data\\Test.txt". The
double-backslashes are required because a backslash is a reserved
character. From Chapter 4, a "\\" represents a single "\".

Alternately, this syntax could be used:


string strInputFileName = @"C:\Data\test.txt";

where the @-sign indicates a verbatim literal, exactly as typed.

Chapter 12 - Reading and W riting ASCII Files Page: 26


textBox1 is the multi-lined textBox on the main form. Each ascii line will be
read, dropped into a holding variable, then appended to the textBox,
essentially repeating the file.

Program 12.1: ReadTextFile Priming Read; Method 1, preliminary


1 private void A110_ReadTextFile()
2 {
3 string strReadLine; //variable for current record
4 string strInputFileName = "C:\\Data\\test.txt";
5 textBox1.Clear();
6
7 //Open the file:
8 StreamReader myTextFile =
new StreamReader(strInputFileName);
9
10 //Read the first record from the file (Priming):
1 strReadLine = myTextFile.ReadLine();
2
3 while (strReadLine != null)
4 {
5 textBox1.Text = textBox1.Text + strReadLine + "\r\n";
6 //Read the next line while looping:
7 strReadLine = myTextFile.ReadLine();
8 }
9
20 myTextFile.Close();
1 myTextFile.Dispose();
2
3 }

comments:

• Line 8:
StreamReader myTextFile = new StreamReader (strInputFilename)

uses "strInputFileName", defined in line 4, and tells the program to "open" the file,
preparing record pointers for the read. The filename was hard-coded with
"C:\\Data\\test.txt".

Technically it is proper to say the command instantiates a new StreamReader class


and this is the same type of command used when CL800_Util was instantiated

• The "StreamReader" command does not "read" the file; this is left for another
statement.

The "StreamReader" is declaring a variable named "myTextFile" and this is no


different than declaring a "string myName". In this case, instead of a string, you are
declaring a "StreamReader". (Many online websites and Microsoft commonly use
the variable name"sr" in their examples, as in StreamReader sr = new....)

Chapter 12 - Reading and W riting ASCII Files Page: 27


To word this another way, the "new" makes "myTextFile" a new class (of type
StreamReader). The new class "myTextFile" is a copy of a StreamReader and it gets
(inherits) all the methods of its parent. As you will see, myTextFile has other
methods such as "Close" and "ReadLine".

• Although not recommended, the (strInputFileName) could be hardcoded directly into


the statement:

StreamReader myTextFile = new StreamReader ("C:\\Data\\test.txt");

In the real-world, filenames are seldom hard-coded; instead the names are found in
other locations, such as INI files, registry, or user-prompts. But, if a literal filename
is used, be sure to assign it to a variable at the top of the module, making it easer to
find and edit. If stored in a separate variable, it can also be used in error messages,
user prompts, and other such commands.

This routine is flawed because it does not have a try-catch. Opening external
files is fraught with dangers - the file may not exist, may be locked, etc.
Errors should be trapped. More on this in a moment.

Chapter 12 - Reading and W riting ASCII Files Page: 28


Reading a Record from the File:

• Line 11: strReadLine = myTextFile.ReadLine();


is the statement that "reads" the current record.

The found-record ("Test Line = 1") is assigned to a string variable, "strReadLine".

The statement works like this: Earlier, a new StreamReader class, "myTextFile",
opened the file and set an internal record pointer at the top. Next, the
"myTextFile.ReadLine" method retrieves the record and assigns what-was-read to a
holding variable called strReadLine. ReadLine reads up-to the CRLF. Looking at
the top of this module, you will see strReadLine was declared as a string.

Chapter 12 - Reading and W riting ASCII Files Page: 29


The syntax, myTextFile.ReadLine( ) is similar to other commands, such as
util.IsBlank( ); the class name first, followed by the method. myTextFile is the
class; ReadLine is the method that runs against the class.

• Neither "myTextFile" nor "strReadLine" are keywords; any name could be used.

Priming Read:

Ultimately, the program will loop through the file, reading each line one at-a-time. There
are several ways of doing this, but the method I like the most is called a "Priming Read."

After the file is opened with the StreamReader command, read the first record and assign
it to a working variable, strReadLine. This acts as a priming read for the main loop.
The code from above is re-produced here:

Setting up a Priming Read; partial listing, repeated


:
7 //Open the file
8 StreamReader myTextFile = new StreamReader(strInputFileName);
9
10 //Read the first record from the file (Priming Read):
1 strReadLine = myTextFile.ReadLine();
2
3 while (strReadLine != null)
4 {
5 textBox1.Text = textBox1.Text + strReadLine + "\r\n";
6 //Read the next line while looping:
7 strReadLine = myTextFile.ReadLine();
8 }

• Just before the loop, at line 11, read the first-line and stores the record in
"strReadLine." This gives the while-loop at line 13 something to look at; hence the
term "Priming Read."

"while (strReadLine != null)"

Chapter 12 - Reading and W riting ASCII Files Page: 30


If the ASCII text file were empty (e.g., the file exists, but no lines were typed inside),
the while-loop would detect "null" and none of the statements within the loop would
run – this would be correct behavior.

Near the bottom of the loop, at line 17, a second read-statement reads the next record
and loops back to the top. Since this is (likely) not the last record, the loop checks
"is strReadLine not-equal to null (is it populated)"? If it has "stuff" do the things
inside the loop again, each time fetching the next record at the bottom of the loop.
Ultimately, it reaches end-of-file is reached and the loop stops.

• With each found record the interior of the loop takes the current (working variable),
strReadLine, and appends the results to textBox1. At the bottom of the loop, it reads
the next record and wraps back to the top. This is standard loop logic, as seen in
Chapter 3.

• The last line in the ASCII text file is a control-Z-End-of-File (EOF) marker,
represented as a null-character. When the last record is read, the program loads the
"null" into strReadLine and loops around the top. The while-loop detects this and
drops out of the loop.

Testing the Program:

• Ensure the notepad test file was built and saved to "C:\Data\Test.txt" (see above)
• Note the last line in the test file
• Press F5 to run the program
• Click BtnProcess; scroll to the bottom of the text box and confirm the last line.

j When testing, always confirm the first and last lines were processed correctly. If you
forget to write the priming read or write the loop incorrectly, either the first or last
record is easily lost.

Chapter 12 - Reading and W riting ASCII Files Page: 31


Additional testing:

• Remove the + "\r\n" from line 15 and test again. Explain the results.

• Mis-spell the filename on line 4 (e.g. C:\\Data\\testxxx.txt"), simulating a file-not-


found event. Then, run the program again. Note the errors. This will be corrected
in a moment.

Ending the Loop and Closing Files:

• When the last record is read, C# sets strReadLine to null. The loop ends and
control falls to the next statement below the loop's closing brace (line 18).

• At Line 20, the opened-file is closed using "myTextFile.Close ( );"


This is a basic rule of programming: If you open it, close it.

"myTextFile.Dispose( );" releases the file-resource and allows a later, automatic


step called 'Garbage Collection' to run. In a small program, the resource is disposed
when the program ends, but if your program opens and closes many files or the same
program tends to run all day (data-entry clerks), then the resource should be
'disposed.'

• Closing a file relinquishes the file-lock and allows other programs, users or tape-
backups, etc. to open the file. This is particularly important in a networked
environment. See Exercise 4 (Simulating a File Lock) at the end of this chapter for
an interesting test of opened and closed files.

Chapter 12 - Reading and W riting ASCII Files Page: 32


Using an EndOfStream Read (Flawed Method):

Instead of testing the loop with a "while (strReadLine != null)", you can use a
"while (!myTextFile.EndOfStream)" (while !not) with the same effect. However, I've
seen several other (web) publications attempt to infer more powers to this method than it
deserves. They are attempting to use the EndOfStream in ways that supposedly avoid the
complexities of a priming read. EndOfStream is perfectly legitimate – but if used
improperly, the program fails.

Consider a version of the same example program where a few minor changes were made.
First, the "while" statement checks for an "end-of-stream" flag and the "Read" moves to
the top of the loop; avoiding the priming Read. Everything else is the same. At first
blush, this seems like a cleaner version.

private void A110_ReadTextFile();


{
// This routine is not recommended
string strReadLine; //variable for current record
string strInputFileName = "C:\\Data\\test.txt"
textBox1.Clear();

//Open the file; no priming read


StreamReader myTextFile = new StreamReader(strInputFileName);

while (!myTextFile.EndOfStream)
{
//(Move to the top of the loop: Read the 1st and next lines...
strReadLine = myTextFile.ReadLine();

//<Other loop details, such as parsing could go here>

//Append the found record to textBox1 and loop around:


textBox1.Text += strReadLine + "\r\n";
}

myTextFile.Close();
myTextFile.Dispose();
}

Although this example works correctly (mainly because the interior of the loop only
appends the found text to a textBox, the logic is flawed.

Here is the concern: When the last record is read, and End-of-file is reached, strReadLine
gets a null value. The null value still passes through the loop's (parsing) details. This
example was too simplistic and no harm was done when the null was appended. But, if
the program did other work, such as parsing, the null will cause problems.

To keep the program from abending, should it have more complicated logic in the loop,
you are forced to add logic working around the last-record problem. This, of course,
ruins the perceived simplicity of the loop. A first-blush response would be to insert a
"String.IsNullOrEmpty" test...

Chapter 12 - Reading and W riting ASCII Files Page: 33


This solves the immediate problem, but introduces a new one. Often, input files have
embedded blank lines in the middle of the file (these are lines with only a CRLF). The
"IsNullOrEmpty-test" detects these and promptly ends the loop, leaving the remaining
records unprocessed. (Blank lines/empty-strings are common in ASCII text files, but
there will never be a null in the middle of the file because the null is the end-of-file
marker for the operating system.)

(Try this yourself by adding the logic above and then inserting a blank line in the middle
of the ASCII text file, C:\data\test.txt.)

In order to keep the last record from being processed, there is only one solution: Test
specifically for "null" using one of these two methods:

//Check to see if the record is null and if so, exit the loop:
//However, this idea is not recommended; see text
if (String.Compare (strReadLine, null) == 0)
break;
or
if (strReadLine == null)
break;

This solves the problem at the expense of a simple loop. Now the loop has an "early-
exit" at the top. Worse, in a 10,000 record file, each record is checked against a null
value – all this because the last record has a null? A priming read is the best design to
use.

Chapter 12 - Reading and W riting ASCII Files Page: 34


More Concise Read Logic:

The read-file logic described above is considered verbose by some developers, but it
clearly demonstrates how to open and read a file. There is a more concise method,
which combines the priming read, the while-statement, and the subsequent reads into one
command.

Alternate Method for Reading ASCII Files


//A more concise way to read a file.
//With this, do not use a priming read or a next-record read

string strInputFileName = "C:\\temp\\test.txt";


string strReadLine;

try
{
StreamReader myInputFile;
myInputFile = new StreamReader(strInputFileName); //Open file

while (strReadLine = myInputFile.ReadLine() != null)


{
// <loop details>
}
}
(Exception e)
{
MessageBox.Show("File Problem: " + "\r\n" + e.Message);
}

Benefit:
With this design, you do not need a priming read (this is handled within the while-
statement), and each subsequent read is also handled.

Drawback:
The drawback to this design is a multi-layered try-catch (described next) cannot be used,
limiting the program to a single-error trap.

The "Using" Clause:

Instantiating a file (myInputFile = new StreamReader...), along with the highly-


recommended .Close and .Dispose are recommended. But if you, the developer, forgets
to close and (optionally dispose) the file, the program will run through its final statement
correctly, without an error – but if you do other things with the file, such as re-opening
the same file, the program will die a horrible death.

Naturally, it is the developer's responsibility to properly close a file. But Microsoft,


seeing a problem in this area, came up with an alternate design. A "using" cause can
take care of the closing (different than a 'using' at the top of the program). Note the
changes to the instantiation and the deleted .Close and .Dispose methods.

Chapter 12 - Reading and W riting ASCII Files Page: 35


Program 12.1: ReadTextFile Priming Read; Method 1, preliminary, with 'using'
1 private void A110_ReadTextFile()
2 {
3 string strReadLine; //variable for current record
4 string strInputFileName = "C:\\Data\\test.txt";
5 textBox1.Clear();
6
7 //Open the file:
* using (StreamReader myTextFile =
new StreamReader(strInputFileName))
* {
9
10 //Read the first record from the file (Priming):
1 strReadLine = myTextFile.ReadLine();
2
3 while (strReadLine != null)
4 {
5 textBox1.Text = textBox1.Text + strReadLine + "\r\n";
6 //Read the next line while looping:
7 strReadLine = myTextFile.ReadLine();
8 }
* }
9
20 // myTextFile.Close(); //No longer needed with 'using'
1 // myTextFile.Dispose();
2
3 }

The author prefers the more explicit Close and Dispose statements, for no other reason
than they are explicit, and prefers more robust error-trapping with multiple try-catches
(next). However, either method works.

The next section completes the recommend design for reading ASCII files by introducing
error-trapping logic, which is required in all serious programs.

Chapter 12 - Reading and W riting ASCII Files Page: 36


ASCII File Reads with try-catch

When working with input files, there are a laundry-list of potential problems. For
example, the file could be opened by another program or you may not have network
rights; disk-packs move; users move to new security groups; files are deleted; errors
happen. Your program must detect and gracefully react to these problems. If an error is
intercepted, the program can present the user with a reason and allow them to exit
without crashing. C# uses a "try-catch" block to capture these types of errors.

With a "try/catch", the program "tries" a command (such as opening a non-existent file)
and on failure jumps to a "catch" routine. Normally a program treats a missing file as a
catastrophe but the "catch" intercepts the error, allowing the program to tolerate the
condition.

Using a try-catch:

Place a try-command above the StreamReader (Open) statement, along with a requisite
opening brace. For reasons discussed below, the try-closing-brace should go after the
Close-Dispose statements. Be sure to type the braces in the correct location. The code
should automatically indent, as illustrated below.

After the new closing brace, type a "catch (Exception e)" statement, with a pair of
opening and closing braces:

Chapter 12 - Reading and W riting ASCII Files Page: 37


Program 12.4: try/catch, preliminary
1 private void A110_ReadTextFile();
2 {
3 string strReadLine; //var for current record
4 string strInputFileName = "C:\\Data\\test.txt"
5 textBox1.Clear();
6
7 try
8 {
9 //Open the file
10 StreamReader myTextFile = new StreamReader(strInputFileName)
1
2 //Read the first record from the file (Priming):
3 strReadLine = myTextFile.ReadLine();
4
5 while (strReadLine != null)
6 {
7 //All record processing logic goes here:
8 textBox1.Text = textBox1.Text + strReadLine + "\r\n";
9
20 //Read the next line as the last step in the loop:
1 strReadLine = myTextFile.ReadLine();
2 }
3
4 myTextFile.Close();
5 myTextFile.Displose();
6
7 }
8 catch (Exception e)
9 {
30 MessageBox.Show
1 ("Something bad happened at the file level: \r\n"
2 + e.Message);
3 }
4
5 } //End A110

where:

• This try-catch is subtly flawed. It needs a two-layered try-catch, described below.

A final version of this program, with complete try-catch and


other logic, can be found below; see Program 12.5.

The catch-statement:

The catch-statement's Exception clause is one of many different parameters that can be
passed. " (Exception e)" being the broadest and most extensive, captures all errors, of
any type, calling them "e".

catch (Exception e)
{

Chapter 12 - Reading and W riting ASCII Files Page: 38


With a runtime error (file not found, etc.), the catch gets control and the 'reason' for the
error is passed to a variable traditionally called "e". From here, program can do anything
it wants, including displaying a user-friendly message followed by a more technical
description (e.Message), or it could take other actions, such as prompting the user for a
different filename.

MessageBox.Show("Error is " + e.Message);

As you will see later, specific error conditions can be targeted and trapped by using
variations of the e.Message parameter.

Issues with try/catch and Close:

Below is a pseudo-code representation of the example program. When the program


successfully opens a file and loops through the records, it then "Closes" and all is well.
If it fails to open the file, it immediately jumps to the Catch, bypassing the Close. This
too is okay because the file was never opened.

Now imagine the program successfully opens the file and begins processing through the
loop. While doing its stuff, it stumbles into some other problem that has nothing to do
with the file-open, and this problem causes an abend. For example, it might accidentally
calculate a divide-by-zero. Because the try/catch is still active, it intercepts the error.

Chapter 12 - Reading and W riting ASCII Files Page: 39


The "catch" was originally designed for file-open errors and the logic offers appropriate
end-user messages for those events – but if the program crashes due to some other
catastrophe (divide by zero, subscript out of range, etc.), the same "catch" logic will not
make sense.

The crash has another side-effect. When the "catch" intercepts the divide-by-zero, the
"close" statement is skipped, leaving the data file open and unavailable to other programs
or people until the program ends.

If your code "catches" an error, the program assumes you are


taking care of the problem and the program will not "crash" or
abend. But, without a try-catch, divide-by-zero, subscripts of
out bound, and other such errors, crash the program without
friendly help or diagnostics. The crashed program ends
abruptly and control is returned to the desktop.

Flawed try/catch Solutions:

The first possible solution changes the try/catch to a try/catch/finally. The "finally" says
to run this block of code unconditionally, after either the "try" or the "catch". Returning
to the pseudo-code, the Close statement could be moved to the "finally" section and the
Close should execute no matter which path the program takes.

try

Open with StreamReader


Read Records...
Divide by zero

catch (Exception e)
MessageBox.Show("Something bad happened at " + e.Message);
finally
myTextFile.Close(); //but doesn't work here either...

Sadly this does not work. The compiler claims that "myTextFile.Close does not exist in
the current context", which is another way of saying "myTextFile" fell out of scope. In
the example, StreamReader's "myTextFile" was defined inside the try-statement. By the
time you reach the "finally," moving past the try's closing brace, that construct was
released from memory and the Close fails. The "Close" can not move into the catch-
statements because then it would only run when things crashed.

You could resolve the "myTextFile.Close out of scope" problem by moving the
StreamReader statement above the "try". By doing this, the StreamReader survives the
collapse of the try/catch and it allows you to keep the Close statement in the "finally"
clause. Consider this pseudo-code:

Chapter 12 - Reading and W riting ASCII Files Page: 40


Open with StreamReader (open above the try)
try
Read and loop the records
Divide by zero

catch (Exception e)
MessageBox.Show("Something bad happened");
finally
myTextFile.Close(); //sr is in scope but seriously flawed

This solves the Close problem but introduces a flaw by leaving the StreamReader's Open
statement unprotected by a try/catch. If the file-open bombs (File-not-found, etc.), there
is not a try/catch authorized to intercept the error. Ideally, the "finally" paragraph would
maintain the same scope as the "catch", but Microsoft did not write the command to
behave this way.

The Solution to the try/catch Problem:

There are two good solutions to the try-catch problem. The first is to embed another
try/catch within the first "try" and this is my favorite.

The outside "try-catch" intercepts file-errors and the inner "try" captures other errors
(such as, divide-by-zero).

Chapter 12 - Reading and W riting ASCII Files Page: 41


This solves several problems because the open-file and loop-details are each given their
own error-routines and each can react to a different set of problems, each with their own
"(Exception e)" or "(Exception e2)".

As each try-statement becomes active, it has the pleasure of intercepting *all* errors
until it falls out of scope. In the illustration above, the outer try reigns supreme on the
StreamReader Open statement, but then relinquishes control when the loop starts. When
the interior loop ends, the outside try/catch regains control. This keeps the "Close"
statement properly aligned with the Open.

More importantly, notice how the file closes properly even if an error is
found within the interior loop. The inside "try" catches the error (and
presumably displays an error message), then control falls through to the
myTextFile.Close statement and the file closes properly. See below,
ReadFile Summary, for a completed code example.

Chaining catch-statements:

A second solution uses multiple (chained) catch-statements against the single "try."

Recall how the examples above showed how "(Exception e)" captured all possible
exceptions. The statement can be replaced with multiple catches, each aimed at a
particular error, starting with the most targeted first, followed by more general errors.

The list of available exceptions can be found in the pop-up help. For example, hover the
mouse over the "new StreamReader" clause, noting the exceptions:

This snippet shows how to chain the catch-statements. Each catch can have different
error logic and different behaviors. Put the most specific catch-errors at the top and
leave the general Exceptions for the bottom (see the order in the list above). Make the
last catch as the most general "(Exception e)" – which is not listed.

Test how chained catch-statements operate by typing a dummy filename. The routine
attempts to open a bogus file and uses three catch-statements for the single "try":

Chapter 12 - Reading and W riting ASCII Files Page: 42


DirectoryNotFoundException
FileNotFoundException
(and a general, all encompassing "all other" exception)

Program 12.42: Chained try-catch Statements, acceptable design


private void button1_Click (object sender, EventArgs e)
{
string strInputFileName =
"C:\\does not exist path\\NoFileHere.txt";

try
{
StreamReader test = new StreamReader (strInputFileName);
//Stuff goes here

test.Close();
test.Dispose();
}

catch (DirectoryNotFoundException e1)


{
MessageBox.Show("Directory not found: " + e1.Message);
}

catch (FileNotFoundException e2)


{
MessageBox.Show("File not found: " + e2.Message);
}

catch (Exception e3)


{
MessageBox.Show("Some other Error detected: " + e3.Message);
}
}

comments:

• The exampled strInputFileName contained two errors: a bad path and a bad filename.
The DirectoryNotFoundException was the first-found error so it wins the right to
execute and the other catch-statements are ignored.

• button1_Click's signature contains "EventArgs e") which prevents you from using
the same variable ("e") in the exception clauses: "A local variable named 'e' cannot
be declared in this scope because it would give a different meaning to 'e', which is
already used in a 'parent or current' scope."

This is not a problem. Invent a new name (choosing "e1", e2, etc., for lack of better
names). If preferred, you could make the names more meaningful with
eFileNotFound, etc., but little is gained.

Chapter 12 - Reading and W riting ASCII Files Page: 43


Additional Details:

The Exception class provides other information that you may find useful. Notice that
some of these are methods( ) and others are properties.

e.Message A summary of the error; usually suitable for end-users.


e.ToString( ) A more verbose error message.
e.StackTrace Often the same as ToString; note the last line which shows the exact
line that caused the error.

Summary on Try/Catch:

try/catch captures runtime errors and can prompt the end-user with an appropriate error
messages or other actions. When working with external files, always protect the
program with try/catch.

Later chapters discuss how to handle program logic when a routine, such as
A100_ReadTextFile, detects an error; how should the rest of the program react?

try/catch can be used in other areas, not just for file-open. Routines that have to work
with bad data can use try/catch logic to bypass bad records. However, explicitly
detecting errors using a non-numeric tests or with other program code, gives more
control over the logic and performance is better than using a try-catch.

Other Processing Within the Read Loop:

Inside the while-loop, with a fully-populated strReadLine as the current record, the
program's logic has complete control over what it can do with the data. This is the place
to parse the data. You could, for example, parse the line looking for an equal-sign and
then only print the data on the right-side of the delimiter.

Test line = 1
Test line = 2

This is easy to do with the CL800_Util class. Returning to the Priming Read example,
consider this code snippet:

Chapter 12 - Reading and W riting ASCII Files Page: 44


Partial Code: Reading, Parsing and only displaying the Right-side of equal-sign
:
1 while (strReadLine != null)
2 {
3 recordCount++;
4
5 //Skip blank records and skip records that don't
6 //have something on the other side of the equal-sign.
7
8 //Parse the last-found strReadLine, looking for
9 //an equal-sign.
10
1 if (util.IsFilled(util.RightStr(strReadLine, '='))
2 {
3 //process record here... Append to textBox 1:
4 textBox1.Text = textBox1.Text +
"found - " + util.RightStr(strReadLine, "=") +
"\r\n";
5 }
6 else
7 {
8 //Discard other found lines with no equal-sign
9 }
20
1 //Read the next line and re-loop:
2 strReadLine = myTextFile.ReadLine();
3 }

comments:

• Commands such as util.RightStr and util.IsFilled are not a native C# instructions.


See Chapter 6 and 8 for building these methods, or substitute your own logic.

• Line 11 looks to see if anything was found after an equal-sign. If no equal-sign,


jump to the else-statement. Otherwise, print what was found.

Results (using the sample data from the start of this chapter): textBox1 displays only the
numbers "1" and "2" from "Test line = 1", "Test line = 2". All other records are
discarded.

Reading from a Server or Share:

Reading (and writing) from a server or shared disk is no different than using a local disk,
provided you are already authenticated via a manual login or an Active Directory
connection.

Opening a File on a Pre-Authenticated Connection:

Using similar code as Program 12.1, set the filename using either of the following
syntax. Note the quad-backslashes, which represents the remote server's UNC name
"\\remoteComputerName":

Chapter 12 - Reading and W riting ASCII Files Page: 45


string outputFileName = "\\\\remoteComputerName\\TestShare\\Test.txt";
or
string outputFileName = @"\\remoteComputerName\TestShare\Test.txt";

If the connection to the remote server has not been established, see later in this chapter
for a way to pass User Credentials to the remote server.

Chapter 12 - Reading and W riting ASCII Files Page: 46


Completed ASCII ReadFile - Program 12.5

The final versions of the example programs contain several commonly-needed


enhancements that were not described in the text above:

• recordCounter showing the total number of records read


• Improved detail-loop error message that shows which line had the error
• Logic that skips "blank" records, but continues to read the remainder of the file

Use this as a template for all of your ASCII read routines. Use an event, such as
button1_Click to call A110_ReadTextFile() or put this logic at any place where a file
needs to be written.:

Program 12.5: ASCII ReadFile, final version


Template

private void A110_ReadTextFile();


{
//Open an ASCII file; read all non-blank records; write what was
//found into textBox1.Text.
//Call this routine from (button1_click)

string strReadLine; //variable for current record


string strInputFileName = "C:\\Data\\test.txt";
int iRecordCount = 0;
textBox1.Clear();

try //file-open-try
{
//Open the file
StreamReader myTest = new StreamReader(strInputFileName);

//Read the first record from the file (Priming):


strReadLine = myTest.ReadLine();

try //detail-try
{
while (strReadLine != null)
{
iRecordCount++;

//Skip blank records but continue reading:


if (util.IsFilled(strReadLine))
{
//process record details here...
//append the record into textBox1:
textBox1.Text += strReadLine + "\r\n";
}

//Read the next line as the last step in the loop:


strReadLine = myTest.ReadLine();
}
}

Chapter 12 - Reading and W riting ASCII Files Page: 47


catch (Exception e)
{
MessageBox.Show("Detail problem in loop: \r\n" +
"Record: " + Convert.ToString(iRecordCount) +
strReadLine + "\r\n" + e.Message);
}

myTest.Close(); //Close the file after the loop and the


//detail-try
myTest.Dispose();
}
catch (DirectoryNotFoundException e2)
{
MessageBox.Show("Directory not found for " +
strInputFileName +
"\r\n" +
e2.Message);
}
catch (FileNotFoundException e3)
{
MessageBox.Show("File not found for " +
strInputFileName +
"\r\n" +
e3.Message);
}
catch (Exception e4)
{
//This is the catch for file-level problems:
MessageBox.Show("File level problem: \r\n" + e4.Message);
}

MessageBox.Show = "Processed " + iLineCount.ToString() + " lines";

Chapter 12 - Reading and W riting ASCII Files Page: 48


Writing ASCII (text) Files

Writing ASCII files is similar to the Read methods. As before, the program needs to
worry about disk errors, including user-rights, read-only files, and pre-existing file
names.

Simple Write Routine:

Using the same program from the Read section, above (Program 12.5), add a second
button (button2) and have this routine write the proverbial "Hello World" ten times.
Build the example with these steps:

1. Near the top of the program, confirm this statement is in place:


using System.IO;

2. From the Toolbox flyout, place a third button on the form.

• In Properties, name the button "btnWrite"


• Change the button's Text label from "button2" to "&Write"

3. Double-click btnWrite, opening Code View.

Add this call to a new Write routine. As with the Read, compartmentalize the code by
putting the actual write statements into their own module, which makes the code easier to
understand.

private void btnWrite_Click (object sender, EventArgs e)


{
A120_WriteFile();
}

Chapter 12 - Reading and W riting ASCII Files Page: 49


4. Create a new function, "A120_WriteFile", by declaring the new function on a blank line,
below A110's closing brace.

Manually type:
"private void A120_WriteFile()", along with its opening and closing braces.
Fill out the routine with the following statements:

Program 12.7: WriteFile (non-append), completed


private void A120_WriteFile()
{
//Write Hello World ten times

string stroutputFileName = "C:\\data\\test2.txt";

try
{
StreamWriter myOutFile = new StreamWriter(stroutputFileName);

myOutFile.WriteLine ("This file shows Hello World ten times");

for (int j =1; j <= 10; ++j)


{
myOutFile.WriteLine(Convert.ToString(j) + " Hello World");
}

myOutFile.Close();
myOutFile.Dispose();
label1.Text = "File Written";
}

catch (Exception e)
{
label1.Text = "File Not Written";
MessageBox.Show("Something horrible happened: \r\n" +
e.Message);
}
)

Testing WriteFile:

• Press F5 to run the program, then click btnWrite


• Note the onscreen message at label1: "File Written"

• Select Start, Run


Then type this command, including quotes: Notepad.exe "C:\data\test2.txt"

• Admire the file and close notepad

• Exit the program and return to the editor

Chapter 12 - Reading and W riting ASCII Files Page: 50


Results:
This file shows Hello World ten times
1 Hello World
2 Hello World...etc...

where:

• A new StreamWriter class, "myOutFile." is instantiated with a "new" command.


This allocates the file and prepares it for writing.

• The output filename is stored as a separate variable making it easier to find and
change. Use double-backslashes or use the "@"-verbatim literal. See the ReadFile
section for more information.

• By default, if the target file already exists, it is replaced with a new copy.

• Good technique Closes the file as soon as the loop has completed. A Dispose is
recommended, but in small programs, the Dispose will happen when the program
ends.

• The "myOutFile.WriteLine" method includes an automatic carriage-return/linefeed


("\r\n") as each line is written.

If you do not want carriage-return/linefeeds in the output, use "myOutFile.Write"


(instead of WriteLine).

Results with .Write:


1 Hello World2 Hello World3 Hello World4 Hello World...

User Feedback:

Users need feedback telling them the Write succeeded. In the code above, notice how
label1's Text was changed to "File Written". This is an amazingly important feature. To
prove the point, try the following.

Comment-out "label1.Text =..."


//label1.Text = "File Written";

Run the program again, clicking btnWrite. Note the uncomfortable feeling as you
wonder if the file was written or not.

User Feedback wasn't needed with the Read because the user saw textBox1 populated.
But with almost all file-write modules, the user needs feedback that the file was written.
Use a progressBar if the file takes time to write.

Chapter 12 - Reading and W riting ASCII Files Page: 51


I typically avoid MessageBoxes because they require the user to click OK; save them the
trouble and write the results to a label or other onscreen text field.

Complicated Reports:

If the program produces a complicated report, where the program logic is substantial, the
write routines may not fit neatly into single module, such as A120_Write file.

For these types of routines, make these changes:

a. Declare the new StreamWriter class higher-up in the program, usually at the class-
level. Declare the name, but do not instantiate:

public partial class Form1 : Form


{
CL800_Util util;
StreamWriter myOutFile;
string strOutputFileName = "C:\\data\\test.txt";
:

By defining the variable higher-up in the program, it remains in scope across


multiple routines and methods.

b. Then, in an initialization routine, instantiate the class.


For example, a Form1_Load event could call module A010_Initialize(),

private void A010_Initialize()


{
try
{
myOutFile = new StreamWriter(strOutputFileName);

//write an optional header record


myOutFile.WriteLine ("Col1" + "\t" + "Col2" + "\t");
}
catch (Exception e)
{
MessageBox.Show("File Open error on " + strOutputFileName);
}
}

c. In the main program loop, do all of your normal report processing, writing data-lines,
as needed:

A000-Some-Loop()
{
:
myOutFile.WriteLine (... various data fields and calculations here );
:
: reloop
}

Chapter 12 - Reading and W riting ASCII Files Page: 52


close.myOutFile();
dispose.myOutfile();

This design allows for complicated routines, with the file-open logic insulated from the
hairy-details of the actual program. Try-catch logic targets the proper errors at the
proper time.

Chapter 12 - Reading and W riting ASCII Files Page: 53


Appending ASCII (text) Files - Advanced Open Methods

The StreamReader and Writer commands have so-far used a default open method, but
sometimes more control is needed.

FileModes include:
Append*
Create
CreateNew
Open
OpenOrCreate
Truncate

FileAccess includes:
Read
ReadWrite
Write

These advanced features must use a two-step file-open design. The first stage sets a
"file-stream", which then feeds into the same StreamReader class used before. VB
programmers will recognize this design.

Here is an example of a ReadOnly method, where the file can be opened without a file-
lock at the server:

Program 12.71: Advanced File Open Method


:
FileStream fs_sales = new FileStream
(strinputFileName, FileMode.Open, FileAccess.Read)

StreamReader sales = new StreamReader(fs_sales);

//Read a record; see below for Append examples:


strReadLine = sales.ReadLine();
:

Chapter 12 - Reading and W riting ASCII Files Page: 54


Appending on Write:

The StreamWriter class has an overload allowing new data to be appended to existing
files. If the file does not exist, it is allocated as new.

Program 12.7: WriteFile - Revisted with an append, completed


private void A120_WriteFile()
{
//Write Hello World ten times

string stroutputFileName = "C:\\data\\test2.txt";

try
{
StreamWriter myOutFile =
new StreamWriter(stroutputFileName, true); //<-Change

myOutFile.WriteLine ("This file shows Hello World ten times");

for (int j =1; j <= 10; ++j)


{
myOutFile.WriteLine(Convert.ToString(j) + " Hello World");
}

myOutFile.Close();
myOutFile.Dispose();
label1.Text = "File Written";
}

catch (Exception e)
{
label1.Text = "File Not Written";
MessageBox.Show("Something horrible happened: \r\n" +
e.Message);
}
)

With program 12.7 (repeated above), backspace over the StreamWriter's parameter list
and begin re-typing the command. As you type the opening parenthesis,
"SteamWriter(...", the overload list displays. Overload 4 shows "bool append".

"bool append" means a boolean true/false goes after the string-filename. Literally type
either "true" or "false"; do not include the word "append", as this is just a hint on the
nature of the switch.

Chapter 12 - Reading and W riting ASCII Files Page: 55


Testing an ASCII File Append:

Modify Program 12.7 (A120_WriteFile), adding an boolean true for an append-switch.


Next, make these changes:

a. Modify the text " Hello World"; changing it to "Greetings Earthling"

b. Re-Run the program by pressing F5.


Click BtnWrite one time.

c. From Windows, Start, Run Notepad.exe.


File, Open "C:\data\test2.txt"

Results: Ten new "Greetings" lines should be appended to the end of the file.

d. Close Notepad and re-click btnWrite several more times. Re-open the notepad
document; the same text has been added multiple times.

Preventing Append from Running Twice:

If the user pounds a dozen times on the "Write" button, they may get more than
bargained for. This type of problem is fixed by disabling the button after the first click.
Consider this change in the calling routine:

Example: Logic to Disable btnWrite button After Append


private void BtnWrite_Click (object sender, EventArgs e)
{
//Logic to prevent the append from happening more than once.
//This routine disables btnWrite

A120_WriteFile();
BtnWrite.Enabled = false;
}

Comments on Restricting Appends:

The logic disabling the button was added to the calling "click" event. It could have been
added in the called A120 routine but doing so restricts the usefulness of the Write
module. A120_WriteFiles should do one thing – write the file. If it does other things,
such as enable and disable buttons, it can only work for this event. If another routine
needs to write this same file, it would inadvertently lock a button it has no business
messing with.

Admittedly, this is a do-nothing routine, but imagine if the module did something like
"Write Customer File"; the same module has the potential to be called from multiple
locations in the program, from buttons, to menus, to extract routines. Do not shackle the
logic with a BtnWrite.Enabled statement.

Chapter 12 - Reading and W riting ASCII Files Page: 56


Multiple ASCII Input Files

A program may have need to open and read multiple, different ASCII files. The logic to
do this is no different than the examples above; all that is needed are more statements.
There are no real restrictions on how many can be opened at any one time. Consider this
example code:

private void A110_ReadFile()


{
string strFileName1 = "C:\\data\\test1.txt";
string strFileName2 = "C:\\data\\test2.txt";
string strOutputFileName = "C:\\data\\outputFile.txt";

try
{
StreamReader sr1 = new StreamReader (strFileName1);
StreamReader sr2 = new StreamReader (strFileName2);
StreamWriter sw = new StreamWriter (strOutputFile.txt);

// <Do stuff here that deals with the files>

sr1.Close();
sr2.Close();
sw.Close();

sr1.Dispose();
sr2.Dispose();
sw.Dispose();
}
catch (Exception e)
{
MessageBox.Show("A110: problem opening one of the files: " +
"\r\n" + e.Message);
return;
}
}

comments:

• Each file needs its own Stream name (stream reader prefix) sr1, sr2, etc. The style
"sr1" is reminiscent of Visual Basic, where each file was given a number (e.g. #1,
#2). For any reasonably complicated program, these names should be more
descriptive: empl for Employee File; pm for PayMaster, etc. Most programmers tend
to keep the labels relatively short.

• The example is sloppy, having a single try/catch for all three files. Each file could
(should) have its own try/catch because each can have differing opening errors.

Chapter 12 - Reading and W riting ASCII Files Page: 57


ASCII File Read and Write Exercises

A. Skipping Records:

Read an ASCII text file and put each line into textBox1.
Discard all lines that begin with any of these characters:

'(tic),
;(semicolon),
//(slash-slash), and
blank lines.

Discardable lines can be in the first column of the file or can be indented with spaces, as
in:

This line would be accepted


//This line would be discarded
;Discard this line
'And this line
;As well as this one
//and me too
But you would accept this line.
And this one, but not purely blank lines.

Create your test file using Notepad.

B. Skipping Records; Writing two output files:

Using Exercise A, write all retained data lines to a new file, "C:\data\retain.txt" and write
all discarded lines to file "C:\data\discard.txt".

On the main form, display counts showing how many records were written to each file.

C. Truncating inline comments:

Expand Exercise A so it truncates inline-comments.


In the sample data below, do not display the "//This is a comment" or ";this comment"

This line would be accepted.


This line would also be accepted but not the stuff out here //This is a comment
This line is acceptable but not ;this comment

D. Study, then attach the following code to button 1.


Create a suitable ASCII text file (C:\data\Test.txt) with embedded blank lines as well as
normal data.

Chapter 12 - Reading and W riting ASCII Files Page: 58


The intention of the program is to display all non-blank lines in textBox1.
This routine assumes you have cl800_Util.cs linked into your test program.

Run the program.


Explain what happens.
Repair the program.

Hint: Consider adding a break-point (red-ball) and step through the program using "F11".

Exercise 12.5: Fix the Flaw in this Program


private void A110_ReadTextFile();
{
string strReadLine; //variable for current record
string strInputFileName = "C:\\Data\\test.txt"
int iRecordCount = 0;
textBox1.Clear();

try //file-open-try
{
//Open the file
StreamReader myInputFile = new StreamReader(strInputFileName)

//Read the first record from the file (Priming):


strReadLine = myInputFile.ReadLine();

while (strReadLine != null)


{
iRecordCount++;

if (util.IsBlank(strReadLine))
continue;

//Read the next line as the last step in the loop:


strReadLine = myInputFile.ReadLine();
}

myInputFile.Close();
myInputFile.Dispose();
}
catch (Exception e)
{
MessageBox.Show("Error: " + e.Message);
}

E. Simulating a File-Lock:

In a networked environment files can be locked by other users or by your own program
when it fails to close a data-file. Simulate this on your workstation by using the
debugger to "hang" your program (this keeps the program from properly closing the file);
then, using Notepad, then Excel, attempt to open and save the Opened file.

Chapter 12 - Reading and W riting ASCII Files Page: 59


The try/catch logic in Programs 12.5, 12.6 and the repaired Exercise D, can all be used
for this simulation. The example assumes your program is reading C:\Data\Test.txt.

Do the following to simulate the file-lock. The lock will be "accidentally" set by your
test program.

a. Place a breakpoint on the sr.Close statement by clicking your mouse in the gray
margin (red-ball). This will suspend your program just prior to the "Close"
statement.

b. Run the example program.


Click btnProcess to begin the loop.
This program runs, up to the Close statement, the pauses at the Close statement.

c. While in the suspended state, click the Windows Start button and launch Notepad:
• Click Start, Run, "Notepad.exe"
• Select File, Open "C:\Data\Test.txt"

Notepad is not the sharpest knife in the drawer and it won't complain about the file
being "in use" until you make a change and try to save the file. With the file "in
use", it offers a "Save-As" instead.

d. Cancel the changes and close Notepad without saving.

e. Launch Microsoft Excel.

Select File, Open and open the same file (you may need to change the file-type from
".xls" to "all files")

Excel reports the file is in use as you try to load it.


Close and exit Excel.

The moral: if you open a file, close it. Close the file as soon as possible and that is
usually when the loop ends. Consider what would happen if the user launched your
program, left it running, then went to lunch.

Also, if the program ends, either when the user clicks "Close", "X", or abnormally,
opened files are automatically closed but don't rely on this. First, files on a network
drive may remain unexpectedly locked if the connection to the PC is dropped. It may
take a lengthy timeout (or worse, a server reboot) to recover the file lock.

F: Optional Appends:

Modify the append example (See previous section, Appending ASCII Text Files). When
"btnWrite" is clicked, write "Hello World" 10 times.

Chapter 12 - Reading and W riting ASCII Files Page: 60


If the Append Output box is checked, append the strings; if unchecked, overwrite (do not
append).

Chapter 12 - Reading and W riting ASCII Files Page: 61


Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 62
Appendixes
Appendix A - Compiler Error Messages

Alphabetic Listing

This is an alphabetic listing of various compiler messages with likely solutions. These are
from Visual Studio 2005, SP1 through VS 2014.

Errors and warnings are sorted alphabetically. Search by the first non <variable> word.
e.g. "Argument '2': cannot convert from 'double' to 'float' will be found under "cannot..."
Messages such as "The type arguments..." will be under "The"; Messages that begin with
punctuation ("; expected") are listed first.

Additional Comments on Error Messages:


If you have repaired a mis-typed line in your program but the same error message
continues to show, try the following from the editor window: Select menu choice: "Build,
Rebuild Solution".

"; expected" (semi-colon expected)


Also: Invalid Expression Term "."

Symptoms:
The compiler normally shows exactly where a semi-colon is expected and when you get this
error it is normally flagged at the very end of a line. If the compiler shows it in the middle
of a line, it can get confusing.

Problem:
This incorrectly typed command would show an expected missing semi-colon at the
Convert.ToString phrase.:
MessageBox.Show Convert.ToString(loopCounter); //missing paren

Solution:
In this MessageBox example, note that the MessageBox.Show phrase was incorrectly typed;
it is missing a set of parenthesis. This confuses the compiler like something awful. The
correct syntax is:

MessageBox.Show (Convert.ToString(loopCounter));

Problem
In this incorrectly typed command, the word "if" is 'misspelled' with a capital "I" instead of
a lower-cased "if":
If (IsBlank(testString)) //Capital "If" is wrong

A110: Database problems, various

See "A Network-related or instance-specific error occurred while establishing a connection


to SQL Server"

A constant value is expected

Common Error Messages and Solutions Appendix A: 3


Possible Solution:
In a 'switch' statement, a 'case' is using a variable instead of a hard-coded literal or a
constant. Replace the case <variableName> with case "quoted-string" or number.

A list that is this enumerator is bound to has been modified. An Enumerator can only be used if the list
does not change. (Sic)

Symptoms:
Attempting to delete an item from an array, comboBox, listBox, etc, while in the middle of
a foreach loop.

Issue:
You cannot delete an array-item while in the midst of a foreach loop.
Mark the item's position (counter) – typically in another temporary array and use a separate
loop to remove them, after the first loop completes.

A local variable named 'e' cannot be declared in this scope because it would give a different meaning to
'e', which is already used in a 'parent or current' scope...

Symptoms:
The top of the module, typically button1_Click, already has an 'e', as in "EventArgs e" and
you probably have a try-catch that also uses "(Exception e)"

Recommendations:
See button1_Click's signature line and compare it with the catch statement's signature lines

Change the "(Exception e)" to "(Exception e2)" with corresponding changes to e2.Message.
Or consider moving (most) of the logic from button1_Click to its own routine: e.g.
A100_Process(); which won't have an 'e' in its declaration.

A Namespace does not directly contain members such as fields or methods.

Possible Solution:
Do not define variables or methods (functions) above the form level; form-class level.
If you are trying to make a "global" variable, see Chapter 7.

A Network-related or instance-specific error occurred while establishing a connection to SQL Server

Symptoms:
While opening a form that uses SQL server resources.

Solution:
Confirm that the SQL Server is running and you have rights to the database.
If the SQL Server is running locally, on the LocalHost, confirm the Microsoft SQL Server
Services are started. From Windows, Start-Run, "Services.msc"; Confirm SQL Server
(SQLExpress) is started

An Error has occurred while establishing a connection to the server.


When connecting to SQL Server 2005/2008, ... (this failure may indicate) ...

Common Error Messages and Solutions Appendix A: 4


SQL Sever does not allow remote connections.
Could not open a connection to SQL Server.

See SQL: An Error has occurred while establishing a connection to the server....

An object of a type convertible to 'string' is required.

Issue:
"return" is not returning the correct 'type'.

Solution:
Examine the method's signature line to see if it returns a string, integer or other type of
object. The corresponding return statement(s) within the module must also return that same
'type'.

For example, a string function needs to return a <string> value.


It cannot return (nothing)

example code:
private string myFunction()
{
if (util.IsBlank(mystring))
return mystring;

Meanwhile, a void function can only return (nothing):

private void myFunction2()


{
stuff
return;

An object reference is required for the nonstatic field....

This is a generic error that generally means the compiler cannot find the variable or an
associated class was not instantiated.

Possible Solution:
If the variable or method in question is in a different Class, do one of the following:
a) Declare the variable as "public" or "internal" and instantiate the class within your
Form/Class using the "new" keyword. See Chapter 6, External Class Libraries, for
details.

b) Declare the variable as "public static" <string> or

c) If the method is in error, consider declaring the method as "public static...." or better
yet, "internal static" as in

internal static string B000_INILoad.B021_DiscoverINIFileName();


//returns string

Possible Solution:

Common Error Messages and Solutions Appendix A: 5


If calling a class.method within another 'sub-Class' (see Chapter 6; where cl800_Util was
placed within the PayrollTools.cs Class), there may not be a "constructor" in the new
(payroll) class and because of this, the (cl800_Util) declaration may not have a place to run.

Move the declaration into another method, directly above the Instantiation.
In simpler terms, move "cl800_Util util;" just above the line "util = new cl800_Util();"

Possible Solution:
If you have just switched a variable from a local variable to a "public static" variable, re-
compile the program using menu Build, Rebuild Solution.

Possible Solution:
Misspelled or wrong case variable name.

Possible Solution:
Especially when using a (Form's) properties. Do not use the current Form's name (it was not
instantiated within itself); instead, use "this."

ProgramGlobal.IformLeftPos = frmA000Form.Left;
ProgramGlobal.IformLeftPos = this.Left;

Note: You could also simply use "... = Left;", which is considered too vague for
most people even though the code would work.

ArgumentoutOfRangeException was unhandled

Argument out of range exception (s) are always due to an array being unalloacted, un-
available or a value [x] within square-brackets was using a larger number than the size of
the array. This always indicates a logic or counting problem and often the problem happens
at the end of a loop, where you over-shoot by one position. Remember, arrays are base-0; a
ten-item array's last position is [9].

See also "Index out of range"

If manipulating SQL data, statements, such as:


dataGridView1.Columsn[0].xxxxx = "yyyy"
may indicate the SQL Server service has not started on your development machine or the
remote SQL server is not available.

In any case, array arithmetic should be protected with a try-catch (if using a for-next loop or
are addressing [addresses] directly. Consider using a for-each loop, if logic is appropriate.

Argument '1': cannot convert from 'object' to 'string'


Also: The best overload method match for '<form(parameter)>' has some invalid arguments.

Issue:
The parameter you are trying to send is something other than a <string>; often the results
are an object-type or a "collection".

example:
frmA031CategoryAdd addCat = new frmA031CategoryAdd
(dataGridView1.SelectedRows[0].Cells[0].Value);

Common Error Messages and Solutions Appendix A: 6


...SelectedRows[0].Cells[0].Value is not necessarily a string. You can test this
by adding a .ToString() method and placing a debugging breakpoint at the statement. While
debugging, hover the mouse before the ".ToString()" method to see the value is missing
quotes – indicating it is not a string.

Possible Solution:
Convert it to a string using one of these two techniques:
... (dataGridView1.SelectedRows[0].Cells[0].Value.ToString());
... ("" + dataGridView1.SelectedRows[0].Cells[0].Value);

Argument '1' must be passed with the 'ref' keyword


Also: The best overloaded method match for '<class>(ref string, string)' has some invalid
arguments.

Solution:
When using "By Reference" (ref), both the calling and the called functions need the 'ref'
keyword. C# requires this for documentation purposes.

Example:
appendDefaultAreaCode (ref myPhoneNumber, locationDefaultAreaCode)

private void appendDefaultAreaCode


(ref myPhoneNumber, locationDefaultAreaCode)
{

Array creation must have array size or array initializer

Issue:
Sometimes you can declare an open-ended array with a simple statement, such as:
string [] afoundFields;
but if the array is used inside of a loop (while-statement), C# often requires that the array be
initialized with a starting value or by declaring a fixed array size. This is incase the while
statement never runs and downstream commands may panic.

Solution:
Initialize the array with an item count. Consider over-allocating.
string [] aFoundFields = new string [100];

Array does not have that many dimensions

Possible Solution:
Assuming a single-dimension array (a linear array),
aArrayName.GetUpperBound(0);

where (integer 0) is the first dimension of the array, "aArrayName".

Presumably you used aArrayName[x,x], when the array only had one dimension,
aArrayName[x].

'<btnClose>' is a 'field' but is used like a 'method'

Common Error Messages and Solutions Appendix A: 7


<btnClose> is a field but is used like a method

Summary:
Typed as btnClose()
Should be typed as an Event: btnClose_Click(null, null);

See also: "is a field but is used...."

Build Failed - with no compiler error messages

Likely solutions - Do all:


Close Visual Studio
Using Windows Explorer, open the Project's folder; delete all "*.suo" files
Re-Launch VS; select top-menu View, Output Window
Rebuild solution.

If still an error, look in the output Window. (See top-menu, View, Output)

Related: See "The type or namespace name 'Tasks'....

Cannot access a closed registry key

Symptoms:
Usually while performing a .GetValue(stringName)

Solution:
Move the RegKey.Close command below the GetValue statements. If the GetValues are in
a loop, be sure the Close is after the loop.

Cannot Assign to '<string name>' because it is a 'foreach iteration variable'

Symptoms:
Attempting to manipulate an array-element from within a foreach loop.

Issue:
Within a foreach loop, you cannot modify or change the values used by the foreach loop.
More to the point, you cannot transform or change the array's internal elements with a
foreach loop.

Solutions:
If you are merely trying to change the value of the array's element, move the value to a
secondary (intermediate) temp-string. Consider this example, with particular attention on
strtempString:

foreach (string strtempString in aTestArray)


{
//Note: strtempString cannot be manipulated directly
//within the loop

//Use an intermediate value to manipulate


string tstring = strtempString;

Common Error Messages and Solutions Appendix A: 8


tstring = tstring.ToUpper();
}

If your intent is to actually change the value(s) of the items in the array, you cannot use a
foreach loop. Instead, use a for-next loop.

for (int ii = 0; ii <= aTestArray.Length - 1; ii++)


{
//This actually modifies the values in the array...
aTestArray[ii] = aTestArray[ii].ToUpper();
}

Note the loop runs to the Array's length, minus-1 – a base-0 calculation
See the Array Chapter, "Transforming Array Elements" for more details.

Cannot connect to <Server> (SQL Server Management Studio)

Symptoms:
When attempting to launch SQL Server Management Studio

Possible Solution:
Are the services (Start, Run, Services.msc) "SQL Server" started?

<argument>: cannot convert from 'double' to 'float'

Solution:
A numeric parameter must be specified as a floating point number "F"
e.g.
Pen myPen = new Pen (Color.Black, 0.3) should be
Pen myPen = new Pen (Color.Black, 0.3F)

Cannot convert method group '<various: GetLength, etc>' to non-delegate type 'int'. Did you intend to
invoke this method?

Possible solution:
ilastHighlighted = myFiles.GetLength();

Did you remember the closing parenthesis?

Cannot Convert method group '<name>' to non-delegate type 'bool'. Did you intent to invoke this
method?

Possible Solution:
if using an implied comparison in an if-statement:

if (A100_SomeMethod_ThatReturns_Bool)
{
//Incorrect, missing ()
}

you forgot the parenthesis after the method name. Instead:

if (A100_SomeMethod_ThatReturns_Bool() )

Common Error Messages and Solutions Appendix A: 9


{
//corrected with ()
}

if (A100_SomeMethod_ThatReturns_Bool() == true)
{
//optional
}

Cannot currently modify this text in the editor. It is read-only

Solution:
Close the running program before attempting to modify either the code or the design-view.
You cannot edit while the program is running.

Either close the running VS program (your program) or in the Visual Studio Editor (ISE),
click ribbon-bar "Red Square" icon to abruptly close your program.

Cannot convert null to 'System.DateTime' because it is a non-nullable value type

Issue:
A DateTime method is attempting to return a null value to the calling module when only
"DateTimes" are allowed. This often happens in a try-catch error condition.

Solution:

Change the signature line from


private DateTime A100_MethodName()

to a "nullable" DateTime, where the <brackets> are required.

private Nullable <DateTime> A100_MethodName()


{
try
{
//Do stuff here
}
catch
{
return null;
}
}

Test in the calling routine using


if (returnedVariable.HasValue)

where the HasValue method only operates on items with a nullable data-type. See below for
more information on this.

Optionally, in the case of this examle's DateTime value, you could also use this command,
bypassing the Nullable solution: return DateTime.MinValue;

Solution:

Common Error Messages and Solutions Appendix A: 10


Change the original DateTime value to a "nullable" variable by using a question-mark in the
declaration.

:
DateTime? dtValue = (some date/time or null if not available);

With this, the downsteam function can return a null, if it has the need to do so.

:
if (dtValue.HasValue)
return dtValue.Value;
else
return null;

<procedureName> cannot declare instance members in a static class

Symptoms:
Usually when building a new method or function near the "static class program" / "static
void Main" class – the main driving procedure for your program. You have tried to use a
"private void <functionName>" within a "static" class.

Solution:
Consider changing
private void <functionName> to
private static void <functionName>

Cannot implicitly convert type 'int' to 'string'

Symptoms:
Code is trying to display a text message, a MessageBox, assign a text label, or assign a text
field with both text (string) data and numeric data. The numeric data refuses to cooperate.

Solution:
Use Convert.ToString on any numeric fields (or other non-string data-types) before moving
them or concatenating them to another string [field].

MessageBox.Show ("variable 'i' is set to this value: " + Convet.ToString(I));


textBox1.Text = "The number is equal to " + Convert.ToString(valueA));

also: <variableName>.ToString();

Cannot implicitly convert type 'long' to 'int' (are you missing a cast?)

Possible Solution:
Examine the return values of the command you are using. It likely is returning a 'long'
value, not an integer. The error will be flagged deep within the code, but it is the function's
(method's) signature line where you may need to make the fix.

For example:
private int A630_ReturnFileLength (string strpassedFileName)

Common Error Messages and Solutions Appendix A: 11


and: return fi.Length; <error complains here

but the fix may be changing the "int" to "long" on the Signature line.
Change "private int ..." to "private long ..."

CS0029
Cannot implicitly convert type 'string' to 'System.Windows.Forms.Label'

Solution:
Be sure to use a ".Text" when populating a label.
For example, assigning a blank string to a label:

Incorrect: lblmyField = "";


Correct: lblmyField.Text = "";

Cannot implicitly convert type 'System.Data.CommandType' to 'SystemLdata.Sqlclient.SqlCommand'

Issue:
Missing method name ".CommandType"

example code:
SqlCommand refCategoryCMD = new SqlCommand("RecordCategoryDelete");
refCategoryCMD = CommandType.StoredProcedure; //In error

Solution:
refCategoryCMD.CommandType = CommandType.StoredProcedure;

Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a
cast?)

Symptoms:
You are using a string array and attempting to assign a value to another text field.

For example:
lblDisplay.Text = aNames[1]; //fails
MessageBox.Show(aNames[1]); //fails

Solution:
Convert to String prior to assigning. This can be done explicitly or implicitly:

lblDisplay.Text = aNames[1].ToString();
lblDisplay.Text = (string)aNames[1];
MessageBox.Show("" + aNames[1]);

Common Error Messages and Solutions Appendix A: 12


CS0029
Cannot implicitly convert type 'string' to 'bool'
Cannot implicitly convert type 'int' to 'bool'

Symptoms:
In an "if" or other conditional.

Likely solution:
Did you use a required double-equal in the conditional?
if (testString = "Smith") vs
if (testString == "Smith")

Cannot implicitly convert type 'string[*,*]' to 'string[]'

Likely explanation: A multi-dimensioned (dynamically-sized) string array was declared in a


higher scope using and later dimensioned with actual sizes:

string [,] aCollectionNames; //Declared at a higher scope

and then later, in a different method, initialize with a fixed size, as in:

aCollectionNames = new string [15,4]; //Dimensioned

The author had this error after several mistyped array definitions. But once the array was
declared, as described above, the error persisted. Finally, after selecting menu "Build,
Rebuild Solution"; the problem went away.

Cannot implicitly convert type 'string' to 'System.Windows.Forms.TextBox'

Likely Solution:
You neglected the ".Text" appendage.

For example:
Incorrect:
textBox1 = textBox1 + Convert.ToString(<variable>);

Correct:
textBox1.Text = textBox1.Text +
Convert.ToString(<variable>);

For example:
MessageBox.Show("'" + pnlCategoryCode + "'");
vs
MessageBox.Show("'" + pnlCategoryCode.Text + "'");

Cannot implicitly convert type 'System.DateTime?' to 'System.DateTime'. An explicit conversion exists


(are you missing a cast?)

Issue: You are using a Nullable <DateTime> and since a Null is allowed, you must re-
convert to the same type. This seems redundant in code because the called function may
already be returning a Date Time. Re-cast the returned value:

Common Error Messages and Solutions Appendix A: 13


Example Syntax:

DateTime dtfileDate;
dtfileDate = (DateTime)A700_ReturnFileCreateDate(textBox1.Text);

See also "Cannot convert null to 'System.DateTime' because it is a non-nullable value


type" and consider a "nullable" declaration or cast (e.g. DateTime? dtValue;)

See Chapter 24 for further discussions.

Cannot Insert an explicit value into a timestamp column (SQL)

Issue:
SQL data field was defined as 'Timestamp' but C# code is trying to insert a Date. Change
the SQL field definition to a date-time or date format.

Cannot use local variable '<variable>' before it is declared

Likely Solution:
Declare (and possibly initialize) the variable before using:
string myString = "";
if (myString = "House")

Also, you can see this message if an if-statement, either above or below the first error has a
mis-spelled "Else" (vs "else") or missing parenthesis. This may take a while to locate in
large modules.

Possible (and likely) Solution:


In the function or method, is the last "return" statement mis-spelled, as in capital-R-Return?
or is the "return" clause otherwise mal-formed.

Changes are not allowed while code is running...

Solution:
Your program is still running from your last compile (F5 / Run). Locate the program on the
task bar and close before attempting to run it again. Alternately, from the Editor, press
Shift-F5 to force-close the program.

Command Line Arguments not parsed; Command Line Arguments ignored

By default, Visual Studio will not allow passed command line arguments, even
though the Start Options are set in the Project's properties.

Symptoms:
The program will behave as if no command-line arguments were passed, especially
if you compile a Release version of the program. Make this additional change in
the program:

Common Error Messages and Solutions Appendix A: 14


In Project Properties, Security, click [x] Enable ClickOnce Security Settings.
Then click "This is a partial trust application".
Click the Advanced button
Unclick "Debug this application with the selected permission set"
Click OK
Click "This is a full trust application"

Alternately: In Project Properties, left-nav, click Security.


Uncheck "Enable ClickOnce Security Settings".

Control cannot fall through from one case label ('case "<label>":') to another

Solution:
In a 'switch' statement, a "case" statement is missing a break; command, as in

case "Green":
<do stuff here>
break;
case "Red":
<do other stuff here>
break;

<formanme> does not contain a constructor that takes 1 arguments

Solution:
The call, typically on btnFormName_Click, instantiates a new form, as in:
frmA031CategoryMaint catMaint = new frmA031CategoryMaint("");
catMaint.InstanceRef = this;
catMaint.ShowDialog();

where it is passing one parameter, in this case, null, typed as ("").

But in frmA031's constructor, at

public frmA031CategoryMaint()
{

(this example) does not show any parameters.

The method's signature line must match the calling statement's (values). The two must
match the same count of parameters.

<DataGridView> does not contain a definition for Cells and no extension method Cells accepting a first
argument....

See below: "does not contain a definition for 'Cells'....

CS1061

Common Error Messages and Solutions Appendix A: 15


'<Classname>' does not contain a definition for "<MethodName>' and no extension method
'<MethodName>' accepting a first argument of type '<ClassName>' could be found
(are you missing a using directive or assembly reference)

For example: 'MainProgram' does not contain a definition for "A000_Base' and no extnsion
method 'A000_Base' accepting a first argument of type 'MainProgram' could be found (are
you missing a using directive or assembly reference)

Likely solution:
In another class (e.g. MainProgram.cs), you have not yet created or have misspelled a
method called "A000_Base".

<formname> does not contain a definition for <event such as 'checkBox1_CheckChanged'>


<formname> does not contain a definition for <'textBox1_TextChanged'>

Symptoms:
This is an Event problem where the original Event's code was either deleted or renamed in
Code View, but the pointer to the event was not changed in the Event Properties.

Solution(s):
There are two ways to correct this error. Either is acceptable.

1. Double-click the error and the editor will take you to the [Form1.Designer.cs] class;
and as scary as this may look, delete the entire highlighted line.

2. Or, open the <event> properties (Lightning Bolt) for the control in question and delete
the event information from the property screen. For example, if this were a
textBox1_TextChanged event, delete the detail-text after the (lightning-bolt) event.
Doing so still leaves the "textChanged" code, orphaned, in the program. It should be
deleted by hand.

Common Error Messages and Solutions Appendix A: 16


<System.Windows.Forms.DataGridView> does not contain a definition for Cells and no extension
method Cells accepting a first argument....

Solution:
In the foreach clause, did you use "DataGridViewRows" (and not just "DataGridView")?

foreach (DataGridView currentRow in dataGridView1.SelectedRows)


MessageBox.Show("Selected: " + currentRow.Cells[0].Value;

Entering Break Mode failed for the following reasons: Source file <server-drive....form.cs> does not
belong to the product being debugged.

Cause:
A previous project was moved from a server-drive to a local disk.
Reference paths still point to the (old) server location.

Solution:
With Visual Studio 2005 or above, select menu Build, Clean Solution followed by Build,
Rebuild Solution.

With Visual Studio Express, these menu choices may not be present. Do the following:
a. Close the Visual Studio Project
b. Using Windows Explorer, locate the solution; delete the "bin" and "obj" sub-
directories. Re-open the Solution and the problem should be fixed.

error CS0234: The type or namespace name 'Tasks' does not exist in the namespace
'System.Threading'

Error visible in the View, Output pane.


See error message "The Type or namespace name 'Tasks'

ExecuteNonQuery: Connection Property has not been initialized.

When using a Stored Procedure and attempting a SAVE or INSERT (ADD) operation.
Missing a connection clause with the SqlCommand. For example:

Incorrect:
SqlCommand refCategoryCMD =
new SqlCommand("RecordCategoryUpdate");

Corrected:
SqlCommand refCategoryCMD =
new SqlCommand("RecordCategoryUpdate", refCategoryConn);

where "refCategoryConn" was the connection defined earlier in the routine, as in:
string strConnection = "Data Source = <servername\\SQLExpress;" +
"Initial Catalog = <database name>;" +
"User ID=<sa>; Password = <password>";
refCategoryConn = new SqlConnection(strConnection);

Common Error Messages and Solutions Appendix A: 17


<method name> hides inherited member 'SystemWindows.Forms.<object>' Use the new keyword if
hiding was intended.

example message: Form1.left(string, char)' hides inherited member


'system.windows.forms.control.left'.

Cause: The name of your function/procedure/method is the same as a built-in name. e.g., if
you built a function called "Left". This is a new warning, starting with Visual Studio 2010.

Solution:
This error can be ignored. But consider renaming your function. For example, instead of
"Left", use "LeftStr". In general, single-word functions, such as Left, Mid, Right should not
be used.

Field '<name>' is never assigned to, and will always have its default value null (warning)

Possible Solution:
A variable was declared but was never set equal to anything. The 'variable' does not need to
be a normal variable, it could be a class name. Consider this example when declaring an
external class library with the "new" statement either commented or not typed in the proper
location:

clSiteGlobals SiteGlobals;
//SiteGlobals = new clSiteGlobals();

IDE1006 Naming rule violation: These words must begin with upper case characters: <button1_Click>

This is an informational message. Rename the procedure or method, shifting the first
character to upper-case. This is to follow recommended naming standards for cross-
platform programs.

Identifier Expected

Possible Solution:
When declaring a function, are all the parameters in the parameter list prefixed with a data-
type? Missing "string", "int", etc.

Incorrect: static bool IsNumeric(passedString)


Correct: static bool IsNumeric(string passedString)

'<btnClose>' is a 'field' but is used like a 'method'


<btnClose> is a field but is used like a method

Likely Solution:
You are calling a button-event from another location but forgot or mis-typed the event-
name.
btnClose ("", null); //Incorrect – not just the btn name
btnClose_Click ("", null); //Corrected: _Click was missing

Index Out of Range (DataGridView)

Symptoms:

Common Error Messages and Solutions Appendix A: 18


Commands that use a column index position, such as

dataGridView1.Columns[0].HeaderText = "SEQ";
dataGridView1.Columns[1].Width = 55;

must be written after the statement that populates the actual grid. See Chapter 27 for
examples.
GetMyData(strSelectString)

Confirm the SQL Server (SQLExpress) services are running (Services.msc) or the remote
server is available.

See also: ArgumentoutOfRangeException was unhandled

Index was outside the bounds of the array

Symptoms:
A generated error, usually from a try-catch, where array operation attempted to access a
point not in the array – usually one position beyond the end of the array [max n + 1].

If you are not looping through the array and are directly accessing the array (e.g. variable
[n]), then likely the array was not populated with data; especially with a previous .Split
command.

Possible Diagnostics:
If using a foreach loop, place a debug break point at the top of the loop and monitor the
loop. If you suspect the error is (1000 records) into the loop, add this diagnostic logic to the
program and break within the if-statement:

foreach ....
{
if (recordCount > 999)
MessageBox.Show
("Reached suspected error; put break point here");
// <regular processing here>>
}

If using a ".Split" and a subsequent command accesses a variable-field [n] directly, likely
the split found an empty record and had nothing to split into the array. After the split, check
for blank records before executing the (parsing) logic within the loop.

Symptoms:
If you are processing a CommandLine (arguments list), what happens when no command-
line arguments are passed? If you reference aargs[1], the program would abend. Consider
this statement:

if (aargs.Length >= 2 && aargs[1].ToUpper() == "/DIAG")

where the double-ampersand is absolutely required in the test.

Index was outside the bounds of the Array (SQL)


Also: Index was out of Range

Symptoms:

Common Error Messages and Solutions Appendix A: 19


After executing a SQL statement, such as a ExecuteReader.

Possible Solutions:
Confirm the SQL Service is running (Start, Run, Services.msc; look for MSSQL/SQL
Server).

Confirm you have the proper "strConnection" (Data Source=<Franken8>)

Confirm the SQL SELECT statement (an assembled string) includes the field-name you
need and it is punctuated with appropriate commas and spaces, especially within the
assembled string.

Are you trying to reference a [column] position before a DataGridView was populated?

Invalid Expression Term ',' (plus "; expected") when using a picture clause

Likely symptoms:
You are using a picture clause (with a String.Format).

Solution:
Did you forget the words "String.Format ("?

Invalid Expression Term '{' (when using a picture clause)

Solution:
String.Format requires a string, even if a single numeric variable is being formatted.
Encompass the phrase with quotes:
textBox1.Text = String.Format ({0:dddd}, dtValue); //Incorrect
textBox1.Text = String.Format ("0:dddd}", dtValue); //Correct

Invalid Expression Term "."


See "; expected".

Invalid Expression term 'else' and ";expected"

Possible Solutions:
The "if" clause above the errored line requires braces for the "then" section. Sections with
more than one command require braces to group them.

if (valueA == valueB)
{
<stuff>
<more stuff>
}

Possible Solutions:
Does the if-statement-clause have an unneeded semicolon on the if-clause itself? Remove
the semicolon.

InvalidCastException was unhandled

Common Error Messages and Solutions Appendix A: 20


See Error: Unable to cast object of type 'System.Windows.Forms.TextBox' to type
'System.IConvertible'.

Invalid Column Name '<field>' (SQL Read)

Symptoms:
An Invalid Column Name <field name> during a SQL Read or SQL ExecuteReader and the
field name is obviously right, when examined in SQLServer Management Studio.

Possible Solution:
The assembled strSQLstmt (the SELECT statement) is mal-formed, usually a space is
missing in a quoted string, especially on the last field-name, just before the FROM clause.
Set a breakpoint at the ExecuteReader and examine the IntelliTrace. For example, in this
illustration, notice how the "FROM" is crammed next to the field "Comment":

Invalid token '{' in class, struct, or interface member declaration

This message generally means the compiler is confused about an opening or closing brace
or there is a mis-placed semi-colon that confuses where the compiler expects a brace.

Possible Solution:
You have a semicolon at the end of an if-statement; while-loop or for-next-loop or remove
an unnecessary semi-colon from the end of a statement:

Common Error Messages and Solutions Appendix A: 21


private void xxxxx (object sender, EventArgs e); (bad semicolon)
while (loop stuff); (bad semicolon)
if (condition); (bad semicolon)

Possible Solution:
There is a statement or group of statements typed after the module's closing brace. Make
sure all your code is above the closing brace (e.g. above button1_Click's closing brace).

Possible Solution:
Check to make sure that all opening braces have a closing brace and all braces are lined-up
properly. Especially near the end of the program/namespace.

Invalid token 'string' in class, struct, or interface member declaration

Likely solution:
In a statement, such as:

public string SomeMethodNameHere()

where "string" is flagged as an error.


Be sure the word "public" (private, etc.) is lower-case. Not "Public".

Login Failed for user <xxxx>. Reason: Server is in script upgrade mode. Only the administrator can
connect at this time. Error 18401.

Solution:

The SQL Server service just started and the engine is updating tables. Wait a few minutes
and try the SQL connection again.

<method> is inaccessible due to its protection level

Solution:
The method in the Class Library are "private".
Set to either:
"public" if the Class is instantiated, or if the class is in the same namespace as the calling
routine.

Set to "public static" if the Class is not instantiated and it is in another namespace.

MessageBox is a 'type' but is used like a 'variable'

Solution:
You forgot to use a dot-method with the command.
For example: MessageBox.Show (...)
where the .Show was missing

Must declare the scalar variable "@<variable name".

Common Error Messages and Solutions Appendix A: 22


Symptoms:
During a btnSave event, when using replaceable parameters

Solution:
A field was used in the UPDATE/INSERT statement but it was not defined with an
"AddWithValue" clause. For example:

refCategoryCMD.Parameters.AddWithValue
("@NonRequiredField",
util.StripSQLinjections(pnlNonRequiredField.Text));

Also check the SQLstmt in two places (once for INSERT and once for EDIT), making sure
the field-name is listed, and within the parenthesis of the field list:

strSQLstmt = "INSERT INTO refCategory " +


"(RecordCategoryCode, RecordCategoryDesc, DeleteInhibit, NonRequiredField) " +
"Values ( @CategoryCode, " +
"@CategoryDesc, " +
"@CheckBox, " +
"@NonRequiredField )";

strSQLstmt = "UPDATE refCategory SET " +


"RecordCategoryCode = @CategoryCode, " +
"RecordCategoryDesc = @CategoryDesc, " +
"DeleteInhibit = @CheckBox, " +
"NonRequiredField = @NonRequiredField " +
"WHERE (RecordCategorySeq = '" + strEditPassedRecordSeq + "')";

Newline in constant

Likely Solution:
You are appending a "\" backslash character in a string, probably to build a directory-path.
Backslash is a reserved character. Use double-backslashes to represent a single backslash.

serverName + "\" + directoryName //error


serverName + "\\" + directoryName //corrected

No overload for method '<method name>' takes 0 arguments

Likely solution:
Typically with a button or other on-screen event, such as a button, notice the signature line
of the button; there are two parameters. For example, btnSomething_Click(object
sender, EventArgs e). When calling an event like this, make your call in this fashion:

btnSomething_Click(null, null);
Passing a null value for each item in the signature line.

No overload for method '<method name>' takes '1' arguments

Solution:

Common Error Messages and Solutions Appendix A: 23


You are attempting to pass <1> parameter via the signature line to a function that either
needs more than one value or no values. In other words, the two signature lines need to
match, parameter-to-parameter. Double-click the error to locate the invalid call statement.
Then locate the routine it is calling; once found, look at its signature line (the items in
parenthesis; they must match).

Non-invocable member 'System.IO.FileInfo.Length' cannot be used like a method


Non-invocable member 'System.Windows.Forms.Control.Text' cannot be used like a method

Solution:
You are attempting to use a 'property' as-if it were a method. In other words, remove the
trailing parenthesis. For example:
fi.Length ( ); //is incorrect; use instead:
fi.Length;

or: pnlMsg.Text ("some text in quotes here"); //incorrect; use instead:


pnlMsg.Text = "some text here";

Object reference not set to an instance of an object


Use the "new" keyword to create an object instance.

This is a generic error that can be hard to resolve.

Solution:
Generally it means something is mis-spelled.

For example: RegKey.GetValue("ApplicationzzzName").ToString()


has a mis-spelled parameter.

Solution:
You are calling another method, in another library, without having first instantiating the
object. For example, when using the cl710_Formatting.cs library's "ProperNames"
function, you may need to declare the library either at the top (Class level) or within the
current function (e.g. button1_Click):

formatting = new cl710_Formatting();


then: formatting.ProperNames(<stuff>);

Solution:
You have declared a variable, such as a string, an array, a number, but have not initialized it
to a value; the variable still contains nulls.

For instance, a string declared but not initialized:


string myName;

if (myName.Length == 5) ... Generates this error

For instance:
string [] aMyArray;

with: aMyArray[1] = "Dog" will generate the error.

Common Error Messages and Solutions Appendix A: 24


In each case, initialize the variable with a non-null value before using it in any equation or
comparison. With an array, instantiate the value with the "new" keyword:

Only assignment, call, increment, decrement, and new object expressions can be used as a statement.

Symptom 1:
In a for-next statement you have mis-keyed one of the three required phrases. For example,
this statement has an error in the first phrase:
for(i; i <= 10; ++i)

Possible Solution:
you can't use a simple variable in the first part of the phrase; it must have an assignment
clause. The statement correctly typed is:
for(i=1; i <= 10; ++i)

Possible Solution:
A method, such as
sr.Close(); or
A180_ClearDateEntryFields();
was typed without opening and closing parenthesis.

Operator '&' cannot be applied to operands of type 'string' and 'string'

Solution:
Use the "+" symbol to concatenate strings. You used to be a Visual Basic programmer,
weren't you?

Operator '&&' cannot be applied to operands of type 'bool' and 'string'

Possible solution:
In a complex if-statement or while-loop clause, would an extra set of parenthesis help?

while (lineCount < linesPerPage &&


( strReadLine = myAsciiFile.ReadLine() ) != null)

Operator '==' cannot be applied to operands of type 'string' and 'method group'
Operator '==" cannot be applied to operands of type 'method group'
Operator '+' cannot be applied to operands of type 'string' and 'method group'

Likely Solution:
You forgot a "( )" after a function name.
"ToString" vs "ToString()" is commonly missed.

For example:
If using a method, such as .ToLower; as in ...textB.ToLower
did you remember the required parenthesis, as in: textB.ToLower()
if (textB.ToLower == "dog") //incorrect
if(textB.ToLower() == "dog") //correct

For example, also in a written method call:


if (A027_CheckPreviousCount == 15) //incorrect
if (A027_CheckPreviousCount() == 15) //correct

Common Error Messages and Solutions Appendix A: 25


Operator '>=' cannot be applied to operands of type 'string' and 'string'

Symptoms:
You are using a > or < conditional when comparing two strings, as in:
if (testString >= "Brown")

Solution:
You can't use >, < operators against two strings. This is different than (VB). Instead, see
string.Compare(string1, string2, T|F);
string.CompareOrdinal(string1, string2);

Operator "||" cannot be applied to operands of type 'int' and 'int'

Symptoms:
You are using an equal sign in an if-statement; you need double-equals for the comparison.
e.g.
if (passedPhoneNumber.Length = 7 || passedPhoneNumber.Length = 8)

should be:
if (passedPhoneNumber.Length == 7 || passedPhoneNumber.Length == 8)

CS0019
Operator '+' Cannot be applied to operands of type 'TextBox' and 'TextBox'
Operator '+' cannot be applied to operands of type 'System.Windows. Forms.TextBox'

Solution:
You forgot to include the object's (field) dot-property after the object's name.

Consider this code:


label1.Text = textBox1 + textBox2;
label1.Text = textBox1.Text + textBox2.Text;

CS0642
Possible mistaken empty statement

Symptom:
On an if-statement, while, or for-next statement

Likely Solution:
Although this is a warning, it is most likely a true error. Do you have a superfluous
semicolon after an if-clause, for-next, or other loop statement?
Remove the semicolon and let the next line (or the next set of braces) act as the end-of-line.

if (stringA == stringB); // <- Remove this semicolon


{

Warning: Possible unintended reference comparison; to get a value comparison, cast the left hand side
to type string

Common Error Messages and Solutions Appendix A: 26


Symptom:
When comparing a string array to a string:
if (alSomeArray[iposition] == "some fixed string")
The ~ warning appears after runtime, not during editing

Solution:
Do one of the following by casting explicitly or implicitly:

if (alSomeArray[iposition].ToString() == "some fixed string")


if ((string)alSomeArray[iposition] == "some fixed string")

Note: The error will only clear after run-time; it will not clear during the editing session
(VS2010).

Property of indexer '<class.variable>' cannot be assigned to – it is read only.


Property or indexer '<class variable>' cannot be assigned to - it is read only.

Solution:
If this is in an if-statement, did you remember to use double-equals (==)?

Solution:
In the "get/set" routines, typically in a Global External Class Library, there is not any logic
for the "set". If your intention is to make a read-only variable, either remove the logic in
your program that is trying to set the variable's value (e.g. myName = "Smith") or add an
empty-set routine, which ignores the myName = Value statement. Fixing the error is
preferable.

Property Value Not Valid (Dialog box)


Property Not Valid

Solution:
Your program is still running while trying to edit the source code. Close your running
program before changing the code or an object's property (e.g. Click the editor's ribbon
icon: "Red Square").

Send Error Report / Don't Send "Please tell Microsoft about this problem"

Symptom:
When you ctrl-alt-Break your program and your program may be in an infinite loop or
otherwise crashed. Microsoft sees this as a problem and offers to send a diagnostic error
report to Redmond. This message is annoying and should be disabled.

Solutions:
Click "Don't Send," then make this registry key change to your workstation.

Start/Run/Regedit
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PCHealth\ErrorReporting. Dword
Value: DoReport, 0 = Don't Send.

Common Error Messages and Solutions Appendix A: 27


SQL: An Error has occurred while establishing a connection to the server. When connecting to SQL
Server 2005/2008, ... (this failure may indicate) ... SQL Sever does not allow remote
connections. Could not open a connection to SQL Server.

Possible Solutions:
1) If you are connecting from a remote computer: In Microsoft's Surface Area
Configuration tool (SQL 2005), confirm that "Local and remote connections" is
selected. Choose TCP/IP

See: "C:\Program Files\Microsoft SQL Server\90\Shared\SqlSAC.exe"

2) If you are connecting from a remote computer: Consider starting the Windows Service:
SQL BROWSER

3) Does the SQL Express Server have a software Firewall installed? For example, if using
Windows XP SP2 with Windows Firewall, open the Firewall's control panel; click
Exceptions: Add this program: sqlserver.exe. Also add SQLbrowser (udp port 1434).

Other Solutions:

4) You are using the wrong server name in the connection string.

With SQL Server 2008:


string strConnection =
"Data Source = Franken8;" +
"Initial Catalog=Address;" +
"User ID=sa;Password=<yourpassword>";

With SQL Server 2005:


string strConnection =
"User ID=sa;Initial Catalog=Address;Data Source=FRANKEN8\\SQLEXPRESS";

or use ...Data Source=LOCALHOST\SQLEXPRESS If a local database

This can be especially true if you have moved your application from one computer to
another (when in development) and your Development SQL Server also moved. The
author had this problem when moving from a Desktop to a Laptop.

5) You are using the wrong Catalog (database name). e.g. from Chapter 16 "Address"

6) And of course, the wrong username and or password. If the password is encrypted; did
you decrypt it prior to executing the command?

SQL: Login Failed for user <xxxx>. Reason: Server is in script upgrade mode. Only the administrator
can connect at this time. Error 18401.

Solution:

The SQL Server service just started and the engine is updating tables. Wait a few minutes
and try the SQL connection again.

Common Error Messages and Solutions Appendix A: 28


Static member '<namespace.class.variablename>' cannot be accessed with an instance reference;
qualify it with a type name instead.

Solution:
Was the External Class Instantiated (with a "new" keyword)?
If so, remove the "static" modifier from the variable's declaration and use the "new"
variable's name as a variable prefix.

e.g. SiteGlobals = new clSiteGlobals ();


then: MessageBox.Show (SiteGlobals.CompanyName)

If the External Class was not Instantiated (using Quick and Dirty Global Variables), prefix
the variable name using the physical Class Name, as seen in Solution Explorer. Do not use
the "new" keyword.

For example:

Use:
MessageBox.Show(<namespace name.> clSiteGlobals.CompanyName);

'String' does not contain a definition for 'length' and no extension method 'length' accepting a first
argument of type 'string' could be found (are you missing a directive or an assembly
reference?)

Solution:
Capitalize the .Length property, as in:

switch (strfoundString.Length)
{
:
}

'System.Configuration.ConfigurationSettings.AppSettings' is obsolete: 'This method is obsolete, it has


been replaced by System.Configuration!
System.Configuration.ConfigurationManager.AppSettings (depricated)

Symptoms:
When attempting to use an app.config file and
MessageBox.Show (ConfigurationSettings.AppSettings ["<variable name>"]);
And yet, the statement still works properly, except for a compiler warning.

Solution:
In Solution Explorer, References, add a Reference to .NET, System.Configuration.dll
Then change the call statement to
MessageBox.Show (ConfigurationManager.AppSettings ["<variable name>"]);
See the App.Config chapter for full details.

'System.DateTime.Now' is a 'property' but is used like a 'method'

Solution:
Remove the parenthesis from the .Now. This is not Excel.

Common Error Messages and Solutions Appendix A: 29


= DateTime.Now; //Not DateTime.Now()

System.FormatException: 'Input string was not in the correct format.'

Likely solution:
You are attempting to Convert.ToInt32(textBox1.Text) and the textBox was empty or
contained non-numeric values, such as a hyphen or other character.

This is a run-time error that should have a try-catch clause.

System.InvalidCastException: 'Unable to cast object of type 'System.Windows.Forms.TextBox' to type


'System.IConvertible'.'

You will see this message if a textBox or other string field is blank and is trying to be
converted to a numeric value - Visual Studio 2012 and older.

You will also see this if Convert.ToInt32(textBox1.Text) - where the .Text property is
missing.

Note: This is a run-time error (an unhandled exception) and your program has crashed.
Provided this is not a syntax error, consider a try-catch block.

System.Windows.Forms.TextBox - various:

Issue: Likely missing ".Text" appendage.


See:
Cannot implicitly convert type 'string' to 'System.Windows.Forms.TextBox'

CS1061
'System.Windows.Forms.<field>' does not contain a definition for 'text'

Possible Solution:
Usually this means you mis-typed or more likely, mis-capitalized a field's property value.

Consider:
Field1.text (with a lower-cased .text) vs
Field1.Text

Other properties exhibit similar type messages when mis-spelled.

CS1061
'MainProgram' does not contain a definition for "A000_Base' and no extnsion method 'A000_Base' accepting
a first argument of type 'MainProgram' could be found (are you missing a using directive or
assembly reference)

Likely solution:
In another class (e.g. MainProgram.cs), you have not yet created or have misspelled a
method called "A000_Base".

'System.Windows.Forms.MessageBox' is a 'type' but is used like a 'variable'.

Common Error Messages and Solutions Appendix A: 30


Solution:
You forgot to use a method: MessageBox.Show (
e.g., you forgot the ".Show"

This is incorrect: MessageBox("Hello World");


Corrected: MessageBox.Show("Hello World");

The best overload method match for '<form(parameter)>' has some invalid arguments

See "Argument '1': cannot convert from 'object' to 'string'.

The best overloaded method match for 'string.PadRight(int,char)' has some invalid arguments

Solution:
When PadLeft or PadRight, the pad-fill is a character, not a string. Delimit a single
character with tic marks, not quotes.
e.g. strtestValue.PadRight(ipadLength, '*');

The best overload method for 'System.Windows.Forms MessageBox.Show(string)' has some invalid
arguments.

Possible Solution:
MessageBox.Show must have a string as the first item in the "show list". For example:

MessageBox.Show (comboBox1.SelectedItem); //errors; not really a string.


MessageBox.Show (Convert.ToString(comboBox1.SelectedItem)); //works

You can also trick the method by appending an empty-string before the first object being
displayed. Often, the object is converted to a string automatically, but the MessageBox
doesn't know this. Force it to get past the compiler by putting an empty-string in the front:
MessageBox.Show ("" + comboBox1.SelectedItem);

Another way around this problem is to use a ".ToString()" method. For example, with this
RegistryKey example (snippet):

MessageBox.Show (RegKey.GetValue("ApplicationName").ToString());

The class name '?' is not a valid identifier for this language

Likely Solution:
Close all frm (forms), then close and re-open the solution. It appears the development
environment can get confused, especially if you have been deleting methods.

The current project settings specify that the project will be debugged with specific security settings

Symptoms:
When using File-IO functions or Command-Line arguments (and others)

Select Project, Properties, Security


Uncheck the "Enable ClickOnce Security Settings"

Common Error Messages and Solutions Appendix A: 31


The Insert Statement conflicted with the Foreign Key constraint <key name>... (SQL)

Symptoms:
When attempting a SQL Insert where the main table has a relationship with a sub-table. For
example, adding a new NAMES record, pointing to Record Category = 1, when using:
tblNamesCMD.Parameters.AddWithValue("@RecordCategorySeq", 1);

Problem:
@RecordCategorySeq = 1
Looking in the RecordCategory table, there is not a record with a value "1"

The left-hand side of an assignment must be a variable, property or indexer.

Example Problem Statement:


if (String.Compare (strReadLine, null) = 0)

Possible Solution:
With if-statements, use a double-equal signs (not single) when comparing values; as in:
if (String.Compare (strReadLine, null) == 0)

The name 'ConfigurationManager' does not exist in the current context.

Solution:
See "System.Configuration.ConfigurationSettings.AppSettings' is obsolete: "

Common Error Messages and Solutions Appendix A: 32


CS0103
The name '<variable>' does not exist in the current context.

Symptom 1:
You have not declared the variable using "string", "int", "float", etc, as in:

string myString;
int aNumber;

Or you have declared the value as "myString" but used the variable later as "mystring"
(case-sensitive).

Or you declared the variable in another (routine or module) and that declaration is outside
the scope of your current routine/method/module. A variable was declared in another
construct (such as within a for-next loop) and that construct has ended.

Consider the integer i, which is declared as part of the for-next loop but was used in a
MessageBox outside of the loop; in this case, variable "i" was out of scope and cannot be
used.

for (int i=1; i <= 10; ++i)


{
<stuff to do>
}
MessageBox.Show ("Variable i = " + Convert.ToString(i));

Solutions vary. Check variable declarations.

Symptom 2:
Error: The name '<FixedSingle>' does not exist in the current context.
You are setting a Property incorrectly, such as
textBox1.BorderStyle = FixedSingle

Possible Solution:
The item on the Right-side of the equal sign may need to be prefixed with a property name,
as in:
textBox1.BorderStyle = BorderStyle.FixedSingle

Note: BorderStyle requires a "using System.Windows.Forms;" statement or you can fully-


qualify the name, as in:
textBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle

Possible Solution:
If the 'name' is a keyword-like name:
The name 'IsNumeric' does not exist.... do you need a Class Library prefix, such as:
util.IsNumeric?

The type arguments for method 'System.Array.Resize<T>(ref T[], int)' cannot be inferred from the
usage. Try specifying the Type Arguments explicitly

Issue:

Common Error Messages and Solutions Appendix A: 33


Array.Resize only works with single-dimensioned arrays. You cannot resize multi-
dimensioned arrays; you cannot resize List<T> arrays.

Solution:
Manually copy existing array to a new, larger array -- but you will have problems that the
new array will have a different name. There does not seem to be a good solution to this
problem.

The type or namespace name 'boolean' could not be found (are you missing a using directive or an
assembly reference?)

Consider this called function, which returns a boolean:


static boolean IsBlank(string passedString)

Should be typed as:


static bool IsBlank(string passedString)

C# is inconsistent in how one should spell boolean. When used in a function, use "bool".

The type or namespace name 'CurrentUser' | 'Local Machine' does not exist in the namespace
'Registry' (are you missing an assembly reference?)

Symptoms:
When attempting to read a specific registry key from the Windows Registry

Solution:
Confirm you have a "using Microsoft.Win32;" at the top of the program.
Then use this prefix in the RegistryKey command:

RegistryKey RegKey =
Microsoft.Win32.Registry.LocalMachine.OpenSubKey
(@"Software\Test");

The author is unsure why the "Microsoft.Win32.Registry" prefix is required when a "using"
statement is in place.

More generally:
You are missing a 'using' statement (e.g. using System.Management;).
if this does not resolve the problem, often you can add a new "Reference" (in Solution
Explorer). The name will usually be the same ("System.Management").

The type or namespace name 'DllImport' could not be found (are you missing a using directive or an
assembly reference?)

Possible Solution:
Add these two statements at the top of the (DllImport) class:

using System.Collections;
using System.Runtime.InteropServices;

Note "DllImport" is spelled with lower-cased -el's Dll's

Common Error Messages and Solutions Appendix A: 34


The type or namespace name 'Return' could not be found...

Solution:
Spell "return" with a lower-case 'r'.

If a Void function:
return;

If a non-Void function:
return (some-variable);

The type or namespace name 'single' could not be found (are you missing a using directive or an
assembly reference?)

Solution:
With floating point numbers,
Use Single (with a capital S) or "float" instead.

Unlike "int", Single does not have a shorter alias. Many programmers prefer "float".

The type or namespace name 'StreamWriter' / 'StreamReader' / 'WriteLine' could not be found (are
you missing a using directive or an assembly reference?)

Likely solutions:
Confirm "using System.IO;" near the top of the program.

Confirm you are using the variable name on the WriteLine method.

For example, this is incorrect:


System.IO.WriteLine("my text to write");

Use this:
myreviewFile.WriteLine...

The type or namespace name 'Tasks' does not exist in the namespace 'System.Threading'

You are likely using one of the Wait methods and System.Threading.Tasks is only available
in dot net 4.0 and higher.

Solution:
In the project, select top-menu "Project, Project Properties".
Change the target framework from .NET Framework (3.5) to version 4.0 or newer.
Re-compile.

The type or namespace 'Windows' does not exist in the namespace 'System' (are you missing an
assembly reference?) File: cl800_Util.cs

Likely solution:

Common Error Messages and Solutions Appendix A: 35


You are writing a Console application and have imported the cl800_Util library. cl800's
"Wait" routines call a System.Windows.Form module -- which Console applications do not
allow.

Recommended Solution:
Delete cl800_Util from Solution Explorer and re-add as a "Copy" (not as a link). Once
added, locate the WAIT routines and remove them from the cl800_Util library. Because
cl800 is copied, you are only damaging this program's local version. If you write a lot of
console applications and wish to continue using cl800, move the WAIT logic into its own
library.

Note: The text was changed to reflect this need. All Wait routines were moved into their
own class library. You would see this message if you tried to combine them contrary to
what the book recommends.

Related Solutions:
Console applications cannot call any "Windows-like" method. For example,
MessageBox.Show will not work in a console application. Adding a "using
System.Windows.Forms" defeats the purpose of a console application.

There were build errors. Would you like to continue and run the last successful build?

Symptoms:
When you compile (F5) your newly-written program.

Solutions:
Select checkbox "Do not show again" and click No. In other words, you would never want
to run the previous version of your code (before newly introduced bugs; you really want to
see the current bugs).

If you had already checked yes, see Tools, Options, "Projects and Solutions", "Build and
Run". Set "On Run, when build or deployment errors occur" to "Do not Launch".

Unable to cast object of type 'System.Int32' to type 'System.String' (SQL ExecuteRead)


Unable to read record (SQL)

Symptoms:
When attempting to read a SQL record.

Solution:
When assembling the SELECT statement (strSQLstmt), the "WHERE" clause's record
number (e.g. usually a SEQuence number), must be enclosed in tic-marks. For example:

Incorrect:
:
"WHERE NameSeq = " +
strEditPassedNameSeq;

Corrected:
:
"WHERE NameSeq = " +
"'" + strEditPassedNameSeq + "'";

Common Error Messages and Solutions Appendix A: 36


See also:
Invalid Column Name (SQL)

Unable to cast object of type 'System.Windows.Forms.TextBox' to type 'System.IConvertible'. When


casting from a number, the value must be a number less than infinity.

Likely Solution:
A Convert.To phrase is missing a dot-property

Consider this flawed for-next loop fragment:


for (int loopCounter = 1;
loopCounter <= Convert.ToInt32(textBox2); ....

The "Convert.ToInt32( )" does not point to a particular property.

It should read
Convert.ToInt32(textBox2.Text)

I bet you used to be a VB programmer.

Unable to Read Record (SQL)


See Invalid Column Name (SQL)
See Unable to cast object of type 'System.Int32' to type 'System.String' (SQL)

Unrecognized escape sequence

A string was found with a "\" (backslash) character. This is a reserved character needed for
"escape sequences." If you need a backslash character in a string (typically for a file-
name\path), double-up the backslashes, as in: "C:\\data\\filename.ext"

\t = tab
\r = carriage return
\n = newline
\r\n = crlf
\\ = backslash
\' = tic
\" = quote

See Appendix Special Characters for details.

Use of unassigned local variable <"myInteger"> | <"myString">, etc

Possible Solution:
In your declarations, usually at the top of your routine, a variable, such as

int <myInteger>; or string <myString>

was declared but not initialized with an explicit value. Or you attempted to use a variable
on the right-side of an (equals) statement when it has not yet been populated by another
statement earlier in the code.

Common Error Messages and Solutions Appendix A: 37


Or, the variable was declared, but because of logic, was never assigned a value before being
used in another statement or calculation.

The variable needs an initial value, either explicitly or programmatically. Remember,


declaring a variable does not initialize it..

Another likely scenario is the variable was not initialized and a "while" loop was going to
set the value but the loop never ran (or more likely, the compiler thought the loop had a
possibility of never running).

Recommendations:
Consider using this type of syntax:

int myInteger;
myInteger = 0

or declare and initialize on the same line, as in:


int myInteger = 0;

Possible Solution:
The variable was declared in another module and has fallen out of scope.

Visual Studio cannot start debugging because the debug target <your project name\bin\debug> is
missing. Please build the project and retry, or set the OutputPath and AssemblyName
properties appropriately to point at the correct location for the target assembly.

Solution:
The Program must compile at least one time without errors or you will see this message.
Delete or comment-out the line causing a compiler error.
Run the program again (even if the program does nothing but display the form)
Close the running program and re-introduce the errors. This error should go away.

Solution:
Immediately after starting any new project, press F5 to compile the first empty-screen.
Then immediately close the running program and begin your coding work.

Solution (untested):
Select Menu: Project, Properties.
Go to "Build"; check the "Output" section at the bottom
Browse to your project's main directory/path, choosing Bin\debug"; this is where the actual
exe/dll lives.

When casting a number, the value must be a number less than infinity...

See
Error: Unable to cast object of type 'System.Windows.Forms.TextBox' to type
'System.IConvertible'.

Common Error Messages and Solutions Appendix A: 38


Appendix B - Compile and Distribution

This section discusses how to compile and distribute an .EXE.

Background:

When developing and testing a program, pressing F5 (top menu Debug, Start
Debugging) compiles the program, writes a temporary executable, and then
launches that .EXE as a separate task on the Windows task bar.

On the disk, Visual Studio builds a Debug folder in the Project's directory and in
there you will find a compiled .EXE and other support files – but only the .EXE is
needed for distribution. If you compile for "Release" (described below), a new
directory, "Release" is populated similarly.

The debug version (the .exe) contains code overhead that helps you test and
develop and this version is about 10 to 15% larger than a release version.
Although you can distribute the debug version to end-users, it is not recommended.

How to Compile and Distribute EXEs Appendix B: 39


Cheap and Easy EXE Distribution:

Follow these steps to compile your program as a stand-alone executable and to


give it a formal version number and release date. The resulting .EXE is not a full-
fledged, installable program, but it can be manually distributed (without a setup
routine) and the executable can be run from a server or from a thumb-drive, etc.
This method works well in corporate environments.

1. Open your Visual Studio solution as you would normally.

2. Select top-menu Project, (project name) Properties.

a. In the Project Properties screen, click the left-nav "Application" tab.


Change the "Startup object" from "not set" to your program's main routine,
often <ProgramName.Program>.

b. Click button, "Assembly Information".

Type a short description for the program and fill out the company, copyright,
etc.
Manually set an assembly version (version number) and a file version. The
GUID is a random number, which you should leave as-is.

c. On the left-nav, select "Build".


Change the top Configuration menu from "Debug" to "Active (Release)"
Recommend leaving the platform at "Active (Any CPU)".

3. Close the Properties tab and return to the editor.


On the top ribbon, change from "Debug" to "Release".

How to Compile and Distribute EXEs Appendix B: 40


4. Build the final code by choosing top-menu "Build", then "Build <your project's
name>"

5. Once built, use Windows File Explorer to open the project's "bin\Release" folder
(for example: C:\data\Proj\VS\FileManipulation\bin\Release)

The file (e.g.) FileManipulation.exe is distributable to end users or can be


positioned on a server. The file version is visible from Windows File Explorer.

How to Compile and Distribute EXEs Appendix B: 41


Virus Risks:

EXE files placed on a file server / file share, like all executables, are susceptible to
being infected by viruses. Be sure the EXE is in a read-only directory and your
development staff does not have write-access to the EXE or any DLL's in this
directory. This includes you. Use a service account, from a secured workstation,
when updating shared executables.

Protecting executables from viruses is a real-world


problem experienced by the author. A helpdesk employee's
machine was infected and they in-turn infected a variety of
executables on a main login-script server. As each
workstation logged in, they all were infected. It wasn't
until the next day the company's virus signatures were
updated. The Author now believes it is safer to distribute
executables locally, on each workstation. This makes
house-wide infections less-likely but is more problematic
when updating.

How to Compile and Distribute EXEs Appendix B: 42


EXE Icons

The taskbar and shortcut icon will be a default Visual Studio icon and your
program deserves better. Unfortunately, you will have to create, buy or steal your
own icon. Of the three techniques, one of them requires a bit of artistry and it is,
of course the most fun.

Obviously, thieving an icon is reprehensible and can get confusing if your program
shares the same icon as another. To help, Microsoft provides a free library of
icons, which can be found in the Microsoft Visual Studio Image Library,
downloadable at this link:

http://msdn.microsoft.com/en-us/library/ms246582.aspx

As of 2015.01, this is a downloadable .zip file. Extract all


files in the archive, then tunnel to
ImageLibrary\Actions\ICO. Other ICO libraries are near-
by.

The number of (Application) icons is limited, but the number of toolbar icons is
expansive. Regardless, it provides a good starting point, especially if you want to
draw a complete set of icons, with more on this in a moment.

Icon Files:

Icon files (.ico) are peculiar because they contain multiple images, at different
resolutions and different color depths. A fully-populated icon has these images
embedded:

16 x 16, 4-bit color


16 x 16, 8-bit color
16 x 16, 32-bit color

32 x 32, 4-bit color

How to Compile and Distribute EXEs Appendix B: 43


32 x 32, 8-bit color
32 x 32, 32-bit color

48 x 48, 32-bit color

256 x 256, 32-bit

To do an icon properly, you need a full-fidelity version at 256 x 256 pixels and
another at 48 x 48 pixels, followed by progressively smaller and less-detailed
versions. You will have poor results if you take a full-sized version and attempt to
scale it down to the smaller sizes; color shading and pixellation will occur and it
is beyond the scope of this book to describe the intricacies. As you will learn,
there is an art to creating icons.

Editing Icon .ICO files

Contrary to popular belief, you cannot create icons with most photo editors and
you can't edit them properly with MSPaint (it only sees one icon within the file).
There are ways to draw an icon locally, saved as a PNG, and then upload to a
website for ico conversion, but these are generally limited to one size, one icon.

As a free solution, Microsoft recommends this web-based editor. It will not build
the larger Windows-8 style tiles, but it is generally workable:

www.xiconeditor.com:
http://msdn.microsoft.com/en-us/library/gg491740%28v=vs.85%29.aspx

Using Visual Studio to Edit Icons

Amazingly, Visual Studio, the editor, can also edit ico (icon) files, but it comes
with infuriating limitations, only editing the 16 and 32 pixel icons. And it does not
seem to give full control over color pallets.

With the editor, you can view, but not modify 48 and 256-pixel icons. Also, it
does not appear capable of building a new icon file – it only works against existing
ones, but this restriction is easily worked around.

Even with these limitations, it is worth a moment to explore. Do the following:

A. Because you cannot create a new ico file with Visual studio, you must begin your
work with an existing icon. Locate a larger, full-fidelity icon, one with multiple
icons within the file. For example, from the downloaded ImageLibrary (see
above):

How to Compile and Distribute EXEs Appendix B: 44


ImageLibrary\Objects\ico\ActiveServerPage(asp)_11272.ico

Or search C:\Windows for any *.ico files.

Protect the original file by copying the .ico to a temporary location before editing.
I recommend creating an "Images" folder within your project and storing icon and
clipart files there.

B. From any Visual Studio Project, select File, Open. Tunnel to and open the .ico file
and it will open in a tabbed-window, next to your code and form designs. The left-
nav shows each of the different sizes. Note the editing ribbon bar is only available
on 16 and 32-pixel icons.

Example Icon in Visual Studio, showing multiple sizes

Once edited, close the tab and save the changes.

Attaching Icon Files to your Project:

The icon needs to be attached in two locations: One for the file system and a
second for the running program.

How to Compile and Distribute EXEs Appendix B: 45


1. Open the top-menu, Project, (your project's name) Properties. Select left-nav
"Application". In the Resources section, set the icon file by browsing to the icon
file. Illustrated "ActiveServerPage(asp)_11272.ico". Once selected, File Explorer
will show this as the EXE's default icon and it will automatically pick the
correctly-sized icon.

2. Return to the Form Editor (Form1, Design View). In the form's properties, locate
the "Icon" setting. Browse to the same .ico file and select. Again, the editor will
choose the properly-sized icon from within the file.

How to Compile and Distribute EXEs Appendix B: 46


3. Re-compile the program for the changes using F5 or F6-re-build. The icon will
show in File Explorer, on the program's (form's) title bar, on the Task bar, and on
any desktop shortcuts created. The icon file appears as a resource in Solution
Explorer.

How to Compile and Distribute EXEs Appendix B: 47


Creating Publishing / Distribution Packages

The "cheap and easy EXE" distribution method described above is my favorite
way of distributing a compiled program, but you can build a setup.exe that
automatically installs the software and builds an un-install routine. The benefits of
this design are:

• Setup.exe installs your program in a location of your choosing


• Adds an un-install to the Control Panel's "Add Remove Software" (Programs
and Features).
• It builds a desktop icon for the current user (In Windows 8, adds a tile to the
All Apps menu.

For example, from Windows 8's Control Panel, Programs and Features:

and from the Tile screen:

Building a Distribution Package:

1. Create a Release version of your application, as described earlier in this chapter,


then close the project.

How to Compile and Distribute EXEs Appendix B: 48


2. If this is the first time building an MSI package, you must install a Microsoft-
recommended InstallShield ("InstallShield Limited Edition for Visual Studio" - a
free product from a third-party).

Flexera Software
http://learn.flexerasoftware.com/content/IS-EVAL-InstallShield-Limited-Edition-V
isual-Studio

From the web site, download and follow the instructions. Once downloaded and
installed, close and restart Visual Studio.

3. Re-launch Visual Studio, selecting


New Project, Other Project Types, "Setup and Deployment"
Choose the "InstallShield Limited Edition Project" template

For the Location, type a path that is near but separate from your original Visual
Studio Solution. For example: C:\data\Source\FileManipulation\ (Deployment),
where "Deployment" is the recommended name. As usual, I recommend leaving
Create Directory for the solution and letting the "Name" become the actual
directory.

The new solution opens into an Install Shield Wizard with a row of buttons/icons
showing each step, starting with "Application Information." This is called the
Project Assistant and if you get lost, look in Solution Explorer and double-click the
Project Assistant

How to Compile and Distribute EXEs Appendix B: 49


4. In the InstallShield wizard's first step, "Application Information", fill out your
company name, web-address, and version number (not illustrated).

5. In Step 2/Icon 2 – "Installation Requirements", choose any restrictions you may


have, such as only installable on Windows 7 or newer and choose any required
software, typically Microsoft.Net Framework version 4.x. The screen is self-
explanatory.

6. In "Application Files", rename the default [ProgramFilesFolder] from


"InstallShield" to "MyCompany" or "MyApplication". This becomes the default
installation folder.

In the right-hand Name section, "other-mouse-click" and browse to your Release


version and add your final compiled EXE to the list. Add any additional INI files,
Readme.txt, etc, in this same location.

How to Compile and Distribute EXEs Appendix B: 50


7. In the "Application Shortcuts" section, choose where you want icons built,
typically the Start Menu (In Windows 8 this is the All Programs tile screen).

8. In the "Installation Interview" section, choose options as needed. Typically:

No - Do not display license agreement


No - Do not make users type their company or username
Yes - Allow users to modify the Installation Location
Yes - Allow the user to launch the application after install

9. Finally, select top-menu "Build", "Build Solution". Note that this is not part of the
Wizard steps. This completes the MSI build.

10. In Windows Explorer, tunnel to ...\Express\CD_ROM\DiskImages\Disk1

This is your Deployment directory – not your original program solution!

Results:
Note the Setup.exe, Setup.INI and .MSI file.

This entire directory can be positioned on a Share, CD, thumb-drive, etc. and is
ready for use.

Testing:

Launch Setup.exe and allow the program to install.

How to Compile and Distribute EXEs Appendix B: 51


Results:
• In this example, installation arrives at "C:\Program Files
(x86)\MyCompany\*.exe"
• Note desktop icon (if selected)
• If Windows 8, note the All Programs Tile installed as "Launch (your program
name)"
• In Control Panel, Programs and Features (Add Remove), you can un-install.

Possible Warning:

Warning: -7235: InstallShield could not create the software identification tag
because the Tag Creator ID Setting in General Information View is empty.
ISEXP: Warning.

Solution:

This is a warning and can be ignored with no harm. It suggests files required for
automatic inventory scanning are not in place and implies a corporate install. As
of 2014.03, this design is not in wide-spread use.

The warning can be resolved in one of two ways.

1. Disable the Inventory feature: In the Deployment Package's solution, Solution


Explorer. Tunnel to "Orgainize your Setup", "General Information". Scroll
down to the Use Software Identification Tag. Set Use Tag = no

2. Or Enable the Software Inventory by filling out the fields in the "Software
Identification Tag" section.

In Solution Explorer, tunnel to "General Information"

How to Compile and Distribute EXEs Appendix B: 52


Complete these fields:
Tag Creator Name: Your business name
Tag Creator ID: (See below to generate)
– example: regid.2009-04.com.yourBusinessName

Generating a Creator Tag:

Go to this site:
Magnicomp Software Tag Maker (free)
http://www.magnicomp.com/cgi-bin/mcswtagmaker.cgi

Complete the online form and generate an XML tag file.


Download and store the Tag file in your deployment's root directory.

My tag file was named this way:


2009-04.com.keyliner\regid.2009-4.com.keyliner.examplefilemanipulation_13
96226547.swidtag

a. In your Package's "Application Files" section, add the tag file, so it installs
at the same level as your .EXE program.

b. A second copy of the tag file must also be copied, using "Application
Files": (example file name):

%PROGRAMDATA%\2009-04.com.keyliner\regid.2009-4.com.keyliner.e
xamplefilemanipulation_1396226547.swidtag

Note: The Vendor's Generated Tag webpage will have the exact link and
name you should use – cut and paste.

Your MSI package must also build the Program Data directory:
"%PROGRAMDATA%\2009-04.com.keyliner"

where %PROGRAMDATA% value is a Windows system environment


variable. On Windows Vista and later the value is usually
C:\ProgramData

Recompiling:

If your original program is pulled for maintenance or enhancements, you must


rebuild the Release version *and* rebuild the Deployment version. Remember,
your source code and the deployment solution are two different Visual Studio
projects.

There are other features, such as automatic updates when version numbers change.
This is beyond the scope of this chapter.

How to Compile and Distribute EXEs Appendix B: 53


This completes the Compile and Distribution chapter.

How to Compile and Distribute EXEs Appendix B: 54


How to Compile and Distribute EXEs Appendix B: 55
How to Compile and Distribute EXEs Appendix B: 56
Version History:

1.01 2015.04.10
Initial Release. Submitted to Wrox Publishers; declined due to length and
competition with other titles.
Advanced copy to D.Parks for review

1.02
Chapter 19 Wait States
Expanded Auto-launch example, 19.5

Chapter 20 Printing
Added reference to "Add Reference" for System.Printing on Console
apps

Chapter 23. Files


Added "Directory.Create" exception note dealing with
C:\Program Files (x86)
1.03
Split into three volumes
CH0 - 11 + Appendixes A,B (Through Multiple Forms)
CH12 - 21 ASCII - Formatting + Appendix C
CH22 - 27 Arrays - SQL + Appendix D

How to Compile and Distribute EXEs Appendix B: 57


A Beginners Guide to C-Sharp - Volume 2
Visual Studio C# 2017
ASCII through Advanced Formatting
by Tim R. Wolf
© 2017.06.01 1.02
Table of Contents

9 Chapter 12 - ASCII Files 17


Reading ASCII (Text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
StreamReader
Priming Read
Ending the Loop and Closing Files
Using an EndOfStream Read
The "Using" Clause
ASCII File Reads with try-catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Issues with try/catch and Close
Chaining catch-statements
Completed ASCII ReadFile - Program 12.5. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Writing ASCII (text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Appending ASCII (text) Files - Advanced Open Methods. . . . . . . . . . . . . . . . . . . . 54
Preventing Append from Running Twice

9 Chapter 13 - Parsing Tab and CSV Files 65


Automatic Parsing by Delimiter (Split).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Declaring the Destination Array
foreach
Alternate Loops
.Split Limitations
CSV Files
Split Example: Phone-Numbers
Manually Parsing Comma-Delimited Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
First Field Logic
for-next Logic
Substring
Last-field Logic
Tab-Delimited Files
Parsing a Variable Number of Columns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Parsing ASCII CSV Files with Embedded Commas.. . . . . . . . . . . . . . . . . . . . . . . 105
Error Processing in CSV Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Moving ParseCSVLine to the Utility Library

9 Chapter 14 - INI Files 147


INI File Structure and Design. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
A015: Loop Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A017: Parse INI Detail Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A028: Finding the Application's Default INI Location.. . . . . . . . . . . . . . . . . . . . . 178
Command-Line Override.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Simulate a Command Line Options
Embedded Spaces in the Parameters
Command-line ini=
A029: Write Default INI when Missing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Basic INI File Read: Complete Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Using a Program Class - CL860 INI File Read. . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
CL860 Basic INI Read Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Using CL860_BasicINIRead

9 Chapter 15 - xml and App.config Files 223


xml File Structure
app.config xml.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Using (Reading) app.config
Building a Manual xml File.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Reading xml File Sequentially. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

9 Chapter 16 - Windows Registry 249


Organization of the Registry
Reading a Specific Registry Key.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Opening the Registry
Closing
Reading a Specific Registry Value
Reading Mulitple Keys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Reading All Values within a SubKey. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Multi-Line String Registry Values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Creating/Modifying Name-Value Pairs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Opening the Registry in 'Writeable' Mode
Creating Sub-SubKeys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Deleting Values and Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Deleting Entire SubTrees
Deleting the Current Key
Enumerating SubKey (folders). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

9 Chapter 17 - Reading Excel and Access 289


Reading an Excel File using a COM object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Open the Workbook
parameter list
Reading a Row
comboBoxes based on Access Database Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Attaching Microsoft Access Data to a comboBox. . . . . . . . . . . . . . . . . . . . . . . . . 317
ODBC Call
Connecting to the comboBox
Refreshing the List
Performance Considerations

9 Chapter 18 - External Programs (Shell) 333


Starting a New Process. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Launching Notepad with a Specific Filename
Building an Exit Event
Using Code to Start a New Process.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
proc.WaitForExit.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
DOS Output.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Capturing DOS output into an Array
Multiple Instances of an Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Simple Instance Test
Mutex: A Better Solution for Multiple Instances. . . . . . . . . . . . . . . . . . . . . . . . . . 355

9 Chapter 19 - Waits, Delays and Pauses 361


Poor Wait States – Not Recommended
Empty loops
current time and loop until xx seconds
Sleep events
System.Threading.Thread.Sleep (milliseconds). . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Completed wait Simulation - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Wait as a Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Using cs805_Wait
Calling Wait
Timer Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Basic Timer Code
Event Horizons
Countdown and Timer Example
Disabling the Close "X" Mid-Transaction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Intercepting a Close Event
Countdown and Timer Program - Completed. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Simple Startup Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Splash Screens.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

9 Chapter 20 - Printing 415


Printing Simple Text. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Print Layout / Print Rendering
Pixel Positions in Millimeters (x,y)
OverShooting
Printing Horizontal Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Printing Rectangles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Filling
Printing Graphics.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Printer Dialogs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Adding a Custom Printer Dialog
Print Preview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Printer Setup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Printing Text Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Font Height Calculations - Lines per Page
Page-Break Logic
GraphicsUnit.Millimeters
Setting New Margins, Landscape. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
Current Margin
Setting New (Printable) Margins
Begin and End Print Events.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

9 Chapter 21 - Formatting 469


Font Color, Font Bold. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Font Bold
Font Style Ariel
Basic String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
String.Format with Alignment.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Proportional Fonts
Numeric Formatting with String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Standard Decimals
Formatting with Alignment
Commas and Currency
Numeric Picture Clauses. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Padding Zero with Leading and Trailing Decimals
Place Holder (#)
Thousands Separator ( , )
Variable Picture Clauses (Group Separators)
Date and Time Pictures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Converting from Strings
Converting Dates
Other Date Time Properties and Methods.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
DateTime Formatting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Pictures
Display Dates, Year first
Format Class Library - cl710_Formatting.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Phone Number Formatting
Proper Names
Proper Addresses
PhoneNumberFormat Method - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
PhoneNumberFormat Module - Coding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Extension Extract
Strip Normal Punctuation
Strip "1-" Prefixes
Punctuate
Punctuating the AreaCode
"ProperNames" Formatting - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
ProperNamesFormat - Coding.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
A Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 13 - Parsing Tab and CSV Files; Parsing Delimited Fields
Chapter 13 - Parsing Tab and CSV Files

This chapter deals with parsing tab and comma-delimited CSV files. You will be able
read a line of data and parse (split) each field into constituent parts and this is a common
data-processing task. Several techniques are explored, from a simple but imperfect
"split", to sophisticated routines that can handle complicated files. The code developed
in this chapter can be placed into the CL800_Util libraries. From here, they can be
linked into any program.

Topics:

• Automatic Parsing with the .Split command


• Phone-Number Split Example
• Manually parsing a line
• Using ifrontSideNDX (index) set at -1
• for-next loops with a known number of columns
• Shifting delimiters to the next field within the loop
• Last-field logic
• Tab-delimited files
• Tabular data, unknown or variable columns

• Why use a manual method instead of a Split


• Parsing CSV (Comma-separated) files; in detail
• Error Processing; blank lines, bad data, etc.
• Parsing embedded commas in CSV files, with multiple records
• Moving the finished parsing to util.ParseCSVLine library

Examples:

Financial information and other tabular data is often presented as a delimited list, where
each field is separated by a comma or a tab. These types of records are called tab-
delimited ".TAB" or comma-separated values ".CSV" files. The files typically contain
multiple records, one line per record and each line contains multiple fields. On a
spreadsheet, the records may look like this:

Description Jan Feb Mar

Fuel Expenses: 1000.00 1130.50 1200.00

Maintenance Expenses: 300.00 0.00 720.00

and in a (CSV) file, the same data might appear like this:

Description,Jan,Feb,Mar
Fuel Expenses:,1000.00,1130.50,1200.00
Maintenance Expenses:,300.00,0.00,720.00

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 65


Tabular data like this is typically produced by another program, often by Microsoft Excel
or a database.

There are several ways to parse the data, ranging from manual loops to "Split" routines,
to automatic utility functions; each is explored. The manual method teaches many useful
programming techniques. This chapter relies heavily on the concepts described in

Chapter 4 (string functions)


Chapter 6 (utility functions)
Chapter 8 (class libraries)
Chapter 12 (ASCII files)

This is a challenging subject and will take time to absorb. The


ultimate goal is the Parse utility function. Once written and
debugged, it can be called from any program and you will never
write this logic again.

Rather than jumping directly into the final code, this chapter
shows the evolution of the module and you are encouraged to
learn the techniques.

Overview:

Manual parsing, using a soon-to-be-built "util.ParseCSVLine", works with all types of


data including comma-delimited, tab-delimited, and CSV files with embedded quotes and
commas. Here is how the module is typically called:

Overview: Parsing using a new utility Function


//Send detail line strReadLine; return an array:

string[] astrParsedData; //Declare the result-array


int ifieldCount = 0;
string strMsg = "";

//Use the new utility function to parse a comma-delimited


//detail line.

astrParsedData = util.ParseCSVLine (strReadLine, ",")

//Loop through the array, processing each field as appropriate


foreach (string strtempString in astrParsedData)
{
ifieldCount++;
strMsg += "Field #" + ifieldCount.ToString() + ": " +
strtempString + "\r\n";
}

//Display all of the parsed fields:


MessageBox.Show(strMsg);

ParseCSVLine is not a native C# command and is not available until written.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 66


Overview: Using .Split to Parse a Detail Record
string [] astrFieldNames;

astrFieldNames = myStrInputLine.Split(',');

foreach (string strFoundField in astrFieldNames)


{
MessageBox.Show (strFoundField);
}

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 67


Automatic Parsing by Delimiter (Split)

The ".Split" method can automatically parse a string, dividing on a delimiter, placing
each found value into an array. The command works well and only takes a few
statements – one to parse and a second set to process the found values.

Parsing tab-delimited and CSV files is hard. To do it reliably,


the program needs more sophistication than normal. In this
section, the .Split method is the easiest design but it often fails
on other types of files. The real answer is in the "Parsing CSV
Files with Commas section."

The .Split method is wonderfully easy but it has limitations,


described later in this section. Additionally, this method uses
several new concepts which are not explained until the Array
chapter, but the idea is easy to implement even if all the
concepts have not been fully explained.1

.Split Summary

Follow these steps when using .Split. This example assumes a comma-delimited
input-line:

1. Declare an unsized-array to hold each of the parsed fields


string [] astrFieldNames;

2. "Split" the input-line into individual fields/values:


astrFieldNames = myStrInputLine.Split (',');

3. Typically process each field of the array in a foreach loop:


foreach (string strFoundField in astrFieldNames)
{
MessageBox.Show (strFoundField);
}

Array Definition:

An array is a numbered list of like-minded variables, all with the same name. Using this
sample data, consider this line, which needs to be parsed by commas:
string strInputLine = "Fuel Expenses:,1000.00,1200.00,13.56,780.00";

1
An excellent use of the ".Split" method can be found in Chapter 21, Formatting with
"ProperNames."

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 68


The .Split command places each found value into an array, where each comma-separated
field gets an array position. The first array position is zero (base-zero). In a later step, a
for-each loop can rifle through all the values for subsequent processing:

Although the data is a mixture of text and numbers, all fields will be treated
as text (strings). Arrays can only hold one type of data; in this case, string.
Later steps can convert the 'numbers' into true numeric values.

Program 13.1; Using Split on Comma-Delimited


Example Program:

Throughout this program, keep in mind the original input string was a long list of words
and numbers, separated by a carriage-return line-feed between records. The goal is to
have each constituent part separated into its own variable.

To follow along, build a new test program. Use button1 to call a function named
A100_ParseLine. Within that function, declare a simple string variable (strInputLine)
and an array, with detailed steps below. strinputLine will be "split" into the array and
the array will be processed by a "foreach" loop, which was briefly introduced in
Chapter 2. The entire program is short and easy to read. Even though arrays are
described in Chapter 22, these steps should be easy to follow.

Declaring the Destination Array:

The "Split" requires a string-based destination array. String arrays are declared in much
the same way as a regular string with the only difference being an added set of brackets.
Like any declared string, it needs a name, in this case "astrinputLineFields". Out of
habit, I prefix arrays with the letter "a" to remind me it is an array:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 69


Declaring an unsized array
private void A100_ParseLine()
{
string strinputLine =
"Fuel Expenses:,1000.00,1130.00,1200.00,45.00";

//Array required by .split method


string [] astrinputLineFields;
:
}

string strinputLine is an example data-line from a file. It is being hard-coded to


keep the example simple. In real life, this would be read from an ASCII file.

Using the Split:

"Split" is a string method written like this: somestring.methodname() versus a function


which looks like this: somefunction(somestring). In other words, the string name is
typed first, followed by the method's name and any parameters inside of parenthesis.

For this input line, split on a comma, which is being passed into the method's parameter
as a 'char'. Note the tic-marks, indicating character data. If delimited with tabs, use '\t'
instead of ' , ' comma:

Basic Syntax for a .Split command into an array


:
string [] astrinputLineFields;

astrinputLineFields = strinputLine.Split(',');

When the method runs, each comma is automatically parsed, including the first and last
fields. The resulting words and phrases are written to the array. Each array position is
given a number, starting at base-zero. The array looks like this:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 70


The end result is each field is automatically parsed into the array without using a
complicated MidString. As in this example, where the text-string is reliably delimited, a
.Split is a good choice for parsing individual fields.

Be aware there are common situations where a Split does not


work as well.

Each position is referenced directly by its row-number. Because this is an array, the
same variable name is used by each – "astrinputLineFields" – followed by an index-
number in [brackets]. For example:

MessageBox.Show(astrinputLineFields[0]);

TextBox1.Text = astrinputLineFields[1];

The loop – foreach:

When ever I see a stack of variables lined up in order, 0,1,2.3...,


I think of loops. And because this is an array, C# has a special
loop dedicated specifically for this situation. Behold the
"foreach" loop...

A "foreach" loop is almost fun. This verb, which is only useful with arrays, has the
benefit of knowing everything about the array; it knows where the array starts and where
it ends. Because of this, the program does not concern itself with any of a loop's normal
controls. One simple statement handles the first, middle and last positions of the array
and it makes for a simple routine.

Example:

foreach (string strfoundField in astrinputlineFields)


{
MessageBox.Show(strfoundField);
}

The foreach loop translates the individual cells ("astrinputLineFields[0]", [1], etc.)
from a numerically-indexed name to a generic/temporary name, written here as
"strfoundField". As the loop iterates, each value in the array is moved into a temporary

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 71


string which gives the interior of the loop a predictable name to work with. Because the
temporary variable, "strfoundField" is re-initialized with a "string", it is essentially
destroyed and re-created with each pass.

If you've followed previous discussions about variable-scope,


the "strfoundField" variable falls out of scope and is destroyed
at the end of each loop's iteration. Then, when it loops around,
the variable is re-created as an empty string. This is a cool
design.

The completed program is listed below. While studying the code, notice there is no
special logic required for either the first or last field and no fancy logic is required for
delimiter parsing. Even the floating-point line-total calculations were simplified – only
one copy is needed inside the loop. From a programming point of view, this is easy to
understand.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 72


Program 13.1: Split with Comma-Delimiters, completed
private void button1_Click(object sender, EventArgs e)
{
// Using a split, take a comma-delimited input line, with an
// unknown number of fields; total all numeric data

string strinputLine =
"Fuel Expenses:,1000.00,1130.00,1200.00,45.00";

// define the string array needed by the split:


// invented variable name:
string [] astrinputLineFields;

// Load the array by parsing by comma:


astrinputLineFields = strinputLine.Split(',');

//loop through each value; create a holding


//variable named strFoundField

foreach (string strfoundField in astrinputLineFields)


{
// MessageBox.Show(strfoundField); // Diagnostic code

// compute the line-total, using an inefficient try-catch


// The next chapter has better solution
try
{
flineTotal = flineTotal + Convert.ToSingle(strfoundField);
}
catch
{
// not a valid number; skip for now
}
}

// Display final results:


MessageBox.Show (astrinputLineFields[0] + " total: " +
convert.ToString(flineTotal));
}

where:

• This version is functionally identical to the manual parsing methods described later
in this chapter, but this code is shorter and easier to understand.

• "foreach" is a type of loop designed specifically for arrays. Since the computer
"knows" how big the array is, it knows about "each" item in the array. The Array
Chapter describes this in more detail.

• "foreach" loops require a temporary variable (e.g. strfoundField), declared in the


loop statement as a string. This is the variable used by the inside details of the loop;
it has been "genericised." When outside of the loop, variables have to be referenced
by their array position (see MessageBox.Show astrinputLineFields[0]), but
inside the loop the friendly-name can be used.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 73


• "strfoundField" is declared as a string within the loop's definition. This is required
by the syntax and you cannot declare above the loop. The reason is the compiler
needs to destroy and re-create the variable each time the loop iterates.

• A try-catch is needed because the first column "Fuel Expenses:" will not take kindly
to be converted to a floating point number. The try-catch is a poor way to work
around this problem.

Alternate Loops:

Instead of a "foreach" loop, you could rifle through the array, replacing the foreach with
either a "while" or "for-next" loop. The array has a property that is helpful with these
types of loops: astrinputLineFields.Length – where the length is the number of
elements in the array. The length of the array is 5, but remember, all array fields are
referenced with a base-0 [count].

In the admittedly useless code below, the loop runs through the columns (fields) in the
array and displays the row number for each column found. The "for-next" is controlled
by an array property ("astrinputLineFields.Length"):

Program 13.2: Using a For-Next loop, completed


private void button1_Click (object sender, EventArgs e)
{
string strinputLine =
"Fuel Expenses:,1000.00,1130.00,1200.00,45.00";

string [] astrinputLineFields;
string strresults = "\r\n";

astrinputLineFields = strinputLine.Split(',');

for (int iloopControl = 0;


iloopControl < astrinputLineFields.Length;
iloopControl++)
{
strresults =
strresults +
iloopControl.ToString() + ": " +
astrinputLineFields[iloopControl] + "\r\n";
}

MessageBox.Show ("Found columns values: " + strresults);


}

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 74


The for-next loop starts at zero, because [arrays] are always counted from base-0 – thus
"iloopControl = 0".

Be careful with the loop-ending (while iloopControl < array.Length). On first blush,
many would use "while loopControl <= the array's.Length;" this would be a mistake.
The array is base-zero (0,1,2,3,4) but the .Length property is base-1 (5 Long, 5 columns).
Because of the base-1 count, use 'less-than-the-length (e.g. less-than-five) rather than
'less-than-r-equal-to'. Less-than 5 = 4. The array counts as 0,1,2,3,4 – five items. Yes,
this can get confusing.

In C#, positions are always base-0; lengths are always base-1.

Totaling Numeric Values:

A benefit of the for-next loop is you can sum-total the numeric columns by starting the
loop at 1, instead of zero, bypassing "Fuel Expense". This is hard to do with a for-each
loop because it can only process all columns in the array:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 75


.Split Limitations:

Parsing with a ".Split" is easier than other loop designs but nothing in this world is free.
Building and maintaining arrays takes more processing cycles underneath the hood. If
you were rifling through hundreds of thousands of records you might consider the
manual methods. On the other hand, computers seem to have enough idle-time where
they have nothing better to do than your work. And there are certain types of files where
a .Split fails - in particular with .CSV files.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 76


CSV Files:

The .Split method has a fatal drawback, which limits its usefulness.

See later in this chapter for a more robust CSV file parser.

CSV (Comma Separated Values), commonly produced by Excel and other programs,
behave differently if an embedded comma is found within a field. For example, this
company name "Albert and Sons, inc." has an embedded comma as part of the name.
When the CSV file is written, a pair of quotes is put around the field name. In the same
file, the very next record may not have the extra quotes:

The .Split will have a cow when it sees the embedded comma and will produce a sixth
field, separating the company name into two separate entities, leaving the quotes in the
field name:

1234 "Albert and Sons _inc" 45 22.00 Wingnut


1234 Johnson and Beeswax 65 18.32 Transmorgifier

If there were no embedded commas in any field, the CSV will


behave as a normal, uncomplicated comma-delimited file and
the .Split will work perfectly.

Simulate this in a test program by modifying strinputLine. Hard-code the "Albert and
Sons, inc" field by using escape characters for the quote (there are quotes within the
quotes).

string strinputLine = "1234,\"Albert and Sons, inc\", 45,22.00,Wingnut";

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 77


Program 13.2: Embedded Commas in a CSV file, completed
private void button1_Click (object sender, EventArgs e)
{
string strinputLine =
"1234,\"Albert and Sons, inc\",45,22.00,Wingnut";

string [] astrinputLineFields;
string strresults = "\r\n";

astrinputLineFields = strinputLine.Split(',');

for (int iloopControl = 0;


iloopControl < astrinputLineFields.Length;
iloopControl++)
{
strresults =
strresults +
iloopControl.ToString() + ": " +
astrinputLineFields[iloopControl] + "\r\n";
}

MessageBox.Show ("Found column values: " + strresults);


}

The .Split improperly parses the embedded comma, treating it like a separate field and
for this one record, you will get an extra position in the array. The point is this: In many
situation, the .split is not sophisticated enough.

See later in this chapter for an improved method:


"ParseCSVLine".

Tab-delimited files do not have these issues, but are less


common than .CSV.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 78


Another Split Example: Phone-Numbers:

Just for the fun of it, consider this .Split problem: Users can type a phone number into
textBox1 with a variety of punctuation ranging from parenthesis, dashes, dots, and x-
extensions. Write a routine that splits what was typed and return a result that only
includes the numbers, minus the normal phone-number punctuations.

For example,
"(208) 555-1234" returns "2085551234"
"208-555-1234 x1456" returns "2085551234 x1456"

Better routines were demonstrated in Chapter 6 and 7:


"StripNonNumerics" and a sophisticated phone-number
formatting routine can be found in Chapter 21. This method
demonstrates a way to use Splits with similar results.

To write this routine, use a split-overload that uses a separate char[ ] array instead of a
single ',' comma delimiter. The routine is remarkably short. (There are many different
ways this could be written. This example is meant to show another way of using the Split
command.)

Previous splits used a single-comma, but the phone-number routine must split on more
than one possible character. The command has an overload supporting this. The syntax
is devilishly hard to type. Here is an illustration to help:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 79


Program 13.3: Phone-Number Split, completed

(See Chapter 21 for a more sophisticated routine)

1 private void button1_Click(object sender, EventArgs e)


2 {
3 // Use textBox1 for the unaudited phoneNumber
4 // Expect users to type (208) 555-1234, 208.555.1234, etc.
5 // Return "2085551234"
6
7 // define the string array needed by the split:
8 // define the delimiter char array used by the split:
9 // define the final, de-nuded phoneNumber minus its punctuation
10 string [] aphoneNumber;
1 char [] afindThese = {'(',')',' ', '-', '.', 'x', 'X', '/', '#'};
2 string nudePhoneNumber = ""; // Final results
3
4 // Load the array by parsing by findThese char values:
5 // this will include some blank fields
6 aphoneNumber= textBox1.Text.Split(afindThese);
7
8 // loop through each value; create a holding variable
9 // called "strfoundPart":
20 foreach (string strfoundPart in aphoneNumber)
1 {
2 // MessageBox.Show(strfoundPart); // Diagnostic code
3
4 // See if the found results have data; do this by appending
5 // an empty-string (which clears null fields); then trim.
6 // if the string-length > 0 then it has data.
7 if ((strfoundPart +"").TrimStart().Length > 0)
8 {
9 // re-assemble each part, as found, appending to the
30 // previous:
1 nudePhoneNumber = nudePhoneNumber + strfoundPart;
2 }
3 else
4 {
5 // MessageBox.Show("Do not append empty fields");
6 }
7 }
8
9 MessageBox.Show("Naked Phone Number: " + nudePhoneNumber);
40 }

Phone-Number Split, end

To use the program, press F5 to run; type a punctuated phone-number in textBox1; then
click button1. Try this code now. Each phone-number segment is stored in the array;
details below.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 80


where:

• The array "afindThese", defined at line 11, is a character array ("char [ ]") and it
contains every imaginable phone number punctuation. The array is declared and
populated in a single hard-to-read statement. The statement is assembled in this
fashion, always using tic-marks (not quotes) for character data:

char [] afindThese = { <stuff goes in here between braces }

char [] afindThese = { '(' } //Example of one item in the array

char [] afindThese =
{ '(', ')', ',', ' ', '-', '.', 'x', '#' };

Notice embedded commas, spaces, hyphens, periods and other commonly-found


phone number punctuations. This is a hard statement to type and even harder to
read, once typed.

• A .Split command at Line 16 loads the array int a variable, "aphoneNumber". The
phone number is split at each possible punctuation character. Notice both the
punctuation array and the split live above the loop.:

Split using multiple characters as delimiters


char [] afindThese = {'(',')',' ', '-', '.', 'x', 'X', '/', '#'};

aphoneNumber= textBox1.Text.Split(findThese);

• The "Split" parses the phone number at each delimiter. In other words, instead of
just using a comma for a delimiter, any of the characters above can act as a delimiter.

The phone number essentially splits along these lines, where all the numbers are
loaded into an array and all delimiters are discarded:

The final array contains 5 items, including two empty strings:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 81


• The foreach loop (line 20) moves each found array value into a temporary
"strfoundPart" variable, allowing the foreach loop to standardize the variable's name.
Notice, because of the foreach loop, the logic does not need the array's typical
[index] positions (Chapter 22).

• It is possible for an strfoundPart to be "blank". For example, the first character is a


"(" delimiter. Because there is nothing before the first character, the split will
generate an empty cell in the final array. Similarly, a space is found after the area-
code's closing ")" – making two delimiters in a row. When the split sees this, it takes
everything between them (e.g. – nothing) and puts the results into another blank cell
in the array.

• You can watch the array being populated by adding a breakpoint at the foreach loop
and monitoring the variables. (Place the break point, run the program. Click inside
the editor to anchor the debugger. Then press F11 to step through each statement.)

Line 31, "nudePhoneNumber = nudePhoneNumber + strfoundPart" assembles the final


results, with "208+555+1234", discarding blanks. It appends each non-blank part to the
final result.

The routine fails if something like "bob" was found inside the original string. To resolve
this, an "IsNumeric" check is required, discussed later in this chapter.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 82


Testing:

To test, try typing these values in textBox1; click button1 to run.

208-555-1234
(208) 555-1234
555.1234
555.1234 x5678
(208555-1234
(208) bob-1234
etc.

See also, Chapter 6/7 and 21 for a better phone number routines.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 83


Manually Parsing Comma-Delimited Data

This section dives into manual parsing techniques. The goal is three-fold: First, you will
need a routine that can work around the limitations of a .Split command. Second, the
end result will be a re-usable utility that can be called by any program; it can parse the
most complicated CSV, Tab or any other type of delimited file. And third, this is a
teaching event, showing real parsing techniques you will need in the real world. This is
somewhat of a scenic route to the final solution.

When completed, the "ParseCSVKune will be a generic


module with more capabilities than a split – and it will be
nearly as easy to use.

Many of the ideas discussed here are complicated, but are also flexible – able to work in
a variety of situations. These techniques resolve the shortcomings of the .Split method:

• Embedded commas in a CSV


• Able to easily process variable or undetermined number of fields (these routines can
figure out how many fields are in the input line)
• Under the hood, it is probably slightly more efficient than the .split

Once written and saved into the CL800_Utility library, I use this routine, instead of a
.Split, in all but the simplest programs.

If you want to skip the teaching-event, jump to program 13.5


for the completed version, which includes CSV logic.

Program 13.4; Manual Parsing

Note: To keep the examples from being too complicated, a sample comma-delimited
record is simulated with a hard-coded variable, strInputLine. Then later in the chapter, a
multi-lined inputfile will be used.

Parsing will use C#'s native Substring command. You are


welcome to substitute util.MidStr commands from the
CL800_Util libraries.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 84


Setting up the Example Program:

Returning to the Fuel Expense example, begin by building a simple project with a single
button. The goal is to parse input lines similar to this:

The logic extracts each field (one description, three monthly-values), adding the results,
giving a line-total. As the examples progress, the sample data will become more life-like
and will have bad data, embedded commas, extra columns, etc.

For the initial development, a hard-coded string simulates the data:

string strInputLine = "Fuel Expenses:,1000.00,1130.50,1200.00";

The first and last fields are of special concern because they do not have a matching pair
of delimiters.

To parse the line, the final program will use a "rolling mid-string" that skips or hops
from delimiter to delimiter. Two delimiters are required: one to track the first found-
comma and a second for the field's trailing comma. The first comma will be named the
"frontside" delimiter and the other will be the "backside." Once two delimiters are
found, snag everything between them.

Loop Setup:

The sample data-line contains four fields. Because the count is fixed, a for-next loop is a
good candidate for field-hopping. Because there are only three delimiters, the for-next
loop will only run for 3 iterations, leaving the last field for special logic and this is
typical for these types of loops. But because the first field does not have a leading
delimiter, a trick has to be employed.

The loop looks like this:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 85


Later, the loop will be changed to a while-loop so it can work with a variable number of
fields.

First Field Logic:

Ideally the main loop behaves the same for each field, regardless of commas. In other
words, a happy-loop would find a "frontside" comma, then a "backside" comma,
snagging everything in the middle. It would be nice if the same parsing logic worked for
all four fields. We will get close to this design.

Parsing happens in a series of mid-strings. Recall from Chapter 4, mid-strings require


either a [frontside delimiter and a length] or a [front and backside delimiter - from which
the length is derived]. But the first and last fields are only partially delimited, requiring
more care.

Ignoring the first field for a moment, consider the inner fields, which are easier to
understand. A mid-string would have to find the first comma, then find the second. By
this step, two delimiter index positions are known - a.k.a, the "front" and the "backside"
delimiters.

The first comma is found at index position 14 (a base-0 count).


The second at 22:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 86


On the next loop, shift the frontside delimiter's position from [14] to [22], making the
next field's frontSide at 22. This is done with a simple assignment:

frontside-delimiter's position = the backside's last-known position or


ifrontSideNDX = ibackSideNDX

Once the "shift" happens, begin a search for a new backside delimiter. Again, start the
search at 22 + 1. When a new comma is found (illustrated above at "1130.00"), you are
ready to substring-out that value.

Since the last field does not have a closing comma, the loop runs 3 times, The last field
has have special considerations. It would be nice if the first field could be parsed with
the same logic, within the same loop.

First Field Considerations:

The first field ("Fuel Expenses:,") has a trailing but not a leading comma.
But if you think about it, you never "search" for a leading comma – it is
always assigned.

You could imagine a comma just before the word ",Fuel" – an imaginary
comma. With that, the mid-string could work with the same logic: "Find the
first comma, then the second." Since all index positions start with a base-
zero count, a fake comma could live at position -1.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 87


Now the parse is easier. In the loop, start with a -1 frontside delimiter and search for the
next comma, which is found at the end of "Fuel Expenses". Everything in between is the
field value and it can be stored in temporary variable, as before. Then shift the frontside
delimiter to the other's position and do it again. This is the skip or hop across all byt the
last field.

With a frontSide delimiter starting at -1, the loop will be none-the-wiser. It will treat this
as a new front-side delimiter, add 1 and begin searching for the backside. By prepping in
this way, the up-and-coming "mid-string" has both a front and back delimiter to snack on
– even though the location is bogus. This won't be a problem because one of the cardinal
rules of any mid-string is to skip past the frontside delimiter before substringing (see
Chapter 4). This removes the need for special first-field logic. There are more details
and concrete examples in a moment.

Begin Writing Code Now:

A. Build a new project, any name. In Form1, place a button1.

B. Double-click button1 to stub-in the on-click event.

C. In button1_Click, add this code:

private void button1_Click (object sender, EventArgs e)


{
A100_ParseLine();
}

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 88


D. Hover over the A100 statement and click the lightbulb icon in the margin. Allow it to
generate the A100_ParseLine module or alternately, type the function by hand. Type
these string and integer declarations, comments, etc., as illustrated:

Program 13.4: Variables needed by the parsing routines, in progress


private void A100_ParseLine()
{
string strInputLine = "Fuel Expenses:,1000.00,1130.00,1200.00";

string strtempFieldString;
string strfoundHeader;

int ifrontSideNDX;
int ibackSideNDX;
Single fLineTotal; //A floating point number

//Prime the frontSide index to -1 so it jumps to zero in the


//first loop.
//Prime fLineTotal to zero.
ifrontSideNDX = -1;
flineTotal = 0;

//Loop through each field here...


}

where:

• strInputLine represents the line that needs to be parsed, as read from the CSV
(spreadsheet) file. For this initial development, simulate this with a hard-coded text
string. Later it will be replaced with actual data from an ASCII file.

• ifrontSideNDX and ibackSideNDX (indexes) represent the position of the first and
second commas found in the csv line. These are integers.

• Strings strtempFieldString and strfoundHeader are holding values for the things
being parsed.

• flineTotal is a Single (FloatingPoint) number, which will hold a sum-total for the
numeric values. Be sure to use a capital "S" on the type-declaration, "Single".
Alternately, use "float" (lowercase "f"). Decimal values require floatingPoint
numbers.

Style notes: Out of convention, most variables are camelCased with an initial lower-
case, then every word begins with an upper-case (e.g. strtempFieldString). Many
developers use this syntax to indicate the variable is locally-scoped and is not a global or
class-level variable. However, because strInputLine will later move to another routine, it
is being coded with a capital "I" (Input).

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 89


for-next Logic:

As discussed, the field-count is known and a for-next loop should be used. After the
variable declarations in A100_ParseLine, write the loop now:

for (int ifieldCount = 1; ifieldCount < 4; ifieldCount++)


{

The for-next loop cycles with a temporary variable "ifieldCount"; starting at one and
incrementing by one. The conditional is set "while ifieldCount < 4" – which is another
way of saying "3" – and three matches the comma count. This leaves the fourth field
dangling, and as shown earlier, it can not live inside the loop because there is no
backside delimiter to find. Fortunately, it can be parsed with a simple right-string and no
other logic will be needed to get its contents.

Interior Loop Logic:

Just before the loop, the frontSideNDX position was set at an imaginary -1. Now the
loop begins and its first job is to locate the second backSideNDX delimiter. When
searching for the second delimiter, begin looking for that comma *after* the first
delimiter. Although we lied about a comma living at -1, this is no problem. Begin
searching by taking -1 + 1, which equals 0 (zero). That happens to be the first character,
"F", in "Fuel Expenses" and this is exactly where the search should begin.

A standard "IndexOf" command begins the search for the next comma. It looks at the
"F", "u", "e", "l", etc., until it finds a comma at index position 14.

Later, as the field "slides," the next round of delimiters are


searched by taking the current backside delimiter's and dropping
it into the frontSide's counter. Then a new backside starts at +1
so it doesn't "find itself."

As you can tell, I'm a fan of building these routines one step at a
time. Do the easy stuff first, then add more details as you go.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 90


Program 13.4 Detail: The Substring Loop, partial code
:
:
ifrontSideNDX = -1; // Prime the first delimiter
flineTotal = 0;

for (int ifieldCount = 1; ifieldCount < 4; ifieldCount++)


{
ibackSideNDX = strInputLine.IndexOf(',', ifrontSideNDX + 1);

// <mid-string logic goes here>

ifrontSideNDX = ibackSideNDX; //Leap-frog here


}

At the end of the (field) loop, after the mid-string, "leap-frog" the delimiters to the next
field. Do this by setting the frontSide equal to the backSide. This effectively shifts the
next mid-string to the next comma.

The loop logic works like this:

• Before the loop, prime the frontSide delimiter to -1


• At the top of the loop, find the backSide delimiter
• At the bottom of the loop, shift to the next frontSide delimiter

At the bottom of the loop, when the next frontSide delimiter is set, the
backSide is at an illogical place – being set before the newly-found frontSide
delimiter. This is temporary and corrected at the top of the next loop.

Interior Loop Logic: the Substring:

Between the two ibackSideNDX and ifrontSideNDX commands, insert the mid-string
logic. Mid-string uses the frontSide delimiter's position and calculates a length based on
the backSide delimiter. This is an exact copy of the util.MidStr function described in
Chapter 4:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 91


Program 13.4 Detail: The loop with the Substring, partial code
:
for (int ifieldCount = 1; ifieldCount < 4; ifieldCount++)
{
ibackSideNDX = strInputLine.IndexOf(",", ifrontSideNDX + 1);

// Mid-string logic:
strtempFieldString = strInputLine.Substring
(ifrontSideNDX + 1,
ibackSideNDX - ifrontSideNDX -1)
.Trim();

ifrontSideNDX = ibackSideNDX; // Slide or Leapfrog


}

The mid-string captures a comma-delimited field and places the results in


strtempFieldString. Each field, except the last, is visited by the loop.

The Substring logic could be replaced with a CL800_Util


operation:
util.MidStr(strInputLine, ifrontSideNDX + 1, ",")

The results, "strtempFieldString" contains the parsed value and you can display this in a
MessageBox or use the debugger, which is described next.

Testing the Interior Loop with a Breakpoint:

With the program stubbed-in from above, it is ready for an initial test. But because there
are no output statements and no other real work being done, the program would run, but
results would not be visible. Slow the program down by inserting a debugging
"Breakpoint." In the left-hand margin, next to the strtempFieldString statement, click the
mouse, as illustrated, which inserts a red-ball break-point.

the breakpoint causes the program to run up to this point and then stop (hesitate might be
a better word). While the program is stopped, examine the variables their values by
hovering the mouse over the variable names.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 92


After inserting the breakpoint (red ball), press F5 to run, then button1. The program runs
up to the breakpoint and stops. The yellow-highlighted statement shows which line is
poised to run next.

While at the break, it is important to click inside of the editor, bringing it to the
foreground. When the editor becomes active, the debugger is active and the running
program (button1) slips into the background. In the editor, hover the mouse over a
variable to see its value. As illustrated above, "ibackSideNDX" has a value of 14
(characters) and "ifieldCount" is set to 1.

Repeatedly press F11 to step through the program one command at a time. At each
press, the yellow highlight shifts, showing which line will execute next. Keep pressing
F11, stepping through the program. Alternately, press the button-bar's green triangle to
run until the next breakpoint or press the toolbar's red-square to stop the program. Click
the breakpoint (red-ball) a second time to remove it. Multiple breakpoints can be placed
in the program.

If you have reservations about how this still works, the debugger will walk through each
step. Here is what happens the first time through the loop:

• ifrontSideNDX is set to -1 and hits the loop

• The ibackSideNDX delimiter is found (locating the first comma), frontSide is still -1

• The mid-string shifts ifrontSideNDX by +1, sliding the index from -1 to zero (see
below). The mid-string is captured.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 93


• At the bottom of the loop, ifrontSideNDX skips forward to the backSide's position,
getting ready for another loop. At the top of the loop, the next backSide comma is
found, ready for another mid-string. Ultimately the first three fields in the four-field
string are processed.

Last-field Logic:

After the loop runs three times, the last field is unprocessed. This is typical of most
parsing loops. Because the last field does not have a closing delimiter, different logic is
required. To make it easy, add a step outside the loop, using the "last-field logic,"
documented below.

This is a graceful solution. The last field is processed with a


simple mid-string - no special logic is required to detect it is the
last field – the command runs unconditionally. If the interior of
the loop contains special logic (an if-statement) detecting either
the first or last field, you are probably coding it wrong.

You could for example, make the loop run 4 times (instead of 3) and do special logic on
the fourth time. But this would require testing for the last position's count at each of the
intermediate fields, wasting cycles. Granted, with only four fields, this is a minor
concern, but a 200,000 record file, 4 fields each, would be noticeable.

With the for-next loop, you know exactly when the last field needs to be processed –
doing it immediately after the loop. There is no need for an if-statement and there are no
decisions. Unconditionally run the statement when the loop ends. This makes for clear,
unambiguous logic. As a bonus, the last field's parse already knows the frontSide
delimiter's position – it is the backside from the previous slide (this is no coincidence).
A simple "Right-string" completes the field parsing. Add this logic immediately after the
for-next closing brace:

Program 13.4 Detail: Last field processing, partial code


: (previous loop stuff)
:
ifrontSideNDX = ibackSideNDX; // Leapfrog
} //end of loop

// Last Field Processing using a Right-string:


strtempFieldString = strInputLine.Substring(ifrontSideNDX + 1);

To aid with diagnostics, you could add a MessageBox statement after each found
strtempFieldString:

MessageBox.Show(strtempFieldString);

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 94


fLineTotal Logic:

As each numeric field is found, add the field's value to a floating-point line total.
However, the first field ("Fuel Expenses") will not like being converted to a number.
There are two ways to handle this. First, you could include logic identifying field-1,
skipping past the calculation:

if (ifieldCount !=1)
{
//add the found substring to the fLineTotal
}

This design works but it has the same problem discussed before: each field has to be
checked, seeing if it is field-1, hogging processing cycles. This method also has another
subtle flaw. What if one of the monthly fuel expenses, normally numeric, contained non-
numeric text? The program would explode as it tries to add the value. (Why would
string data like that be hidden inside your input line? Bad users, typing bad data. Users
are unpredictable.

A better solution is to check for "numeric-ness" before trying to add it to the fLineTotal.
This way both the first field and subsequent corrupted fields are ignored. Of course, it
might be best to use both methods. Ignore the first field (because what if it had a label,
such as "529") and then check all subsequent fields for valid data.

"try and catch" Exception Handling:

As explained in previous chapters, there is amazingly no "IsNumeric" verb in C# and this


means writing your own. This can be handled gracefully or ungracefully. If you have
linked in the CL800_Util libraries from Chapter 8, use the util.IsNumeric test. If these
libraries are not available, use a less-than-ideal "try-catch".

When a program does something distasteful, such as divide by zero, blows past the
bottom of an array, or tries to convert "Bob" to a number, it crashes and produces an
"Unhandled Exception Error." Although this isn't the time to discuss exception handling,
the errors can be captured using a "try-catch," which was introduced in the ASCII File
chapter.

A "try-catch" says "try this and if it blows up, catch the error and don't panic." In the
sample code, when the first field ("Fuel") tries to convert to a Single (floating point)
number, it will certainly blow up. Intercept the error with a try-catch:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 95


:
//<Mid-string logic here>
strtempFieldString = strInputLine.Substring (....);

ifrontSideNDX = ibackSideNDX + 1; //Slide or leapfrog to next fld

try
{
flineTotal =
flineTotal + Convert.ToSingle(strtempFieldString);
}
catch
{
// not a valid numeric field; skip for now
}

Try-Catch is a primitive way of numeric checking and it involves


expensive overhead, however, it solves the immediate problem.
A more elegant solution can be found in Chapter 7 with the
CL800_Util IsNumeric check.

"try" statements always require a "catch" but in this case, if the LineTotal fails, do not
error; instead, ignore the condition and bypass adding the value to the total. To make
this work, look at the code above and note how the "catch" does nothing between the
braces. The effect is simple: If the Convert.ToSingle fails, skip the field and move on.
In the real world you might display an error to the end-user or record the failure in a
transaction log. In this case, the first field is always expected to fail. The last-field also
needs to test for numericness.

The Completed (Manual Parsing) Program:

This program loops through a fixed-width (four-field) input line, adding each of the
numeric fields for a line total. Sample data is encoded in the routine.

There is one flaw in this routine: "CSV" data may contain embedded commas within a
text field. For example, "Expenses, (comma) Fuel:" will cause this routine to fail. A
solution for this is covered later in this chapter.

Program 13.4: Manual Parsing Comma Separated, first example completed

Simple Comma-parsing; cannot handle embedded commas or quotes.

private void A100_ParseLine (object sender, EventArgs e)


{
// loop through a comma-delimited line of data with a known
// number of fields and total the numeric values.

string strInputLine = "Fuel Expenses:,1000.00,1130.00,1200.00";

int ifrontSideNDX; // first found delimiter


int ibackSideNDX; // second found delimiter

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 96


string strtempFieldString;
float fLineTotal; // subTotal of each field, inc. float/Decimals.

// Prime ifrontSideNDX to -1 so it jumps to zero


// on the first loop.
// Set fLineTotal to zero

ifrontSideNDX = -1; // Prime before the string


fLineTotal = 0;

for (int ifieldCount = 1; ifieldCount < 4; ifieldCount++)


{
// locate the second delimiter for the current field:
ibackSideNDX = strInputLine.IndexOf(',', ifrontSideNDX + 1);

// Mid-string the field between delimiters:


strtempFieldString = strInputLine.Substring
(ifrontSideNDX + 1,
ibackSideNDX - ifrontSideNDX - 1)
.Trim();

// If possible, add field value to grand-total:


// This is a quasi ifNumeric check; see next chapter
// for better design.
// Ignore the field if Convert.To fails
try
{
fLineTotal = fLineTotal +
Convert.ToSingle(strtempFieldString);
}
catch
{
// not a valid number; skip for now
}

// Leap-frog to the next delimiter


ifrontSideNDX = ibackSideNDX;

} // End of for-next loop

// last-field processing using a Right-string:


strtempFieldString = strInputLine.Substring(ifrontSideNDX + 1);
try
{
fLineTotal =
fLineTotal + Convert.ToSingle(strtempFieldString);
}
catch
{
// not a valid number; skip for now
}

MessageBox.Show("The Line Total is " +


Convert.ToString(fLineTotal));

} // End A100 Event

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 97


To test: Remove any breakpoints by re-clicking the red ball. Then press F5, button1 to
run. Notice the line total is displayed with a Messagebox in the last statement of the
completed code. Line total = 3330.00.

Final Testing:

What would happen if strInputLine had empty data in one or more fields? In Excel, a
typical sheet would be exported as a CSV file with comma-commas, in this manner:

Fuel Expenses:,1000.00,,1200.00 (Missing a middle value)


Fuel Expenses:,1000.00,, (Missing two ending values)
Fuel Expenses:,,,1200.00 (Missing two starting values)
Fuel Expenses:,,, (this record has no values)
Fuel Expenses: (Missing delimiters - unlikely)

The program correctly loops through all of the empty fields, except for the last and that
last possibility isn't a fair option because Excel doesn't produce this type of output.

When building test data, be sure to consider the following possibilities, all of which
could exist in the input data streams:

• Test for empty fields in the first and last position. ",1000.00,1130.00,1200.00"
• Test for two empty fields in a row
• Test for all empty fields (no data)
• Test for fields that are only one character/digit wide. "Fuel:,1000.00,9,1200.00"
• Test for non-numeric data in numeric fields. "Fuel:1000.00,$1130.00,1200.00-"
• Test for non-numeric data in the first and last fields.

Tab-Delimited Files:

The previous examples processed comma-separated fields. If the file uses tab delimiters
instead of commas, the same logic is used. The only change is in the ".IndexOf" method
looks for a tab instead of a comma.

Since the editor uses a tab as an editing keystroke, a tab cannot be typed directly in the
code. Instead, represent tabs with an "escape sequence" (a code), '\t' (backslash-t).
Even though it is typed as two physical characters, consider it as a single-character.
When coding, use tic-marks instead of quotes.

ibackSideNDX = strInputLine.IndexOf('\t', ifrontSideNDX + 1);

The test data, "string strInputLine =..." can also be written with tabs instead of
commas using the "\t" sequence. The characters "\t" are typed literally into the string,
without quotes or tic marks.

string strInputLine = "Fuel Expenses:\t1000.00\t1300.50\t1200.00";

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 98


Change the index command and the sample data line to use tabs and retest the new logic.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 99


Parsing a Variable Number of Columns

Program 13.4 used a for-next loop to manually parse through an input line. The number
of fields were known and the loop ran while the counter was less than 4. This section
detects the number of columns and automatically adjusts the loop. You may be surprised
that only a few lines of code change from the previous example. This design will
become the basis for the final version, which will become a new method in the
CL800_Utility library.

A "while" loop, replaces the for-next because this is the best choice when there are an
unknown number of iterations. The big question is how to control the loop. In other
words, what makes the loop end.

When I first wrote this routine, I pondered the frontSide and


ibackSideNDX variables. Could they be used to end the while
loop? For example, when the ibackSideNDX .IndexOf command
reached the end, and didn't find a new comma, it sets itself to -1.
But I found having 2 negative-1's in the the same loop statement
confusing. In the end, I preferred a more straightforward design
– using a dedicated variable – mostly for readability.

Program 13.5; initial setup

Probably the easiest way to control the "while" loop is to create a variable that does
nothing but end the loop. Then, inside the loop, when conditions are right, set the
variable and let the loop die. When I create these types of variables, I typically give
them an "SW" suffix – "Switch" – to remind me they are True/False switches.
Admittedly redundant, given my penchant for prefixing with "bool".

Near the top of the A100 routine, above the loop, declare the new switch and initialize it
to true – allowing the loop to run. And, as before, prime the frontSide delimiter to a
negative-1, before the start of the string:

bool boolparseLoopSW = true;


ifrontSideNDX = -1; // Prime the mid-string before the string

Convert the previous program (13.4) from a for-next to a "while" loop. The while loop's
conditional is "boolparseLoopSW." Since the variable was forced to "true", the loop
runs "while true".

bool boolparseLoopSW = true;


ifrontSideNDX = -1; // Prime the mid-string before the string

while (boolparseLoopSW == true)


{
//do all the same stuff as before; including
//the mid-string, try-catch, etc.
//<see program 13.4>
}

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 100


Inside the "while" loop there must be logic that changes the loop condition to false or the
loop would never end. Each field is parsed, as before. Ultimately when the last field is
reached, the program detects a missing backside delimiter by finding its position at -1 –
this is the trigger to end the loop.

It is important to test for the -1 immediately after the ibackSideNDX is set – before any
other logic has a chance to run. The test is easy:

while (boolparseLoopSW == true)


{
//Look for the backSide (second) delimiter now:
ibackSideNDX = strInputLine.IndexOf (',', ifrontSideNDX + 1);

if (ibackSideNDX == -1)
{
//no more delimiters found; you have reached the last field
boolparseLoopSW = false; //redundant but nice to see
break; //– end the loop
}

//MidString logic:
: <the rest of the routine lives here...>

If the backside delimiter search slips to -1, immediately end the loop with a "break" –
this jumps to the statement after the while-loop's closing brace. Setting
"boolparseLoopSW" to false is redundant because the break-statement has the final say
but the while condition requires something in its parenthesis and setting the value
explicitly to false helps document the code. (Admittedly, this test is inefficient because it
runs on each field but sometimes this type of logic is unavoidable.)

Everything else in the program is the same as before, including the dangling "last-field"
processing after the loop; look to Program 13.4 for the remainder of the source-code.
Only two things changed: a "while" loop was used, and a check for -1.

The completed code is listed here (but the final version is waiting for later in the chapter.
Plus it needs a few small additional features):

Program 13.5: Parsing Tabular Data, Variable Column Count, completed

Simple comma-parsing; cannot handle embedded commas.


At the end of this chapter is a more powerful and complete CSV routine,
ParseCSV.

1 private void A100_ParseLine (object sender, EventArgs e)


2 {
3 // loop through a comma-delimited line of data, with an unknown
4 // number of fields and total the numeric fields
5
6 string strInputLine = "Fuel Expenses:,1000.00,1130.00,1200.00";

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 101


7
8 int ifrontSideNDX; // first delim; init to before the string
9 int ibackSideNDX; // second delimiter - should initialize
// to zero; see notes at the end of this
// section.
10
1 string strtempFieldString;
2 float fLineTotal=0; // Total of each field, inc. Decimals.
3
4 //Prime ifrontSideNDX to -1 so it jumps to zero on
the first loop.
5 bool boolparseLoopSW = true;
6 ifrontSideNDX = -1;
7
8 while (boolparseLoopSW == true)
9 {
20 //Look for the backside (second) delimiter one past the front:
1 ibackSideNDX = strInputLine.IndexOf (',', ifrontSideNDX + 1);
2
3 if (ibackSideNDX == -1)
4 {
5 //no more delimiters found; this must be the last field.
6 boolparseLoopSW = false;
7 break; // end the loop & let the dangling logic take over
8 }
9
30 //Mid-string the field between delimiters; replacing .split:
1 //(First-time through, the frontSide delimiter is -1)
2 //(This logic never processes the last field)
3 strtempFieldString = strInputLine.Substring
4 (ifrontSideNDX + 1,
5 ibackSideNDX - ifrontSideNDX - 1)
6 .Trim();
7
8 // If possible, add field value to grand-total:
9 // This is a quasi ifNumeric check; CH 6/7 for better logic
40 try
1 fLineTotal =
fLineTotal + Convert.ToSingle(strtempFieldString);
2 catch
3 {
4 // not a valid number; skip for now
5 }
6
7 ifrontSideNDX = ibackSideNDX; // Leap-frog to next delimiter
8
9 } // End of while loop
50
1 // last-field processing using a standard Right-string:
2 strtempFieldString = strInputLine.Substring(ifrontSideNDX + 1);
3 try
4 fLineTotal =
fLineTotal + Convert.ToSingle(strtempFieldString );
5 catch
6 {
7 // not a valid number; skip for now
8 }
9
60 MessageBox.Show("The Line Total is " +
Convert.ToString(fLineTotal));
1
2 }

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 102


end: Program 13.5

where:

• The variable "boolparseLoopSW" (switch), Lines 15 and 18, serves no other purpose
than to give the "while" loop something to chew on. A "while ( )" loop, without
parameters and a "break," would have worked just as well but the compiler does not
allow this syntax. Because of this, you are forced to give it a bone.

• Inside the if-statement, where ibackSideNDX was checked for a -1 (line 23),
boolparseLoopSW was set to false for no other reason than documentation. The next
line is a "break" that immediately ends the loop.

• The last field is not processed by the loop and is instead picked up by the last-field
routine, lines 51-58. As with the previous for-next loop example, this works well
because the routine runs unconditionally and does not require an if-statement. This
keeps the interior of the loop relatively clean.

Testing:

To test, try these hard-coded strInputLines. Each should process correctly. Note some
have more than 4 fields. As you type them, take care with commas, decimals, and
quotes.

strInputLine = "Fuel:,1000.00,1130.50,1200.00,45.00";
strInputLine = "Fuel:,1000.00,bobby,1200";
strInputLine = "Fuel:,1000.00,,";
strInputLine = "Fuel:,1000.00,1130.50,1200.00,45.00,13.25,10.00";
strInputLine = "Fuel:,,,,,,,,55.00";

A Digression: Potential Unassigned Variable Problem:

In the completed code above, notice the line at the end of the loop, in "Last-field
Processing" (line 47):

ifrontSideNDX = ibackSideNDX; // Slide or Leap-frog

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 103


The two variables are set to the same value, in preparation for a re-loop, priming the
ifrontSideNDX for a new starting position. Near the top of the loop, the program looks
ahead for the next delimiter's position. If it happens to be at the end of the record, the -1
check runs and the final leap never happens. Because the loop (abruptly) ends with a
break-statement, this leaves both the frontSide and ibackSideNDX variables set to the
same value.

After the loop, in the Last-field processing, this line is poised to run:

// last-field processing using a Right-string:


strtempFieldString = strInputLine.Substring(ifrontSideNDX + 1);

What would happen if the variable ifrontSideNDX were replaced with


ibackSideNDX? It should work because both values are the same value. But
if you try this (swap the variable names now), the compiler complains about
an "unassigned variable." How can this be when both are equal?

Returning the original line's variable back to "ifrontSideNDX" (resolving the compiler
error), use the debugger with a breakpoint and check the values of the variables. You
will find both are set the same. Was the variable forgotten? Did the variable fall out of
scope when the loop ended? No. Even a "diagnostic" MessageBox shows both numbers
set correctly. And the most telling evidence against the "unassigned variable" message is
"ibackSideNDX" was set to a value three times previously in the loop! Clearly the
variable was assigned. Why does the compiler error when using the ibackSideNDX
variable and yet the ifrontSideNDX works?

The reason:
The compiler knows "while loops" may or may-not execute the first time, depending on
the conditional statement. In other words, the while-loop may never run and the logic
would jump directly to the last-field statement (with the ibackSideNDX). Knowing this
is a possibility, the compiler protects itself from having an un-initialized ibackSideNDX.
If the loop never ran, the ibackSideNDX has a possibility of not being set.

The compiler isn't smart enough to notice the "boolprocessLoopSW" is true – this isn't
resolved until run-time. Because of this, you get the "unassigned variable" error before
the program has a chance to run.

ifrontSideNDX survives this pre-compiler check because at the top of the module it is
initialized to -1, and has a value, no matter how illogical. Thus, if you wanted to use
ibackSideNDX, initialize it when declared, changing line 9 to int ibackSideNDX = 0;.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 104


Parsing ASCII CSV Files with Embedded Commas

"CSV" (Comma Separated Values) are comma-delimited ASCII files containing one
record per line – with the distinguishing factor being a comma as the field-separator
rather than a tab. This file format is often used to move data from one type of program to
another and Excel and most databases are particularly fond of this format. TAB files are
similar in design and are easier to work because they do not have the idiosyncracies that
CSV's have, but for some reason, CSV files are more prevalent. Both CSV and Tab files
are a low-tech but reliable method for moving data between unrelated programs.

Commas cause problems in CSV files:

The files in this section are similar to the formats seen earlier in this chapter,
except there is one twist that makes them annoyingly difficult. If an
individual field contains an embedded comma then the field is surrounded in
quotes.

C#'s native .Split command was elegant because all parsing was resolved with a single
line of code and all delimiters were predictable. But now, because of the embedded
commas and quotes, the .split method fails and a more sophisticated approach is
required.

Consider how Excel (and all other programs that can generate a .CSV file) handles
columnar data.

Date, Type, Item, Qty, Amount


1/1/08, Sale, Blue Jeans, 1, 33.97
1/2/08, Sale, "Sweater, Blue" 1, 14.95

Excel knows that column-C (Item) is one "thing" and it knows the extra comma will
upset the CSV field-count. To work around this, the field is surrounded with a pair of
quotes. However, those cells without an embedded comma will not have quotes, making
the record appear inconsistent and this complicates the parse.

Both the manual and automatic routines described earlier in this chapter go nuts when
they find an unexpected comma. In particular, the .Split command becomes useless.

With minor changes the manual parsing routines can work around this. Of course, all of
this is a good reason to use "Tab-delimited" instead of "comma-delimited" files – but
many programs still write comma-separated value files and you have to deal with them.

In the examples that follow, this issue will be resolved with a routine that
properly detects the quotes and preserves embedded commas. Later, the
program will be enhanced to read an ASCII input file and it will be able to
recover from data errors. Ultimately, the goal is to add a new parsing
routine to the CL800_Util library: "ParseCSVLine".

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 105


Modifying Program 13.6 for CSV:

With any field, if quotes are found in the first position, the parsing routine has to switch
to a different parsing method – one that uses the quotes. Because you can't predict which
fields will have quotes, each field has to be examined. Yes, this is a pain.

Once this issue is resolved, you will have a generic parsing


routine that can be used with any CSV file. This routine will be
placed in the CL800 utility library and you will never have to
write this routine again.

1. As in earlier example, have BtnProcess (button1) call A110_ReadTextFile();

private void BtnProcess_Click(object sender, EventArgs e)


{
A110_ReadTextFile();
}

Create the A110 module by hovering the mouse over the line and clicking the lightbulb
icon or manually type the method using this next step:

2. A110_ReadTextFile will house all the logic to open a test ASCII file, and read each
detail line. This logic is not needed until later, and to keep this example from being
unnecessarily cluttered, the ASCII data will be simulated.

Instead of using "strInputLine," change the variable name to strReadLine, which you
should recognize from the previous chapter. This represents a single line from an input
file. When keying the default value, note the backslash-quotes – because one of the
fields ["Sweater, blue"] has embedded quotes and an internal comma:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 106


A110_ReadTextFile, initial re-write
private void A110_ReadTextFile()
{
//Simulate a data line:
string strReadLine = "1/2/2008,Sale,\"Sweater, Blue\",1,14.95";

//Declare an array to hold the resulting parsed values:


string [] astrinputFields;

//Then, call a new module to do the actual parsing


//Results will be returned into the array.
astrinputFields = A115_ParseCSVLine (strReadLine);

The parsed data also needs a place to live. Declare an unsized string array, and do not
populate it with default data. This is a string array, as denoted by the opening and
closing [brackets]. This is the same design as the .Split command discussed earlier.

string[] astrinputFields;

Finally, A110 needs to call a new routine to do the actual parsing. Why a new routine?
A110_ReadTextFile should do one thing... read an ASCII file. The name does nothing to
suggest it also parses. Both the Read and Parse have significant logic and complexity
and because of this, they should be separated.

Write the call now, as illustrated above:

astrinputFields = A115_ParseCSVLine (strReadLine);

In this new routine, instead of returning a string, an entire array of strings is returned. In
other words, "astrinputFields" is on the left-side of the equal-sign.

Compare this to most of the routines you have written so far in this book: A string was
passed to a downstream module and that routine often returned another string. For
example, a util.LeftStr might return a value into strtemp:

strtemp = util.LeftStr("Some test string of interest", 4);

3. After typing the A115 statement, hover the mouse over the statement and click the
lightbulb icon. Allow it to generate the A115 module:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 107


Resulting in a new method with the signatures already built. It accepts a string as input
and will return an array:

strReadLine is being passed into A115_ParseCSVLine, much like "some test string of
interest" was being passed to the LeftString. When A115_ParseCSVLine completes its
work, it will return a fully-formed string-array. The array is expected to contain 5
elements, but because the parse routine can handle any number of columns, any number
of strings can be returned.

Interior A115 Loop Details:

This logic used to live in A110 and is now being moved


to A115_ParseCSVLine.

A115's goal is to read a sales figure from a future-ASCII file and parse into constituent
fields. The parsing logic written in program 13.5 is almost right, needing only a change
to dance around embedded quotes and commas and it is being moved into a separate
module. Here is the pseudo-code:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 108


As before, loop through the line, separating one field from the next. The key difference
between this version and Program 13.5 is a series of if-statements that decide what type
of delimiter is being used. Beyond that, the logic is the same.

4. Begin writing A115_ParseCSVLine now by declaring the same variables and loops as in
the previous parsing routines. strInputLine is being replaced with strReadLine and a
counter is being added to keep track of how many fields were found.

Additionally, a new temporary array (astrfoundFields) will hold each of the found fields
and this is what will be returned to the calling routine. At line 13, notice the array is
declared with 500 positions - more than the number of expected fields in any CSV:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 109


Program 13.6, initial parsing using 13.5's logic
1 private string[] A115_ParseCSVLine (string strReadLine)
2 {
3 // loop through a comma-delimited line of data, with an
4 // unknown number of fields. Store results in an array
5
6* //sample input: "1/2/2008,Sale,\"Sweater, Blue\",1,14.95";
7
8 int ifrontSideNDX = -1; //Prime to jump at first field
9 int ibackSideNDX = 0; //Must initialize to zero for last-field
10 int ifieldCount = 0;
1
2 string strtempFieldString;
3 string[] astrfoundFields = new string[500]; //overallocated
4
5 bool boolparseLoopSW = true;
6
7 while (boolparseLoopSW == true)
8 {
* //For each field, look at the frontSide delimiter +1 to see
* //what type of delimiter it is; it could be comma or quote:
*
* if(util.MidStr(strInputLine, ifrontSideNDX +1,1) == "\"")
* // <the csv field has quotes>
* else
* // <the field is only delimited by commas.

Review the pseudo-code. The first-step in the loop's interior detects the backSide
delimiter's position (e.g., the second comma). However, there is a twist because the
program has to figure out which delimiter to use – it will be either a comma or a quote.

An if-statement decides which path to choose. It looks at the first delimiter and peeks
one character to the right. If a quote is found, then the string must have an embedded
comma and you will use quotes for both the front and backside delimiters, otherwise, the
same comma-logic as before.

The check is a one-character mid-string (using the CL800_Util libraries from Chapter 8).
Starting at the front-side delimiter, skip one character to the right and substring the
position. Naturally, you can write your own mid-string. Compare the found-character
with a quote ( \" ):

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 110


where:

• The ifrontSideNDX delimiter's position was set by the previous field-search (when it
leap-frogged), or it was set to -1 on the first-time through.

• A one-character mid-string, starting at the frontSide-delimiter's position, plus 1 is


compared against a literal-quote. If the original string contained [,Blue Jeans,] –
there is not a quote and it could safely assume a normal comma-delimiter. But if the
found string +1 is a quote, as in [,"Sweater, Blue",], it would know to change the
backside delimiter.

• Since quotes are a reserved character, an escape-code was used, much like a "\r\n"
(crlf) or a "\t" (tab). A backslash-quote ( "\"" ) marks the single character. As usual
with escape-codes, the two symbols, \" represent one physical character – a quote.
A verbatim == @""" does not work here.

• Since util.MidStr() only returns strings, the results had to be compared with a
string value (not a character value)

The first-time through the loop, the frontSide delimiter's position was -1, priming the
loop. Within the MidString, a +1 shifts it to position zero. Even on the first field it
properly detects quotes.

5. With the delimiter decided, the program skips past the frontSide delimiter and locates the
other-side using a standard .IndexOf command. In pseudo-code, the logic looks like this:

if (Delimter == "\"")
Locate the backSide delimiter, quote, by skipping 2 past the frontSide and
looking downstream. If no backSide delimiter is found, you must be on the last
field... Use a standard MidString (with special length considerations)
else
Locate the backSide delimiter, comma, by skipping 1 past the frontSide.
If no backSide delimiter is found, you must be on the last field...
MidString (with normal length considerations)

Converting the "if-side" to actual code, the results read like this (a completed version of
the entire routine can be found in a few pages):

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 111


Program 13.6, A115 Quote-delimiter modifications, in progress
:
:

15 bool boolparseLoopSW = true;


6
7 while (boolparseLoopSW == true)
8 {
9 //For each field, look at the frontSide delimiter +1 to see
20 //what type of delimiter it is; it could be comma or quote:
1
2 if(util.MidStr(strReadLine, ifrontSideNDX +1, 1) == "\"")
3 {
4 // The CSV field has quotes
5* // Search for the backSide quote; be sure to jump +2 past
6* // the frontSide-delimiter's position [,"Sweater, Blue"]
7* ibackSideNDX =
strReadLine.IndexOf("\"", ifrontSideNDX + 2);
8*
9* if(ibackSideNDX == -1)
30* {
1* //No delimiters left to process; must be last field
2* boolparseLoopSW = false; //For documentation
3* break; //end the loop
4* }
5*
6* //Capture the found-field using a manual mid-string...
7* strtempFieldString = strReadLine.Substring
(ifrontSideNDX + 2,
ibackSideNDX - ifrontSideNDX - 2).Trim();
8*
9* //The ibackSideNDX needs to shift one char to the right,
40* //moving from the quote to the comma...
1* ibackSideNDX++;
2 }
3 else
4 {
//The field is only delimited by commas
:
}

The else-side (where the delimiter is only a comma) follows the same logic, with only
minor changes in the .IndexOf and mid-string statements: The difference is a shift of
only +1 (a comma) instead of +2 (a comma and a quote):

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 112


...parsing CSV Files; else-side
:
43 else
4 {
5 //The field is only delimited by commas - standard comma
6* ibackSideNDX =
strReadLine.IndexOf(',', ifrontSideNDX + 1);
7*
8* if (ibackSideNDX == -1)
9* {
50* //no delimiters left to process; must be last field...
1* boolparseLoopSW = false;
2* break; //end of loop
3* }
4*
5* //Capture the found-field, using a manual mid-string:
6* strtempFieldString = strReadLine.Substring
(ifrontSideNDX + 1,
ibackSideNDX - ifrontSideNDX - 1).Trim();
7*
8* //The ibackSideNDX is positioned properly at closing comma.
9 }
60
1 //Assign found-field to array position, starting at base-0

// continue the loop here...

Both the "if" and the "else" side are essentially the same code discussed earlier (Parsing
Tabbed lists, unknown number or columns). If studied for a minute, the logic is straight
forward with the only complexity in the midstring.

6. As the logic flows past the if-statement, the field in question has been correctly parsed
and the results are waiting inside strtempFieldString. Move the strtempFieldString into
the array, which was defined at the top of the routine.

The first parsed field goes in array position [zero], the second goes in [1], etc. To keep
track of the count, use an integer counter, ifieldCount. What you have done here is build
your own .Split routine.

After the if-statement's closing brace, at line 61/62:

• Move strtempFieldString to the the holding array's [ifieldCount] position, where the
first position is zero.
• Then increment the field counter for the next loop's iteration.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 113


...adding the found field to the array
: (continuing after the else-closing brace)
58 //The ibackSideNDX is positioned properly at closing comma
9 }
60
1 //Assign found-field to array position, starting at base-0:
2 astrfoundFields [ifieldCount] = strtempFieldString;
3 ifieldCount++;
4
5 //Leapfrog to the next pair of delimiters; in prep for reloop:
6 ifrontSideNDX = ibackSideNDX;
7
8 } // end while-loop

Remember, after the if-statement, you are still in a while-loop, waiting to process the
next field.

7. As the loop runs, it ultimately runs out of backSide delimiters, leaving the last field
"dangling." Process this last field manually. This last field needs to go through the same
logic as the internal fields, looking for embedded commas and quotes.

When all is done, the array is populated. Using an Array.Resize command (which is
admittedly covered in more detail in a few chapters), resize the array from its over-
allocated [500] to its final size. Return the array to the calling module, A110.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 114


Program 13.6 A115 - process the last (dangling) field; completed
:
61 //Assign found-fields to array positions, starting base-0:
2 astrfoundFields [ifieldCount] = strtempFieldString
3 ifieldCount++;
4
5 //Leapfrog to the next pair delimiters; in prep for reloop
6 ifrontSideNDX = ibackSideNDX;
7
8 //A special check for the last field
9 if ((ifrontSideNDX + 1) > strReadLine.Length)
70 {
1 //this last field must have been a quoted string and it
2 //was already processed. Nothing left to do
3 boolparseLoopSw = false;
4 }
5
6 } // end while-loop
7
8 //Last-field processing; use a standard Right-string to
9 //parse the data.
80 //Remember to take into account possible quoted strings
1 //This is the same logic as inside the loop
2
3 if((ifrontSideNDX + 1) > strReadLine.Length)
4 {
5 //The last field was a quoted string and was already
6 //parsed in the loop above. Do not process
7 ifieldCount–; //Subtract one!
8 }
9 else
90 {
1 //ifieldCount++ //already accounted for
2 strtempFieldString = strReadLine.Substring
(ifrontSideNDX + 1; //Standard rightstring
3 astrfoundFields[ifieldCount] = strtempFieldString;
4 }
5
6 Array.Resize(ref astrfoundFields, ifieldCount + 1);
7 return astrfoundfields;
8
9 } //A115 end

The entire program listing is found in the next section.

Testing:

A. Have BtnProcess_Click (butto1) call A110_ReadTextFile()

B. In A110, create a test "strReadLine" variable and allocate a holding array to house the
results. For testing, a sixth field was added to help test other quoted strings. Note some
of the test strings contain empty fields, two quoted strings in a row, unquoted last strings,
etc.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 115


A110 Testing ParseCSVLine
private void A110_ReadTextFile()
{
//string strReadLine =
// "1/2/2008,Sale,\"Sweater, Blue\",1,14.95,\"Test, Test\"";
//string strReadLine =
// "\"1/2/2008\",Sale,\"Sweater, Blue\",1,14.95,Test Test";
//string strReadLine =
// "\"1/2/2008\",\"Sale\",\"Sweater, Blue\",1,14.95,Test Test";
//string strReadLine =
// "1/2/2008,Sale,\"Sweater, Blue\",,,Test Test";

string strReadLine =
"1/2/2008,Sale,\"Sweater, Blue\",1,14.95,Test Test";

string[] astrinputFields;

astrinputFields = A115_ParseCSVLine(strReadLine);

//for testing, add a red-ball breakpoint to the closing brace


//by clicking in the grey-margin, next to the line numbers
}

where:

• string[] astrinputFields will hold each of the parsed values.


• Place a break-point at the routine's closing brace (click in the grey-margin next to the
line number to add a "red-ball" breakpoint

Press F5 to run the program. Click BtnProcess to run.


When the compiler stops, highlighting the closing brace with a yellow-highlight, click
once in the editor to activate the screen.

Then, on the top-menu, click "Debug", Windows, "Immediate". This opens the
Immediate pane.

Click inside the "Immediate Window".


Type this word (case-sensitive): astrinputFields
Press Enter

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 116


Results: Peering inside of the array, you should find all six fields parsed and ready.
Embedded commas were handled properly. Other logic, such as a foreach loop, could
process the array and this is covered shortly.

The next step is to open an ASCII input file and read multiple lines. The code above is
in a good position to make these improvements with only a few code changes.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 117


Building Sample Data With Excel:

The next phase in the parsing program replaces the hard-coded test string (strReadLine)
with a multi-lined ASCII text file. This file will be opened using the ASCII Read
techniques from the previous chapter. Each line will be fed into the
A115_ParseCSVLine routine and an array will be returned. Because of the design, it
will be easy to insert the steps into A110_ReadTextFile and pass the values into the
subroutine.

Before doing this, use Microsoft Excel (or with more difficulty, Notepad), create a test
data file with the following column headers, starting in Column A, Row 1:

Column A: Date
Column B: TransactionType
Column C: ItemDescription
Column D: Qty
Column E: Amount

Starting in Row 2, type in any reasonable test data. Add a several dozen rows. Make the
dates sequential (Jan-1, Jan-2, etc). For the transaction "Type" column, use the word
"Sale" with an occasional "Return". The Description column can contain any short
product description - but for now, do not include embedded commas. The quantity
column contains integer data. The Amount column can be a mixture of integer and
floating (dollars.cents).

For now, do not introduce any data-errors, blank cells, or embedded commas. See the
Excel illustration below for a rough layout of the spreadsheet, as well as an illustration
showing the final CSV file output in Notepad. Read further to see how to build the CSV
exported file from Excel.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 118


Example Data:

Date Type Item Qty Amount


1/1/2008 Sale Pants 1 11.95
1/2/2008 Sale Sweater 1 14.98
1/3/2008 Sale Coat 2 17
1/4/2008 Return Gloves 1 5.99

Possible Output showing extended calculations:


1: Pants 11.95
2: Sweater 14.98
3: Coat 34 (17.00 x 2)
+ grand totals showing total rows, total Qty, total extended price.

Exporting Sample Data as CSV:

In Excel, build the test data, as described above. Be sure to include the header column in
row 1. Create about a 100 lines of detail by using Excel to copy the initial example data
and repeat them multiple times. Then, randomly pick and choose records, making slight
individual changes. Do not worry if you have duplicate detail lines. (If you do not have a
spreadsheet, use Notepad to create the CSV file, as illustrated in the inset, above.)

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 119


While still in the spreadsheet, do the following to export the file (if using Notepad, skip
these steps):

1. In Excel, select File, Save As.


2. Type "C:\data\transactions.csv" (type quotes around the name and assumes the
data folder was already in place)
3. Choose "CSV" from the "Save As Type" from the pull-down.

4. At the "The selected file type does not support multiple sheets." prompt, click OK.
5. If prompted, click Yes at the "...may contain features that are not compatible"
prompt.

View the resulting file:


6. Start, Run, "Notepad.exe"
7. Select File, Open, type this name: "C:\data\transactions.csv"
8. Confirm the file looks similar to the inset in the illustration above. This is the
comma-separated-value ASCII file.
9. Close Notepad
10. Close Excel; optionally save the sheet again as an XLS file or discard.

Program 13.7; Reading and Parsing ASCII CSV

Modify your a new C# Windows Form project (e.g. Form1), by adding the following:

multi-lined textbox,
Label, "label1"
Button, named BtnProcess.

Double-click the button and stub-in a call A110_ReadFile; which should be set from the
previous examples. The form should look like this, with values added as the program
runs:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 120


Here are some hints on how the program will run:

In BtnProcess (button1), use a StreamReader class to open the file (from Chapter 12).
Read the first record with a priming read. Since the first record is a Header row, a
second priming read is needed to arrive at the first detail record.

Each record is dropped into a temporary variable called "strReadLine" and this is passed
into the already-written parsing routines.

The previous example's design purposely split the ASCII Read from the parsing modules.
Separating the two keeps the READ from being cluttered by a bunch of substringing
logic and it will allow you to later move the A115_ParseCSVLine method to the utility
library and you will never have to write this logic again.

Write A110_ReadFile.

In A110, declare a StreamReader variable, open the file with 2 priming reads, once
strReadLine is populated, send it into A115 using the same steps as in the previous
section. Attempt these steps now, before reading on.

Program 13.7; A110 Read ASCII, completed

1 private void A110_ReadFile()


2 {
3 //Declare the input file-name as a variable; a good practice:
4 string strinputFileName = @"C:\data\transactions.csv";
5
6 string [] astrinputFields; //array for split routines
7 string strReadLine; //working string for current ascii
8
9 int irecordCount = 0;

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 121


10 int itotalQty = 0;
1 float ftotalSales = 0.0F; //float - grand total Extensions
2
3 try
4 {
5 StreamReader sales = new StreamReader(strinputFileName);
6
7 //Priming Reads:
8 //Read header row and first record with two reads
9 strReadLine = sales.ReadLine();
20 irecordCount++
1 strReadLine = sales.ReadLine(); //(other counts below)
2
3 //Reminder for array astrinputFields:
4 // [0] = Date
5 // [1] = Type (Sale, Return)
6 // [2] = Description
7 // [3] = Qty
8 // [4] = Price
9
30 while (strReadLine != null)
1 {
2 irecordCount++;
3 int ilineQty = 0;
4 float flinePrice = 0.0F;
5 float flineExtension = 0.0F; //float Qty*Price = Extension
6
7 //Parse all CSV fields by their comma
8 astrinputFields = A115_ParseCSVLine(strReadLine);
9
40 try
1 {
2 //Convert each detail to their proper number types:
3 ilineQty = Convert.ToInt32(astrinputFields[3]);
4 flinePrice = Convert.ToSingle(astrinputFields[4]);
5
6 //Calculate this line's extension and
7 //add to grand totals:
8 flineExtension = ilineQty * flinePrice;
9 itotalQty = itotalQty + ilineQty;
50 ftotalSales += flineExtension; //alternate way
1
2 //append the output to the textBox for
3 //viewing pleasures:
4 textBox1.Text +=
Convert.ToString(irecordCount) + ": " +
astrinputFields [2] + " " +
"Extended: " +
Convert.ToString(flineExtension) +
"\r\n";
5
6 }
7 catch
8 {
9 MessageBox.Show
("Some type of numeric problem with line: " +
convert.ToString(irecordCount);
60 }
1
2 //Read the next line in the ASCII file's record loop:
3 strReadLine = sales.ReadLine();
4 }
5
6 sales.Close();

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 122


7 sales.Dispose();
8
9 label1.Text = "Items: " + Convert.ToString(irecordCount) +
" " + "QTY: " + Convert.ToString(itotalQty) + " " +
"Sales: " + Convert.ToString(ftotalSales);
70 }
1 catch (Exception e)
2 {
3 Label1.Text = "Internal program error";
4 MessageBox.Show("A110: file Problem : '" + strinputFileName +
"'" + "\r\n" +
e.Message);
5 return;
6 }
7 }

where:

Line 4 Sets the input file's name using


string strinputFileName = @"C:\data\transactions.csv";
where the string uses a verbatim indicator, "@", and the backslashes do not
have to be doubled-up (Chapter 4).

Line 6 astrinputFields[] is a string array of undetermined length that is


populated by the new A115 parsing routine.

Line 15 A new StreamReader class is instantiated as "sales". It is protected by a try-


catch incase the file is not found. From Chapter 12, you could add more
sophisticated error-trapping.

Line 19 Reads the CSV's "Header" record, which isn't needed by the program. The
line is discarded, but counted as a line-number to help in diagnostics, should
a record show up as a problem A second priming read arrives at the first real
record.

Line 30 Begins a standard ASCII-file-read loop. At the top of the loop, selected line-
item variables are allocated. These will be used to convert the string QTY
and SALES figures to integer and floating point numbers. Once converted,
you can perform math operations on them.

Floating point numbers are initialized as 0.0F, per Chapter 5.

Initializing the line-item variables within the loop is a neat concept. At the
bottom of the loop, the variables are discarded and re-initialized. This way,
prior values are not accidentally introduced if the next record happens to be
blank.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 123


Meanwhile, the grand-total variables, itotalQty and ftotalSales are declared
above the loop. They need to survive each new record.

Line 38 This is where the current ReadLine is parsed into individual fields.
strReadLine is passed into A115, and a five-position array is returned.

Within the loop, each field can be looked at by their position in the array:

[0] = Date
[1] = Type (Sale, Return)
[2] = Description
[3] = Qty
[4] = Price

when a new record is retrieve, this array is re-built

Before A115, you could have parsed the readline string with this command:

astrinputFields = strReadLine.Split(',');

But it would have failed with embedded commas.

Line 43 Calculates the line item quantities - for that specific line. Again, these
calculations are protected with a nested try-catch.

Line 49 Takes the same line-values and adds them to the grand totals.

Line 54 "textBox1.Text += ..." assembles the found information and displays it in


the textBox. Notice how it uses a mixture of temporary calculated variables
and current fields (using square brackets).

Discussion on the Methodology:

After the (A115 -.Split) would you have considered processing the returned array with a
nested loop, along the lines of:

foreach (string strfoundField in astrinputFields)


if (field-1) skip...?
if (field-2) skip...?

Although earlier chapters recommended a foreach loop when working with arrays, in this
scenario the idea doesn't work as well because there is no need to examine the Date,
Type, or Item columns. In other words, what would you do with "each" field as the loop
skipped from field to field? You would have to add a bunch of logic that said "if field-1,
skip. if field-2, skip..."

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 124


Problems with "Single ftotalSales"?:

You may have had syntactical problems with line 11, "float ftotalSales;" and later
"Convert.ToSingle". "float" is an old-school alias for "Single" and float is lower-cased,
while Single is upper-cased. The declaration "int" is an alias for the real data-type
"Int32". C# is inconsistent in this area, just because of convention. Be careful of the
casing.

You may have also had a problem with "Use of unassigned local variable 'ftotalSales'"
(line 11), with the compiler complaining specifically about this later statement:

label1.Text = Convert.ToString(irecordCount) + ": " +


Total: " + Convert.ToString(ftotalSales);

To simulate this error (see code, above), remove the "=0" from the fTotalSales
declaration, line 11. And try running the program.

This has been discussed before. The "while" loop has a "possibility" of never executing
– you know it will always execute, but the compiler does not share your confidence.
Because of this, the compiler flags this as a potential error.

In reality, the compiler isn't off the mark. Consider this situation: Imagine if the
transactions.csv file were empty, having a header-line but no detail records? The file
would exist; the file would open; the first record would read, but the loop wouldn't run.
End-of-file was reached after the first (header) record and ftotalSales does not populate
with a value. In other words "ftotalSales" would never be initialized and the compiler
would be right to complain. This is why line 11 is initialized with =0.

Performance Issues:

If your test file were large enough, say several hundred lines, you may have seen a delay
after clicking btnProcess. Because each of the 200 detail line are displayed (and
appended to) textBox1, performance will be lack-luster. The same issue was seen in
Chapter 2 (Program 2.3). Performance can be improved by moving all the write-
operations to an internally-declared variable and avoid the user-interface until the last
possible moment.

At line 11a, insert a new declaration and initialize it for the same reasons that ftotalSales
was initialized:

string strtempString = "";

At line 54, modify the statement so the results are appended to the newly-declared
strtempString: change the statement from

Instead of: "textBox1.Text += ...", change to


strtempString += ...

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 125


Finally, after the loop ends, and the file is closed, move "strtempString" to textBox1 in
one fell-swoop by adding this command at Line 67a:

textBox1.Text = strtempString;

Here is what happens when you make this minor change. Instead of writing (200) lines
of output, one line at-a-time to textBox1, the computer can shuffle them off to an internal
variable. This must be a thousand times faster than forcing the computer to fiddle with a
user-visible textBox and all the formatting that requires.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 126


Error Processing in CSV Files

Even though the program correctly raced through the file and did flawless work, it is
incomplete without considering data errors. The odds of having flawed data in a CSV
are high. It is typical for a program to spend 30% to 40% of its logic dealing with error-
processing. The current program is susceptible to each of these data errors:

• Blank row in the file


• Missing dollar amount
• Missing Qty
• Non-numeric Qty or Amount
• Empty Files with header records but no detail data

Where is this leading? When all the concepts in this chapter are
combined into one routine, it will be a module that can parse
any tab or csv-delimited file, reliably, without error, and without
having to repeat any of this logic. The entire routine will be
encoded into one module in a utility library.

Bad Data:

Modify the test data in the CSV file by introducing data errors. It is probably easiest to
make the changes in Notepad, rather than Excel.

When testing, make one data-error at a time. Test, then once resolved, introduce
another error. Follow these steps:

1. Start, Run, Notepad.exe


File, Open "C:\data\transactions.csv"

2. Four or five lines down in the file, insert a blank line.

3. (Later), on another random record, remove a Qty number, leaving the comma.
Then on another, change a dollar amount to "Bob".
Find a record and remove a dollar amount, leaving a comma.

4. Save the Notepad file.

Make changes to the records near the top of the list. The illustration below demonstrates
some possible changes. As illustrated, the table is expanded for readability – but the
actual document is "scrunched up"; be sure to leave it compressed. Close Notepad and
save the changes:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 127


Modifying Program 13.5 to Accommodate Data Errors:

After introducing an error, run the previously-written test program (Program 13.6) and
watch what happens. The try/catch intended for File-open problems is still active, and
captures all errors until intercepted with the inside try-catch. The Inside-catch solves the
immediate problem, keeping the program from crashing with numeric problems, but you
and your users need better error-trapping. We deserve a better description of the
problem – including the offending record's line number and perhaps the field in question.

You have probably see other programs that crash with a


message along the lines of "Error detected" – with no hint on the
nature of the error. You can guess where they probably coded
the try/catch. It takes only a few moments to write better error
logic.

The CL800 utility routines IsBlank, IsFilled, IsNumeric will be particularly useful; be
sure they are linked in (see Chapter 8, Libraries).

Testing for Blank Records:

Start with the easiest problem first – blank records. Try and write the logic now before
reading further – this will happen in A100_ReadTextFile. As you think about this,
decide what you want to do with a blank record: is this error egregious enough to end the
program or should you ignore the record and continue reading? Perhaps it is better to
ask if blank lines are even a problem; they may be a natural part of the data. For this
example, assume blank records are expected; skip past them and continue reading the
remainder of the file.

Test for blank or empty records at the right location. The test should happen within the
main record-logic, typically near the top of the loop, before any real work is done.

A first (and incorrect) response to a blank record might include a test for
IsBlank with a "continue;" (recall, "continue" instructs the loop to stop and
immediately move to the loop's closing brace). Study the following

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 128


illustration for a moment. The test is in the correct location and is reasonable,
but the action is wrong:

When a blank record is found, "continue" says to "reloop." Mentally you


may be thinking "Reloop and get the next record" – the trouble is, the next
record "isn't got". Instead, the program loops to the while-statement, missing
the read-next statement! strReadLine is still populated with the previously-
found "blank record." Looping around to the top, it detects the same blank
and loops again. This is an infinite loop.

The program would "run" with no compiler errors -- but nothing would happen. The
screen would not change, there would be no hour-glass, and you would not get mouse-
control back. Ultimately the program would have to be forcibly stopped by clicking the
editor's Red-Square "Stop" button.

To resolve this, flip the IsBlank statement up-side-down, testing for IsFilled instead of
IsBlank. Add the if-statement and its opening brace at lines 36a and 36b. Put the closing
(end-if) brace at line 61a , which is just above the next-record Read:

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 129


The if-statement now works as intended: Blank records bypass the detail logic – but the
read command runs unconditionally – regardless of data. This is a good solution for this
type of problem.

As an aside, the "irecordCount" statement could be placed above the if-statement or


within the if-statement. If placed above, it counts which physical line you are in the file
– even if the line is empty. If it is placed within the if-statement, it counts all "non-
blank" records, telling you how many valid records there were. Position this according
to your needs. There are arguments for both locations.

Amounts and Qty Problems:

When Excel exports a CSV, it treats empty fields as it would any other, marking the field
with a beginning and ending delimiter. Empty fields appear as two commas in a row,
" ,, " (comma-comma). Also, as Excel exports the fields, it does not audit for
reasonable numeric values. Note these three records; some with empty fields and some
with bad numeric data.

1/4/2008,Sale,Gloves,,5.99 //Missing Qty (comma-comma)


1/5/2008,Sale,Mittens,, //Missing Qty *and* Amount
1/3/2008,Sale,Coat,2,Bob //Non-numeric Amount
,,,, //All empty records technically look
// like this but there can still
// be blank lines

Both the traditional '.Split' and program 13.7 treats each field as string even if it
contains numeric or empty data. In other words once parsed, the returned values are
always strings and they live in a string array.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 130


At some point the two numeric-like string fields need to be converted to numeric in order
to do the math. Disaster is sure to follow if non-numeric data sneaks into a line-total
calculation.

There are several ways to handle bad or missing data but the easiest is to parse each field
as-if normal, even if "empty," and then check the results for numerics. Remember, C#
does not have a good way to test for numeric data; you have to provide that code
yourself. The numeric functions in CL800_Util make this easy.

Adding a Numeric Check:

As a reminder, each field was split or parsed into an array. The code can directly
examine Qty, and Amount for 'numeric-ness' by referencing the field's array position
(base-zero), as in:

astrInputFields[3] and
astrInputFields[4]

Chapter 7's "IsNumeric" method allows one decimal point and an optional sign. Since
the function has already been written and is well-trusted, it is easy to add. Insert these
statements at lines 41a, and b including the opening brace. Note the double-ampersands
(AND); which makes the calculations more efficient:

//Check for numerics before running the calculations:


if (util.IsNumeric(astrInputFields[3]) &&
util.IsNumeric(astrInputFields[4]))
{

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 131


At lines 55a place the else/end-if clauses. The intervening code should automatically
indent when the closing braces are typed. Here is a partial code snippet:

Program 13.7; Simple CSV ASCII File Read and Total; Part II preliminary

30 while (strReadLine != null)


1 {
2 irecordCount++;
3 int ilineQty = 0;
4 float flinePrice = 0.0F;
5 float flineExtension = 0.0F; //float Qty*Price = Extension
6
36a if(util.IsFilled(strReadLine) //Previous blank-line check
36b {
7 //Parse all CSV fields by their comma
8 astrinputFields = A115_ParseCSVLine(strReadLine);
9
40 try //This try-catch is now redundant
1 {
41a if(util.IsNumeric(astrinputFields[3]) &&
util.IsNumeric(astrinputFields[4]))
41b {
2 //Convert each detail to their proper number types:
3 ilineQty = Convert.ToInt32(astrinputFields[3]);
4 flinePrice = Convert.ToSingle(astrinputFields[4]);
5
6 //Calculate this line's extension and
7 //add to grand totals:
8 flineExtension = ilineQty * flinePrice;
9 itotalQty = itotalQty + ilineQty;
50 ftotalSales += flineExtension; //alternate way
1
2 //append the output to the textBox for
3 //viewing pleasures:
4 textBox1.Text +=
Convert.ToString(irecordCount) + ": " +
astrinputFields [2] + " " +
"Extended: " +
Convert.ToString(flineExtension) +
"\r\n";
54a }
54b else
54c {
54d textBox1.Text += "Error at line: " +
Convert.ToString(irecordCount) + "\r\n";
54e }
5
6 }
7 catch
8 {
9 MessageBox.Show
("Some type of numeric problem with line: " +
convert.ToString(irecordCount);
60 }
61a } //BlankLine-check end-if
1
2 //Read the next line in the ASCII file's record loop:
3 strReadLine = sales.ReadLine();
4 }

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 132


This makes the previous try-catch at line 40 and 57 redundant. I prefer an explicit
(numeric) test rather than a general try-catch.

Testing the program:

Using Notepad, edit the ASCII text file "C:\data\transactions.csv" and confirm you have
"bad" data in some of the records. Press F5 to run, then click btnProcess. Confirm all
the records, except the bad ones, appear in textBox1.

Bad Data Traps:

In the snippet above, bad data was ignored. Consider the following:

• Count the number of bad-records and display the count to the end-user as an after-
the-fact message.

• Stop all processing when an error is found and insist the user correct it and re-try the
program later.

• Write the bad records to a new (error-log) file and process the remaining records.

• Display the bad record to the user and offer them a chance to correct or ignore.

Moving ParseCSVLine to the Utility Library:

The function A115_ParseCSVLine seems useful enough to move into the CL800_Utility
library, making it re-usable. When moving a module to a generic class, ask these
questions:

1. Can the routine stand alone? Does it rely on other modules within the existing
program?

2. Is the routine "generic" enough to be useful in a wide variety of places?

3. Are there any hard-coded values that should be variablized?

j In this example, there are hard-coded commas and quotes. The quotes are
acceptable, but the commas are not – what if you wanted to parse a tab-delimited
line or a line marked with another, unexpected character? This is fixed in the
final version.)

4. Does it return something meaningful that could be used in a variety of different


places. In other words, if the routine returns a proprietary string, it may be more
trouble to figure out what to do with the answer than it is worth. But, if the routine
returns a simple string, number, or some other easy-to-understand construct, then it
could be useful.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 133


5. Are there any limitations in what it can process (e.g. only 100 fields)? Is this
limitation too small?

I recommend moving or coding the ParseCSVLine


module into the utility library. This routine is too handy
to not have in your toolbox. See the end of the chapter
for a complete code listing.

Current Flaws:

A115 (Program 13.6) expects a comma as a delimiter but what if you wanted the routine
to parse by Tab, #-sign, or some other character? It would make sense to modify the
routine, making the delimiter more generic. To accomplish this, add a parameter to the
signature at line 1:

In module A115_ParseCSVLine's signature (not A110_ReadTextFile), add a char-


parameter:

private string [] A115_ParseCSVLine


(string strPassedReadLine, char charDelimiter)

Then, everywhere in the routine that used a hard-coded comma, replace it with the new
charDelimiter. This is easy to do because only one line in the routine needs to be
modified:

In module A115_ParseCSVLine (not A110_ReadTextFile)


Line 46: Change ',' to "charDelimiter" (no quotes)

Moving A115:

Move A115 into the Utility libraries:

1. Link the CL800_Utility libraries using the steps outlined in Chapter 8. (If you have been
following these examples, it is already linked in the current test program.)

2. From the example program, highlight all of the statements within A115's opening and
closing braces, but do not include the braces. Select Edit Copy (or Edit Cut if you are
confident).

3. In Solution Explorer, double-click CL800_Util.cs


Scroll to the bottom of the file, but above the final closing brace.
Type this new method-name, along with its opening and closing braces:

public string[ ] ParseCSVLine


(string strPassedReadLine, char charDelimiter)
{

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 134


4. Paste the copied A115 routine, all code from between the braces.
Confirm the new signature line matches the variable names used within the pasted code;
in these examples it should.

5. Because the newly-pasted code is now in the CL800 Util library, locate all calls in the
newly-pasted code that have a ".util". Remove the ".util" prefix. For example:

util.MidStr(strPassedReadLine....) becomes
MidStr(strPassedReadLine ....)

6. Inside the newly-pasted code, locate all references to "strReadLine" and replace with
"strPassedReadLine". This is mostly for documentation, but the name must match the
variable's name in the arriving signature.

7. In the original (calling) program (frmProcess), change the call from "A115..." to the new
ParseCSVLine. In the example program, at button1_Click:

private void BtnProcess_Click (object sender, EventArgs e)


{
string [] astrinputFields;
string strReadLine;

//Note the new passed comma-field!


astrinputFields = util.ParseCSVLine (strReadLine, ',');

MessageBox.Show (astrinputFields[2]);
}

You are passing two values to the utility library: The input string to parse and what
delimiter is expected.

When you close your editing session (or File, Save All), Visual Studio will prompt you
to change the cCL800_Utility modules; allow the changes to be saved. All other
programs that have linked in this library can now benefit from the newly-added routine.

After this lengthy discussion, you may be surprised at how small the final routine is.
Once completed, any program which links in the CL800 Util library can pass any CSV or
TAB-delimited line and get a fully-parsed answer as a result.

ParseCSVLine, completed:

This routine is part of the CL800_Util library. See the pages above for use, but in
general:

A. In a calling routine, allocate a temporary string array, to hold the resulting parse.:

string [] astrfoundFields;

B. Pass an un-parsed string (the string with the tab-delimited or csv-delimited fields) into
the Parse routine and expect it to return an array.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 135


As a second parameter, also pass the field (character) delimiter, typically a comma or a
tab (\t):

astrfoundFields = util.ParseCSVLine (strReadLine, ',');

The routine returns a populated astrfoundFields array. Use a "foreach" or a "for-next"


loop to loop through the returned field list. See below for a complete example.

C. Typically, in a real program, a loop is processing an ASCII file. As each record is read,
take the unparsed line and pass it to the Parsing routine.

ParseCSVLine in the CL800_Util Library, completed

This is the final version, as placed in the CL800_Utility libraries. Type <summary>
comments after writing the routine.

///<summary>
///Parses CSV and TAB lines using a passed delimiter. Can navigate
quoted strings and variable-width lines. To use: strArray[] =
ParseCSVLine(strPassedLine, 'char delimiter').
///</summary>
///<param name="strPassedReadLine">The line to parse, typically
strReadLine. Include all fields</param>
///<param name="charDelimiter">Character delimiter. Typically ','
(comma) or 'backslash-t' (tab).</param>
///<returns>A string array LTE 500 items. Array is resized as
returned.</return>

1 public string [] ParseCSVLine


(string strPassedReadLine, char charDelimiter)
2 {
3 int ifrontSideNDX = -1;
4 int ibackSideNDX = 0;
5 int ifieldCount = 0;
6
7 string strtempFieldString;
8 bool boolparseLoopSW = true;
9
10 string[] astrfoundFields = new string[500];
1
2 while (boolprocessLoopSW == true)
3 {
4 //Take the passed CSV string and passed delimiter type
5 //and parse each field into an array position
6 //Typically accept a comma as a delimiter
7 //Must support "quoted fields with embedded commas"
8 //For each field, look at the frontSide Delimiter + 1
9 //to see what type of delimter it is: comma or quote
20
1 if (Mid(strPassedReadLine, ifrontSideNDX + 1, 1) == "\"")
2 {
3 //Search for backSide quote; be sure to jump +2 past
4 //the frontSide delimiter's posotion [,"Pants, Green"]
5 ibackSideNDX =

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 136


strPassedReadLine.IndexOf("\"", ifrontSideNDX + 2);
6
7 if (ibackSideNDX == -1)
8 {
9 //no delimiters left to process; this must be the last
30 //field in the string:
1 boolprocessLoopSW = false; //for documentation only
2 break; // end the loop
3 }
4
5 //Capture the found-field using a manual mid-string:
6 strtempFieldString = strPassedReadLine.Substring
(ifrontSideNDX + 2,
ibackSideNDX - ifrontSideNDX -2).Trim();
7
8 //The ibackSideNDX needs to shift one character to the
9 //right, moving from the quote to the comma...
40 ibackSideNDX ++;
1 }
2 else
3 {
4 //Standard (comma)-delimiter; no quotes:
5 ibackSideNDX = strPassedReadLine.IndexOf
(charDelimiter, ifrontSideNDX + 1);
6
7 if (ibackSideNDX == -1)
8 {
9 //no more delimiters; this must be the last field
50 //in the string...
1 boolprocessLoopSW = false; //for documentation only
2 break; //end the loop
3 }
4
5 //Mid-string the field:
6 strtempFieldString = strPassedReadLine.Substring
(ifrontSideNDX + 1,
ibackSideNDX - ifrontSideNDX -1).Trim();
7
8 //The ibackSideNDX is positioned properly at the
9 //closing (comma).
60 }
1
2 //Assign found-field to an array position, base-zero
3 astrfoundFields[ifieldCount] = strtempFieldString;
4 ifieldCount++;
5
6 //Leapfrog to the next pair of delimiters, prepping for reloop
7 ifrontSideNDX = ibackSideNDX;
8
9 //Check to see if this is the last-field quoted string; rare
70 if((ifrontSideNDX + 1) > strPassedReadLine.Length)
1 {
2 boolparseLoopSW == false;
3 }
4
5 } //end while-loop
6
7 //Last Field (dangling field) processing here.
8 //Use a standard Right-string to parse the data; then assign
9 //to array position.
80 //Remember to take into account possible quotes strings.
1
2 if ((ifrontSideNDX + 1) > strPassedReadLine.Length)
3 {
4 //The last field was a quoted string and it was detected in

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 137


5 //the main loop; do not process
6 ifieldCount--; //Minus-minus = subtract 1
7 }
8 else
9 {
90 //ifieldCount++ //already counted
1 strtempFieldString = strPassedReadLine.Substring
(ifrontSideNDX + 1);
2 astrfoundFields[ifieldCount] = strtempFieldString;
3 }
4
5 Array.Resize(ref astrfoundFields, ifieldCount + 1);
6 return astrfoundFields;
7
8 } //end ParseCSVLine

End: ParseCSVLine

Note: Be sure to add this routine to your CL800 Library, as discussed. This is too
handy to not have in your toolbox.

Full Demonstration, reading an ASCII text file, parsing each line and performing a few
numeric calculations. This uses the new util.ParseCSVLine:

Example data from the start of the chapter, saved as "Transactions.tab"


Date Type Item Qty Amount
1/1/2008 Sale Pants 1 11.95
1/2/2008 Sale Sweater 1 14.98
1/3/2008 Sale Coat 2 17
1/4/2008 Return Gloves 1 5.99

Program 13.8 - ASCI Read and Parse using util.ParseCSVLine, completed

using System;
using System.Windows.Forms;
using NS800_Util;
using System.IO;

namespace WindowsFormsApplication
{
public partial class Form1 : Form
{
cl800_Util util;

public Form1()
{
InitializeComponent();

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 138


util = new cl800_Util();
}

private void button1_Click (object sender, EventArgs e)


{
A110_ReadTextFile();
}

private void A110_ReadTextFile()


{

//Open a tab-delimted ASCII text file.


//Read and parse each line
string strmyFileName = "C:\\temp\\transactions.tab";

string[] astrinputFields;
string strReadLine; //Current ascii line

int irecordCount = 0;
int itotalQty = 0;
float ftotalSales = 0.0F;

try
{
StreamReader sales = new StreamReader(strinputFileName);

//Priming reads
strReadLine = sales.ReadLine();
irecordCount++;
strReadLine = sales.ReadLine();

while (strReadLine != null)


{
irecordCount++;
int ilineQty = 0;
float flinePrice = 0.0F;
float flineExtension = 0.0F;

if(util.IsFilled(strReadLine))
{
astrinputFields = util.ParseCSVLine(strReadLine, ',');

try
{
if(util.IsNumeric(astrinputFields[3]) &&
util.IsNumeric(astrinputFields[4]))
{
ilineQty = Convert.ToInt32(astrinputFields[3]);
flinePrice = Convert.ToInt32(astrinputFields[4]);

flineExtension = ilineQty * flinePrice;


itotalQty = itotalQty + ilineQty
ftotalSales += flineExtension; //Alternate method

textBox1.Text +=
Convert.ToString(irecordCount) + ": " +
astrinputFields[2] + " " +
"Extended: " + Convert.ToString(flineExtension) +
"\r\n";
}
else
{
textBox1.Text += "Error at line: " +
Convert.ToString(irecordCount) + "\r\n";

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 139


}
}
catch
{
MessageBox.Show
("Some type of numeric problem at line: " +
Convert.ToString(irecordCount);
}

//Get next ascii record


strReadLine = sales.ReadLine();

sales.Close();
sales.Dispose();

label1.Text = Convert.ToString
(irecordCount) + ": " +
"Qty: " + Convert.ToString(itotalQty) + " " +
"Sales: " + Convert.ToString(ftotalSales);
}
catch (Exception e)
{
label1.Text = "Internal Program Error";
MessageBox.Show("A110 File Problem: '" +
strinputFileName + "'" + "\r\n" +
e.Message);
return;
}
}

}
}

where:

• Notice how the astrfoundFields[] array is declared and sized within the next-record
loop? With this, the array is completely destroyed and re-created with each record.
This way it starts fresh, and previous fields are erased.

• Not illustrated are the normal steps where the CL800_Util library is linked into the
program. This example assumes the Parsing routine was moved into the Utility
library.

• The file-open logic uses the techniques from Chapter 12, Reading ASCII text files.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 140


• On the call to ParseCSVLine, change from a ',' (comma - csv files) to a tab (\t) if you
are processing a tab-delimited file. Notice the passed second parameter is a
'character,' and not "string" parameter.

• The main program is only a couple of dozen lines to open and read the ASCII file,
and to perform a variety of subtotal and grand-total calculations. The parsing logic
is off in another library and can be called from any program.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 141


Parsing Exercises

A. Using program 13.8 as a model, write a new program that reads this Tab file.
This is an ASCII-file-read program with a call to ParseCSVLine. Use Excel to create the
table, saving as a DOS Tab-Delimited file. Or, if using Notepad to create this file, type
one tab between columns.

Date Time Size Filename


07/16/2016 04:42 AM 61,440 bfsvc.exe
11/11/2016 02:56 AM 4,673,304 explorer.exe
07/16/2016 04:42 AM 975,360 HelpPane.exe
07/16/2016 04:42 AM 18,432 hh.exe
07/16/2016 04:43 AM 243,200 notepad.exe
07/16/2016 04:42 AM 320,512 regedit.exe
10/14/2016 08:59 PM 130,560 splwow64.exe
07/16/2016 04:42 AM 10,240 winhlp32.exe
07/16/2016 04:42 AM 11,264 write.exe

Report back how many files were found.


Report their total size.

Note: This must be a TAB delimited file.

B. Program 13.8 (the final example in this chapter) has a flaw. Some of the ASCII records
are not "Sales" records; they are Returns.

Example Data:

Date Type Item Qty Amount


1/1/2008 Sale Pants 1 11.95
1/2/2008 Sale Sweater 1 14.98
1/3/2008 Sale Coat 2 17
1/4/2008 Return Gloves 1 5.99

Keep the variable iRecordCount as-is.


Add two new record counters: iSaleCount and iReturnCount.
Report back how may Sales records there were vs. how many return records. The two
should total to the original iRecordCount (decide how blank data-lines should be
handled, if any are present).

Change the fTotalSales calculations: If the record is a SALE, add it to the total. If the
record is a RETURN, subtract. Reflect this total in the label1.Text report near the end of
the program.

C. For extra credit, return to the first exercise with this problem:

1. Using Windows Explorer, create a C:\Data folder on the C: drive


(or use any other folder of your chosing)

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 142


2. Open a DOS Command-prompt (Widnows-key-R, "CMD") and build a new test file,
using these DOS Commands:

C:\>
dir C:\Windows\System32\*.* >C:\Data\testfile.txt

3. Open the file using Notepad and snoop around.

Note it is not delimited by commas or tabs. This is what is called a fixed-width ACII
text file. This type of file cannot be easily parsed with a .Split or with the
ParseCSVLine routines.

Close Notepad, making no editing changes.

4. Write an ASCII Read program that opens C:\Dta\testfile.txt and reads each record.

5. Have the program skip over all lines that are not files.
For the priming read, the first 5 lines can be skipped over (5 priming reads)

6. Manually substring each line (using util.MidStr), looking for "<DIR>"


If a <DIR> (Directory), skip this line. Only consider processing lines that are actual
files.

Hint: The <DIR> is always in the same position, starting at position 25, for a length
of 5.

7. When an actual filename is found (as opposed to a header or a directory),


Pass that strReadLine into another routine (for example A115_Parse).

Discover if the file is a .DLL or a .EXE.

8. Report back how many DLL's and EXE's were found.

Comments: Because this file is not delimited well, it is a nuisance to parse. But if you
think about it, all the positions for each column are the same. util.MidStrings(x,y) can
handle this with ease.

Once parsed, move the fields into an array.


Trim the results.
Return the array to the calling module and do your if-statements and math.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 143


Why this exercise? Sometimes you get crappy data to work with.

This concludes the parsing chapter. This was admittedly difficult, but the need to do this
is all-to-common. The Parsing routines will go a long way to making the task easier.

Chapter 13 - Parsing Stings, Tab-delimited and CSV Files Page: 144


A Beginners Guide to C-Sharp - Volume 2
Visual Studio C# 2017
ASCII through Advanced Formatting
by Tim R. Wolf
© 2017.06.01 1.02
Table of Contents

9 Chapter 12 - ASCII Files 17


Reading ASCII (Text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
StreamReader
Priming Read
Ending the Loop and Closing Files
Using an EndOfStream Read
The "Using" Clause
ASCII File Reads with try-catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Issues with try/catch and Close
Chaining catch-statements
Completed ASCII ReadFile - Program 12.5. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Writing ASCII (text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Appending ASCII (text) Files - Advanced Open Methods. . . . . . . . . . . . . . . . . . . . 54
Preventing Append from Running Twice

9 Chapter 13 - Parsing Tab and CSV Files 65


Automatic Parsing by Delimiter (Split).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Declaring the Destination Array
foreach
Alternate Loops
.Split Limitations
CSV Files
Split Example: Phone-Numbers
Manually Parsing Comma-Delimited Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
First Field Logic
for-next Logic
Substring
Last-field Logic
Tab-Delimited Files
Parsing a Variable Number of Columns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Parsing ASCII CSV Files with Embedded Commas.. . . . . . . . . . . . . . . . . . . . . . . 105
Error Processing in CSV Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Moving ParseCSVLine to the Utility Library

9 Chapter 14 - INI Files 147


INI File Structure and Design. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
A015: Loop Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A017: Parse INI Detail Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A028: Finding the Application's Default INI Location.. . . . . . . . . . . . . . . . . . . . . 178
Command-Line Override.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Simulate a Command Line Options
Embedded Spaces in the Parameters
Command-line ini=
A029: Write Default INI when Missing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Basic INI File Read: Complete Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Using a Program Class - CL860 INI File Read. . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
CL860 Basic INI Read Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Using CL860_BasicINIRead

9 Chapter 15 - xml and App.config Files 223


xml File Structure
app.config xml.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Using (Reading) app.config
Building a Manual xml File.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Reading xml File Sequentially. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

9 Chapter 16 - Windows Registry 249


Organization of the Registry
Reading a Specific Registry Key.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Opening the Registry
Closing
Reading a Specific Registry Value
Reading Mulitple Keys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Reading All Values within a SubKey. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Multi-Line String Registry Values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Creating/Modifying Name-Value Pairs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Opening the Registry in 'Writeable' Mode
Creating Sub-SubKeys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Deleting Values and Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Deleting Entire SubTrees
Deleting the Current Key
Enumerating SubKey (folders). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

9 Chapter 17 - Reading Excel and Access 289


Reading an Excel File using a COM object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Open the Workbook
parameter list
Reading a Row
comboBoxes based on Access Database Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Attaching Microsoft Access Data to a comboBox. . . . . . . . . . . . . . . . . . . . . . . . . 317
ODBC Call
Connecting to the comboBox
Refreshing the List
Performance Considerations

9 Chapter 18 - External Programs (Shell) 333


Starting a New Process. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Launching Notepad with a Specific Filename
Building an Exit Event
Using Code to Start a New Process.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
proc.WaitForExit.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
DOS Output.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Capturing DOS output into an Array
Multiple Instances of an Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Simple Instance Test
Mutex: A Better Solution for Multiple Instances. . . . . . . . . . . . . . . . . . . . . . . . . . 355

9 Chapter 19 - Waits, Delays and Pauses 361


Poor Wait States – Not Recommended
Empty loops
current time and loop until xx seconds
Sleep events
System.Threading.Thread.Sleep (milliseconds). . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Completed wait Simulation - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Wait as a Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Using cs805_Wait
Calling Wait
Timer Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Basic Timer Code
Event Horizons
Countdown and Timer Example
Disabling the Close "X" Mid-Transaction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Intercepting a Close Event
Countdown and Timer Program - Completed. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Simple Startup Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Splash Screens.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

9 Chapter 20 - Printing 415


Printing Simple Text. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Print Layout / Print Rendering
Pixel Positions in Millimeters (x,y)
OverShooting
Printing Horizontal Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Printing Rectangles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Filling
Printing Graphics.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Printer Dialogs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Adding a Custom Printer Dialog
Print Preview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Printer Setup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Printing Text Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Font Height Calculations - Lines per Page
Page-Break Logic
GraphicsUnit.Millimeters
Setting New Margins, Landscape. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
Current Margin
Setting New (Printable) Margins
Begin and End Print Events.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

9 Chapter 21 - Formatting 469


Font Color, Font Bold. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Font Bold
Font Style Ariel
Basic String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
String.Format with Alignment.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Proportional Fonts
Numeric Formatting with String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Standard Decimals
Formatting with Alignment
Commas and Currency
Numeric Picture Clauses. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Padding Zero with Leading and Trailing Decimals
Place Holder (#)
Thousands Separator ( , )
Variable Picture Clauses (Group Separators)
Date and Time Pictures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Converting from Strings
Converting Dates
Other Date Time Properties and Methods.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
DateTime Formatting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Pictures
Display Dates, Year first
Format Class Library - cl710_Formatting.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Phone Number Formatting
Proper Names
Proper Addresses
PhoneNumberFormat Method - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
PhoneNumberFormat Module - Coding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Extension Extract
Strip Normal Punctuation
Strip "1-" Prefixes
Punctuate
Punctuating the AreaCode
"ProperNames" Formatting - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
ProperNamesFormat - Coding.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
A Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 14 - INI Files
Chapter 14 - INI Files

Chapters 12 and 13 showed how to open, read, parse and write ASCII Text files. This
chapter takes these concepts and ties them into one program that reads (and writes)
Windows INI configuration files.

INI files are simple ASCII files that often contain application preferences and settings.
You will typically find stored server and database names, last-used user settings, such as
window sizes and positions, or any other parameter that a program needs to reference.
These setting are usually read when the program first starts and might be re-written when
the program closes.

Example INI File:

This is an example INI file that will be used by this chapter. The file is created in
Notepad.

;Program INI File


;Keyword-dependent INI File; Use spaces, not tabs, for beauty

[General]
ProgramVersion = 2.01
ServerName = omni
ShareName = vol1\kb
UserAuthentication = Pay_Act ;System Account
UserPassword = 14%BDB010C1FB16014.D79.4B1BF1.DD
AllowPasswordSave = Y

INI files are considered old fashioned and these files have often
been replaced by app.config and XML files (serving the same
purpose), but do not discount this chapter. Even if you have no
need for INI files, the techniques covered are interesting and
build useful skills. The entire chapter should be considered an
exercise.

Topics:

• Building a simple ASCII Text INI file


• Building the test program, with BtnClose
• Opening an INI file and limiting the scope of the loop with A015_GetPrefs
• Building Form-level (class) variables to hold the found values
• Skipping blank and comment Lines
• Infinite loops - by accident
• End-of-File (EOF) / blank record processing

• Modularizing the Read (A019_ReadNextDetail)

Chapter 14 - Reading INI Files Page: 147


• Passing variables 'by REF' - resolving Scope problems with StreamReaders
• Parsing Key-Value detail lines with util.ParseKeyValue (A017)
• Testing for bad or incomplete data
• Write a default INI file
• Locating the INI file using a Command-line (ini=filename.ini)
• Locating the INI in the current directory (File.Exists)
• Command-line arguments, using Environment.GetComandLineArgs

• Completed Standard INI File - Code in Form1


• Using a separate Program Class (-recommended: CL860_BasicINIRead)

• [Multi-Sectioned] INI files - See Appendix D.

The next chapter covers "app.config" and XML files, which are similar to INI files,
serving the same purpose.

Near the end of this chapter, see the highly-recommended


CL860_BasicINIRead Program Class. Instead of writing your own
routines, you can use this chapter's Class Library and automate most of the
steps. This module uses all the techniques taught in this chapter.

Notes:

On the net you can find class libraries to read INI files but in this chapter you will write
your own. You will learn how to break up a program into smaller subroutines that make
testing and debugging easier and you will learn a variety of other techniques that are
useful far-beyond INI file processing. This is an involved subject and it spans many
topics. A complete INI File-read program can be found at the end of this chapter.

Why not the Registry?

Why not use the Windows Registry for program preferences. The Registry has the
benefit of being in a known location, but often an INI file is preferable, especially if the
program needs to run with several different sets of preferences, such as Test or Prod
settings. Of course, when compared to the registry, an INI file is much more readable
and more accessible to non-technical users. But INI files are not without problems
because they are fragile and have no auditing. Never-the-less, I still like to use them to
variablize program controls.

Chapter 14 - Reading INI Files Page: 148


Summary:

The end-result of this chapter is a new class library that can read INI files
(CL860_StandardINIRead). Once written, this library can be imported into any program.

Because each INI file is unique, customization is always required. Here is a summary of
the steps if you are using the Class Library. These steps assume you have written the
class library previously:

Overview: Using CL860 Basic INI Read


1. In Solution Explorer, Copy (do not link) CL860_BasicINIRead.cs

2. Edit the module, adding unique statements, etc. See below.

3. In Form1_Load (typical)

//(Instead of a "Using NS860_INIRead readINI;")


NS860_INIRead.CL860_StandardINIRead readINI;
readINI = new NS860_INIRead.CL860_BasicINIRead();

//Call the routine:


readINI.B000_ReadINI();

if (readINI.boolpubStartupError == true)
{
MessageBox.Show("Serious startup errors found in INI");
}
else
{
if (util.IsFilled(readINI.strpubPNLMsg))
{
//optionally show message in queue
//pnlMsg.Text = readINI.strpubPNLMsg); or
//MessageBox.Show(readINI.strpubPNLMsg);
}

//Fetch found values (examples):


readINI.strpubProgramVersion;
}

Chapter 14 - Reading INI Files Page: 149


Overview: Customize changes to local copy CL860 Standard INI Read
In CL860_StandardINIRead (the copy), make these custom changes:

1. In the (Form) Global variable section


Change the default strINIFilename

2. In the (Form) Global variable section,


Create a variable for each value to return to Form1. Name these anyway desired.
For these examples, using a prefix "local" to remind you these are local variables,
being manipulated by CL860 until ready to return. Local values could be
abandoned and the program could write directly to the Get/Set routines.

e.g.
strLocalProgramVersion = "";
strLocalServerName = "";

3. In the (Form) Global variable section,


Create "InstanceRef"-like variables (Get/Set) for each value that needs to be
returned to Form1. If you have numerous values to return, this can be tedious.

e.g.
private string strprivProgramVersion = "";
public string strpubProgramVersion
{
get { return strprivProgramVersion; }
set { strprivProgramVersion = value; }
}

4. In public method B000_ReadINI,


Modify the "Return all stored values" section, setting all "local" values to their
"Get/Set - private values. This gives you a chance to do last-minute changes to
the values:

e.g.
strprivProgramVersion = strLocalProgramVersion;

5. Modify function "B017_ParseINIDetail", adding statements in the switch-


statement for each value found in the INI file.

6. Consider modifications to B029_WriteDefaultINI

Be careful: Once custom changes are made to CL860, do not delete from Solution
Explorer and re-import or your changes will be lost. Consider renaming the class to
CL860_Modified, to act as a reminder.

Chapter 14 - Reading INI Files Page: 150


The next sections show how to read and process an INI file using standard ASCII read
techniques. Then, later in the chapter, this routine will be moved into the
CL860_StandardINIRead utility library for later re-use.

Chapter 14 - Reading INI Files Page: 151


INI File Structure and Design

An INI file is an ASCII text file, usually created with Windows Notepad.exe. The file
contains preferences and other settings for a given program and is meant to be edited and
changed by humans. Although INI files are considered old fashioned, they are simple
and understandable.

Related: See the next chapter for XML and app.config files.

Creating the Test INI File:

The example program in this chapter reads a simple INI file, illustrated below. Using
Windows Notepad (Start, Run, "Notepad"), create a file with this information. For this
example, use spaces (not tabs) to line up the columns; this makes the file visually
interesting and I do this out of personal preference. Also, be sure to enter the blank lines,
as illustrated. Temporarily save the file in C:\data\ExampleProgram.ini. Later, during
testing, it will be saved in the bin\debug directory and then into other locations.

Create INI file using Notepad: Save as C:\data\ExampleProgram.ini


Later save into (Project)\bin\debug\ExampleProgram.ini
;Program INI File
;Keyword-dependent INI File; Use spaces, not tabs, for beauty

[General]
ProgramVersion = 2.01
ServerName = omni
ShareName = vol1\kb
UserAuthentication = Pay_Act ;System Account
UserPassword = 14%BDB010C1FB16014.D79.4B1BF1.DD
AllowPasswordSave = Y

j When creating an INI file, keep this thought in mind: The file can appear any way
you like. It can have any number of sections, or no sections at all. It can use equal-
signs for delimiters or another delimiter. It can have tab-delimited lines, it can have
multiple parameters on a single line. It can be written in Spanish or Klingon. It can
be designed in any way desired because your application is the one doing the
decoding; you are in complete control.

This Program's Goal:

The ultimate goal is to store and retrieve parameters that can be used by the program in
later steps. Placing changeable parameters in an external file means users can change the
program's behavior without having to re-compile.

Chapter 14 - Reading INI Files Page: 152


The first section places all of the read steps within Form1 and explains the logic. Later,
these same routines will be moved into a recommended separate Program Class. In both
cases, here are the program's main goals.

• Read and ignore a variable number of comments and blank lines at the top of the file.

• Read the Name-equal-Value pairs and store the found values into program default
variables.

• With the found values, do basic consistency checking.


For example, in the AllowPasswordSave, the file can contain a Y or N. If something
else is found, such as "y" or "Yes", correct if possible. Otherwise, set a default
value.

• With the ServerName and ShareName fields, the user may type leading and trailing
backslashes and mistaken slashes. Remove these. e.g.:
ServerName = \\omni
ServerName = //omni/
becomes: ServerName = omni

• Individual detail lines can contain trailing comments (see the name-value pair:
UserAuthentication). Remove the comments using CL800_Util libraries, which
were developed in Chapter 6,7 and 8.

• If the INI file is not found, offer to create a "factory-default" version.

• Notice the password is "encrypted". This can be a fun digression, but for now, read
and store the encrypted string as-is. For this example, you can type any password
string. Appendix D has details on how to encrypt and decrypt clear-text passwords
using a fun, home-built encryption routine.

Building the Test Program:

1. Using Notepad, create the test INI file, illustrated above, saving the file in a known
location, such as "C:\data\ExampleProgram.ini".

2. In Visual Studio, create a new standard project, using Windows C# application.


The initial screen design will look like the following illustration with details below:

Chapter 14 - Reading INI Files Page: 153


3. From the toolbox,

Drop button1 and button2 on the form.


Change button2's properties, setting Name to "BtnClose"
Change BtnClose Text property to "&Close" (ampersand Close)

Double-click button Close, adding this standard code:

BtnClose Event
private void BtnClose_Click (object sender, EventArgs e)
{
this.Close();
Application.Exit();
}

4. Link the NS800_Util libraries.

This library, from Chapter 8, is required and is assumed that you've written or
downloaded it prior to beginning the work here. If you do not have these routines
written, some of the parsing steps will have to be written by hand and this makes the
program more complicated. As a reminder, link in the CL800 Utility library with these
steps (illustrated details can be found in Chapter 8):

In Solution Explorer, "Add Existing Item" as Link:


CL800_Util.cs

Chapter 14 - Reading INI Files Page: 154


Then add these statements, as described in previous chapters:
using NS800_Util; //near the top of the program
CL800_Util util; //after Form Declaration
util = new CL800_Util(); //after InitializeComponent

5. Near the bottom of the form, place a label, which acts as a message-center for the
program. Illustrated at the top of this section. Use these field-properties / attributes.

(Label) Name = pnlMsg


Autosize = false
BorderStyle = None
Text = (nothing)

Once set, stretch the field across the width of the panel.

6. Run the program one time now by pressing F5.

This builds necessary directories on the hard drive, in particular, the bin\debug directory.
Close the running program. The base program is now ready to accept code. The next
section outlines the main INI file read-loop.

Initial Coding:

After building the main Form, start the groundwork for the main loop with these initial
steps. If needed, review Chapter 12 for details on opening and reading ASCII files.

1. Because an ASCII file is being opened, it requires a "using System.IO;" statement and
you can place it near the existing "using NS800_Util;" near the top of the program.
The order or positioning in the 'using' list is not important but the two statements are
case-sensitive.

INI File, Required 'using' Statements


using System.IO;
using NS800_Util;

2. Create a short list of Form-level variables.

Below the statement: "public partial class form1 : Form" (and above "public
form1"), create several variables to hold the values that will be found in the INI file.
Because the variables are defined at this location, their scope makes them visible to all
functions and methods in this Form.

Chapter 14 - Reading INI Files Page: 155


Create Form-level Variables
public partial class form1 : Form
{
CL800_Util util;

//Working variables:
string strReadLine;
string strINIFileName = @"C:\Data\ExampleProgram.ini";

//Variables to be set from the INI file:


//This content varies, depending on the INI file
string strProgramVersion;
string strServerName;
string strShareName;
string strUserAuthentication; //These variables will be
string strUserPassword; //ignored in the remainder
string strAllowPasswordSave; //of the examples

public form1()
{
InitializeComponent();
util = new CL800_Util();
}

: (etc.)

For now, "strINIFileName" is a hard-coded string holding the default location for the INI
file. This is not a recommended design. Later, this name will be decided by other
techniques.

3. In design view, double-click the form's title-bar to build the Form_Load event.

The Form_Load event is a logical place to read an INI or Config file, however, this
involves a substantial amount of code. As a matter of programming style, have the Load
event call another function that does the actual work. Inside the Form1_Load event,
invent a new function-name ("A015_GetPrefs"). The name A015 is somewhat arbitrary:

bool boollocalStartError = A015_GetPrefs();

After typing the A015 line, click the "lightbulb" icon in the left-margin, selecting
"Generate". This stubs-in a new function. It will appear just below the Form_Load
event's closing brace.

Chapter 14 - Reading INI Files Page: 156


A015's purpose is to open the INI file, read all records, parse and populate program
variables, as described in Chapter 12. This is a tall order for one module and there are
opportunities to break it into smaller subroutines.

4. Continue with these additional statements in the Form_Load event:

Build the Form_Load event by double-clicking the form's background...


private void form1_Load (object sender, EventArgs e)
{
this.Show(); //force the form to display now
bool boollocalStartupErrorSW = A015_GetPrefs();

if (boollocalStartupError == true)
{
MessageBox.Show
("Error: '" + strINIFileName + "' was not processed");
BtnClose_Click (null, null);
}
}

where:

• "this.Show( )" forces the form to display earlier than it would like. If a startup
error occurs (such as an INI file not found), the user has a better visual clue on which
program generated the error; errors will be displayed on top of the main form.

• The call to A015 is on the otherside of a boolean equal-sign.

bool boollocalStartupErrorSW = A015_GetPrefs();

This means A015 must return either a true or a false – "true" if an error was found,
false if no errors detected. If A015_GetPrefs detects an error serious enough to close
the program it will set the flag and return. Notice how the if-statement next calls
BtnClose.

Chapter 14 - Reading INI Files Page: 157


5. Scroll below the Load event's closing brace and locate the previously stubbed-in
"A015_GetPrefs" routine. Delete the auto-generated "Throw new Exception" line and
write this code. In here, use a try-catch to capture any file-open problems and call a
separate module for the priming read:

A015 Logic to Read INI file, preliminary


private bool A015_GetPrefs()
{
//Process the INI file; looping through each record:

int ilineCounter = 0;

strINIFileName = A028_DiscoverINIFileName();
if (util.IsBlank(strINIFileName))
{
return true;
}

try
{
//Open the Preference INI File:
StreamReader mainINIFile = new StreamReader (strINIFileName);

//Prime the Read:


strReadLine =
A019_ReadNextDetail(ref mainINIFile, ref ilineCounter);

//** The Main Detail Loop goes here **


// Each [Section] will be processed separately

mainINIFile.Close();
mainINIFile.Dispose();

}
catch (Exception e) //File Open Error logic here
{
//INI file can't be loaded or found

MessageBox.Show ("INI File Load Error: " + "'" +


strINIFileName + "' \r\n" + e.Message);
return true;
}

//Otherwise
return false; //No errors found

} //end A015

Click the lightbulb icon next to strINIFileName = A028_DiscoverINIFIleName()


and let it stub-in this module, or write by hand:

Chapter 14 - Reading INI Files Page: 158


A028_DiscoverINIFileName, preliminary
private string A028_DiscoverINIFileName
{
//For now, hardcode the results and return a fixed string.
//Later, this will be fleshed out

return "C:\\Data\\ExampleProgram.ini";
}

Click the lightbulb icon next to A019_ReadNextDetail(...) and stub-in this module:

A019_ReadNextDetail, completed
private string A019_ReadNextDetail
(ref StreamReader mainINIFile, ref int ilineCounter)
{
//Read the next detail line and increment line counters.
//Work with temp variables to avoid problems with last/empty
//records.
//Note the by "ref" calls

string tstrReadLine;
ilineCounter++;

tstrReadLine = mainINIFile.ReadLine();

if(util.IsFilled(tstrReadLine))
{
//Only allow a trim if the found line has data; worried about
//eof processing
tstrReadLine = tstrReadLine.Trim();
}

//Unconditionally return either a trimmed line or a null record.


//Let up-stream routines decide what to do with blank records...

return tstrReadLine;
}

where:

• In A015, the variable "strReadLine" is defined as a class-level (form) variable near


the top of the program and it has a broad enough scope to be used in this module.

• Integer "ilineCounter" was defined in A015 because it is only needed here. The
linecounter helps display end-user-error messages, such as "INI File: Invalid record
found at line x".

Chapter 14 - Reading INI Files Page: 159


• A028_DiscoverINIFile locates the INI file's actual location on the disk. For now, it
is hard-coded. The StreamReader class was instantiated and was given the arbitrary
name "mainINIFile"; it could have just as easily been called "Bob". As with all file-
open statements, it is protected with a try-catch.

The catch-paragraph captures file-open errors and if errors are found, returns a "true"
(errors found) to the calling routine. Later, this routine will be embellished with
logic that offers to build a new default INI file.

• The main detail loop was stubbed in with a comment; more code shortly.

• Notice the "mainINIFile.Close( ); statement. Here is my rule: when a file is


opened, take the time to write the close-statement before you forget. Generally, the
close goes immediately after the main detail loop. If you forget to close the file, the
program will probably still work because the file automatically closes when the EXE
ends, but during testing and debugging, you may run into "file is already opened"
problems without the close.

The .Dispose() statement allows the garbage collectors to clean up underlying file-
links sooner than normal, saving memory. If I close, I dispose.

6. Testing. Although nothing will happen, the program is testable for syntax and other
obvious problems. Press F5 to run the program.

Results:
The Form_Load event kicks off, calls A015
A015 opens the file, reads a priming record, then closes the file, with no obvious screen
activity. If there are no errors in the code, assume all is well. Optionally, place a break-
point (red-ball in the left-side margin) on the mainINIFile.Close( ) statement. While the
program pauses, check the value of strReadLine by hovering the mouse over the variable
name and confirming the first line was read properly.

By this stage, the program has form-level variables, opens and reads the INI file, and
properly closes the file.

Function A019_ReadNextDetail is a string-function that returns the next-found


"ReadLine". Note how the signature also uses the "ref" keywords.

• A019_ReadNextDetail will be called from multiple locations in the program, starting


with the priming read and in the loop details. A separate routine is being called
because there is an ilineCounter increment and a trim-clause and you do not want to
duplicate that code if it can be helped.

Chapter 14 - Reading INI Files Page: 160


• Notice the signature line:

private string A019_ReadNextDetail


(ref StreamReader mainINIFile, ref int ilineCounter)

which uses a keyword,"ref" (which I often call "by ref"). Normally, when variables
are passed to a downstream routine, they are "copies" of the original variable (see
the "Scope" chapter for more details). By prefacing the declaration with "ref", the
variables are no longer copies – they are the actual values (pointers to the actual
values). For example, the ilineCounter variable can now be directly updated in
A019 without resorting to a global or class-level variable.

The "ref" keyword essentially keeps A015's variables in scope, even though they are
in a different module. "ref" must be used both in the Calls and in the destination
routine.

"Typing" Variables - StreamReader:

When variables are passed through a signature line, they must be "typed." For example,
"ilineCounter" was "typed" as an integer. With this in mind, notice how A019 needs to
"read" a new record and it needs to use the same variables used in the priming read - e.g.
the "mainINIFile" variable.

Earlier in the program, mainINIFile was declared:

StreamReader mainINIFile = new StreamReader (strINIFileName);

"mainINIFile" was 'typed' as a "StreamReader" – much like ilineCounter was typed as an


integer. Because the ini-file's indirect name, "mainINIFile", is needed by these
downstream routines – and because it was passed 'by ref' through the signature line, it
must be 'typed' like any other variable. Thus, in A019's signature line, "mainINIFile"
was prefixed with a variable-type "StreamReader".

The completed signature line is:

private string A019_ReadNextDetail


(ref StreamReader mainINIFile, ref int ilineCounter)

A Digression on "by reference":

The A019_ReadNextDetail module not only reads the next record, it also needs to bump-
up a lineCounter and trim what it finds. Normally, a call to A019 would look something
like this:

Chapter 14 - Reading INI Files Page: 161


But there are issues with this thought. Consider this code:

These two variables are of concern:

iloopCounter and
StreamReader mainINIFile

both are declared in A015 but would normally fall out of scope when
A019_ReadNextDetail is called (without the "ref").

Traditional Variable Passing:

As a reminder, functions normally pass variables to downstream routines via the


signature line and those passed values are copies of the original (Chapter 6 and 7).
Imagine this (flawed) signature line, where the mainINIFile and ilineCounter are passed
into a new ReadNext routine without a "ref" keyword:

Chapter 14 - Reading INI Files Page: 162


As in all functions, passed values are copies of the original (ilineCounter). When the
function ends, the copies are discarded and their values are lost. ilineCounter would
revert to its old value. In the case of mainINIFile, a copy does not work because it needs
to see the original file-pointer in order to keep track of its position in the file; the copy
would not have this.

One way to resolve this problem is to move the variables declarations for mainINIFile
and ilineCounter higher up in the program, converting them to form-level (class-level)
variables. This is messy because the INI Files are only needed at the start of the program
and there is little sense in leaving them active for the duration.

The best way to resolve this is to change the signature lines so the values are passed by
"ref" (By Reference, Chapter 9 Scopes). "By Reference" variables allow downstream
functions to reach up and use the original variables from the calling routine – all without
worrying about copies or class variables.

Null Record Concerns:

When the actual detail line is read, it would be an ideal time to trim leading and trailing
spaces from the newly-found record. An initial blush would write a statement like this,
but this is subtly flawed and discovering the bug is difficult:

strReadLine = mainINIFile.ReadLine.Trim(); //flawed statement

EOF (End of File):

Chapter 14 - Reading INI Files Page: 163


The end of an ASCII file is a hidden "null" record - an EOF flag and this will cause
problems with Trim statements. This must be tested for with an is-null (or with a CL800
Utility-function call...

Protecting the Last Record Trim from Nulls


:
tstrReadLine = mainINIFile.ReadLine();

if (util.IsFilled(tstrReadLine))
{
//Only allow a trim if the found line is
//not null! (Worried about EOF record processing)
tstrReadLine = tstrReadLine.Trim():
}

The read statement writes its value to a temporary value, "tstrReadLine" (where the
prefix "t" reminds you this is temporary). Then it can be checked for null before
trimming. If the record is null, the trim is skipped and the null-record is returned to the
calling routine. From there, it can be checked for the end-of the while-loop.

As an aside, A019_ReadNextDetail could also be


modified to automatically skip blank records and
comment lines, absolving the main loop from this task.
This would make the ReadNextDetail routine more
robust. A019 could then do everything possible to get
the next valid detail record. To do this, A019 needs its
own internal loop, and it would have to check for EOF,
as well as some kind of switch to notify the calling
routine that the end of file was reached. This idea is left
for your study and is not important for these examples.

Chapter 14 - Reading INI Files Page: 164


A015: Loop Details

In A015_GetPrefs add logic to read the INI file's detail records. This can be done in
several ways. The easiest and sleaziest way is to simply have a list of Read statements,
one after another, essentially hard-coding the INI file's order:

strReadLine = mainINIFile.ReadLine(); //Read the first line in INI


<do stuff to parse>

strReadLine = mainINIFile.ReadLine(); //Read the second line


<do stuff to parse>

strReadLine = mainINIFile.ReadLine(); //Read the third line


<do stuff to parse>
etc.

This idea simplistic and does not accommodate INI-lines typed in different orders or
variations in cosmetic blank lines. Now is the time to flesh-out the main loop's parsing
details.

Building the main Loop:

A015_GetPrefs has opened the INI file, set a line counter, and has read the first record,
"priming" variable "strReadLine". At all points in the program, strReadLine contains the
current INI record. The while-loop sees the record is populated and is ready to begin
iterating through the file, one line at-a-time.

Chapter 14 - Reading INI Files Page: 165


1. Replace the commented-loop detail with a real while-loop, shown in its proper position,
below. This will be wrapped in a protective (nested) while-loop. At the bottom of the
loop, it reads the next record in the file (illustrated with an asterisk, same as the priming
read):

A015 While Loop, preliminary


private bool A015_GetPrefs()
{
int ilineCounter = 0;
strINIFileName = A0128_DiscoverINIFileName();
if(util.IsBlank(strINIFileName))
{
return true;
}

try
{
//Open the preference/INIfile:
StreamReader mainINIFile = new StreamReader(strINIFileName);

//Prime the Read:


strReadLine =
A010_ReadNextDetail(ref mainINIFile, ref ilineCounter);

//** Main detail loop **


try
{
while (strReadLine != null)
{
//More loop details go here

//Get the next record at the bottom of the loop:


* strReadLine = A019_ReadNextDetail
(ref mainINIFile, ref ilineCounter);
}
}
catch
{
//For detail-record errors and problems
if(util.IsBlank(pnlMsg.Text))
{
pnlMsg.Text = "Prefs error at line: " + ilineCounter +
" '" + util.LeftStr(strReadLine,20).Trim() + "...'";
return true; //return true-there-was-an-error
}
}

//Housekeeping
mainINIFile.Close();
mainINIFile.Dispose();

}
catch (Exception e)
{
:

Chapter 14 - Reading INI Files Page: 166


The priming Read, where strReadLine is populated for the first time, is an important part
of the loop's design. At some point, the loop needs to decide when to quit and it uses the
end-of-file's "null" record to mark the end the while-loop – but because of this,
strReadLine must be populated with data before the first while-loop executes. The "not-
null" clause is admittedly hard to interpret.

while (strReadLine != null)

As a reminder, a null-end-of-file is different than blank (empty) lines ("" - empty string);
blank lines will not end the loop.

Skipping Comments:

Often, INI files have comments typed by end-users or the original program designer.
Depending on the age and experiences of the person who wrote the INI file might
indicate a comment in different ways. Some people like to use

; semicolons as comments
' tic marks as comments
// double-slashes as comments

Exercise:
At the top of the main detail-loop, write an if-statement to check the left-most
character(s) – previously trimmed – to see if this is a comment. This will be a multi-
claused if-statement. If a comment is found, briefly consider using a "continue;" to skip
the line (causing it to re-loop to the next record), but understand, this is flawed and will
be discussed in a moment. Similarly, use logic to toss-out blank lines.

Solution (with an important flaw):

Chapter 14 - Reading INI Files Page: 167


A015: Detecting and Skipping Comments, flawed
private void A015_GetPrefs()
{
:
: (File Open logic here inside an outside-try)

//(Priming Read logic here)


strReadLine =
A019_ReadNextDetail(ref mainINIFile, ref ilineCounter);

try
{
//** Main Detail Loop **
while (strReadLine != null)
{

if (util.LeftStr(strReadLine, 1) == ";" ||
util.LeftStr(strReadLine, 1) == "'" ||
util.LeftStr(strReadLine, 2) == "//" ||
util.IsBlank(strReadLine))
{
//What to do...?
continue; //this would be a mistake
}

//Loop details pending; More loop details would go here

//Get the next record:


strReadLine =
A019_ReadNextDetail(ref mainINIFile, ref ilineCounter);

} //This is the end of the loop


}
catch
{
//For detail-record errors and problems
:

where:

• The comment-detecting logic is correct; add this to the main loop now. Note the
double-bars for "OR".

• The "continue" statement is wrong and needs further discussion. The "loop details"
will be completed in a few minutes.

Initial Testing:

This is a good time to test. Follow these steps carefully because there is an infinite loop
lurking within the program.

A. Confirm the test INI file is written and saved, with commented lines at the top of the
file.

Chapter 14 - Reading INI Files Page: 168


B. Place a break-point (red-ball) on the if-statement by clicking the mouse in the far-
right, grey area.

C. Press F5 to run the program; the code pauses on the if-statement.


Press F11 one time to step into the if-statement.

D. Press F11 again; taking you into the util.LeftStr method.

Since you know the util.LeftStr method works properly, there is no reason to
step through each of its commands. When the cursor moves inside the
util.LeftStr method's top-signature line, press Shift-F11 to jump past the
entire LEFT module and return to the program. (The if-statement contains 3
calls to LeftString; Shift-F11 three times as you arrive in each module. Wait
until you arrive in the module before pressing the keystroke. Similarly, on
the fourth iteration, Shift-F11 when you arrive in Util.IsBlank)

E. Control returns to the original if-statement. The condition will be true (a semi-colon
was found in position 1 of the first detail line).

Press F11 (F11 means run the next line; not Shift-F11 which says skip the module).
Program control then passes to the "then-side" of the if-statement (F11).

F. Press F11 again, jumping to the "continue;" statement.

Control falls to the bottom of the loop, where a new record should be read,
but instead, it falls to the while-statement's closing brace.

One more F11 takes you to the top of the loop and presumably it is on the
next record, where the while-statement is tested again. At this point,
investigate strReadLine (hover the mouse over the variable name) and a

Chapter 14 - Reading INI Files Page: 169


problem is apparent. strReadLine is on the same record even though it
theoretically looped to a new record.

G. Continue looping to the "next record." strReadLine did not change; it never picks up
the next record in the INI file and the same if-statement/comment check runs again.
Congratulations if you spotted this beforehand. Most beginning programmers have
troubles with this part of the logic. Without the breakpoints, this would be an
infinite loop.

Stop the running program by pressing the red-square on the top ribbon-bar and return to
the editor.

Infinite Loops:

In its current state the program has an infinite loop. When a comment is found, the
"continue" statement (at position 2) tells the compiler to "re-loop" - which means jump to
the closing brace. The very act of re-looping bypasses the (future) Read statement
waiting just above position 3. Of interest, if no comments or blank lines were in the INI
file, it would not be in an infinite loop.

The Fix:

To fix this problem, a read-statement needs to live somewhere within the comment-
checking IF statement or the if-statement needs to navigate somehow to the read-next-
record at the bottom of the loop. There are several ways to solve this problem and it is
somewhat a matter of programming style, with this question: "Is it proper to exit from the
middle of an if-statement with a "continue"? There are two schools of thought. The first
says only exit a function or routine at the bottom; never from the middle. The second
says "break" and "continue" are perfectly good commands and used with care, the flow
of the program is un-harmed. When possible and practical, I recommend a single-exit at
the bottom but sometimes that in-itself can complicate a program.

Chapter 14 - Reading INI Files Page: 170


Consider this recommended code snippet where the comment-if-statement stays as-
before, but instead of a 'continue', insert an else clause and let the logic skip all of the
"detail-code pending". At the bottom of the else, both paths arrive at the Read-next
record statement:

A015_GetPrefs: Comment-Statements, revisited


private void A015_GetPrefs()
{
:
:
//** Main detail loop
try
{
while (strReadLine != null)
{

if (util.LeftStr(strReadLine, 1) == ";" ||
util.LeftStr(strReadLine, 1) == "'" ||
util.LeftStr(strReadLine, 2) == "//" ||
util.IsBlank(strReadLine))
{
continue;
//do nothing with commented or blank lines and
//let the else handle the logic-fall-through
}
else //Add this else-clause
{

//detail code pending...

//Move the ReadNext below the end-if...


}

//(new position) Read the next record before re-looping...


strReadLine =
A019_ReadNextDetail (ref mainINIFile, ref ilineCounter);
}
}
catch
{
//For detail-record errors and problems
:
:

where:

• See below for a completed A015 module.

• As before, comments and blank detail lines are detected in the compound if-
statement, but now, if a comment is found, do nothing (notice the now-empty "then"
clause); do not "continue".

• An "else" statement was added and the (yet-to-be-written) detail logic was moved
inside.

Chapter 14 - Reading INI Files Page: 171


• The final Read-next statement is outside the if-else. As before, this reads the next
record, but now it reads the next record regardless if the record is a comment or a
legitimate record. In other words, in this position unconditionally run.

• This new design has a priming read and a single Read-next read at the bottom of the
loop. This is a classic design and is a recommended change.

Optional Testing:

The program is testable and can be tested without breakpoints. Do the following:

A. Confirm the TEST.INI file is built and ready to go. Remove previous break-points, if
present.

B. In A019, add a MessageBox.Show statement just before the final "return":

:
MessageBox.Show (tstrReadLine);
return tstrReadLine;

C. Press F5 to open and run the program.

Results: A messageBox will display for each found line.

Remaining Work:

Logic to parse detail lines is still needed. This routine separates the name-value pairs
and stores the results into the variables defined at the top of the program. The utility
methods, "util.ParseKeyWord" and "util.ParseKeyValue" makes this easy. Details next.

Other Resources:

This example demonstrated how to open and process a simple INI file, treating the file as
a long list of name-value pairs. Appendix D demonstrates a more complicated INI file,
where [Section] headers are processed independently with independent logic.

j See the end of this section for a completed "basic" INI file program – complete with
error messages, auto-builds of missing INI files, and parsing logic for each detail line
value-pair. This code can be used in most INI-file projects.

Chapter 14 - Reading INI Files Page: 172


A017: Parse INI Detail Lines

The loop next needs to parse the detail lines. Because the values are "name-value" pairs,
the parsing logic is straight-forward and can use a switch statement running against the
found "KeyWord."

This part of the code varies with each project because each INI file has a
unique set of variables.

Rather than saddle the already-busy A015 module, let A015 find an eligible detail line
and then call a new detail parsing routine to parse the details. The new module is called
"A017_ParseINIDetail." In A015, at the "//More loop details go here", which is just
inside of the comment-else clause:

A015's call to A017_Parse INI Details


:
//Do nothing with comments
}
else
{
//A017 Detail-line Processing...
boollocalStartupError = A017_ParseINIDetail (strReadLine);
if (boollocalStartupError == true)
{
break; //Stop all, if serious error found
}
}

//Read the next record before re-looping...


strReadLine =
A019_ReadNextDetail(ref mainINIFile, ref ilineCounter);
}
:

Here is the completed A017 Parse INI Details, including some basic audits. Notice how
the signature line accepts the current "strReadLine" and returns a boolean "true" if an
error was found.

This module is unique to each INI file:

Chapter 14 - Reading INI Files Page: 173


A017: Parse INI Detail, completed

private bool A017_ParseINIDetail (string strReadLine)


{
//Parse the keyword/keyvalue pairs and populate Form-level variables
//This routine only concerns itself with individual detail lines
//eg. SERVERNAME = myServerName
//Return true if error is found
//Return false is all is well

string strKeyWord;
string strKeyValue;

//Early Exit:
if (util.LeftStr(strReadLine, 1) == "[")
{
//skip section [headers], treating as a comment
return false;
}

//Early Exit:
//Confirm this is a valid INI-detail line:
//(Comment and blank lines removed earlier)
if (!strReadLine.Contains("="))
return false;

//The detail line is eligible for parsing...


strKeyWord = util.ParseKeyName (strReadLine, "=").ToLower();
strKeyValue = util.ParseKeyValue (strReadLine, "=");

switch (strKeyWord)
{
//Note: these are lower-cased for the test!

case "programversion":
strProgramVersion = strKeyValue;
break;

case "servername":
strServerName = util.StripSlashes(strKeyValue);
//See later in this chapter for details on stripSlashes
break;

case "sharename":
strShareName = util.StripSlashes(strKeyValue);
break;

case "userauthentication":
strUserAuthentication = strKeyValue;
break;

case "userpassword":
//Decryption routine can be found in appendix
//for now, leave as clear-text
strUserPassword = strKeyValue;
break;

case "allowpasswordsave":

Chapter 14 - Reading INI Files Page: 174


strAllowPasswordSave = Util.VerifyYN(strKeyValue, "N");
break;

default:
pnlMsg.Text =
"Prefs Error: Invalid or unrecognized INI Line '" +
strKeyWord + " = " + strKeyValue + "'";
return true;
//break; //Not needed because of "Return"
}

return false; //Return nothing, Form-level variables were set


}

where:

• strKeyWord and strKeyValue are declared as new variables each time A017 is
called. Thus, each newly-found detail line starts with a clean slate and no residual
values from previous lines.

• [Section Header] detail lines are skipped here, rather than in the Comment-detection
routines in order to be consistent with Appendix D's example program.

• If the detail line does not contain an equal-sign for a named-value pair, the entire line
is discarded without comment. Optionally, you could flag this as an error and return
a true.

• The utility routines "ParseKeyWord" and "ParseKeyValue" are part of the already-
linked-in CL800_Util library from Chapter 8. If you do not have these routines, you
write your own parsing routines. Similarly, "LeftStr" also comes from the utility
library.

The Utility libraries are recommended. Note how ParseKeyValue automatically


discards trailing comments. For example, the "System Account" comments are
automatically discarded by this routine:

UserAuthentication = Pay_Acct ;System Account

• The actual switch statements compare the "lower-cased" versions of the "found"
KeyWords and parses them. Straightforward logic. Your routines may have more
complicated needs.

Testing:

To Test the completed INI File Read program:

Chapter 14 - Reading INI Files Page: 175


A. Make sure you have a test INI file (see top of this chapter) in a hard-coded location
C:\Data\ExampleProgram.ini. Later in this chapter the location will be variablized.

B. Place a breakpoint after the call to A015 (at the "if (boolStartupError..." line)).

C. Run the program. At the break, hover the mouse over the strServerName, strShareName
variables defined at the top of the program.

Expected Results: str-variables are populated and these variables can be used in other
parts of the program.

Optionally, after the A015_GetPrefs statement, you could add a series of


MessageBox.Show statements to display the found values, as illustrated above.

Chapter 14 - Reading INI Files Page: 176


The remaining sections in this chapter show additional fluff that can make the program
more useful and user-friendly. In particular, the INI file name needs to move to a
variable and the location should not be hard-coded.

Chapter 14 - Reading INI Files Page: 177


A028: Finding the Application's Default INI Location

If a program uses an INI file for startup preferences, it needs to know where to find the
file and a hard-coded location is never recommended. Hard-coded names limit where the
program can be run from (usually defeating thumb-drives, external disks and servers) and
hardcoded names do not support multiple INI files (Production and Test, for example).
This problem is more difficult than might be expected.

Programs can locate their default INI settings in several different ways:

• Pass the name of the INI file in the program's Command Line
• Look in the current (application) directory
• Look in a known location (Documents and Settings)
• A value set in the Windows Registry (Chapter 16)

Discounting the Registry for the time being, a well-designed routine would use all of the
methods listed. In other words, when the program launches, check to see if a command-
line variable was passed; if not, look for the INI file in the current directory and then, as
a last resort, look in a known directory.

Imagine this scenario: Your compiled program lives in this directory:


C:\Program Files\Util\MyProgram.exe and the INI-file lives in the same directory. This
is an elegant and easy-to-understand solution. As the program launches, it looks in the
current directory for its preferences. With this, users can cut and paste the EXE and its
associated INI to a thumb-drive, or a server, and the program will still run.

With the command-line option, multiple configurations can be used. For example, one
INI file might point to a PROD database and another to TEST – each with different
configurations. The need for this happens surprisingly often and I almost always code
for this possibility.

All three default INI examples are described next. See the Windows Registry chapter for
details on that option.

Steps: Locating the Default INI File:

Prepare your Test.INI file by following these steps. Use Program 14.1, along with all the
changes so far in this chapter:

1. Change the original program's hard-coded variable "strINIFileName" so it has only a


filename – without a path, and without an @-sign:

string strINIFileName = "ExampleProgram.ini";


//Forces the program to look
//in the current directory

Chapter 14 - Reading INI Files Page: 178


When a StreamReader is only given a file-name (without a path), it looks by default in
the current directory – typically the directory the EXE is launched from.

Normally, a compiled EXE can be placed in any directory and its associated
"INI File" could live in the same location. But while developing and testing
in Visual Studio's Editing environment, the current directory is controlled by
Visual Studio, which can be found in the \bin\debug directory.

2. Using Windows Explorer, copy the ExampleProgram.INI from C:\Data to the


"..\bin\Debug directory. The actual location depends on where you built your project but
it will be similar to this:

C:\Data\VSProj\ExampleProgram\bin\Debug

This is the same directory that you will find your program's compiled EXE (see below for
the exact location on your computer). Each time you press F5/Run, it re-compiles as an
EXE into this location. If the INI file is not found in the "current directory,", your
program will generate an exception and if you wrote the A029_WriteDefaltINI, a new
INI will be built in this directory.

To find the project's current directory:


From Visual Studio, form1.cs's code module,
Select File, "Save form1.cs AS"
Note the path (illustrated here as "Example Program")
Cancel the Save-As.

This is the base path. Tunnel two folders deeper, into ...\bin\release. This is where the
INI-file needs to be placed.

Chapter 14 - Reading INI Files Page: 179


Your project may be in a different location (commonly, C:\Documents and Settings\
<your login>).

Using Windows Explorer, copy the INI file, then tunnel to the bin\debug folder and
paste.

Of interest, this EXE is a ready-to-use compiled program,


and it gets re-written each time you press F5 and test your
program. This version of the EXE has debugging code and
has not been optimzized. See Appendix B - Compile, for
details for making production-ready code.).

Writing A028_DiscoverINIFileName:

Now write a routine that searches all likely file-locations for the program's INI file. If
not found, offer to build a new default file.

Again, the search order will be:


- Command-Line options, first
- Current Directory, next
- Known location (usually Documents and Settings)
- Windows Registry (not in this chapter)

1. In function A015_GetPrefs, above the first try-statement, create a new call that discovers
the correct INI file's name. In this case, invent a new function name,
"A028_DiscoverINIFileName".

A015's Call to Discover the INI File's Name


private void A015_GetPrefs()
{
int ilineCounter = 0;
bool boollocalStartupError = false;

strINIFileName = A028_DiscoverINIFileName();
if(util.IsBlank(strINIFileName))
{
return true;
}

try
{
:

The results of the new Discovery are stored in the previously-used iniFileName variable.

After you type the call to "A028_DiscoverINIFileName( )"', Visual Studio offers to build
the stubbed-in code for you. Create this new string function by clicking the lightbulb
marker in the left-margin or by keying the procedure by hand.

Chapter 14 - Reading INI Files Page: 180


A028_DiscoverINIFileName looks in the "current directory" for it's default INI file,
currently "ExampleProgram.ini". The phrase, "current directory" is a DOS term, which
still holds sway in Windows. The directory is literally the current directory from where
the program was launched. In most cases, this is the Windows-icon "Start In" path. But
during development and testing, the current directory is the bin\debug directory used by
the editor. Later it becomes what ever directory the final compiled EXE is run from.

This function uses several new (and interesting) commands, each of which is explained
shortly:

• File.Exists(filename)
• new FileInfo(filename)
• Environment.GetEnvironmentVariable("windir")
• Environment.GetFolderPath(Environment.SpecialFolder.System)
• Environment.CommandLine
• string[] args = Environment.GetCommandLineArgs();

File.Exist and Environment Variables:

Starting with the easiest, issue a DOS File-directory command to see if the INI file exists
in the current directory. This will be a "Directory-listing" using the filename-only,
without a path. If it is not found, continue looking in a more specific area:

Chapter 14 - Reading INI Files Page: 181


A028_DiscoverINIFileName, preliminary

1 private string A028_DiscoverINIFileName ()


2 {
3 //Discover the default INI file by looking in three locations
4 //These are order-dependent searches.
5 //Favor the current directory first; then %SYSTEM%
6
7 bool boolfileFound;
8 string strappData; //Windows Environment variable
9 string strINIFileName = "ExampleProgram.ini";
10 string strfullPathFileName;
1
2 //(This is where the Command-line option goes; see next section)
:
19 //Search for the preference file in the current directory
20 //Note: no path in the File.Exists statement:
1 boolfileFound = File.Exists(strINIFileName);
2 if (boolfileFound == true)
3 {
4 FileInfo fi = new FileInfo(strINIFileName); //assume currdir.
5 strfullPathFileName = fi.FullName;
6 }
7 else
8 {
9 //Search the profile diectory; discoverable with DOS "SET"
30 //LocalAppData C:\users\(you)\appData\Local)
1 strappData =
Environment.GetEnvironmentVariable("LOCALAPPDATA");
2 boolfileFound = File.Exists
(strappData + "\\" + strINIFileName);
3
4 if (boolfileFound == true)
5 strfullPathFileName = strappData + "\\" + strINIFileName;
6 else
7 //File not found in either directory; return simple
8 //name and let downstream routines handle missing file:
9 strfullPathFileName = strINIFileName;
40 }
1
2 return fullPathFileName;
3 }

where:

• Line 21. The File.Exists("ExampleProgram.ini") sets a boolean true/false


value if the indicated file exists. Since a drive-path was not used, the "current"
directory is used. If found, retrieve the entire path and fall to the bottom of the
routine. The fully-qualified name is returned.

• The fully-qualified file path name is found by using a new variable type, "FileInfo".
Line 24 shows the FileInfo variable was named "fi". From here, gather the entire
path with a "fi.FullName". More details on these types of commands can be found

Chapter 14 - Reading INI Files Page: 182


in Chapter 23. In this example, the returned path name is from the compiler's point
of view:

Example returned fi.FullName:


"C:\\Data\\Source\\ExampleProgram\\bin\\debug\\ExampleProgram.ini"

• If the file was not found in the current directory, the else-statement guides the code
into a second test, starting at line 30, which does a similar search in the user's profile
directory.

To find the profile directory, open a DOS prompt and type the command "SET".
This is a standard DOS environment variable.

The code could have used a try-catch when opening the first file. If the file didn't
exist, the "catch" would trigger and you could build another nested try-catch to see
if the second file existed. This is a convoluted idea. It seems more direct to ask if
the file exists.

• Note the marker: //This is where command-line options go, line 12. This is
covered in the next section.

Testing:

Follow these steps to test this routine.

1. Using Windows explorer, copy the INI file to these locations:

...\Bin\Debug\ExampleProgram.ini
C:\Users\(Your Name)\AppData\Local\ExampleProgram.ini

Your path will likely be different. This is the same path where you originally created the
project.

2. Add a break-point (red-ball in the left-margin) at the closing brace for


A028_DiscoverINIFileName.

3. Press F5 to run the program.

When the program breaks in A021, click inside of the editor to activate the window.
Then hover the mouse over the strfullPathFileName variable to see the results.

4. End the program (Toolbar's Red-Square)

5. Rename
Disguise the ...Bin\Debug\ExampleProgram.ini file by renaming to
"ExampleProgram.ini.bak".

Chapter 14 - Reading INI Files Page: 183


6. Re-run step 3.
Confirm the new file-location was found in the profile directory.
Again, end the program by clicking the tool-bar's blue-square.

7. Rename the original bin\Debug version to its original name.

Chapter 14 - Reading INI Files Page: 184


Command-Line Override

The final step is to add a DOS-command-line override. With this, any INI-file, any
name, from any location, can be passed to the program, giving it an unlimited number of
separate configuration files. This is typically used when the program needs to connect to
PROD and TEST databases.

Overview:

Once the program is compiled, the EXE can launch with optional command-line
parameters. Normally, the parameters are typed in the Windows Shortcut/icon. For
example, in the desktop icon's properties, the "Target" field could be modified with
command-line options:

"C:\Program Files\myProgram.exe" ini=C:\Data\Test.ini

Note this is a different directory than before and the INI file could be named with any
name. For example:

"C:\Program Files\MyProgram.exe" ini=C:\data\Production.ini

where "ini=<path>" is a contrived design. The example program will parse the file's
location using the invented "ini=" clause.

Chapter 14 - Reading INI Files Page: 185


Simulate a Command Line Options in the Editor:

While in the Visual Studio editing environment, Windows Shortcuts cannot be used but
the editor provides a way to simulate a command line. Using a Properties screen within
the project, follow these steps:

A. Using Windows Explorer, create another copy of the INI file.


Copy ...bin\debug\ExampleProgram.ini to a separate location, such as C:\data\Test.ini
(or any other location)

B. Return to the Visual Studio editor.


Select Menu choice: Project, (iniFileRead) Properties...

C. On the Left-Nav, choose "Debug"

In the Command Line Arguments field, type


ini=C:\data\Test.ini (without quotes) or
ini="C:\Data\Test Region.ini" (with quotes when the filename has embedded spaces)

Click "X" to close the window.

Chapter 14 - Reading INI Files Page: 186


The Command Line arguments simulate parameters in desktop icon or after a DOS
command-line. This remains in effect only while using the editor and is ignored in the
compiled .exe. Do not forget this option. The editor will continue to use this setting as
long as it is present.

In the Command Line you can place other switches and


instructions to your program. For example, I often add the
ability to pass "/TEST" switches or "/?" for online help.

Chapter 14 - Reading INI Files Page: 187


Important: Required Security Settings:

By default, Visual Studio will not allow passed command line arguments,
even though the options are set in the Project's properties. Symptoms: The
program will behave as if no command-line arguments were passed,
especially if you compile a Release version of the program.

Make this change in the program:

In Project Properties, Security,


Un-check [x] Enable ClickOnce Security Settings.

Command Line Code:

Adding the logic for a command-line override is relatively easy. Near the top of the
function A028_DiscoverINIFileName, line 12, replace the comment "\\This is where the
command line option goes" with the following code.

Start out with an impromptu diagnostic routine, at line 12, just for testing. Add this code
now:

Chapter 14 - Reading INI Files Page: 188


A028: Logic to Parse Command-Line Parameters, in progress with Diagnostic
code
1 private string A028_DiscoverINIFileName()
2 {
//Look in these locations for the default INI file
//These are order-dependent searches.
//First, look in the Command Line for an override
//Then search currentDOS directory; if not found, search
//%windows%
//If still not found, return a forced-string and
//let downstreams fail

bool boolfileFound;
string strappData;
string strfullPathFileName;

12 //Diagnostic routine for command-line:


13 string [] aargs;
14 aargs = Environment.GetCommandLineArgs();
15 foreach (string stritemFound in aargs)
16 {
17 MessageBox.Show(stritemFound); //With diagnostic code
18 }

:
//(Remainder of the File Found/File.Exist code is below here

Diagnostic Testing:

Press F5 to run the program. Form_Load calls A015, which calls


A028_DiscoverINIFileName.

After all the parameters display, the program continues and it will probably offer to build
a new default INI file. Do not allow it to build a new default INI and end the program.

j C# is sophisticated in how it handles command-line parameters. Each command-line


parameter (word/phrase) is loaded into a variable-length string-array, in this case
named "aargs" (array-args), and they are automatically parsed.

Each "command-line parameter" is automatically word-delimited by spaces (or


phrase-delimited with quotes) and each value goes into an array. In the code above,
a diagnostic MessageBox displays each array parameter as it is parsed from the
command-line, helping you understand the routine. Arrays are discussed in
Chapter 22.

The first array item, aargs[0], is always the compiled executable's name and can be
ignored. When testing, your diagnostic message may show a different name, but aarg[0]
will be similar to this:

"C:\Data\Source\VSProj\ExampleProgram\bin\debug\ExampleProgram.vshosts.exe"

Chapter 14 - Reading INI Files Page: 189


The next array item, aargs[1], is the first parsed parameter; aargs[2] is the second parsed
parameter, etc.

Each parameter (as typed in the Command-Line or Command-Line properties) gets a


new position in the array. Since the command-line was "ini=C:\Data\test.ini", with no
embedded spaces, this becomes the second parameter – called aargs position-1; written
as aargs[1].

Embedded Spaces in the Parameters:

If the command-line had embedded spaces, the results would be different. Consider this
hypothetical passed-command-line, which has embedded spaces in one of the
parameters:

myprogram.exe ini=C:\long path name\test.ini

The resulting args-array is probably not your intent:

The ini-filename parameter breaks up into multiple array positions, which is generally
not good. The results are more palatable if the filename is typed with quotes:

Chapter 14 - Reading INI Files Page: 190


As it is parsed, quotes are removed and the result is once again stored as a single
parameter – a single entry in the array. In this example, the "ini=" prefix needs to be
stripped and this is handled later in the code.

Why Use Command-line ini=?

Why use the text "ini=" in the command-line? Command-lines can have multiple
parameters and you cannot guarantee which order they were typed. For example, on the
command-line you might allow an optional parameter "/TEST", which could tell your
program to run but not commit records to a database or "/DIAG" which exposes hidden
MessageBoxes to aid in debugging. The command-lines could look like this:

ini="C:\long path name\test.ini" /DIAG


ini="C:\long path name\test.ini" /DIAG /TEST
/DIAG ini="C:\long path name\test.ini" /TEST
etc.

The program can loop through the command-line list, searching for the keyword "ini="
and once found, it is an easy mid-string parse away from stripping the filename into its
own variable.

Completing the command-line Code:

The completed code is listed on the next page, along with a few minor enhancements.

For testing, place a breakpoint (red-ball) at the File.Exist statement in the command-line
section. Run the program up to the breakpoint and click inside the editor to activate the
debugger. Hovering the mouse over the strINIFileName variable to see if it correctly
stripped the "ini=" prefix. Press F11 to keep stepping through the code.

Chapter 14 - Reading INI Files Page: 191


This is the completed subroutine, ready to place into your code. A028 looks in three
places to find a configuration INI file, starting with an optional pass-ed command-line,
then looking in the current directory and finally, if not found, it looks in the user's
profile. Add this code now to the previously-completed program. Lines 12 through 37
are the new entries:

A028_DiscoverINIFileName, completed
Example reads command-line arguments using non-positional Type=Value arguments,
e.g. ini="C:\file.text"
See below for example code that uses positional arguments

1 private string A028_DiscoverINIFileName ()


2 {
3 //Discover the default INI file by looking in three locations
4 //These are order-dependent searches.
5 //Favor command-line, then current directory, then profile
6
7 bool boolfileFound;
8 string strappData;
9 string strfullPathFileName;
10
1
2 //Parse the command line for arguments, if present. Use if found

Chapter 14 - Reading INI Files Page: 192


3 //MessageBox.Show(Environment.CommandLine);
4
5 string [] aargs;
6 aargs = Environment.GetCommandlineArgs();
7 foreach (string stritemFound in aargs)
8 {
9 if (util.LeftStr(stritemFound, 4).ToLower() == "ini=")
20 {
1 //A commandline parameter was found; strip the prefix
2 strINIFileName = util.MidStr(stritemFound, 4, 999).Trim();
3 boolfileFound = File.Exists(strINIFileName);
4 if (boolfileFound == true)
5 {
6 strfullPathFileName = strINIFileName;
7 return strfullPathFileName;
8 }
9 else
30 {
1 strfullPathFileName = strINIFileName; //bad
2 pnlMsg.Text = "Passed INI file not found";
3 //Let upstream routines fail with this
4 return strfullPathFileName;
5 }
6 }
7 }
8
9 //Otherwise, search for the pref file in the current directory
40 //Note: no path in the File.Exists statement
1 boolfileFound = File.Exists(strINIFilename);
2 if (boolfileFound == true)
3 {
4 //Assume the current directory...
5 FileInfo fi = new FileInfo(strINIFileName);
6 strfullPathFileName = fi.FullName;
7 }
8 else
9 {
50 //Search the user profile directory: LocalAppData
1 //C:\users\<name>\appData\Local
2 //Paths discoverable with a DOS "SET" command
3 strappData =
Environment.GetEnvironmentVariable("LOCALAPPDATA");
4 boolfileFound = File.Exists(strappData + "\\" + strINIFileName;
5
6 if (boolfileFound == true)
7 strfullPathFileName = strappData + "\\" + strINIFileName;
8 else
9 //File not found in expected location
60 //return simple filename; let upstream code fail horribly
1 strfullPathFileName = strINIFileName;
2 }
3
4 return strfullPathFileName; //As assembled
5 }

A028_DiscoverINIFileName, end

Chapter 14 - Reading INI Files Page: 193


where:

• This routine expects command line parameters to be passed using sub-keywords


instead of their position in the command line.

ini="C:\Some path and filename.txt"

where a keyword (such as "ini=") will be stripped after the results are parsed. See
below for a positional method.

• Line 9 sets the filename as a hard-coded value, acting as the current-directory default
filename. If a command-line override is detected, this value is replaced.

• If parameters were not passed, look in the current directory (line 41) and do similar
work

• Lines 15, 16, and 17 take each of the command-line parameters and loads them (one
word or phrase at a time) into an array called "aargs". The first item in the array is
always the calling-executable's name. All other items in the array are searched for
the keyword "ini=". If found, the filename is parsed at line 22 and this name
replaces the hard-coded value set in line 9.

• Line 23 confirms the passed-filename actually exists. If found, the full path is
returned to the calling routine, otherwise an error displays and the bad filename is
returned to the calling routine, where it can deal with it. The design is this: A028
should do little else than discover the intended preference file and send it back
upstream.

• Line 64 returns the determined name to the calling routine, A015. A015 tries to
open the file (whether it exists or not). If it fails on the 'outside-catch', A015 offers
to create a new file:

Chapter 14 - Reading INI Files Page: 194


Design Comments:

Consider what would happen if no parameters were passed, or if an INI= parameter was
not passed through the command-line. For example:

C:\ExampleProgram.exe /Diag (with one parameter, but no filename)


C:\ExampleProgram.exe (with no parameters)

The code uses a foreach (string stritemFound in aargs), and loops through the
choices. If there were no items of interest, the foreach-loop would gracefully exit. But
imagine you were processing the aargs by hand, without the benefit of the foreach loop.
Let's say you were looking for a /DIAG switch or a ini= parameter.

if (aargs[1].ToUpper() == "/DIAG")
{
boolDiagSW = true;

...the program would abend with an "Index was outside the bounds of the array" if no
parameters were passed through the command-line – even though aargs[0] is populated
with the calling program's name. Use this logic instead:

if (aargs.Length >= 2 && aargs[1].ToUpper() == "/DIAG")


{
boolDiagSW = true;

where:

• aargs[0] is always the program name, aargs[1] would be the first real parameter; the
second item in the array.

Chapter 14 - Reading INI Files Page: 195


• aargs.Length is the base-1 count of the items in the array. (Lengths (counters) are
always base-1 – even though the array itself starts with base-0.

• The double-ampersand-test is absolutely required. Recall that the double-ampersand


stops testing at the first false-clause. Thus, if there were no additional items in the
array, do not bother with the ".ToUpper" test; saving an abend.

Warning Message:

You may also see this message: "The current project settings specify that the project will
be debugged with specific security settings." Select Project, Properties, Security,
uncheck the "Enable ClickOnce Security Settings."

Chapter 14 - Reading INI Files Page: 196


A029: Write Default INI when Missing

A015 opens the default INI file and processes each line. But what should happen if the
INI file is not found? Rather than panic during the outside-catch, why not offer to build
a new, default INI file with your company preferences? This is not required by an INI
file-reader, but is often useful.

Setting up the User Prompt:

When A015 attempts to open the INI file (strINIFileName), the routine is nestled within
a try-catch. If the file is not found, the catch-routine can prompt the user with this
question:

To prompt like this, use a MessageBox overload that returns a result into a new type of
variable called "DialogResult". This added feature is coded outside the try-catch

Chapter 14 - Reading INI Files Page: 197


A015: Modified Outside Catch: MessageBox Yes/No Prompt
:
catch
{
//File Open Error
//INI file cannot be opened or found.
//Optionally offer to build a new default INI

DialogResult userReply; //Declare a variable

userReply = MessageBox.Show("Prefs Load Error: Cannot find '" +


strINIFileName + "' \r\n" +
"Build a new, default INI File?",
"New INI File",
MessageBoxButtons.YesNo);

if (userReply == DialogResult.Yes)
{
A029_WriteDefaultINI (strINIFileName);
//Still Error - make user re-launch program
MessageBox.Show("You must Close and re-load program");
return true;
}

//Otherwise:

MessageBox.Show("INI File Load Error: '" +


strINIFileName + "' \r\n" + e.Message);
return true;
}

return false; //If you fall through here; all must be well
}

where:

MessageBox.Show captures the users selection and uses these comma-delimited


parameters to build the dialogue:

MessageBox.Show
Main Message: "Prefs Load Error + filename and 'Build new INI file?'"
TitleBar Message: "New INI File?"
Button options: .YesNo

As the phrase 'MessageBoxButtons' is typed, notice the list of available options from the
pop-up:

Chapter 14 - Reading INI Files Page: 198


Usually, immediately after the MessageBox.Show, the code checks to see what results
were captured from the end-user. The next if-statement queries the new variable type:

if (userReply == DialogResult.Yes)
{
A029_WriteDefaultINI (strINIFileName);

In a Yes/No dialog, check for the positive result and allow all other options to skip the if-
statement. In other words, if the user clicks No, Cancel, or "X", let the code flow to the
next statement, taking action only on a positive result.

A029_WriteDefaultINI:

The logic that writes the new default INI file is the same as covered in Chapter 12. As
usual, with any file-write, wrap the commands with a try-catch.

If this routine fails, set enough flags to ensure that the calling routines detect the failure.
In this case, the global variable "boolStartupError" is set and the user was given a visible
prompt announcing the error.

Chapter 14 - Reading INI Files Page: 199


A029_WriteDefaultINI, completed
private void A029_WriteDefaultINI (string strINIFileName)
{
//Write program default.ini, per company standards

try
{
StreamWriter mainINIFile = new StreamWriter(strINIFileName);

mainINIFile.WriteLine(";Program INI File");


mainINIFile.WriteLine
(";Keyword-dependent INI file; edit with spaces");

mainINIFile.WriteLine(""); //Print blank line


mainINIFile.WriteLine("[General]");
mainINIFile.WriteLine("ProgramVersion = 2.01");
mainINIFile.WriteLine("ServerName = omni");
mainINIFile.WriteLine("ShareName = vol1\\KB");

mainINIFile.WriteLine
("UserAuthentication = PAY_Acct ;SystemAcct");

mainINIFile.WriteLine
("UserPassword = 14%BDB010C1FB16014.D79.4B1BF1.DD");

mainINIFile.WriteLine("AllowPasswordSave = Y");

mainINIFile.Close();
mainINIFile.Dispose();
pnlMsg.Text = "New Default INI written";

}
catch (Exception e)
{
pnlMsg.Text = "Default INI file not written";

MessageBox.Show("Unable to write default INI file: \r\n" +


e.Message);
}
}

Testing:

A. Using Windows Explorer, locate the original ExampleProgram.INI


(C:\Data\ExampleProgram.ini). Rename the file to a different name.

B. Launch the program (F5) and confirm the MessageBox prompt.


Click btnClose to exit your program.

C. Using Notepad, open the newly-built INI file and confirm it was constructed properly.
The, re-launch (F5) your program to confirm all values loaded (consider using a
breakpoint after A015 and investigate the set values.

Chapter 14 - Reading INI Files Page: 200


Basic INI File Read: Complete Code

For reference, below is a complete program that opens and reads an INI file. This is the
final version of the previous section(s) design. All the features described above are in
place, including command-line overrides. Possible [Section] headings, comments and
blank lines are ignored.

Use this code when you want the INI Read routine embedded directly in your
program. Literally copy the entire program, from the Names-space down. Then
edit A017 and A029 for your program's unique needs.

However, if you never want to write an INI Read routine again,


build the CL860 Class Library, described in the next section.

Basic INI File Read, completed


Use as part of Form1/ExampleProgram; see the next section for a recommended
Program Class

using System;
using System.Windows.Forms;
using System.IO;
using NS800_Util;

//Opening and reading a basic INI file


//This routine ignores INI-file[section] headers; treating
//as comments.
//See text for logic on how to process individual
//[Section] headers via A020_ReadToNextSection

namespace ExampleProgram
{
public partial class Form1 : Form
{
CL800_Util util;

string strReadLine;
string strINIFileName = "ExampleProgram.ini"; //Default if none

//Variables to be set from the INI file:


string strProgramVersion;
string strServerName;
string strShareName;

public Form1()
{
InitializeComponent();
util = new CL800_Util();
}

private void Form1_Load(object sender, EventArgs e)


{
this.Show();

Chapter 14 - Reading INI Files Page: 201


bool boollocalStartupError = A015_GetPrefs();
if (boollocalStartupError == true)
{
MessageBox.Show ("Error: '" +
strINIFileName + "' was not processed");
btnClose_Click (null, null);
}

MessageBox.Show("Selected INI File Values: " + "\r\n" +


strProgramVersion + "\r\n" +
strServerName + "\r\n" +
strShareName);
}

private void BtnClose_Click(object sender, EventArgs e)


{
this.Close();
Application.Exit();
}

private bool A015_GetPrefs()


{
int ilineCounter = 0;
bool boollocalStartupError = false;

strINIFileName = A028_DiscoverINIFileName();
if (util.IsBlank(strINIFileName))
{
return true;
}

try
{
//Open the Preference file
StreamReader mainINIFile = new StreamReader(strINIFileName);

//Prime the read:


strReadLine =
A019_ReadNextDetail(ref mainINIFile, ref ilineCounter);

try
{
//Main detail loop here:
while (strReadLine != null)
{
//Skip comments and blank lines
if (util.LeftStr(strReadLine, 1) == ";" ||
util.LeftStr(strReadLine, 1) == "'" ||
util.LeftStr(strReadLine, 2) == "//" ||
util.IsBlank(strReadLine))
{
//Do nothing with commented lines;
//Let the after'else' handle the logic-fall-through
//[Section] Headers are handled below
}
else
{
boollocalStartupError =
A017_ParseINIDetail(strReadLine);
if (boollocalStartupError == true)
{
return true;
}

Chapter 14 - Reading INI Files Page: 202


}

//Read the next record before re-looping:


strReadLine =
A019_ReadNextDetail(ref mainINIFile, ref ilineCounter);
}
}
catch //For detail-record errors and problems
{
if (util.IsBlank(pnlMsg.Text))
{
pnlMsg.Text = "Preference INI Error at line " +
ilineCounter +
" '" + util.LeftStr(strReadLine, 20).Trim() + "...'";
return true;
}
}

mainINIFile.Close();
mainINIFile.Dispose();

}
catch (Exception e) //File Open problems
{
//File Open Error
//INI File cannot be loaded or found
//optional logic to prompt to build a default file

DialogResult userReply; //Declare the variable

userReply = MessageBox.Show("Preference INI Load Error: " +


"Cannot find '" + strINIFileName + "' \r\n" +
"Build a new default INI file?",
"New INI File",
MessageBoxButtons.YesNo);

if (userReply == DialogResult.Yes)
{
A029_WriteDefaultINI(strINIFileName);
//Still error and make them re-launch
MessageBox.Show("You must close and re-load this program");
return true;
}

//Otherwise

MessageBox.Show("INI File Load Error: " +


"'" + strINIFileName + "'" + "\r\n" +
e.Message);
return true;
}

return false; //If you fall through here; all must be well
}

private bool A017_ParseINIDetail(string strReadLine)


{
//Parse the keyword/keyvalue pairs and populate
//Form-level variables. This routine only concerns
//itself with individual detail lines:
//Values here are unique for this program; change values as
//needed; also at the top of this module
//e.g. SERVERNAME = myServerName

Chapter 14 - Reading INI Files Page: 203


string strKeyWord;
string strKeyValue;

//Early Exit
if (util.LeftStr(strReadLine, 1) == "[")
{
//Skip [Section] Headers;
//essentially treat as comments
return false;
}

//Early Exit:
//Confirm the line is a standard INI-line:
//(Comments and blank lines were removed earlier)
//This could set errors but are being ignored
if (!strReadLine.Contains("="))
return false;

//The Detail line is eligible for parsing


strKeyWord = util.ParseKeyName(strReadLine, "=").ToLower();
strKeyValue = util.ParseKeyValue(strReadLine, "=");

switch (strKeyWord)
{

//Only valid keyword=value lines can arrive here


case "programversion":
strProgramVersion = strKeyValue;
break;

case "servername":
strServerName = util.StripSlashes(strKeyValue);
break;

case "sharename":
strShareName = util.StripSlashes(strKeyValue);
break;

default:
pnlMsg.Text = "Preference INI Error: " +
"Invalid or unrecognized line '" +
strKeyWord + " = " + strKeyValue + "'";
return true;
}

//otherwise, all is well


return false; //Return all-is-well
}

private string A019_ReadNextDetail


(ref StreamReader mainINIFile, ref int ilineCounter)
{
//Read next detail line; watching for null-end-of-file records

string tstrReadLine; //temp

ilineCounter++;
tstrReadLine = mainINIFile.ReadLine();

if (util.IsFilled(tstrReadLine))
{
//(only trim if not null; worried about EOF processing)
tstrReadLine = tstrReadLine.Trim();

Chapter 14 - Reading INI Files Page: 204


}
return tstrReadLine;
}

private string A028_DiscoverINIFileName()


{
//Discover the default INI file by looking in three locations
//These are order-dependent searches.
//Favor command-line, then current directory, then profile

bool boolfileFound;
string strappData;
string strfullPathFileName;

//Parse the command line for arguments, if present. Use if found


//MessageBox.Show(Environment.CommandLine);

string [] aargs;
aargs = Environment.GetCommandLineArgs();
foreach (string stritemFound in aargs)
{
if (util.LeftStr(stritemFound, 4).ToLower() == "ini=")
{
//A commandline parameter was found; strip the prefix,
//presumably leaving a pathed-filename minus the "ini="
strINIFileName = util.MidStr(stritemFound, 4, 999).Trim();
boolfileFound = File.Exists(strINIFileName);
if (boolfileFound == true)
{
strfullPathFileName = strINIFileName;
return strfullPathFileName;
}
else
{
strfullPathFileName = ""; // strINIFileName bad
pnlMsg.Text = "Passed INI file not found";
//Let upstream routines fail with this
return strfullPathFileName;
}
}
}

//Otherwise, search for the pref file in the current directory


//using the default INI Name
//Note: no path in the default File.Exists statement
boolfileFound = File.Exists(strINIFileName); //default value
if (boolfileFound == true)
{
//Assume the current directory...
FileInfo fi = new FileInfo(strINIFileName);
strfullPathFileName = fi.FullName;
}
else
{
//Search the user profile directory: LocalAppData
//C:\users\<name>\appData\Local
//Paths discoverable with a DOS "SET" command
strappData =
Environment.GetEnvironmentVariable("LOCALAPPDATA");
boolfileFound = File.Exists(strappData + "\\" + strINIFileName);

if (boolfileFound == true)
strfullPathFileName = strappData + "\\" + strINIFileName;
else
//File not found in expected location

Chapter 14 - Reading INI Files Page: 205


//return simple filename; let upstream code fail horribly
strfullPathFileName = strINIFileName;
}

return strfullPathFileName; //As assembled

private void A029_WriteDefaultINI(string strINIFileName)


{
try
{
StreamWriter mainINIFile = new StreamWriter(strINIFileName);

mainINIFile.WriteLine(";Program INI File");


mainINIFile.WriteLine
(";Keyword-dependent INI file; edit with spaces");

mainINIFile.WriteLine("");
mainINIFile.WriteLine("[General]");
mainINIFile.WriteLine("ProgramVersion = 2.01");
mainINIFile.WriteLine("ServerName = omni");
mainINIFile.WriteLine("ShareName = vol1\\KB");

mainINIFile.Close();
mainINIFile.Dispose();

pnlMsg.Text = "New Default INI written";


}
catch (Exception e)
{
pnlMsg.Text = "Default INI File NOT written";
boolStartupError = true;

strlocalPNLMsg = "Unable to write default INI file:" +


"\r\n" + e.Message;
}
}

}
}

Simple INI File Read, end

Chapter 14 - Reading INI Files Page: 206


Using a Program Class - CL860 INI File Read

If you never want to write another INI Read routine, follow these steps to build a
reusable class. This is a one-time setup and is the recommended way to read INI files.

The code listed below is nearly identical to the examples above, but this version is stored
in an external program class library and uses Form (class) Properties for the variables.
Because of this, there are minor changes in how the variables are built and in how the
modules are called. Beyond that, all logic is identical to the example above. This takes a
few extra minutes to setup but is worth the effort.

For review, see Chapter 8 (Creating an External Class


Library from scratch) and Chapter 11 (Multiple Forms -
Returning Results to the Parent via Properties)

One-Time Construction Steps - building CL860_BasicINIRead

Following Chapter 8 "Creating an External Program Class Library from Scratch", do the
following.

A. Launch a new copy of Visual Studio. Create a Windows "Class Library".


Name: NS860_BasicINIRead
Location: C:\Data\Source\CommonVS

B. In the newly-created class, in Solution Explorer, rename "Class1.cs" to


"CL860_BasicINIRead.cs"

Add Existing Item, as a Link, CL800_Util.

C. At the top of the Class, add these Using statements:

using NS800_Util;
using System.IO;

Note: The "using System.Windows.Forms;" statement has been removed so


this class can be use with console apps as well as windows-forms apps.

D. The big step: Type or copy from the previous section (most) of the source code for the
Basic INI File Read program and then make editing changes, described next, with a
complete source code listing below that.

Chapter 14 - Reading INI Files Page: 207


Make these Changes in the Class Library

• Delete the top "using System.Windows.Forms" statement.

The new CL860 class does not have a Form where error messages, user prompts or
MessageBoxes can live. Look through the code and replace all MessageBox
statements with strLocalPNLMsg = "(the text of the MessageBox)".

At the prompt to build a new default INI file, replace all Yes/No MessageBox logic
with unconditional logic. See code, below.

• In the new class, manually build a Class Constructor method, by typing this code
below near the top of the program.

public CL860_BasicINIRead()
{
//Constructor must be manually built
//InitializeComponent; – Note this is commented
util = new CL800_Util();
}

Delete the Form Load event.

• Change all calls to Btn_CloseClick to a "return;" statement.

• At the top of the class, just below the CL800_Util util statement, create these
variables, one for each value being returned by the INI file. Use the prefix "Local"
to help you remember their intent. For these examples, use these:

strLocalPNLMsg - Simulates the PnlMsg field


strLocalProgramVersion - Values found in the INI file
strLocalServerName
strLocalShareName

Note: These are sample variables. Your program will have unique values from its
own INI file.

• A variety of Class Property (variables) will be written, which were not present in the
original program earlier in the chapter. These are the same as the Form Properties
and InstanceRef variables seen in the MultiForm chapter.

For example, for each INI value, this code will live near the top of the class (don't
write this code yet):

Chapter 14 - Reading INI Files Page: 208


private string strprivProgramVersion
public string strpubProgramVersion

private string strprivServerName


public string strpubServerName
etc. (see source code for the complete list)

• Rename all methods (functions) from A000 to B000 (e.g. A015_GetPrefs becomes
B015_GetPrefs. A019 becomes B019, etc. This is a cosmetic change so it is
obvious these routines are in a different class.

Make the top-level B000_ReadINI a public method.

• Change all references from pnlMsg.Text = to "strprivPNLMsg =".

Unique Variable Changes

This class-example has references to the INI file's strProgramVersion, strServerName,


and strShareName. In real life, when you are ready to use this class, you will "COPY-
link" this class into your program, then change these variables to suit your needs.

From the code below, your program will only need to change in these three
areas and the changes are usually minor.

B000 (moving variables to their final destination),


B017 (Process details, which are always unique to your program), and
B029 (Write default INI, which are unique to your program)

When your Form1 (usually in the Form_Load module) reads the INI, it can retrieve any
value by making a direct call to the public variables. Exact steps are below, after the
code listing.

Chapter 14 - Reading INI Files Page: 209


CL860 Basic INI Read Class Library

Write this routine one time, then copy (Link as a copy) into any program that
needs to read an INI file. Once copied, make minor variable changes. You
have a complete, fully-tested and ready-to-use INI File class.

Follow the one-time construction steps on the previous pages to build the class library.
This is the code inserted in step D. Note, most of this code can be copied, with minor
edits, from earlier in this chapter. See "Summary Changes", above for areas where the
code is different than the original example.

A single call to B000_ReadINI does everything necessary to process an INI file. This
includes opening, parsing, building default values, and closing. With this one call,
multiple results can be pulled back into your program.

If you do not have the cl800_Util library built, you will have to write the utility routines
by hand. The utility library is downloadable from the author.

Save this routine in a known directory, such as


C:\Data\Source\CommonVS\NS860_BasicINIRead

CL860_BasicINIRead.cs, completed

using System;
using NS800_Util;
using System.IO;

//Opens and Reads a basic INI file.


//This routine ignores all INI-file [section] headers, treating as comments.
//Variables are returned to the calling program (form1) via Form Properties.
//
//To use, follow steps outlined in text. In summary:
//Add this module, Add Existing, Copy *do not link* because variables
//will always change with each use.
//
//*** During development, as a standalone class, you will have 9 errors,
//*** all dealing with MessageBox statements. These will resolve once
//*** the class is brought into a parent program.

namespace NS860_INIRead
{
class CL860_BasicINIRead
{
//Requires above
//using System.Windows.Forms;
//using NS800_Util; (with a linked-in cl800_Util class)
//using System.IO;

CL800_Util util;

Chapter 14 - Reading INI Files Page: 210


//Class-Level Variables
string strReadLine;
string strINIFileName = "ExampleProgram.INI"; //default name if none
string strLocalPNLMsg = "";

//Class-Level INI Variable Names (change as required by your program)


//See also B000, B017, BO29
string strLocalProgramVesrion;
string strLocalServerName;
string strLocalShareName;

//Class-Level Properties Variables (change as required by your program)

private string StrprivProgramVersion = "";


public string StrpubProgramVersion
{
get { return StrprivProgramVersion; }
set { StrprivProgramVersion = value; }
}

private string StrprivServerName = "";


public string StrpubServerName
{
get { return StrprivServerName; }
set { StrprivServerName = value; }
}

private string StrprivShareName = "";


public string StrpubShareName
{
get { return StrprivShareName; }
set { StrprivShareName = value; }
}

//See also B000, B017 and B029 for other variable steps

//Required Class-level Properties

private bool BoolprivStartupError = false;


public bool BoolpubStartupError
{
get { return BoolprivStartupError; }
set { BoolprivStartupError = value; }
}

private string StrprivPNLMsg = "";


public string StrpubPNLMsg
{
get { return StrprivPNLMsg; }
set { StrprivPNLMsg = value; }
}

// ****************************************************************

//Manually build a class constructor - much like a form constructor

public CL860_BasicINIRead()
{
//Constructor
//InitializeComponent; - cannot be used; not a form-based program

//Instantiate the Utility library


util = new CL800_Util();

Chapter 14 - Reading INI Files Page: 211


}

//Build the Public Method that will be called by Form1:

public void B000_ReadINI()


{
//Main method; called by Form1

bool boollocalStartupError = B015_GetPrefs();


if (boollocalStartupError == true)
{
BoolprivStartupError = boollocalStartupError;
strLocalPNLMsg = "B000_GetPrefs: Unable to read INI File";
return;
}

//Return all stored values to the calling Form


//Variables are passing through Class Properties
BoolprivStartupError = boollocalStartupError;
strprivPNLMsg = strLocalPNLMsg;

//Your Variables here: - typically the found INI values


strprivServerName = strLocalServerName;
strprivProgramVersion = strLocalProgramVesrion;
strprivShareName = strLocalShareName;
}

private bool B015_GetPrefs()


{
//Open and process the INI file

int ilineCounter = 0;
bool boollocalStartupError = false;

strINIFileName = B028_DiscoverINIFileName();
if (util.IsBlank(strINIFileName))
{
return true; //No INI found
}

try
{
//Open the Preference file
StreamReader mainINIFile = new StreamReader(strINIFileName);

//Prime the read:


strReadLine =
B019_ReadNextDetail(ref mainINIFile, ref ilineCounter);

try
{
//Main detail loop here:
while (strReadLine != null)
{
//Skip comments and blank lines
if (util.LeftStr(strReadLine, 1) == ";" ||
util.LeftStr(strReadLine, 1) == "'" ||
util.LeftStr(strReadLine, 2) == "//" ||

util.IsBlank(strReadLine))
{
//Do nothing with commented lines;
//Let the after'else' handle the logic-fall-through
//[Section] Headers are handled below

Chapter 14 - Reading INI Files Page: 212


}
else
{
boollocalStartupError =
B017_ParseINIDetail(strReadLine);
if (boollocalStartupError == true)
{
return true; //Stop if any serious errors are found
}
}

//Read the next record before re-looping:


strReadLine =
B019_ReadNextDetail(ref mainINIFile, ref ilineCounter);
}
}
catch
{
//For detail-record errors and problems

if (util.IsBlank(strLocalPNLMsg))
{
strLocalPNLMsg = "Preference INI Error at line " +
ilineCounter +
" '" + util.LeftStr(strReadLine, 20).Trim() + "...'";
return true;
}
}

mainINIFile.Close();
mainINIFile.Dispose();

}
catch //File Open problems
{
//File Open Error
//INI File cannot be loaded or found
//optional logic to prompt to build a default file

B029_WriteDefaultINI(strINIFileName);
//Still error and make them re-launch
Console.WriteLine("INI file not found; new default written");
Console.WriteLine("You must close and re-load this program");
return true;
}

return false; //all is well


}

private bool B017_ParseINIDetail(string strReadLine)


{
//Change as needed for your program
//See also B00 and B029

//Parse the keyword/keyvalue pairs and populate


//Form-level variables. This routine only concerns
//itself with individual detail lines:
//Values here are unique for this program; change values as
//needed; also at the top of this module
//e.g. SERVERNAME = myServerName

string strKeyWord;
string strKeyValue;

Chapter 14 - Reading INI Files Page: 213


//Early Exit
if (util.LeftStr(strReadLine, 1) == "[")
{
//Skip [Section] Headers;
//essentially treat as comments
return false;
}

//Early Exit:
//Confirm the line is a standard INI-line:
//(Comments and blank lines were removed earlier)
//This could set errors but are being ignored
if (!strReadLine.Contains("="))
return false;

//The Detail line is eligible for parsing


strKeyWord = util.ParseKeyName(strReadLine, "=").ToLower();
strKeyValue = util.ParseKeyValue(strReadLine, "=");

switch (strKeyWord)
{

//Only valid keyword=value lines can arrive here


case "programversion":
strLocalProgramVesrion = strKeyValue;
break;

case "servername":
//Optionally, strip slashes from user-typed values
strLocalServerName = util.StripSlashes(strKeyValue);
break;

case "sharename":
strLocalShareName = util.StripSlashes(strKeyValue);
break;

default:
strLocalPNLMsg = "Preference INI Error: " +
"Invalid or unrecognized line '" +
strKeyWord + " = " + strKeyValue + "'";
return true;
}

return false; //All is well


}

private string B019_ReadNextDetail


(ref StreamReader mainINIFile, ref int ilineCounter)
{
//Read next detail line; watching for null-end-of-file records

string tstrReadLine; //temp

ilineCounter++;
tstrReadLine = mainINIFile.ReadLine();

if (util.IsFilled(tstrReadLine))
{
//(only trim if not null; worried about EOF processing)
tstrReadLine = tstrReadLine.Trim();
}
return tstrReadLine;
}

Chapter 14 - Reading INI Files Page: 214


private string B028_DiscoverINIFileName()
{
//Discover the default INI file by looking in three locations
//These are order-dependent searches.
//Favor command-line, then current directory, then profile

bool boolfileFound;
string strappData;
string strfullPathFileName;

//Parse the command line for arguments, if present. Use if found


//MessageBox.Show(Environment.CommandLine);

string [] aargs;
aargs = Environment.GetCommandLineArgs();
foreach (string stritemFound in aargs)
{
if (util.LeftStr(stritemFound, 4).ToLower() == "ini=")
{
//A commandline parameter was found; strip the prefix,
//presumably leaving a pathed-filename minus the "ini="
strINIFileName = util.MidStr(stritemFound, 4, 999).Trim();
boolfileFound = File.Exists(strINIFileName);
if (boolfileFound == true)
{
strfullPathFileName = strINIFileName;
return strfullPathFileName;
}
else
{
strfullPathFileName = ""; ///Return empty strINIFileName;
strLocalPNLMsg = "Passed INI File not found";
//Let upstream routines fail with this
return strfullPathFileName;
}
}
}

//Otherwise, search for the pref file in the current directory


//using the default INI Name
//Note: no path in the default File.Exists statement
boolfileFound = File.Exists(strINIFileName); //default value
if (boolfileFound == true)
{
//Assume the current directory...
FileInfo fi = new FileInfo(strINIFileName);
strfullPathFileName = fi.FullName;
}
else
{
//Search the user profile directory: LocalAppData
//C:\users\<name>\appData\Local
//Paths discoverable with a DOS "SET" command
strappData =
Environment.GetEnvironmentVariable("LOCALAPPDATA");
boolfileFound = File.Exists(strappData + "\\" + strINIFileName);

if (boolfileFound == true)
strfullPathFileName = strappData + "\\" + strINIFileName;
else
//File not found in expected location
//return simple filename; let upstream code fail horribly
strfullPathFileName = strINIFileName;
}

Chapter 14 - Reading INI Files Page: 215


return strfullPathFileName; //As assembled

private void B029_WriteDefaultINI(string strINIFileName)


{
try
{
StreamWriter mainINIFile = new StreamWriter(strINIFileName);

mainINIFile.WriteLine(";Program INI File");


mainINIFile.WriteLine
(";Keyword-dependent INI file; edit with spaces");

mainINIFile.WriteLine("");
mainINIFile.WriteLine("[General]");
mainINIFile.WriteLine("ProgramVersion = 2.01");
mainINIFile.WriteLine("ServerName = omni");
mainINIFile.WriteLine("ShareName = vol1\\KB");

mainINIFile.Close();
mainINIFile.Dispose();

strLocalPNLMsg = "New Default INI written";


}
catch (Exception e)
{
boolStartupError = true;

strLocalPNLMsg = "Unable to write default INI file: " +


"\r\n" + e.Message;
}
}

//End of class brackets


}
}

End of cl860_BasicINIRead Class Library

comments:

• Starting in VS2017, Microsoft generates an editor warning when any Public method
or variable does not begin with a capital letter.

For this reason, variables such as "StrpubProgramVersion" are irregularly cased with
a capital "Str..." prefix. Just to be consistent, I also made the private variables the
same. I found this particularly irritating.

Chapter 14 - Reading INI Files Page: 216


private string StrprivProgramVersion = "";
public string StrpubProgramVersion
{
get { return StrprivProgramVersion; }
set { StrprivProgramVersion = value; }
}

Using CL860_BasicINIRead

After all this work, here is the joy. Copying CL860 into any
program, and making a few variable changes, gives you a fully-
functional INI-file reader that you are in complete control of. It
is reasonably easy to use.

For each new project where an INI file needs to be read, follow these "cookbook" steps.
In your program, you will copy CL860_BasicINIRead.cs into your program. The reason
to copy instead of link is because every program has different INI settings. Because of
this, CL860 cannot be linked in like CL800_Util.

When the copy is complete, the module will act as an Inline Program class. The steps for
doing this are easy and assume the original class library was built in the previous
section, and was saved in your Source directory.

Steps:

From your project (e.g. Form1, ExampleProgram, etc.),

1. In Solution Explorer, highlight the program's name

Add Existing Item,


browse to C:\Data\Source\CommonVS\NS860_BasicINIRead.

Select "CL860_BasicINIRead.cs"
Choose "Add" (not Add as Link); this makes a copy of the Class

Chapter 14 - Reading INI Files Page: 217


As a reminder, if your program also uses the CL800_Util library, that library should
also be linked in now, using steps documented in previous chapters. Steps not detailed
here.

2. At the top of Form1, add a "using NS860_INIRead;"

Form1 - Required Using Statement, completed


using System.IO;
using NS800_Util;
using NS860_INIRead;

"using System.IO;" is handled by CL860, but if your program has other file-operations,
you may need it duplicated in your main program.

3. In Form1's "Form_Load" event (typical) or for testing, in button1_Click, add this logic to
run the INI File Read routines and retrieve the found values.

Chapter 14 - Reading INI Files Page: 218


Form1 Call to CL860_BasicINIRead, completed
private void Form1_Load(object sender, EventArgs e)
{
//Instantiate the ReadINI class in the method where needed,
//typically Form1_Load or in button1_Click; assign a friendly
//name "readINI"

CL860_BasicINIRead readINI;
readINI = new CL860_BasicINIRead();

//Call the module's only Public method.


//This one call does the entire Read operation. Hurray!

readINI.B000_ReadINI();

//Check for errors


if (readINI.BoolpubStartupError == true)
{
//Serious problems detected during INI read
//Errors should have displayed; take whatever action
//required
BtnClose_Click(null, null);
}
else
{
if (util.IsFilled(readINI.StrpubPNLMsg))
{
//Show whatever message is in the que with either a
//messagebox or someother statement
//MessageBox.Show(readINI.StrpubPNLMsg);
pnlMsg.Text = readINI.StrpubPNLMsg);
}

// ****************************************************
// Retrieve values with logic similar to this:
// ****************************************************
//Diagnostics or other steps showing the retrieved values
//Change as needed for your program's needs:

MessageBox.Show("Diagnostics: Show found values" + "\r\n" +


readINI.StrpubProgramVersion + "\r\n" +
readINI.StrpubServerName + "\r\n" +
readINI.StrpubShareName);

//Typically, these values should be moved to a local


//variable for longer-term storage
}

where:

• The CL860 class is instantiated with a friendly name "readINI".

• The call to readINI.B000_ReadINI calls the only Public method within the CL860
class. This one call does all the work required in the entire routine. It will find the
INI file, open, read and parse all values, write default INI files if missing, and all of

Chapter 14 - Reading INI Files Page: 219


the other steps from the original program – and then return the results to your
program.

• There is room for errors. The variable "BoolpubStartupError" watches for this. The
True/False value is passed from the CL860 class back to the main program, just like
any other variable in this process. This could have been returned as a value from the
signature line.

Unique Variable Changes

This class-example has references to the INI file's strProgramVersion, strServerName,


and strShareName. In real life, once copied, change these variables to suit your needs.

From the code below, your program will only need to change in these three
areas and the changes are usually minor.

B000 (moving variables to their final destination),


B017 (Process details, which are always unique to your program), and
B029 (Write default INI, which are unique to your program)

This completes the chapter on INI file processing.

Chapter 14 - Reading INI Files Page: 220


A Beginners Guide to C-Sharp - Volume 2
Visual Studio C# 2017
ASCII through Advanced Formatting
by Tim R. Wolf
© 2017.06.01 1.02
Table of Contents

9 Chapter 12 - ASCII Files 17


Reading ASCII (Text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
StreamReader
Priming Read
Ending the Loop and Closing Files
Using an EndOfStream Read
The "Using" Clause
ASCII File Reads with try-catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Issues with try/catch and Close
Chaining catch-statements
Completed ASCII ReadFile - Program 12.5. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Writing ASCII (text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Appending ASCII (text) Files - Advanced Open Methods. . . . . . . . . . . . . . . . . . . . 54
Preventing Append from Running Twice

9 Chapter 13 - Parsing Tab and CSV Files 65


Automatic Parsing by Delimiter (Split).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Declaring the Destination Array
foreach
Alternate Loops
.Split Limitations
CSV Files
Split Example: Phone-Numbers
Manually Parsing Comma-Delimited Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
First Field Logic
for-next Logic
Substring
Last-field Logic
Tab-Delimited Files
Parsing a Variable Number of Columns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Parsing ASCII CSV Files with Embedded Commas.. . . . . . . . . . . . . . . . . . . . . . . 105
Error Processing in CSV Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Moving ParseCSVLine to the Utility Library

9 Chapter 14 - INI Files 147


INI File Structure and Design. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
A015: Loop Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A017: Parse INI Detail Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A028: Finding the Application's Default INI Location.. . . . . . . . . . . . . . . . . . . . . 178
Command-Line Override.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Simulate a Command Line Options
Embedded Spaces in the Parameters
Command-line ini=
A029: Write Default INI when Missing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Basic INI File Read: Complete Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Using a Program Class - CL860 INI File Read. . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
CL860 Basic INI Read Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Using CL860_BasicINIRead

9 Chapter 15 - xml and App.config Files 223


xml File Structure
app.config xml.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Using (Reading) app.config
Building a Manual xml File.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Reading xml File Sequentially. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

9 Chapter 16 - Windows Registry 249


Organization of the Registry
Reading a Specific Registry Key.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Opening the Registry
Closing
Reading a Specific Registry Value
Reading Mulitple Keys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Reading All Values within a SubKey. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Multi-Line String Registry Values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Creating/Modifying Name-Value Pairs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Opening the Registry in 'Writeable' Mode
Creating Sub-SubKeys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Deleting Values and Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Deleting Entire SubTrees
Deleting the Current Key
Enumerating SubKey (folders). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

9 Chapter 17 - Reading Excel and Access 289


Reading an Excel File using a COM object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Open the Workbook
parameter list
Reading a Row
comboBoxes based on Access Database Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Attaching Microsoft Access Data to a comboBox. . . . . . . . . . . . . . . . . . . . . . . . . 317
ODBC Call
Connecting to the comboBox
Refreshing the List
Performance Considerations

9 Chapter 18 - External Programs (Shell) 333


Starting a New Process. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Launching Notepad with a Specific Filename
Building an Exit Event
Using Code to Start a New Process.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
proc.WaitForExit.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
DOS Output.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Capturing DOS output into an Array
Multiple Instances of an Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Simple Instance Test
Mutex: A Better Solution for Multiple Instances. . . . . . . . . . . . . . . . . . . . . . . . . . 355

9 Chapter 19 - Waits, Delays and Pauses 361


Poor Wait States – Not Recommended
Empty loops
current time and loop until xx seconds
Sleep events
System.Threading.Thread.Sleep (milliseconds). . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Completed wait Simulation - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Wait as a Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Using cs805_Wait
Calling Wait
Timer Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Basic Timer Code
Event Horizons
Countdown and Timer Example
Disabling the Close "X" Mid-Transaction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Intercepting a Close Event
Countdown and Timer Program - Completed. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Simple Startup Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Splash Screens.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

9 Chapter 20 - Printing 415


Printing Simple Text. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Print Layout / Print Rendering
Pixel Positions in Millimeters (x,y)
OverShooting
Printing Horizontal Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Printing Rectangles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Filling
Printing Graphics.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Printer Dialogs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Adding a Custom Printer Dialog
Print Preview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Printer Setup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Printing Text Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Font Height Calculations - Lines per Page
Page-Break Logic
GraphicsUnit.Millimeters
Setting New Margins, Landscape. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
Current Margin
Setting New (Printable) Margins
Begin and End Print Events.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

9 Chapter 21 - Formatting 469


Font Color, Font Bold. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Font Bold
Font Style Ariel
Basic String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
String.Format with Alignment.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Proportional Fonts
Numeric Formatting with String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Standard Decimals
Formatting with Alignment
Commas and Currency
Numeric Picture Clauses. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Padding Zero with Leading and Trailing Decimals
Place Holder (#)
Thousands Separator ( , )
Variable Picture Clauses (Group Separators)
Date and Time Pictures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Converting from Strings
Converting Dates
Other Date Time Properties and Methods.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
DateTime Formatting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Pictures
Display Dates, Year first
Format Class Library - cl710_Formatting.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Phone Number Formatting
Proper Names
Proper Addresses
PhoneNumberFormat Method - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
PhoneNumberFormat Module - Coding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Extension Extract
Strip Normal Punctuation
Strip "1-" Prefixes
Punctuate
Punctuating the AreaCode
"ProperNames" Formatting - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
ProperNamesFormat - Coding.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
A Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 15 - XML Files, Config Files
Chapter 15 - xml and App.config Files

xml files (Extensible Markup Language) provides a way to store data in a relatively
simple format, using a series of <tags> and name-value pairs and are similar to HTML
documents. xml files can contain any information and have become the new standard for
transmitting data between computers, often replacing TAB and CSV-delimited files.

Why use xml files? In a standard CSV or TAB file, all records are assumed to have the
same number of columns but with xml, this is not a requirement. Records can be
structured and organized in layers with sections and differing field-layouts. Because
each record contains a complete description of itself, this makes for a flexible file, and of
course, this introduces complexities in how the files are processed.

Chapter Topics:

This chapter starts with a special xml file, called an App.config, which is similar to the
INI files described in the previous chapter, then it will move into a more traditional data-
processing files.

• App.config Preference Files


• Automatic parsing of App.config files
• Sequential XML processing using XmlTextReader
• Element Structures
• xtr.Read()
• XmlNodeType.Element
• xtr.GetAttribute("Category")
• xtr.ReadString()
• Nested While-loops

xml File Structure:

xml files are clear-text ASCII files but they are somewhat chatty because metadata tags
are scattered throughout the file. Two different styles of tags are supported; html-like
open-and-close tags and 'attribute' name-value pairs.

The most common style are open-and-closing tags, such as <Title>Dune</Title>. Here
is sample xml file with two <Book> records:

Chapter 15 - Reading XML Files Page: 223


<?xml version="1.0" encoding="utf-8"?>
<FileExtract xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<BookHeader>

<Book>
<Title>Dune</Title>
<Author>Frank Herbert</Author>
</Book>

<Book>
<Title>The Coming Fury</Title>
<Author>Bruce Catton</Author>
</Book>

</BookHeader>

Instead of using paired-tags, data can also be stored using 'attributes', such as
vendor_name="ABC Corp". Here is a snippet of an xml file that shows two records,
both of which use only attributes to store their data. In this example, both records hold
similar information but are formatted in different (an inconsistent) manner; both patterns
are legal:

<?xml version="1.0" encoding="utf-8"?>


<FileExtract xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<VendorHeader vendor_name="ABC Corp" vendor_number="V526"
effective_date="06/01/2010"/>
<VendorDetail
MemberID="100"
FirstName="John"
MiddleName="R"
LastName="Smith"
BirthDate="12/21/2010"
ReasonCode="9102"
PolicyNumber="1234"
ClassID="ABC"
PremiumAmount="145.65"
ErrorCode="00"
ErrorMessage=""
Action=""
/>
<VendorDetail MemberID="110" FirstName="Mary" MiddleName="K"
LastName="Jones" BirthDate="02/11/1997" ReasonCode="9102"
PolicyNumber="765" ClassID="ABC" PremiumAmount="195.00" ErrorCode="00"
ErrorMessage="" Action="" />

This chapter shows how to process these types of files, as well as the special app.config
file.

Chapter 15 - Reading XML Files Page: 224


app.config xml

An "app.config" xml file can store program preferences, much like the INI files in the
previous chapter. C# can process this type of file automatically, with almost no code.
This technique is good for small preference files, say less than 100 or 200 elements, but
may not be appropriate for larger files.

Overview: Creating and using app.config files

1. In Solution Explorer, select "Add Reference" and add the "System.Configuration"


Reference from the .NET tab.

2. Add "using System.Configuration;

3. Create an AppConfig File in Solution Explorer:


Add, New Item, "Application Configuration"
Name the file "app.config"

4. In the XML file, type your configuration settings. For example:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="ProgramVersion" value="1.01"/>
<add key="ServerName" value="omni"/>
</appSettings>
</configuration>

5. When your program launches, name-value pairs are automatically read. Use this
logic to retrieve a single value:
MessageBox.Show
(ConfigurationManager.AppSettings ["ServerName"]);

or
foreach (string strkey in
ConfigurationManager.AppSettings)

comments:

Microsoft has long pushed developers to abandon INI files and has encouraged using the
Registry. But reading and writing from the Windows Registry is fairly involved (see
next chapter) and writing your program's default preferences into the Registry requires
installation routines.

INI files still serve a useful purpose. By storing an application's configuration outside of
the compiled program, developers and end users can make changes to server names, and

Chapter 15 - Reading XML Files Page: 225


other such settings without having to re-compile the program or without having to edit
the Registry. INI files are almost user-editable and, as seen in the previous section, a
program can point to any number of them for different configurations. But reading an
INI file is a nuisance, especially if you don't have a library, such as
CL860_BasicINIRead to help.

Microsoft, realizing these problems, provided a simpler mechanism through a file called
"app.config". app.config files are similar to INI files, but they are formatted using the
xml standard (a data-interchange format). The file is still ASCII, but all records are
delimited with html-like formatting codes and that makes them vaguely human-readable.

The app.config has one undeniable benefit: Reading and parsing is automatic. But the
new file format does have some problems. The design and formatting is not as flexible
as an INI file. And although it is human-readable, many end-users would be
uncomfortable in making changes.

Building the Sample Program:

app.config files require setup before they can be used. For this section, begin with a new
Visual Studio project:

File, New, Project


Select Visual C#, Windows Classic Desktop, Windows Form App
Name the Project "ExampleProgramXML"

1. At the top of Form1's code, add this statement:

using System.Configuration; //And you must add a reference to .net


//System.Configuration (see below)

2. In Solution Explorer, other-mouse-click References, select "Add Reference"


In the .Net tab, scroll down and select "System.Configuration".
Check the side-box
Click Ok
Do this even though a "using" statement was typed earlier.

Chapter 15 - Reading XML Files Page: 226


Microsoft has changed how it manipulates app.config files. On the net you will find
numerous references to ConfigurationSettings.AppSettings; this is now obsolete; use
ConfigurationManager.AppSettings and the Reference linked described above.

Building the app.config File:

3. Still in Solution Explorer, "other-mouse-click" the project's name (e.g. ExampleProgram)


and select Add, New Item

Chapter 15 - Reading XML Files Page: 227


4. In the Add New Item dialogue, select "Application Configuration File"
Note the filename, app.config (for now, leave this name as-is)
Click Add

5. The xml file appears in Solution Explorer and opens in an editing window along the top
row of tabs.

<?xml version="1.0" encoding="utf-8" ?>


<configuration>
// (Your settings go here)
</configuration>

All of your settings live within the <configuration> </configuration> section. Insert a
blank line between the two markers, giving yourself room to work.

Chapter 15 - Reading XML Files Page: 228


6. In the space between the configuration-section header and footer, begin typing
"<appSettings>", including the greater-than and less-than symbols.

As soon as the closing ">" is typed, the editor inserts the closing-section.
Press Enter to insert a blank line.
In the new section, add key-value pairs, using the syntax below.

Watch your quotes, they type differently than in a C# module.

The completed file should look similar to this:

Sample xml app.config File


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>

<add key ="ProgramVersion" value ="2.01"/>


<add key ="ServerName" value ="omni"/>
<add key ="ShareName" value ="vol1\kb"/>
<add key ="UserAuthentication" value ="PAY_APP"/>
<add key ="UserPassword" value ="xyz"/>
<add key ="AllowPasswordSave" value ="Y"/>

</appSettings>
</configuration>

where the Name-value pairs always begin with "<add key =" and end with a "/>"
(slash-carrot). Syntax in this file is important and punctuation must be watched
carefully. Lining up the columns with spaces is optional but it makes the file easy to
read.

Chapter 15 - Reading XML Files Page: 229


Using (Reading) app.config:

Reading values happens automatically during the program load. Yes, you read this
correctly, no coding required.

Do the following to examine what was found. Add a button1 to Form1.cs [Design] and
write the following. This routine returns a particular name-value pair from the file and
displays it in a MessageBox:

Displaying a Name-Value Pair from app.config


private void button1_Click (object sender, EventArgs e)
{
//Display a name-value pair from app.config:
MessageBox.Show
(ConfigurationManager.AppSettings ["ServerName"]);
}

Results: "omni" from the app.config file, key "ServerName".

app.config Comments:

• Although the app.config was built with Visual Studio's editor, the file can be
maintained using notepad or other ASCII text editors. While testing, it is easiest to
edit app.config from Solution Explorer (double-click the resource). If you want to
edit the file manually using Notepad, look for the file in the solution's main source
directory. For example:

"C:\Data\VSProj\ExampleProgramXML\ExampleProgramXML.exe.config"

When F5 (RUN) is pressed, the app.config.exe.config is copied from the source


directory to the \bin\debug directory. This is a temporary copy and is re-written each
time run and the copy should be ignored.

j Once the program is compiled and released into production, the app.config's full
name is "ExampleProgram.exe.config" (where "ExampleProgram" is the project
name). The config-file must live in the same directory as the EXE.

If you manually rename the program's EXE using Windows Explorer, you must
rename the appconfig.exe.config to match.

• app.config is automatically read when the program loads and the values are written
into an array, called a "collection," with each name-value pair. Collections work like
a dictionary: There is a name used for lookup followed by its value. The name-
lookups are blessedly case-insensitive.

Chapter 15 - Reading XML Files Page: 230


• Name-value pairs are loaded one-time, at program startup, and are stored in memory
for the duration of the program. Some developers use them as a type of program-
wide Global variables. See below for ways to modify the collection.

This sample code displays each of the entries in the app.config collection:

Display All app.config Values


:

foreach (string strkey in ConfigurationManager.AppSettings)


{
//Display all of the values loaded into the
//app.config collection:

string strkeyValue =
ConfigurationManager.AppSettings[strkey];
MessageBox.Show (strkey + ": " + strkeyValue);
}
:

Modifying and Re-Reading app.config:

The only way to permanently modify the values within an app.config is to edit the
original file and re-load the executable. But, within a running program you can
temporarily make changes to the collection for the duration of the run. The following
code adds a new value to the collection and then (changes) an existing value by deleting
and re-adding:

Code Snippet:

//First, open the control with this command:


System.Configuration.Configuration config =
ConfigurationManager.OpenExeConfiguration
(ConfigurationUserLevel.None);

//Use this logic to add a new value:


config.AppSettings.Settings.Add("Modified Date",
DateTime.Now.ToShortDateString());

//Save the changes and Force a reload of the file:


config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("appSettings");

//Display each value for debugging:


foreach (string strkey in ConfigurationManager.AppSettings)
{
string strkeyValue = ConfigurationManager.AppSettings[key];
MessageBox.Show(strKey + ": " + strkeyValue);
}

Chapter 15 - Reading XML Files Page: 231


There is no direct way to modify an existing entry in the collection. Instead, remove the
old entry and re-add. For example:

config.AppSettings.Settings.Remove ("ServerName");
config.AppSettings.Settings.Add("ServerName", "Elrond");
config.Save (ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("appSettings");

app.config Conclusions:

app.config files are easier to use than an INI file. The files are easily understood and
easy to use. The drawbacks are relatively minor:

• The file must be stored with a pre-defined name.

• It must be stored in the application's directory This can be a problem if the


application directories are secured; end-users may not have enough rights to modify
the config file should changes be required.

Other issues are more esoteric. If your program launches multiple threads, each thread
will likely require a separate app.config (often requiring duplicate entries in the config
files). Finally, if you have a need for multiple configurations, such as PROD and TEST,
app.config may not be a good solution. These are admittedly special circumstances that
are easily solved with a traditional INI file. Balance the flexibility of INI files with the
easier-to-use app.config.

Chapter 15 - Reading XML Files Page: 232


Building a Manual xml File

Moving to a more traditional design, large swaths of data can be stored in an xml file and
it can be read and processed using sequential file techniques. File sizes can be small to
monstrous, with literally tens of thousands of records. This section will open and
sequentially read through the xml file, parsing each record. The idea is similar to
reading a .CSV or TAB file.

Building a Simple xml Data File:

Although you can build the xml file in notepad, it is easier to edit within Visual Studio's
editor (or from Microsoft's "Visual Studio Code" editor). From either your existing
ExampleProgram or from any blank Visual Studio editing screen, select top-menu File,
New, "XML File".

The editor will open a new tab, "XMLFile1.xml" and will immediately complain "XML
document must contain a root level element." Ignore the error for a moment.

xml files must contain a beginning root element, which is a name-tag to mark where the
data begins and ends. This name can anything: And for this example, call it "mynames",
for a Name and Address file. The opening and closing tabs, <mynames> </mynames> are
artificial and these represent the root-level of the data-structure; you can think of this as a
class:

Root Level Element, invented


<?xml version="1.0" encoding="utf-8"?>
<!-- Comments look like this, with a opening and closing tag -->
<!-- Note the double-hypens -->

<mynames>

</mynames>

Within the root element, create individual records (also called elements), and once again,
use an invented name, which for the sake of argument will be called "namerecord". In
the example below, three records are stubbed into the file, with details to be added in a
moment:

Chapter 15 - Reading XML Files Page: 233


xmlFile1.xml, preliminary
<?xml version="1.0" encoding="utf-8"?>
<!-- Comments look like this, with a opening and closing tag -->
<!-- Note the double-hypens -->

<mynames>

<namerecord ID="0107" Category="A">

</namerecord>

<namerecord ID="0218" Category="A">

</namerecord>

<namerecord ID="0356" Category="C">

</namerecord>

</mynames>

where:

• "namerecord" is the name of the element and any name could be used. For example,
<employee>, <Model>, etc. By convention and by laziness, these are usually typed
lower-case, but what ever is used, it must match the closing tag </namerecord>,
</Model>, etc.

• Within these records, metadata (information about the record) can be stored in one of
two ways. Record details can be stored with paired tags, as in:

<Fname>John</Fname>
<MidName>L.</MidName>

or as "attributes", stored as keyname=value's, as in

ID="0107"
Category="A"

In these examples, both tagged-paired elements and attributes will be used,


demonstrating both methods. There is no rule on which method to use - metadata
can be stored as name-value pairs or attributes, or a mixture of both. However, the
paired-tags seem more.

A record could just as easily be built in this fashion:

Chapter 15 - Reading XML Files Page: 234


<namerecord>
<ID>0107</ID>
<Category>A</Category>
<FName>John</FName>
<MidName>L.</MidName>
<LastName>Smith</LastName>
<WorkPhone>208.555.1234</WorkPhone>
</namerecord>

<namerecord>
<ID>0218</ID>
:
</namerecord>

Which ever method is chosen, all <namerecord's> share the same pattern, with
allowances for missing fields, as described below.

• Commonly, fields, such as FName, LName, WorkPhone, are nested data elements
within the record – noting again, these are not attributes.

However, many build xml files using only attributes; I find this a poor design:
<record ID="0001" Category="A" FName="John" LastName="Smith".... >.

Basic XML data structures

Chapter 15 - Reading XML Files Page: 235


XMLFile1.xml Test Data

Continue editing the XMLFile1 test data file, filling in these detail records.

This file contains several flaws, including unneeded blank lines,


extra, unexpected details, different types of records
(car_records), and some records are missing fields (such as the
last record's mid-name). The point being not everyone builds
their xml files properly, but your program still needs to navigate
the file.

xmlFile1.xml, completed, with known inconsistencies


<?xml version="1.0" encoding="utf-8"?>
<!-- Test file -->

<mynames>

<namerecord ID="0107" Category="A">


<FName>John</FName>
<MidName>L.</MidName>
<LastName>Smith</LastName>
<WorkPhone>208.555.1234</WorkPhone>
</namerecord>

<namerecord ID="0218" Category="A">


<FName>Mary</FName>
<MidName></MidName>
<LastName>Jones</LastName>
<WorkPhone>206.876.1234</WorkPhone>
<unexpected_details>abcdefg</unexpected_details>
</namerecord>

<car_record>
<BadCar>Pinto</BadCar>
</car_record>

<namerecord ID="0311" Category="M">


<FName>Margaret</FName>
<MidName>M.</MidName>
<LastName>Miller</LastName>
<WorkPhone>515.222.8765</WorkPhone>
</namerecord>

<namerecord ID="0356" Category="C">


<FName>Albert</FName>
<LastName>Stien</LastName>
<WorkPhone>317.333.1234</WorkPhone>
</namerecord>

</mynames>

Chapter 15 - Reading XML Files Page: 236


comments:

• Opening and closing Tags are case-sensitive. This will cause problems.
<FName>John</fnAME>

• Tags should be consistently-cased from record-to-record. Most xml processors are


only looking for one variation of the tag. This example expects all <FName> tags to
be cased the same.

• Blank lines between records are optional. Most xml files would not have them.
These are here to help demonstrate parsing methods.

• This example xml contains records with missing fields, an unexpected field, and an
unexpected car_record. These will be used to demonstrate techniques.

The editor, especially in Visual Studio 2013 and newer, may show an
informational message in the error list "Could not find Schema Information
for the element...". This is fixed below.

Close and save the "XMLFile1.xml"with this same name in the current directory (or save
as any name, any location). To make the examples easier, consider saving
XMLFile1.xml in your example program's ..bin\debug directory.

Could Not Find Schema Information Warning

Some versions of Visual Studio will complain with a message, "Could not find Schema
Information for the element <element name>. This can be ignored for these examples,
but it can be resolved by building a schema file. Visual Studio 2013 and above do this
automatically with these steps:

A. Open the xml file using, File, Open, "File". Browse to "...\bin\debug", locating the test
file "XMLFile1.xml.

B. Select top menu "XML".


Choose option "Create Schema"

Results: A new file, "XMLFile1.xsd" (note extension) is a schema definition. If you had
bad data stored in the original file, these will be reflected in the Schema and they
probably ought-not be there.

C. While editing the schema definition file, XMLFile1.xsd, select File, "Save
XMLFile1.xsd As...", allow it to save in the ExampleProgram's main directory (at the
same level as "bin", "obj", etc.

Note: You manually save the schema; no help on the location. Pick the same
location as the xml file (bin\debug or bin\release).

Chapter 15 - Reading XML Files Page: 237


D. Close both xml files. To re-edit the original xml file, select File, Open, File. Browse to
your program's "...bin\debug" folder, choose "XMLFile1.xml". The informational
messages will be gone. If you re-introduce the bad fields and records (see above), the
editor will warn you.

Chapter 15 - Reading XML Files Page: 238


Reading xml File Sequentially

xml files can be read, in their entirety, into an array or into a LINQ database and then
processed either sequentially or with random-access, but this is not practical or even
possible on large XML files, as they may not fit into RAM. Instead, this example will
read the file, processing each section and each line sequentially. With this, you can
consume huge files without the typical memory concerns. The techniques are similar to
reading ASCII files.

Reading XML Sequentially

The logic in this example program will be simple, with


minimal error checking and minimal output. Results
displayed in a MessageBox. A real program would do
other things with the data, such as write to a text file,
calculate totals, etc.

1. Starting in the same project from the front of this chapter, or starting from a new project,
confirm this statement is near the top of the program:

Required using Statement


using System.Xml;

2. In Form1, add a button (button2_click or button1_click if a new project).


Open the xml file with this logic:

button2_Click event, in progress


private void button2_Click(object sender, EventArgs e)
{
//Open the XML file:
try
{
//For this example xml, the file is assumed to be in the
//project's ...bin\debug folder

XmlTextReader xtr = new XmlTextReader("XMLFile1.xml");


xtr.WhitespaceHandling = WhitespaceHandling.None;

A100_ProcessFile(ref xtr);

xtr.Close();
xtr.Dispose();
}
catch
{
MessageBox.Show("Some horrible error opening xml file");
}
}

Chapter 15 - Reading XML Files Page: 239


where:

• File "XMLFile1.xml" is the file written on the previous page. Be sure the file was
saved to the disk and this example assumes the default directory ...bin\debug.
Naturally, you could specify the exact path or load defaults from this chapter's
discussion on App.Config.

• "xtr" is an arbitrary name (an alias, if you will, for xml Text Reader), assigned to the
opened file. This is similar to the ASCII file reads in previous chapters where the
file was often called "myInputFile".

• xtr.WhitespaceHandling tells the compiler to ignore extraneous whitespace in the


xml file. Basically, skip blank lines.

• A100_ProcessFile is written next. As in the ASCII text chapters, notice how the xtr
file (alias) is being passed by reference to the downstream routine - see the signature
line. With this, we can keep the file-open/try-catch logic in its own module and we
can pass the already opened file to A100 without the open-file falling out of scope.

A100 Process File

This is the main routine. The logic here begins with a 'while-loop' with a read-statement
built into the loop. Notice how this is different than the ASCII chapters – mainly
because we don't have to worry about the last-record processing. In this case, the last
record is always going to be a close-tag (/mynames). This line is never processed.

The shortcut priming-read, embedded in the while-loop suffices; no priming


read or special last-record logic needed [ while (xtr.Read)]

Chapter 15 - Reading XML Files Page: 240


A100_ProcessFile, completed
private void A100_ProcessFile(ref XmlTextReader xtr)
{
//Read until a name-element is found
//Note: the file could be poorly-formed with non-namerecords and
//blank lines, etc.

//A try-catch around the while-loop is advised

while (xtr.Read())
{
if (xtr.NodeType == XmlNodeType.Element &&
xtr.Name == "namerecord")
{
//Arbitrarily decide to process different 'attributes' with
//their own different logic. Category=A do this stuff,
//Category=B can do that stuff. In this case, all are
//the same...

switch (xtr.GetAttribute ("Category"))


{
case "A":
//you might do something different with category A
A110_Details (ref xtr);
break;

case "B":
A110_Details (ref xtr);
break;

case "C":
A110_Details (ref xtr);
break;

default:
//Skip all other categories; do nothing
//Let the while-loop slog through these records...
break;
}
}
}
}

A110_Details

Continue with the detail routine. This rifles through each detail-record using its own
internal loop, looping until the record's closing element is found:

<\namerecord>

This internal loop also uses a priming read. Keep in mind the NameRecord may contain
multiple details – you are reading until the end of the NameRecord. As a reminder:

Chapter 15 - Reading XML Files Page: 241


<namerecord ID="0107" Category="A">
<FName>John</FName>
<MidName>L.</MidName>
<LastName>Smith</LastName>
<WorkPhone>208.555.1234</WorkPhone>
</namerecord>

As each detail is found, logic can do what ever you would like. In this case, the program
appends it to a message-string. When it finds a </namerecord> end tag, it displays what
was found. Naturally, you can dream of more sophisticated logic.

A110_Details, completed

private void A110_Details (ref XmlTextReader xtr)


{
//Loop through the detail lines with a separate, internal loop.
//Display each detail record's details, just to show what
//was found.

string strMsg = ""; //Build-up the new record's stuff


strMsg = xtr.Name + " " +
"ID: " + xtr.GetAttribute("ID") + "\r\n";

xtr.ReadString(); //a priming read for the details

while (xtr.NodeType != XmlNodeType.EndElement)


{
if (xtr.NodeType == XmlNodeType.Element)
{
switch (xtr.Name)
{
case "FName":
//Build a prompt by appending to existing strMsg.
//The xtr.ReadString() reads until the closing
//tag, </FName>

//Process the FirstName record....


strMsg += "First Name: " + xtr.ReadString() + "\r\n";
break;

case "MidName":
strMsg += "Mid Name: " + xtr.ReadString() + "\r\n";
break;

case "LastName":
strMsg += "LastName: " + xtr.ReadString() + "\r\n";
break;

case "WorkPhone":
strMsg += "WorkPhone: " + xtr.ReadString() +
"\r\n";
break;

default:
//Unexpected detail element (.e.g HomePhone)

Chapter 15 - Reading XML Files Page: 242


//read past its closing tag and ignore the record...
xtr.ReadString();
break;
}

//Read through the namerecord's end-tag;


//Note xtr.NodeType will become an "EndElement"
//at the top of the outside loop, it looks for the end/next
//name record; then in this loop, it will look for a
//new detail-element

xtr.Read();

}
}

//Display the record's results, now that all details were either
//processed or skipped:

MessageBox.Show(strMsg);
}

A110_Details, end

Testing:

Test by pressing F5 and allowing the program to run. Click Button2 to engage the main
category-loop.

Expected Results: Each <namerecord> displays a concatenated strMsg field, showing


all the processed details.

RecordID
FirstName
MidName
LastName
WorkPhone

Unexpected detail lines are ignored within the switch-statement's default clause.
Missing details (such as a mid-name) will not be a concern and are properly ignored by
the same switch statement.

Unexpected record-types (car_record) are read-past and ignored by the parent loop in
A100; that routine only pays attention to "<namerecord>'s" and will xtr.Read past all
others.

Chapter 15 - Reading XML Files Page: 243


where:

• xtr.ReadString(); reads until the closing-tag for the current detail element is
found. For example, </FName>

• After the switch-statement, the lonely xtr.Read(), [not an xtr.ReadString()]; reads


until the next </namerecord> end-element is found. Control then returns to the outer
loop, where it looks for a new record.

Complete Sample Program Listing

xml Example Program, complete

using System;
using System.Windows.Forms;
using System.Configuration; //Required for app.config
using System.Xml; //Required for xml

//Add references for System.Xml, System.Configuration


//Example Input file described in text above, see XMLFile1.xml
//Record Format:
//<namerecord ID="0001" Category="A">
// <FName></Fname>
// <MidName></MidName>
// <LastName></LastName>
// <WorkPhone></WorkPhone>
//</namerecord>

namespace ExampleProgramXML
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void Form1_Load(object sender, eventArgs e)


{
}

private void button1_Click(object sender, EventArgs e)


{
//Demonstrating App.Config
MessageBox.Show(ConfigurationManager.AppSettings["ServerName"]);
}

private void button2_Click(object sender, EventArgs e)


{

//Demonstrating XmlTextReader; sequential reading

//Open the XML file:


try
{

Chapter 15 - Reading XML Files Page: 244


//For this example, read the xml file in the default directory
//...bin/debug (folder)
XmlTextRader xtr = new XmlTextReader("XMLFile1.xml");
xtr.WhitespaceHandling = WhitespaceHandling.None;

A100_ProcessFile(ref xtr);

xtr.Close();
xtr.Dispose();
}
catch
{
MessageBox.Show("Error opening XML file");
}
}

private void A100_ProcessFile(ref XmlTextReader xtr)


{
//Read until a name-element is found (<namerecord>)
//Note: the example file from text is poorly formatted with non-
//namerecords in the xml (e.g. the car-record)

//Note: this is an outer-loop that processes the main data element


//This loop does not use a priming read because the last-record
//</mynames> does not need to be processed by the detail loop.
//Because of this, the logic can be simpler than an ASCII file-read.

//Outside loop
while(xtr.Read())
{
if (xtr.NodeType == XmlNodeType.Element &&
xtr.Name == "namerecord")
{
switch (xtr.GetAttribute ("Category"))
{
case "A":
//You might do something different with category A.
//Here is where that difference can be called...
A110_Details (ref xtr);
break;

case "B":
A110_Details (ref xtr);
break;

case "C":
A110_Details (ref xtr);
break;

default:
//Skip all other categories
break;
}
}
else
{
//no action on non-namerecords; skip and keep reading
}
}
}

private void A110_Details (ref XmlTextReader xtr)


{

Chapter 15 - Reading XML Files Page: 245


//Loop through the detail lines
//Display each record element's details by appending to a large
//string field. Other actions could be taken

string strMsg = ""; //For diagnostics


strMsg = xtr.Name + " " + "ID: " + xtr.GetAttributes("ID") + "\r\n";
xtr.ReadString(); //Priming read for detail section

//Inner detail loop


while(xtr.NodeType != XmlNodeType.EndElement)
{
if (xtr.NodeType == XmlNodeType.Element)
{
switch (xtr.Name)
{
//Each ReadString reads to the end-tag

case "FName":
strMsg += "FirstName: " + xtr.ReadString() + "\r\n";
break;
case "MidName":
strMsg += "MidName: " + xtr.ReadString() + "\r\n";
break;
case "LastName":
strMsg += "LastName: " + xtr.ReadString() + "\r\n";
break;
case "WorkPhone":
strMsg += "WorkPhone: " + xtr.ReadString() + "\r\n";
break;
default:
//If unexpected detail elements found; read past their
//closing tag
xtr.ReadString();
break;

//Read through the name-record's end-tag;


//Note that xtr.NodeType will become an EndElement.
//At the top of the outside loop, it will look for the next
//namerecord; then, in this loop, it will look for a new
//begin detail-element...

xtr.Read();

//EndElement is set; you will fall through


}
}

//Diagnostics: Display the results (after record-end-element detect)


MessageBox.Show(strMsg);
}

}
}

End XML Sequential Read program

This concludes the XML chapter.

Chapter 15 - Reading XML Files Page: 246


A Beginners Guide to C-Sharp - Volume 2
Visual Studio C# 2017
ASCII through Advanced Formatting
by Tim R. Wolf
© 2017.06.01 1.02
Table of Contents

9 Chapter 12 - ASCII Files 17


Reading ASCII (Text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
StreamReader
Priming Read
Ending the Loop and Closing Files
Using an EndOfStream Read
The "Using" Clause
ASCII File Reads with try-catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Issues with try/catch and Close
Chaining catch-statements
Completed ASCII ReadFile - Program 12.5. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Writing ASCII (text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Appending ASCII (text) Files - Advanced Open Methods. . . . . . . . . . . . . . . . . . . . 54
Preventing Append from Running Twice

9 Chapter 13 - Parsing Tab and CSV Files 65


Automatic Parsing by Delimiter (Split).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Declaring the Destination Array
foreach
Alternate Loops
.Split Limitations
CSV Files
Split Example: Phone-Numbers
Manually Parsing Comma-Delimited Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
First Field Logic
for-next Logic
Substring
Last-field Logic
Tab-Delimited Files
Parsing a Variable Number of Columns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Parsing ASCII CSV Files with Embedded Commas.. . . . . . . . . . . . . . . . . . . . . . . 105
Error Processing in CSV Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Moving ParseCSVLine to the Utility Library

9 Chapter 14 - INI Files 147


INI File Structure and Design. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
A015: Loop Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A017: Parse INI Detail Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A028: Finding the Application's Default INI Location.. . . . . . . . . . . . . . . . . . . . . 178
Command-Line Override.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Simulate a Command Line Options
Embedded Spaces in the Parameters
Command-line ini=
A029: Write Default INI when Missing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Basic INI File Read: Complete Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Using a Program Class - CL860 INI File Read. . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
CL860 Basic INI Read Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Using CL860_BasicINIRead

9 Chapter 15 - xml and App.config Files 223


xml File Structure
app.config xml.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Using (Reading) app.config
Building a Manual xml File.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Reading xml File Sequentially. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

9 Chapter 16 - Windows Registry 249


Organization of the Registry
Reading a Specific Registry Key.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Opening the Registry
Closing
Reading a Specific Registry Value
Reading Mulitple Keys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Reading All Values within a SubKey. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Multi-Line String Registry Values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Creating/Modifying Name-Value Pairs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Opening the Registry in 'Writeable' Mode
Creating Sub-SubKeys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Deleting Values and Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Deleting Entire SubTrees
Deleting the Current Key
Enumerating SubKey (folders). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

9 Chapter 17 - Reading Excel and Access 289


Reading an Excel File using a COM object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Open the Workbook
parameter list
Reading a Row
comboBoxes based on Access Database Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Attaching Microsoft Access Data to a comboBox. . . . . . . . . . . . . . . . . . . . . . . . . 317
ODBC Call
Connecting to the comboBox
Refreshing the List
Performance Considerations

9 Chapter 18 - External Programs (Shell) 333


Starting a New Process. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Launching Notepad with a Specific Filename
Building an Exit Event
Using Code to Start a New Process.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
proc.WaitForExit.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
DOS Output.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Capturing DOS output into an Array
Multiple Instances of an Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Simple Instance Test
Mutex: A Better Solution for Multiple Instances. . . . . . . . . . . . . . . . . . . . . . . . . . 355

9 Chapter 19 - Waits, Delays and Pauses 361


Poor Wait States – Not Recommended
Empty loops
current time and loop until xx seconds
Sleep events
System.Threading.Thread.Sleep (milliseconds). . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Completed wait Simulation - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Wait as a Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Using cs805_Wait
Calling Wait
Timer Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Basic Timer Code
Event Horizons
Countdown and Timer Example
Disabling the Close "X" Mid-Transaction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Intercepting a Close Event
Countdown and Timer Program - Completed. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Simple Startup Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Splash Screens.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

9 Chapter 20 - Printing 415


Printing Simple Text. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Print Layout / Print Rendering
Pixel Positions in Millimeters (x,y)
OverShooting
Printing Horizontal Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Printing Rectangles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Filling
Printing Graphics.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Printer Dialogs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Adding a Custom Printer Dialog
Print Preview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Printer Setup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Printing Text Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Font Height Calculations - Lines per Page
Page-Break Logic
GraphicsUnit.Millimeters
Setting New Margins, Landscape. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
Current Margin
Setting New (Printable) Margins
Begin and End Print Events.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

9 Chapter 21 - Formatting 469


Font Color, Font Bold. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Font Bold
Font Style Ariel
Basic String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
String.Format with Alignment.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Proportional Fonts
Numeric Formatting with String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Standard Decimals
Formatting with Alignment
Commas and Currency
Numeric Picture Clauses. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Padding Zero with Leading and Trailing Decimals
Place Holder (#)
Thousands Separator ( , )
Variable Picture Clauses (Group Separators)
Date and Time Pictures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Converting from Strings
Converting Dates
Other Date Time Properties and Methods.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
DateTime Formatting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Pictures
Display Dates, Year first
Format Class Library - cl710_Formatting.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Phone Number Formatting
Proper Names
Proper Addresses
PhoneNumberFormat Method - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
PhoneNumberFormat Module - Coding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Extension Extract
Strip Normal Punctuation
Strip "1-" Prefixes
Punctuate
Punctuating the AreaCode
"ProperNames" Formatting - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
ProperNamesFormat - Coding.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
A Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 16 - Windows Registry
Chapter 16 - Windows Registry

The Windows Registry is essentially a database where applications and the Operating
System can store preferences, settings and any other named-value paired information.
This chapter describes how to read and write values in the Registry.

The Registry largely succeeded in replacing INI files, which was


its intent, but now Microsoft is recommending that applications
store their preferences back into config xml files, probably to
relieve pressures on the Registry. But the Registry is still alive
and well. You will find that C# gracefully handles the Registry.

Topics:

• Using Microsoft's RegEdit


• Reading a specific value (.GetValue)
• myRegKey.Close()
• Reading Multi-line String Values
• Reading multiple, different keys
• Reading all values in a key (.GetValueNames)
• Creating new Name-Value pairs / Updating existing Values
• HKLM security concerns
• Creating new SubKeys
• Deleting Name-Values; Deleting Trees
• Error Processing during a Delete

When it comes to writing and changing values, you will find significant
restrictions, starting in Windows Vista. This chapter touches on security
problems, especially in the HKLM key.

Chapter 16 - Reading and W riting in the W indows Registry Page: 249


Summary:

Summary: btnRead (Registry); Display a Single Value


private void btnRead_Click (object sender, EventArgs e)
{
//To locate a specific, single name-value pair:

string strSubKey = "Software\\Test";


try
{
//Open the Registry Key using the strSubKey:
RegistryKey myRegKey = Microsoft.Win32.Registry.
CurrentUser.OpenSubKey(strSubKey);

//Show a specific key (Should have a separate Try-Catch):


MessageBox.Show
(myRegKey.GetValue("ApplicationName").ToString());

//If the registry is opened, close it:


myRegKey.Close();
}
catch (Exception ereg)
{
MessageBox.Show ("Error Reading Registry in btnRead \r\n" +
ereg.Message.ToString());
}
}

Chapter 16 - Reading and W riting in the W indows Registry Page: 250


Summary: btnCreate (Registry); Add/Change New Values
private void btnCreate_Click (object sender, EventArgs e)
{
//Add, Create, Update registry values within an existing
//SubKey

string strSubKey = "Software\\test";

try
{
RegistryKey myRegKey = Microsoft.Win32.Registry.
CurrentUser.OpenSubKey (strSubKey, true);

try
{
//Update an existing value from previous examples:
myRegKey.SetValue ("ApplicationName", "A Neat Program");

//Add a new (non-existing) value:


myRegKey.SetValue ("ApplicationAuthor", "Jsmith");

//Add a new DWord as decimal (auto converts to Hex):


myRegKey.SetValue
("ApplicatonVersionNo", 42, RegistryValueKind.Dword);

myRegKey.Close();
MessageBox.Show("keys changed; check in Regedit");
}
catch (Exception eregModify)
{
MessageBox.Show
("Unable to change Registry value; " + \r\n" +
"This may be a rights issue" + "\r\n" +
eregModify.Message.ToString());
}
}
catch (Exception eregOpen)
{
MessageBox.Show ("Error opening Registry");
}
}

Organization of the Registry:

The Registry is not particularly complex but it is a fairly large database divided into 5
major sections. The areas you are most likely concerned with are HKCU and HKLM.

Key Name Abbreviation


HKey_Classes_Root HKCR Operating System Key
HKey_Current_User HKCU Current User's Preferences
HKey_Local_Machine HKLM Hardware and Operating System
HKey_Users (Ignore: A backup copy of current HKCU)
HKey_Current_Config (Ignore: Operating System)

Microsoft provides a tool (Start, Run, Regedit.exe) which allows you to edit the
database. "Regedit" displays a split-windowed view of the database where the left-side

Chapter 16 - Reading and W riting in the W indows Registry Page: 251


is a tree-diagram, showing the current key, and the right-side shows the details within a
key. Click the arrows to expand. It is common to have keys with no details. On the
detail-side, double-click a value-name to open into edit mode.

On the detail-side, each name-value pair can represent a half-dozen different data-types
but most registry entries are "string" or "dword". dwords (double-word-bytes) are
usually a zero or one (off, on), but larger numbers can be represented.

string ("REG_SZ")
dword (Reg_DWord - double-word – hexadecimal, numeric)
binary (rare)
multi-string
expandable string

In general it is safe to make editing changes in the registry,


especially if you are in keys that you have built. Naturally, use
care with operating system HKLM keys.

Creating Test Data:

The following examples will add and delete entries in the Registry. Rather than risking
the production values, use Microsoft's "Regedit.exe" to create test data in a fictitious
area. Many are nervous about "messing around" in the Registry, but you shouldn't be.
Exercise common sense, and some caution, and all will go well.

Chapter 16 - Reading and W riting in the W indows Registry Page: 252


1. From Windows, Start, Search for "Regedit.exe"

When the registry editor first opens, it may display a previously-viewed key deep within
the tree. Using the tree-side's scroll bar, scroll to the top and condense the major keys
to start at a known position.

2. Add a new test key: HKCU\Software\Test:

• Expand the HKey_Current_User's key by clicking the triangle (aka "+plus").


Hereafter, this key will be called HKCU)
• Using the tree-side scroll bar, near the center of the screen, scroll-down to the
"Software" key. Highlight the "Software" key.
• "Other-mouse-click" (right-click) the yellow Software-folder and select "New, Key"
• In the name field, type "Test"

3. Highlight the newly-created Test folder, create a detail entry:

Chapter 16 - Reading and W riting in the W indows Registry Page: 253


• Highlight the Test Key
• Move to the detail-side of the screen
• In any white-area, "other-mouse-click"
Select "New, String Value"

• In the highlighted/rename-box, type "ApplicationName" (no spaces)


• Press Enter to write the value

4. Double-click the newly-added name-value, ApplicationName, placing you in edit mode.

In the Value-Data box, type "My Program"


Once saved, notice "ApplicationName" is type "REG_SZ" (string)

Chapter 16 - Reading and W riting in the W indows Registry Page: 254


5. Create a second entry by other-mouse-clicking in the white detail-area again and adding
a new name, "ApplicationSwitch".

Make the type a DWord value.


Double-click the newly-added entry and set its value to Hex 1.
Once saved, notice the value-data field: "0x0000001" (0x = hexadecimal number).

6. Close RegEdit and return to Visual Studio.

Building a Test Program:

In this section, create a C# application that demonstrates Registry techniques, including


reading, creating new keys and values, and deleting. This is a relatively simple project.
Begin with these steps:

1. Create a new C# project, accepting default project and form names.

2. Place five standard buttons on the form, as illustrated below.


The buttons will read, create and delete various Registry entries.
Use these names:

btnRead Read a specific Registry Value


btnReadAll Enumerate all Registry Values within a SubKey
btnReadMulti Read Multi-lined values
btnNewSubKey Create a new SubKey below the current one
btnCreate Create and Modify Values within a SubKey
btnDelete Delete an individual Value or all Values and SubKeys

Chapter 16 - Reading and W riting in the W indows Registry Page: 255


3. Registry activity requires this "using" statement at the top of the program (Code View):

Required using Statement for Registry work


using Microsoft.Win32;

The remaining sections in this chapter develop separate routines for the buttons and each
section is independent of the others. The routines manipulate "HKCU\Software\Test"
key built previously with RegEdit. These keys can be modified and deleted without
harming the operating system.

Chapter 16 - Reading and W riting in the W indows Registry Page: 256


Reading a Specific Registry Key

Using btnRead, read a specific Registry value. The next section demonstrates how to
enumerate (list) all values within a key.

Registry Read:

1. Near the top of the program, confirm this "using" statement. This is required for all
Registry commands:

using Microsoft.Win32;

2. In Form1's design view, double-click btnRead to stub-in the btnRead_Click event.

3. Within btnRead_Click, make a call to the soon-to-be-built A010_ReadRegistryKey

private void btnRead_Click (object sender, EventArgs e)


{
A010_ReadRegistryKey();
}

4. Manually create the A010 routine:

Before doing the actual read, there are four things you should always do when setting up
this routine:

Declare a SubKey string, as a matter of style


Build a standard try-catch to house the Open command
Open the Registry
Close the Registry

Chapter 16 - Reading and W riting in the W indows Registry Page: 257


Standard Logic to Open the Registry, preliminary
1 private void A010_ReadRegistryKey()
2 {
3 //Open the Registry and read a specific value in TEST
4
5 string strSubKey = "Software\\Test";
6
7 try
8 {
9 //Open the Registry
10 RegistryKey myRegKey =
Microsoft.Win32.Registry.CurrentUser
.OpenSubKey (strSubKey);
1
2 //<Use GetValue/GetValueNames methods here>
3
4 //Close the Registry
5 myRegKey.Close();
6 }
7 catch
8 {
9 //Open failed
20 }
1
2 //Can't close the registry here; it is out of scope
3 }

Declare a SubKey String:

Line 5, "string strSubKey = "Software\\Test"


contains the ultimate target for the Registry commands. Although the value can be
written within the actual read or write statements, it is easier to see and edit if declared as
a string near the top of the routine. This is also a matter of style.

When building the string, keep these rules in mind:


• Oddly, you cannot specify a top-level root-key in this string (HKCU / HKLM); this
is taken care of in the OpenSubKey (line 10)
• Use double-backslashes (\\) to separate key structures. Double-backslashes because
the character is reserved. This is an escape sequence. Optionally, use @verbatim
strings.
• Do not use leading or trailing backslashes

Important Note: You must open the Registry at the specific SubKey. Although it would
be nice to open the Registry at a higher level (e.g. "Software") and use subsequent
commands to tunnel into "\\Test", the OpenSubKey method does not support this design.
If you have multiple keys to process, Open and Close multiple subkeys, using (for
example) myRegKey1, myRegKey2. More on this in a moment.

Chapter 16 - Reading and W riting in the W indows Registry Page: 258


Opening the Registry:

The declaration "RegistryKey" is where you define the top-level


(Root Key) directory; typically "LocalMachine" or
"CurrentUser"

Line 10 "opens" the Registry database using this command:

RegistryKey myRegKey =
Microsoft.Win32.Registry.CurrentUser.OpenSubKey(strSubKey)

where:

1. Like all other C# declarations, first declare the variable-type then assign the
variable's name. A variable of type "RegistryKey" is declared and named
"myRegKey". Any name can be used; myRegKey is not reserved.

2. The declaration "RegistryKey" is where you define the top-level (Root Key)
directory; typically "LocalMachine" or "CurrentUser".

• Spell out the name "LocalMachine"; do not use HKLM.


• Do not use "Hkey"; it is assumed.
• Remarkably, the names are not case-sensitive.

3. Notice how the "OpenSubKey" was prefixed with "Microsoft.Win32.Registry" even


though a "using Microsoft.Win32" was specified. The author has found that even
with the "using" statement, some computers (Visual Studio 2005 Full vs Express) did
not recognize the prefix and manually specifying the layer resolved compiler
problems. Modern versions behave better.

try-catch Thoughts:

It is always risky to open and read the Registry without the protection of a try-catch.
Sometimes the expected keys are missing or the user does not have rights - typically a
non-administrative user.

As you will see in a moment, two nested try-catch routines are recommended: One to
capture OPEN errors and another to intercept READ and Update errors. This is similar
to the logic used with ASCII files.

Closing the Registry:

If a Registry is opened, it should be closed (line 16); this helps prevent corruption
(especially during write operations). Use the RegistryKey's variable name
("myRegKey") when closing. The close must happen within the try-block or else the
RegistryKey variable falls out of scope. This is the same issue seen in ASCII file-reads.

Chapter 16 - Reading and W riting in the W indows Registry Page: 259


Reading a Specific Registry Value:

Use the .GetValue method to read a value when you know a specific Name within a key.
The vernacular can be somewhat confusing. In RegEdit, the "yellow-folder" is the Key
and the details within the key are called "Name-Value" pairs. With the example registry,
"Software\\TEST" is the Key and "ApplicationName" is the Name. Many people
mistakenly call the "ApplicationName" a key.

Use this basic command to read a specific (string) value from a Key:
myRegKey.GetValue("ApplicationName").ToString();

For example, line 13 displays the found-value with a MessageBox.

Chapter 16 - Reading and W riting in the W indows Registry Page: 260


A010_ReadRegistryKey(); Display a Single Value, completed
1 private void A010_ReadRegistryKey ()
2 {
3 //To locate a specific, single name-value pair:
4
5 string strSubKey = "Software\\Test";
6
7 try
8 {
9 //Open the Registry Key using the strSubKey:
10 RegistryKey myRegKey = Microsoft.Win32.Registry.
CurrentUser.OpenSubKey(strSubKey);
1
2 //Show a specific key (Should have a separate Try-Catch):
3 MessageBox.Show
(myRegKey.GetValue("ApplicationName").ToString());
4
5 //If the registry is opened, close it:
6 myRegKey.Close();
7 }
8 catch (Exception ereg)
9 {
20 MessageBox.Show ("Error Reading Registry in btnRead \r\n" +
1 ereg.Message.ToString());
2 }
3 }

where:

• In the Registry, "ApplicationName" is stored as a string, but C# treats this as an


object and you must convert the value using the .ToString() method.

• Dword values should be converted to Double (float) or Integer, as needed. Registry


Multi-String values are discussed later.

• Neither the RootKey, SubKey and ValueNames are case-sensitive.

Testing:

Even simple Registry reads should have a nested try-catch (not illustrated
above). Try this experiment: In line 13, misspell "ApplicationName" and run
the routine again.

Better Error Trapping:

An improvement would be to have btnRead Open the Registry (with its own try-catch)
and then call A010 to do the actual Read. This has the benefit of un-nesting the
recommended try-catches. To make this work, pass myRegKey by reference, keeping
everything in scope. With this, the nitty-gritty dirty work can still happen in a segregated
routine:

Chapter 16 - Reading and W riting in the W indows Registry Page: 261


Improved Registry Read with call to A020; Part A, completed
private void btnRead_Click (object sender, EventArgs e)
{
//Have btnRead_Click setup the Read but let another
//routine process the actual records. Since RegKey would
//fall out of scope, you must pass the variable to the
//downstream function

string strSubKey = "Software\\Test";

try
{
RegistryKey myRegKey = Microsoft.Win32.Registry.
CurrentUser.OpenSubKey(strSubKey);

A010_ReadKeys (myRegKey);
myRegKey.Close ();
}
catch (Exception eRegOpen)
{
//Error logic here: eRegOpen.Message.ToString()
//or eRegOpen.StackTrace
}
}

A010 accepts the Opened Registry Key and can then do all of its Read commands with
their own, separate nested try-catch error logic:

Chapter 16 - Reading and W riting in the W indows Registry Page: 262


Improved Registry Read A010; Part B, completed
private void A010_ReadKeys (RegistryKey passedRegKey)
{
//Let this routine do the dirty work; freeing btnClick
//Note the passed RegistryKey value; which is a copy
string stringValue1 = "";
string stringValue2 = "";

try
{
stringValue1 = passedRegKey.GetValue
("ApplicationName").ToString();
stringValue2 = myRegKey.GetValue
("ApplicationSwitch").ToString();

MessageBox.Show
("ApplicationName = " + stringValue1 + "\r\n" +
"ApplicationSwitch = " + stringValue2);

}
catch (Exception eRegDetail)
{
MessageBox.Show
("Error found A010: " + eRegDetail.Message.ToString());
//or
MessageBox.Show ("Error found: " + eRegDetail.StackTrace);
}
}

Common Errors when Reading:

Registry keys, especially those living in HKCU, have a habit of disappearing as users un-
install programs or if they roam to different computers without roaming profiles.
Because of this, it is risky to read Registry keys without a try-catch.

For example, opening a non-existent key generates a nasty run-time exception (end-users
see a slightly-different, but equally horrible error):

string strSubKey = "Software\\badtest";

Chapter 16 - Reading and W riting in the W indows Registry Page: 263


The program needs to detect the problem and either create the missing keys or display an
appropriate message to help you, the developer, debug the program.

In the try-catch, consider these error messages, which can help you with debugging:

catch (Exception eRegDetail)


{
MessageBox.Show
("Error found A010: " + eRegDetail.Message.ToString());
//or
MessageBox.Show ("Error found: " + eRegDetail.StackTrace);
}

where "eRegDetail" is any made-up variable-name; use anything but "e" because "e"
collides with the method's EventArgs statement at the top of the routine.

eRegDetail.Message displays the same message as seen in the compiler's Error List box
and it is occasionally useful. In the catch-statement, append the module number because
you will appreciate this when users call to complain about a "bug".

MessageBox.Show ("A010: Error Reading Registry \r\n" +


eregDetail.Message.ToString());

Chapter 16 - Reading and W riting in the W indows Registry Page: 264


More interesting is the .StackTrace – which displays more detail for the developer.

Error Found at Registry.Form1.btnRead_Click (Object sender, EventArgs


e) in C:\Data\Proj\VS\Sample Programs\Registry\Form1.cs:Line 28"

Of course, a better solution would be to create the missing Registry key and not tell the
user of the problem.

Chapter 16 - Reading and W riting in the W indows Registry Page: 265


Reading Mulitple Keys

The OpenSubKey method can only visit a specific, single key (key being a folder in the
tree-side navigation). This means it can not open a higher-level key (e.g. "Software")
and then later tunnel further into the structure. In other words, this does not work:

Regkey.OpenSubKey ("Software\\Test"); //you wish


myRegKey.GetValue ("\\Test\\ApplicationName"); //does not work
myRegKey.GetValue ("\\Junk\\anotherName"); //and does not work

If Registry values needed to be read from both "Software\\Test" and "Software\\Junk"


open two different Registry-Reads, as in:

...myRegKey1.OpenSubKey ("Software\\Test");
...myRegKey2.OpenSubKey ("Software\\Junk"); //Note new variable name

followed by:

myRegKey1.GetValue("ApplicationName");
myRegKey2.GetValue("AnotherName");

Since they have different variable names (myRegKey1 and myRegKey2), they remain
independent and can co-mingle within the same routines.

Of course, close both Open statements with:


myRegkey1.Close();
myRegKey2.Close();

Chapter 16 - Reading and W riting in the W indows Registry Page: 266


Reading All Values within a SubKey

btnReadAll will loop through the "HKEY_CurrentUser\Software\Test" key displaying all


name-value pairs. Use this design when you do not know the specific Name-Value pair
or when you need to process all of the values in that part of the Registry. This example
depends on the Test Registry values built manually at the beginning of this chapter.

Expected Results from "Hkey_CurrentUser\Software\Test" is a list showing each Name-


Value pair in the key:

ApplicationName = My Program
ApplicationSwitch = 1

To retrieve the information, the program goes through a two-step process, first retrieving
the Name(s) into an array, then looping through the array2, scraping out the values.
These methods are used:

GetValueNames Retrieves each of the Names (e.g. ApplicationName)


GetValue Retrieves the actual value.

"GetValueNames" is similar to the ".Split" method seen in earlier chapters, where the
results are stored in an array.

Steps:

1. In design view, double-click btnReadAll.

2. Confirm using Microsoft.Win32; at the top of the program. Then, in the empty
btnReadAll_Click event, follow nearly the same statements used in the btnRead:

2
Once again, arrays are needed and they are discussed in more detail in Chapter 22

Chapter 16 - Reading and W riting in the W indows Registry Page: 267


Define a SubKey: string strSubKey = "Software\\Test";
Build a try-catch (described next)
Open the Registry: RegistryKey myRegKey = ...
Close the Registry: myRegKey.Close();

Loading SubKeyNames into an Array using .GetValueNames, tentative


1 private void btnReadAll_Click (object sender, EventArgs e)
2 {
3 //Open the Registry and read all the values within TEST
4
5 string strSubKey = "Software\\Test";
6
7 try
8 {
9 //Open the Registry
10 RegistryKey myRegKey =
Microsoft.Win32.Registry.CurrentUser.OpenSubKey
(strSubKey);
1
2 string [] afoundValues = myRegKey.GetValueNames();
3
4 //Close the Registry
5 myRegKey.Close();
6 }
7 catch
8 {
9 //Open failed
20 }
1
2 //Can't close the registry here; it is out of scope
3 }

where:

• Line 12 (code example below), declares an open-ended string array and the names
are shoveled into the list with the GetValueNames statement:

string[] afoundValues = myRegKey.GetValueNames();

• The GetValueNames method assumes more than one Name is returned and because
of that, it returns the results in an array. Even if there is only one Name-Value pair,
results are still arrayed. Each retrieved name gets a spot in the list, starting at
position zero.

Additional Comments:

• Notice "myRegKey" is the object and this is the RegistryKey's variable name, which
was declared in the Open command:

Chapter 16 - Reading and W riting in the W indows Registry Page: 268


• The resulting array contains only the name-side of the name-value pairs. The actual
value-data is read in a soon-to-be-written "foreach" loop, which uses the ".GetValue"
method.

Expand the code, starting at line 12, with the actual loop:

btnReadAll: Display all Values in SubKey, completed


1 private void btnReadAll_Click (object sender, EventArgs e)
2 {
3 //Open the Registry and read all the values within TEST
4
5 string strSubKey = "Software\\test";
6
7 try
8 {
9 //Open the Registry to a particular SubKey
10 RegistryKey myRegKey = Microsoft.Win32.Registry.
CurrentUser.OpenSubKey (strSubKey);
11
//To View all of the value-data pairs, create an array and
//loop through all the value-pairs. Populate the array with
//a .GetvalueNames command:

12 string [] afoundValues = myRegKey.GetValueNames();


12a
12b foreach (string strcurrentValue in afoundValues)
12c {
12d //Show the Value=data pair:
12e MessageBox.Show
(strcurrentValue + " = " +
myRegKey.GetValue (strcurrentValue) );
12f }
3
4 //If you open a registry, close it:
5 myRegKey.Close();
6 }
7 catch (Exception ereg)
8 {
9 //Open failed
20 MessageBox.Show
("Error Reading Registry in btnRead \r\n" +
ereg.Message.ToString());
21 }
22
23 }

where:

• At line 12b, a foreach-loop iterates through the array, grabbing one Name at a time
and storing it in a temporary variable called "strcurrentValue." Inside the loop, the
variable is used by .GetValue, which then retrieves the "Value" part. Like all
foreach-loops (with an array), the loops start at array-position zero.

Chapter 16 - Reading and W riting in the W indows Registry Page: 269


• In the loop, the MessageBox statement places the "ApplicationName" and the
"ApplicationSwitch" into the ".GetValue" command. This command then dives into
the Registry, fetching the values and retrieving an individual key. The MessageBox
should display these two values:

"ApplicationName = My Program"
"ApplicationSwitch = 1"

• Notice how the numeric DWord is returned as a string. If needed as a numeric,


convert using

Convert.ToSingle(myRegKey.GetValue (strCurrentValue))

Recommended Style Changes:

As described in previous sections, much of this logic should be moved into its own
module [for example: A030_ReadAll(myRegKey)], freeing the btnReadAll_Click event
from the mundane details of the Read commands.

If you had significant processing to do within the foreach-loop, consider moving that
loop into its own routine, passing the array to a third, downstream module. For example,
change line 12a to a call, passing the string array:

:
12 string [] afoundValues = myRegKey.GetValueNames();
12a A031_ProcessNames(afoundValues);
12b //
14 //Close the Registry
15 myRegKey.Close();
:

Then create a new module, where there might be other logic for each Name-Value pair.
For example:

Chapter 16 - Reading and W riting in the W indows Registry Page: 270


private void A031_ProcessNames(string[] apassedArray)
{
//Rifle through each name and retrieve the actual Registry Value:

foreach (string strcurrentValue in apassedArray)


{
//Show the Value=data pair:
MessageBox.Show
(strcurrentValue + " = " +
);

switch (strcurrentValue.ToUpper())
{
case "APPLICATIONNAME"
string strVariable = myRegKey.GetValue (strcurrentValue);
//do stuff here using retrieved value...
break;
case "APPLICATIONSWITCH"
single intVariable =
Convert.ToSingle(myRegKey.GetValue (strcurrentValue));
//do stuff here using retrieved value...
break;
}
}
}

Chapter 16 - Reading and W riting in the W indows Registry Page: 271


Multi-Line String Registry Values

Most Registry name-value pairs are either "string" or "dword" (numeric) values.
Occasionally, you will find a multiline string value and even if they contain only a
single-line of text, they still must be processed as an array.

This example opens the Registry and retrieves the computer's BIOS Date, BIOS Version
and CPU speed. The BIOS Version is a multi-line Registry value and the Speed exists in
a different key.

btnReadMulti: Reading a Multi-Line String Value, completed

private void btnReadMulti_Click (object sender, EventArgs e)


{
//Retrieve various machine information

string strmyBIOSDate = "";


string strmyWordyBIOSVersion = "";
string strmyCPUSpeed = "";
string[] astrsystemBIOSVersion; //Declare string array for ver.

string strSubKey1 = "Hardware\\Description\\System";


string strSubKey2 =
"Hardware\\Description\\System\\CentralProcessor\\0";

//Open two separate keys:


RegistryKey RegKey1 = Microsoft.Win32.Registry.LocalMachine
.OpenSubKey(strSubKey1);
RegistryKey RegKey2 = Microsoft.Win32.Registry.LocalMachine
.OpenSubKey(strSubKey2);

//Standard string: BIOS Date:


strmyBIOSDate = RegKey1.GetValue("SystemBIOSDate").ToString();

//The SystemBIOSVersion is a Multi_SZ string; treat as an array


//Take each line from the array and append into one long string.
//Note the string-cast as the array is populated:

astrsystemBIOSVersion =
(string[])RegKey1.GetValue("SystemBIOSVersion");

Chapter 16 - Reading and W riting in the W indows Registry Page: 272


foreach (string tempstring in astrsystemBIOSVersion)
{
//Cram every line-item into one long string
strmyWordyBIOSVersion += tempstring + " ";
}

strmyCPUSpeed = RegKey2.GetValue("~MHz").ToString();

MessageBox.Show("Wordy BIOS Version: " + strmyWordyBIOSVersion +


"\r\n" + "\r\n" +
"CPU Speed: " + strmyCPUSpeed);

RegKey1.Close();
RegKey2.Close();

Multi-line String, end

comments:

• strBIOSDate, strCPUSpeed are standard Registry-reads but two keys had to be


opened because they were in different parts of the Registry. FYI: CPU speed is
reporting against processor zero.

• SystemBIOSVersion is mult-line string and must be loaded into a string array


(astrsystemBIOSVersion). Since the results of the "GetValue" method are "objects",
they must be converted into strings before shoving them into the array. In this
example a string "cast" was used.

• "Casting" is another way to convert one data-type to the next. By placing (string) or
(int) – with parenthesis, in front of another object, it will be converted to the new
type, if the source-type is compatible. This is an indirect way of issuing a
".ToString( )" command. In this case, the GetValue objects were cast into a string[ ]-
array. I'm not fond of casting, preferring instead a more explicit route, but in this
case, the compiler does not allow a .ToString.

• If you misspell the quoted string, such as "...GetValue("SystemBIOXVersion");",


you will get a "NullReferencesException" - with no real hint as to the real problem.

Chapter 16 - Reading and W riting in the W indows Registry Page: 273


Because the calls into the Registry use quoted strings, the editor cannot help you
with the correct spelling. Be accurate in this area.

Chapter 16 - Reading and W riting in the W indows Registry Page: 274


Creating/Modifying Name-Value Pairs

The ".SetValue" method modifies existing values in the Registry and also creates new
name-value pairs if they do not exist. If ".SetValue" needs to create a value, it does so
without generating an exception. If the value already exists, existing data is modified. In
this section, you will write code that modifies the ApplicationName and
ApplicationNumber values in the SubKey "HKCU\Software\Test".

Opening the Registry in 'Writeable' Mode:

To make changes to a Registry value, the open SubKey uses a "writeable" overload.
Append a comma-true (,true) to the Open command:

RegistryKey myRegKey = Microsoft.Win32.Registry.


CurrentUser.OpenSubKey ("Software\\Test", true);

The example below opens the registry in Write-mode at line 10.


Line 15 modifies the existing "ApplicationName" value.
Line 18 adds a new value, "ApplicationAuthor"
Line 21 adds a new Dword value "ApplicationVersion"

Chapter 16 - Reading and W riting in the W indows Registry Page: 275


btnCreate (Registry); Add/Change New Values, completed
1 private void btnCreate_Click (object sender, EventArgs e)
2 {
3 //Add, Create, Update registry values within an existing
4 //SubKey
5
6 string strSubKey = "Software\\test";
7
8 try
9 {
10 RegistryKey myRegKey = Microsoft.Win32.Registry.
CurrentUser.OpenSubKey (strSubKey, true);
1
2 try
3 {
4 //Update an existing value from previous examples:
5 myRegKey.SetValue ("ApplicationName", "A Neat Program");
6
7 //Add a new (non-existing) value:
8 myRegKey.SetValue ("ApplicationAuthor", "Jsmith");
9
20 //Add a new DWord as decimal (auto converts to Hex):
1 myRegKey.SetValue
("ApplicatonVersionNo", 42, RegistryValueKind.Dword);
2
3 myRegKey.Close();
4 MessageBox.Show("keys changed; check in Regedit");
5 }
6 catch (Exception eregModify)
7 {
8 MessageBox.Show
("Unable to change Registry value; " + \r\n" +
"This may be a rights issue" + "\r\n" +
eregModify.Message.ToString());
9 }
30 }
1 catch (Exception eregOpen)
2 {
3 MessageBox.Show ("Error opening Registry");
4 }
5 }

where:

• Open the RegistryKey using the ",true" (boolean writable) overload.

Important note: By default, your program only has rights to write in


"CurrentUser". If you attempt to write in another key, such as
"LocalMachine", you will fail with an "Error Writing Registry: Object
reference not set to an instance of an object". This is a security feature
introduced in Windows Vista and newer.

• A nested try-catch captures different types of errors. Many users in corporate


environments do not have rights to modify HKLM registry keys because they are
non-administrative users. In your program, this routine would also probably need to

Chapter 16 - Reading and W riting in the W indows Registry Page: 276


set a variable (such as boolHorrible_Program_Error = true) so the program can shut-
down gracefully.

• The outside try-catch looks for SubKey "Open" errors while the interior try-catch
captures what are essentially "rights" errors.

• Although Dwords are Hex-values, pass a decimal (base-10) number to the method
and it will automatically convert to Hex. See line 21, above.

• Other Registry types are also supported, including Binary, Multiple-string values,
etc. Since these are rarely used, see Microsoft's Technet for additional
documentation.

Testing:

To test the results, run the program and click btnCreate.


Then Start, Run Regedit.exe. Tunnel to HKCU\Software\Test. You may need to press
F5 (Refresh) to see the current data.

Results: New values added; existing values modified.

Writing to HKLM:

Exercise: Change the key from "CurrentUser" to "LocalMachine" and attempt to create
the registry values again. Results: Failure intercepted with a try-catch.

There is not a straight-forward solution to writing to HKLM. In Windows XP, programs


could freely write to HKLM but now this key off-limits. Obviously, if you are trying to
write a preference for "ALL Users" on this computer, HKLM is the only place this
should be. Microsoft's recommendation is to "deploy" your program with an installation
script, and let the script write these values, but this means your program still cannot

Chapter 16 - Reading and W riting in the W indows Registry Page: 277


update the values. This is beyond the scope of this chapter, but it does point to a benefit
in using an app.Config or INI file. The author invites comments.

Chapter 16 - Reading and W riting in the W indows Registry Page: 278


Creating Sub-SubKeys

There are no surprises here. Use the .CreateSubKey method to create new subkeys
within the opened key. In this example, stay within HKCU and create
"Software\Test\SecondLayerDeep". Attach this code to btnNewSubKey:

btnNewSubKey (Registry); Create SubKey, completed


private void btnNewSubKey_Click (object sender, EventArgs e)
{
//Create a new SubKey within an existing Key
//You probably should make sure the upper key exists first

string strSubKey = "Software\\Test";

try
{
RegistryKey myRegKey = Microsoft.Win32.Registry.CurrentUser.
OpenSubKey(strSubKey, true);

myRegKey.CreateSubKey ("SecondLayerDeep");

myRegKey.Close();
}
catch (Exception eregSubKey)
{
MessageBox.Show("You probably don't have rights \r\n" +
"ergeSubKey.Message.ToString());
}
}

where:

• Open the Registry in writeable-mode (OpenSubKey comma-true)

• myRegKey.CreateSubKey ("<new name>"); creates the key

• Pre-existing SubKeys do not generate an error (the try-catch is not triggered); the
routine simply continues with the next command.

• Although not illustrated here, there should be in internal try-catch wrapped around
the CreateSubKey. This allows you to have a different error message if the create-
subkey fails

Chapter 16 - Reading and W riting in the W indows Registry Page: 279


Deleting Values and Trees

Following similar logic, an individual name-value pair can be deleted with the
.DeleteValue method. There are several Exception-concerns and once again, you are
restricted to the CurrentUser registry key:

btnDelete (Registry); Delete Specific Value, completed


private void btnDelete_Click (object sender, EventArgs e)
{
//Delete an individual name-value pair (snippet)
string strSubKey = "Software\\test";

try
{
RegistryKey myRegKey = Microsoft.Win32.Registry.CurrentUser.
OpenSubKey (strSubKey, true);

try
{
//Delete the individual value:
//Be careful about doing multiple deletes; if a middle
//delete fails, the catch would prevent others from
//running. Consider using an alternate overload.

//Use one of these two deletes where one throws an error


//if there is a problem; the second continues as if
//nothing was wrong:
myRegKey.DeleteValue ("ApplicationSwitch"); //throw on err
myRegKey.DeleteValue ("ApplicationSwitch, false);

myRegKey.Close();
}
catch (Exception eregModify)
{
//If not "throw on missing value" and deleting an
//individual key, concerns with a failed Delete:
//Usually the program should do nothing.
//The catch requires opening and closing braces
//with no real-executable code.
}
}
catch (Exception eregCreate)
{
MessageBox.Show ("Error opening Registry");
}
}

Error Processing:

The logic in this method requires additional thought. If you attempt to delete a non-
existent value, using a simple "DeleteValue(<name>), an exception is triggered and
subsequent commands in the routine will not run. For example, assume your program

Chapter 16 - Reading and W riting in the W indows Registry Page: 280


needs to delete four Registry values – but the third does not exist. The third value
generates an exception and the logic jumps to the catch statement, skipping the fourth.

Note: for most deletes, the catch-statement would be an empty routine (a failed delete of
a non-existent key is not a problem per-se).

A better way to handle a delete is to append a comma-false overload to the DeleteValue


method. The editor hints at this with: "throw (exception) on missing value = false". This
tells the program to ignore missing keys and continue without triggering an Exception:

myRegKey.DeleteValue ("ApplicationSwitch", false);

Consider this important possibility:

Unfortunately, skipping failed deletes (with a ",false) introduces another problem. If the
user lacks the rights to delete (typically a non-administrative account on a Windows XP
machine), then all deletes fail – not because the value did not exist but because they
didn't have rights. Each delete runs and fails, with nobody the wiser. In this scenario,
the ",false" overload mis-behaves and lets the code continue. To work around this,
confirm the key (GetValue) really was deleted. If deleting multiple values, there is
probably no need to check the existence of every name-value pair; looking for one is
adequate to prove the rights.

Deleting Entire SubTrees:

IMPORTANT: Make sure your code targets the correct SubKey. If you delete
something important, such as HKCU\Software, you will blow up your PC –
permanently. Confirm you have a backup if you doubt your abilities.

In addition to a disk-backup of the entire C: drive, you should also use RegEdit.
Highlight the key HKCU, Select File, Export: Save the key to "C:\Data\hkcu.reg". If you
blow this up, use Windows Explorer to locate the file. Other-mouse-click and choose
"Merge". (The Author has not tested if your computer would survive long enough to
merge the file).

Chapter 16 - Reading and W riting in the W indows Registry Page: 281


".DeleteSubKey" deletes folders (and all items within that folder). With this method,
you could delete the "SecondLayerDeep" folder built earlier. But you cannot delete the
Key that opened the Registry. In other words, you cannot delete the "Test" key because
it is in-use.

This code deletes the SecondLayerDeep SubKey:

btnDelete (Registry): Deleting a Sub-SubKey, completed with Cautions


private void btnDelete_Click (object sender, EventArgs e)
{
//Deleting SubKeys below the strSubKey

string strSubKey = "Software\\Test";

try
{
RegistryKey myRegKey = Microsoft.Win32.Registry.CurrentUser.
OpenSubKey (strSubKey, true);

try
{
//You can only delete subKeys below the current Key
myRegkey.DeleteSubKeyTree ("SecondLayerDeep");

myRegKey.Close();
}
catch (Exception eregDel2)
{
MessageBox.Show("Tree Delete failed");
}
}
catch (Exception eregDel)
{
MessageBox.Show("Delete failed on Open");
}
}

Testing:

This example assumes you had created a SubKey "SecondLayerDeep"


(HKCU\Software\Test\SecondLayerDeep).

Results: the Sub-SubKey "HKCU\Software\Test\SecondLayerDeep" is deleted and other


values within that key would also delete.. Examine the results by launching RegEdit.exe;
you may need to press F5 to refresh the window.

Deleting the Current Key:

You can't delete the OpenSubKey (e.g. "Software\TEST"). The catch's


eregDel2.Message shows: "Cannot delete a subkey tree because the subkey does not
exist."

Chapter 16 - Reading and W riting in the W indows Registry Page: 282


What if you also wanted to delete the entire HKCU\Software\TEST folder (key) along
with any other subkeys, such as "SecondLayerDeep"? To do this, modify the top-level
strSubKey, moving it one layer up in the tree-diagram:

string strSubKey = "Software";

then issue the cascading-delete with this command:

RegKey.DeleteSubKeyTree("Test");

Results: This should make you somewhat nervous because all you see is the major
"software" key, but the code works, properly, targeting the subkey \Test key and all
values beneath.

Other Registry Methods of Interest:

Other methods may be of interest:

myRegKey.DeleteSubKey Deletes a single-SubKey, assumes the key is empty.

myRegKey.Flush Commits changes to the Registry before a Close event.


Microsoft does not recommend using this method

myRegKey.GetSubKeyNames Enumerates all subkeys (folders). Example, below.

myRegKey.ValueCount Returns how many name-value pairs are in the key

Chapter 16 - Reading and W riting in the W indows Registry Page: 283


Enumerating SubKey (folders)

Enumerate all subkeys (folders) within a selected key and then tunnel into each folder
and display detail information. This example displays all printers and printer
IPAddresses from this Key:

HKLM\Software\Microsoft\Windows NT\CurrentVersion\Print\Printers

On my machine, these four printers are defined:


Brother HL-2170W
Fax
HP PhotoSmart
OneNote

Below is sample code attached to a button1 event. Requires this using statement:

using Microsoft.Win32;

Chapter 16 - Reading and W riting in the W indows Registry Page: 284


Example: Display all Printer Registry Entries, completed
private void button1_Click (object sender, EventArgs e)
{
//Display all printers and IP Address on a workstation

string strprinterGroupKey =
"Software\\Microsoft\\Windows NT\\CurrentVersion\\Print\\Printers";

string strprinterDetailSubKey;

RegistryKey myPrinterGroupRegKey =
Microsoft.Win32.Registry.LocalMachine.OpenSubKey
(strprinterGroupKey);

//Loop through each (subkey/folder)


foreach
(string printerKeyName in myPrinterGroupRegKey.GetSubKeyNames())
{
strprinterDetailSubKey =
strprinterGroupKey + "\\" + printerKeyName;

//Open/Read the particular key


RegistryKey myPrinterDetailKey =
Microsoft.Win32.Registry.LocalMachine.OpenSubKey
(strprinterDetailSubKey);

MessageBox.Show
("Printer Name: " + printerKeyName + " " +
"Port: " + myPrinterDetailKey.GetValue("Port").ToString());

myPrinterDetailKey.Close();
}

//Outside of the subkey/folder loop:


myPrinterGroupRegKey.Close();
}

This completes the Registry chapter.

Chapter 16 - Reading and W riting in the W indows Registry Page: 285


Chapter 17 - Excel and Access Page: 286
A Beginners Guide to C-Sharp - Volume 2
Visual Studio C# 2017
ASCII through Advanced Formatting
by Tim R. Wolf
© 2017.06.01 1.02
Table of Contents

9 Chapter 12 - ASCII Files 17


Reading ASCII (Text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
StreamReader
Priming Read
Ending the Loop and Closing Files
Using an EndOfStream Read
The "Using" Clause
ASCII File Reads with try-catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Issues with try/catch and Close
Chaining catch-statements
Completed ASCII ReadFile - Program 12.5. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Writing ASCII (text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Appending ASCII (text) Files - Advanced Open Methods. . . . . . . . . . . . . . . . . . . . 54
Preventing Append from Running Twice

9 Chapter 13 - Parsing Tab and CSV Files 65


Automatic Parsing by Delimiter (Split).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Declaring the Destination Array
foreach
Alternate Loops
.Split Limitations
CSV Files
Split Example: Phone-Numbers
Manually Parsing Comma-Delimited Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
First Field Logic
for-next Logic
Substring
Last-field Logic
Tab-Delimited Files
Parsing a Variable Number of Columns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Parsing ASCII CSV Files with Embedded Commas.. . . . . . . . . . . . . . . . . . . . . . . 105
Error Processing in CSV Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Moving ParseCSVLine to the Utility Library

9 Chapter 14 - INI Files 147


INI File Structure and Design. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
A015: Loop Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A017: Parse INI Detail Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A028: Finding the Application's Default INI Location.. . . . . . . . . . . . . . . . . . . . . 178
Command-Line Override.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Simulate a Command Line Options
Embedded Spaces in the Parameters
Command-line ini=
A029: Write Default INI when Missing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Basic INI File Read: Complete Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Using a Program Class - CL860 INI File Read. . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
CL860 Basic INI Read Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Using CL860_BasicINIRead

9 Chapter 15 - xml and App.config Files 223


xml File Structure
app.config xml.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Using (Reading) app.config
Building a Manual xml File.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Reading xml File Sequentially. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

9 Chapter 16 - Windows Registry 249


Organization of the Registry
Reading a Specific Registry Key.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Opening the Registry
Closing
Reading a Specific Registry Value
Reading Mulitple Keys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Reading All Values within a SubKey. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Multi-Line String Registry Values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Creating/Modifying Name-Value Pairs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Opening the Registry in 'Writeable' Mode
Creating Sub-SubKeys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Deleting Values and Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Deleting Entire SubTrees
Deleting the Current Key
Enumerating SubKey (folders). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

9 Chapter 17 - Reading Excel and Access 289


Reading an Excel File using a COM object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Open the Workbook
parameter list
Reading a Row
comboBoxes based on Access Database Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Attaching Microsoft Access Data to a comboBox. . . . . . . . . . . . . . . . . . . . . . . . . 317
ODBC Call
Connecting to the comboBox
Refreshing the List
Performance Considerations

9 Chapter 18 - External Programs (Shell) 333


Starting a New Process. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Launching Notepad with a Specific Filename
Building an Exit Event
Using Code to Start a New Process.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
proc.WaitForExit.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
DOS Output.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Capturing DOS output into an Array
Multiple Instances of an Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Simple Instance Test
Mutex: A Better Solution for Multiple Instances. . . . . . . . . . . . . . . . . . . . . . . . . . 355

9 Chapter 19 - Waits, Delays and Pauses 361


Poor Wait States – Not Recommended
Empty loops
current time and loop until xx seconds
Sleep events
System.Threading.Thread.Sleep (milliseconds). . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Completed wait Simulation - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Wait as a Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Using cs805_Wait
Calling Wait
Timer Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Basic Timer Code
Event Horizons
Countdown and Timer Example
Disabling the Close "X" Mid-Transaction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Intercepting a Close Event
Countdown and Timer Program - Completed. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Simple Startup Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Splash Screens.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

9 Chapter 20 - Printing 415


Printing Simple Text. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Print Layout / Print Rendering
Pixel Positions in Millimeters (x,y)
OverShooting
Printing Horizontal Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Printing Rectangles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Filling
Printing Graphics.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Printer Dialogs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Adding a Custom Printer Dialog
Print Preview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Printer Setup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Printing Text Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Font Height Calculations - Lines per Page
Page-Break Logic
GraphicsUnit.Millimeters
Setting New Margins, Landscape. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
Current Margin
Setting New (Printable) Margins
Begin and End Print Events.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

9 Chapter 21 - Formatting 469


Font Color, Font Bold. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Font Bold
Font Style Ariel
Basic String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
String.Format with Alignment.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Proportional Fonts
Numeric Formatting with String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Standard Decimals
Formatting with Alignment
Commas and Currency
Numeric Picture Clauses. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Padding Zero with Leading and Trailing Decimals
Place Holder (#)
Thousands Separator ( , )
Variable Picture Clauses (Group Separators)
Date and Time Pictures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Converting from Strings
Converting Dates
Other Date Time Properties and Methods.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
DateTime Formatting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Pictures
Display Dates, Year first
Format Class Library - cl710_Formatting.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Phone Number Formatting
Proper Names
Proper Addresses
PhoneNumberFormat Method - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
PhoneNumberFormat Module - Coding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Extension Extract
Strip Normal Punctuation
Strip "1-" Prefixes
Punctuate
Punctuating the AreaCode
"ProperNames" Formatting - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
ProperNamesFormat - Coding.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
A Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 17 - Reading Excel and Access
Chapter 17 - Reading Excel and Access

In this chapter, write a program that opens an Excel spreadsheet, reads selected data, and
displays the results in a multi-lined textBox. Microsoft provides several ways to do this
and none are particularly obvious. The second half of this chapter deals with Microsoft
Access and it will attach Access tables to a ComboBox. See Chapter 25 for information
about MSSQL.

Topics:

• Add the Microsoft Excel 10/12 COM library


• Loading the Excel Object
• Opening the Sheet (with 15 parameters)
• Looping through the sheet; skipping top rows
• Reading a Record into an array

• Using Microsoft Access to populate ComboBoxes


• Building an Access Table and Relationships
• Using Access queries instead of tables
• See Chapter 26 for examples with Microsoft SQL

All examples were developed with Microsoft Office 2013. Older and newer versions of
office behave similarly.

Setting up Example Data:

The examples below use the same data from Chapter 13 (Parsing) but instead of an
ASCII CSV file, the data lives directly inside of an Excel (.xlsx) spreadsheet. If needed,
convert the previous chapter's CSV to Excel by doing the following: Using Excel,
retrieve the example CSV from Chapter 13 and then Save-As "C:\Data\Transactions.xls"
(changing the default type from CSV to "Excel"). Alternately, create the data from
scratch using this basic layout; the data values do not matter.

There are five columns of data:


Date: (mm/dd/yy)
Type: "Sales"
Item (description): Pants, Sweaters, etc.
Qty: Integer Value; e.g. 1, 2, etc.
Amount (each): Dollar figure

Chapter 17 - Excel and Access Page: 289


Example Excel Data

Be sure the data appears in the workbook's first tab (sheet1).

The C# program you will develop follows the same design as in previous chapters.
Construct a new program with a multi-lined textbox (textBox1) with a vertical scroll bar.
Link the external class library, CL800_Util, as described in Chapter 8. Add button
"btnReadExcel".

The goal: Open the Excel sheet; read each data-line, and calculate the extension,
(Qty * Amount) for each record.

Chapter 17 - Excel and Access Page: 290


Reading an Excel File using a COM object

Microsoft provides several ways for reading Excel and the method described here uses a
"COM" object. To use this method, Excel must be installed on the same workstation that
is running the C# program. As you will see, the COM object secretly launches Excel and
opens the sheet in the background. You can optionally choose to make Excel visible to
the end-user or it can remain hidden.

1. Add the Excel References to the project:

In Solution Explorer, right-click References.


Select "Add Reference"
Select COM, Type Libraries, on the left-nav
Scroll down the list and select [x] "Microsoft Excel 15 Objects Library"
(where "15" is your version of Excel and your version may vary)

Adding a Com Reference

2. In Form1.cs (Code-View), add this using statement near the top of the program:
Required Using Statement
using Excel = Microsoft.Office.Interop.Excel;

Chapter 17 - Excel and Access Page: 291


Note the unusual syntax with an equal-sign.

3. Declare a place-holder for the ExcelObject by placing this statement at the same location
you would build any form or class-level variables (illustrated below with an asterisk,
which is not typed). It is being declared at the class-level so it can be used in different
locations within the program.

Declare a private Excel Object, initial


public partial class Form1 : Form
{
cl800_Util util;

//Declare the Excel Object:


* private Excel.Application ExcelObj = null;

public Form1()
{
InitializeComponent();
util = new cl800_Util();
:
:etc

Technically, a declaration can be typed anywhere, above or below any method, but good
style suggests declaring all class-level (form-level) variables in a predictable and
common location.

4. In public Form1(), instantiate the Excel object and use code to confirm it opened
properly.
Linking Excel as a COM object, initial
public Form1()
{
InitializeComponent();
util = new cl800_Util();

//Instantiate a new Excel Object and confirm it launches


ExcelObj = new Excel.Application();
if (ExcelObj == null)
{
MessageBox.Show("Excel did not launch properly");
System.Windows.Forms.Application.Exit();
}

//Decide if you want to see Excel loading or not:


ExcelObj.Visible = true;
}

private Excel.Application ExcelObj = null;

private void button1_Click (object sender, EventArgs e)


{
:

Chapter 17 - Excel and Access Page: 292


As the routine runs, Excel (the program) can be hidden or exposed using the
"ExcelObj.Visible = true; statement. For now, leave the object visible:

Open the Workbook:

Up to this point, all that has been accomplished is Excel has been linked to the program,
but the Excel sheet has not been opened. The actual Open command needs a bewildering
15 separate parameters, but only the first few are needed by most mortal programmers.
There are no other overloads and because of this, you must enter all 15 values, making
the command annoyingly long.

Excel Open, illustrated. See below for details

Many of the parameters are not obvious about what they do and
in my experience, they can be ignored by typing a zero, false, or
empty-string. Naturally, you can research the web for more
details but the settings recommended below should work for
most.

5. Write the actual Open statement by double-clicking btnReadExcel (button1) to open the
Click-event. btnReadExcel will open the worksheet/workbook and loop through the
spreadsheet, retrieving each data-line.

Begin with the Workbooks.Open statement and its 15 parameters. Follow this by two
additional sheet commands:

The Open commands should be surrounded with a try-catch, but


for now, keep the logic simplified.

Chapter 17 - Excel and Access Page: 293


Opening the Excel Sheet with 15 Parameters!, completed
private void btnReadExcel_Click (object sender, EventArgs e)
{
//Open the workbook; should use a try-catch here
//The file will be opened ReadOnly
//Note the 15 required parameters;
//Your version of Excel may vary on the count needed!
//This is for Office/Excel 2002 / 2007 / 2010

int excelRowCount = 1;
int blankRowCount = 0;

Excel.Workbook theWorkbook = ExcelObj.Workbooks.Open


("C:\\Data\\Transactions.xls",
0,
true,
5,
"",
"",
true,
Excel.XlPlatform.xlWindows,
"\t",
false, false, 0, false, 1, 0);

//Get the Sheet Names; then get the first sheet's name:
//Note the lower-case "get_Item"
Excel.Sheets sheets = theWorkbook.Worksheets;
Excel.Worksheet worksheet = (Excel.Worksheet)sheets.get_Item(1);

//<loop logic through the sheet goes here>>


}

While typing, notice the inconsistent "get_Item" where "get_Item" is mixed-case.

Also, be careful to select the Workbook(s) property – and not the similarly-spelled
WorkbookOpen method.

Chapter 17 - Excel and Access Page: 294


The parameter list for Excel 2002-2016 is:

The basic syntax for the open command starts out with this statement:

Excel.WorkBook newWorkbookName =
ExcelObj.Workbooks.Open(<15 parameters here>);

string Filename, (xls or xlsx)


UpdateLinks, (use zero for "no")
ReadOnly, (use true)
Format, (always use 5)
Password, (use "" empty-string if sheet does not use an open-password)
WritePassword, (use "" empty-string if sheet does not use an edit-password)
IgnoreReadOnly, (use true incase the physical file is marked ReadOnly – avoiding an
end-user prompt)
Origin, (Hard-code this: Excel.XlPlatform.xlWindows (case-sensitive))
Delimiter, ("\t" – use quotes. Excel uses Tabs to mark the columns)
Editable, (false – your program has no need to edit this sheet; only to Read)
Notify, (use false – Notify when sheet is available, if in-use)
Converter, (use 0 zero)
AddToMru, (false – don't bother adding to Most-Recently-Used list)
Local, (use 1 one)
CorruptLoad, (use 0 zero)

The first parameter is the filename, and like the examples in previous chapters, hard-
coded filenames are enclosed in quotes and use double-backslashes in the path or
@verbatim strings. In real life you would probably get the filename from a configuration
file or a user-prompt.

Chapter 17 - Excel and Access Page: 295


The list varies, depending on which version of Excel is installed. Excel 2000, 2002, and
2003-2016 each have slightly different parameter lists and you'll see the differences in
the popup help, depending on the COM object selected. This can be problematic if your
C# program is distributed to computers with different versions of Excel.

Looping Logic:

Note: For the next few sections, hold-off on actually writing the
code until the logic is fully described.

6. At the position commented in the code above, add the main loop:

//<loop logic through the sheet goes here>>

The program needs to loop through the sheet, retrieving each row, and putting the results
into textBox1. In this case, the loop should begin at row 2, skipping past the column
headers. Deciding where to end the loop is more problematic.

Since the number of rows in the sheet are not known at run-time, a while-loop will
process the list but it needs a trigger to end. One possible indicator could be a blank line
or perhaps two blank lines in a row.

In this example, the loop terminates when two sequential blank rows are found and this
will be controlled by a variable named blankRowCount. The while-loop runs while
(blankRowCount <= 2)". I often use two rows because Excel sheets often have cosmetic
blank rows in the middle of the data; adjust as you see fit. A row-count variable is also
needed and this is described shortly. The rough code:

Chapter 17 - Excel and Access Page: 296


btnReadExcel, Workbook open, in progress
private void btnReadExcel_Click(object sender, EventArgs e)
{
int excelRowCount = 1;
int blankRowCount = 0;

//Open the workbook; could probably use a try-catch here


//The file will be opened ReadOnly
//Note the 15 required parameters; your version of Excel
//may vary on the needed parms!
//This is for Office/Excel 2003/2007

Excel.Workbook theWorkbook = ExcelObj.Workbooks.Open


("C:\\Data\\Transactions.xls",
0,
true,
5,
"",
"",
true,
Excel.XlPlatform.xlWindows,
"\t",
false, false, 0, false, 1, 0);

//Get the Sheet Names; then get the first sheet's name:
Excel.Sheets sheets = theWorkbook.Worksheets;
Excel.Worksheet worksheet = (Excel.Worksheet)sheets.get_Item(1);

//<loop logic through the sheet goes here>>


while (blankRowCount <=2)
{
excelRowCount++;
// <Read the rows here>
// <See if the record is blank and up the counter>
// <Do record-work here>
:
:

The integer, excelRowCount, initializes at "1" – skipping past the spreadsheet's column
header. The blankRow-counter keeps track of the end-of-data.

As the loop begins, blankRowCount is zero and this guarantees the loop will run at least
one time. As soon as it jumps inside of the loop, the excelRowCount variable jumps
from 1 to 2 – meaning it is ready to Read row 2. If a blank row is found, the blank-
counter increments. If two blank rows are detected, the loop ends.

The Basic Idea: Reading a Row:

With each loop iteration, an Excel row is read and processed. You will find the read-
commands are vaguely similar to VBA Excel macros – and this would make sense
because you are calling an Excel routine.

Chapter 17 - Excel and Access Page: 297


Reading a row takes two-steps. First, gets a range of multiple columns, in this case,
columns A-E. Then load the found columns into an array:

Excel.Range foundRange = worksheet.get_Range("A2", "E2");


System.Array afoundValues = (System.Array)foundRange.Cells.Value2;

The results are placed in a dynamic (open-ended) array, afoundValues. The example
above shows "A2" and "E2" – the range. The "2" needs to be "variablized" so it works
with any row. This will be corrected shortly.

Although the worksheet.get_Range reads one row of data, the results are
always loaded into a 2-dimensional array (with width and depth) – regardless
of the number of rows read. Reason: In Excel multiple rows and columns can
be highlighted at the same time; this command makes no distinctions.

Variablizing:

Next, put the commands in a loop and variablize the cell addresses.

Study the worksheet.get_Range ("A2", "E2") command for a moment. It accepts


two string values: One for the beginning cell (column) and a second for the ending.
Excel addresses cells with an alphabetic column designator, A, B, C... followed by a
row-number. Cell "A2" represents column A, row 2. For this example, you want to
always read the same columns, A-E – but the rows vary, 2, 3, 4, etc..

Since the get_Range column parameters are strings, they can be assembled with a simple
string-concatenation. Using the counter, excelRowCount, move from cell "A2" to "A3",
then "A4", by assembling the address in two parts. The "A" and "E" remain fixed while
the variable excelRowCount can increment with the row-number. Because the counter
was initialized with 1, the first-time into the loop it jumps to 2 – skipping the top header
record – exactly as planned.

Assemble the range with a literal "A" + excelRowCount.ToString()


Followed by "E" + excelRowCount.ToString()

Write this module now, noting the logic in the while-loop:

Chapter 17 - Excel and Access Page: 298


btnReadExcel, Variabilizing the starting positions, in progress
private void btnReadExcel_Click(object sender, EventArgs e)
{
int excelRowCount = 1;
int blankRowCount = 0;

:
:<The Excel 15-parm open statement went here>

while (blankRowCount <=2)


{
excelRowCount++;

//This command reads cells An..En and places the results in


//an open-ended (dynamic) array, afoundValues:
* Excel.Range foundRange =
worksheet.get_Range ("A" + excelRowCount.ToString(),
"E" + excelRowCount.ToString() );

System.Array afoundValues =
(System.Array)foundRange.Cells.Value2;

//Convert the two-dimensional array to a single-dimension:


string[] afoundCells = A120_ConvertToStringArray(afoundValues);

//: More....

where:

• Literally the address "A2" is assembled as a string concatenation and the value "2"
increments with the loop.

• A120_ConvertToStringArray will be written in a moment.

The System.Array command takes the results of the get_Range and loads it into a two-
dimensional array. The details of this command are not particularly important but it is
interesting this is a "casting" (as described in the numeric chapter).

Two-dimensional arrays, unlike the single-dimensioned arrays seen in earlier


chapters, are somewhat cumbersome, and these types of arrays are described
in Chapter 22. Because of this, and because we are only processing one row
at a time, I like to create a new function that converts a 2-dimensional array
into a 1-dimensional array. We can get away with this because we know we
are getting a range from only from a single row. The conversion makes
subsequent routines easier to code and understand.

If these new complexities worry you, do not panic because this


new routine, like any good function, can be treated as a black

Chapter 17 - Excel and Access Page: 299


box – the function is called and it returns what you need; you
don't have to worry about what is inside.

For now, the logic was stubbed with a call to A120, with returned results like this:

//Convert the two-dimensional array to a single-dimension:


string[] afoundCells = A120_ConvertToStringArray(afoundValues);

Process the Record:

By this stage, assume the record was loaded into a one-dimensional array (afoundCells).
The program can begin processing the record. Begin by checking to see if the column 1
(cell 0) is blank; if so, assume the rest of the row is blank (a more sophisticated check
could be written, but this suffices). If the record survives the blank-record check, loop
through each cell in the array:

btnReadExcel, looping through the records, in progress

//<loop logic through the sheet here...>>

while (blankRowCount <=2)


{
excelRowCount++;

//This command reads cells Ax..Ex and places the results in


//an open-ended (dynamic) array, afoundValues:
Excel.Range foundRange =
worksheet.get_Range ("A" + excelRowCount.ToString(),
"E" + excelRowCount.ToString() );
System.Array afoundValues = (System.Array)foundRange.Cells.Value2;

Chapter 17 - Excel and Access Page: 300


//Convert the two-dimensional array to a single-dimension:
string[] afoundCells = A120_ConvertToStringArray(afoundValues);

//Check for blank rows:


if (util.IsBlank(afoundCells[0]))
{
blankRowCount++;
continue;
}
else
blankRowCount = 0; //Reset the blankRow counter to keep
//the loop running

//Do the actual work / Diagnostics


//Append each field to textBox1 and print a CRLF between records
foreach (string tempString in afoundCells)
{
textBox1.Text += tempString + " ";
}
textBox1.Text += "\r\n"; //Add a crlf after each record

} //end of while-loop

btnRead, looping; see below for completed program

The foreach loop works the same as others you have seen. The loop "knows" all about
the array, in particular, it knows how many elements are in the array. The loop fetches
each cell and assigns a temporary variable, "tempString". The command
"textBox1.Text += tempString + " "; takes whatever is currently in textBox1 and appends
tempString. Naturally, your program probably has more important requirements than
simply dumping a row to a textbox....

As each cell is appended to textBox1, append a space for readability. When the foreach
loop ends (it finished with the row), insert a carriage-return-line-feed, re-loop around and
increment to the next row-number.

A120_ConvertToStringArray:

The logic thus far assumes a magic function will convert Excel's get_Range from a two-
dimensional result into a single, one-dimensional array. A120 takes the Excel range and
loops through the first row, placing the results in a simpler array while all the time
knowing Excel can return multiple rows – but we know there is only one. (Remember,
pay no attention to the fact that the range selected was only one-dimension; get_Range
does not know this and it treats every range as a grid with width and depth):

Chapter 17 - Excel and Access Page: 301


Each field in a one-dimensional array is described with brackets;
afoundCell[0] is the first field (e.g. "1/1/2008"),
afoundCell[1] is the second field, (e.g. "Sale"), etc.

Two-dimensional arrays use two numeric values to describe a cell. For example, the cell
Array[1,3] refers to the first row, third column (base-one). In the A120 routine you'll see
the first variable is hard-coded to a fixed number 1 while the second (column) varies.
The hard-coded "1" forces the function to only look at the first row in the results.

This section is not meant to be a discussion on arrays. If you


simply copy this routine and drop it into position, it will work,
even if you don't understand the mechanism.

Chapter 17 - Excel and Access Page: 302


A120: Convert Excel's 2-Dimensional Results to a 1-Dimension array, completed
private string[] A120_ConvertToStringArray
(System.Array apassedValues);
{

// This routine accepts a passed 2D array from a get_Range set of


// commands and returns a single-dimensioned string array using
//only the first row of the multi-dimensioned array.
// Note the Excel 'apassedValues' array is 1-based while the
// returned string array is 0-based; there is a shift of -1.

//Declare and instantiate a new array with the same (width)


//as the passed array:
string[] atempArray = new string [apassedValues.Length];

for (int i = 1; i <= apassedValues.Length; i++)


{
if (apassedValues.GetValue(1, i) == null)
atempArray [i-1] = "";
else
atempArray [i-1] = apassedValues.GetValue (1, i).ToString();
}

// once the tempArray is built, return the entire string array


// to the calling routine:
return atempArray;
}

where:

• The Signature line declares a variable 'apassedValues' as type "System.Array". If


you allowed the editor to stub-in the code, the editor may not have included the type-
definition. You may need to correct this by hand.

• The module's "A120" prefix is invented and only serves to help organize the
program.

The Completed Program:

From above, the program is testable now and here is the complete program listing. It
opens an Excel sheet, reads through the data and displays in textBox1. Notice how Excel
is physically opened and closed.

Set textBox1's Font is to Courier New, making it easier to read the field results.

Program 17.1: Reading an Excel Sheet, completed

// Read an Excel file and display the results in textBox1.

Chapter 17 - Excel and Access Page: 303


// Important Pre-requisites:
// In Solution Explorer, Add Reference "Microsoft Excel 12 Objects..."
// Excel Record format: date, Type, Item-desc, qty, amount
// Note: this version does not properly display the date.
// Note: A Try-catch is needed around the Excel Open statements.

1 public partial class Form1 : Form


2 {
3 cl800_Util util;
4
5 public Form1()
6 {
7 InitializeComponent();
8 util = new cl800_Util();
9
10 //Instantiate a new Excel Object and confirm it launches
1 ExcelObj = new Excel.Application();
2 if (ExcelObj == null)
3 {
4 MessageBox.Show("Excel did not launch properly");
5 System.Windows.Forms.Application.Exit();
6 }
7
8 //Decide if you want to see Excel loading or not:
9 ExcelObj.Visible = true;
20 }
1
2 private Excel.Application ExcelObj = null;
3
4 private void btnReadExcel_Click (object sender, EventArgs e)
5 {
6 int excelRowCount = 1;
7 int blankRowCount = 0;
27a //See text, below
8
9 //Open the workbook; could probably use a try-catch here
30 //The file will be opened ReadOnly
1 //Note the 15 required parameters; your version of Excel may vary
2 //on the parms! This is for Office/Excel 2002
3 Excel.Workbook theWorkbook = ExcelObj.Workbooks.Open
("C:\\Data\\Transactions.xls",
0, true, 5,
"", "", true,
Excel.XlPlatform.xlWindows,
"\t", false, false, 0, false, 1, 0);

4 //Get the Sheet Names; then get the first sheet's name:
5 Excel.Sheets sheets = theWorkbook.Worksheets;
6 Excel.Worksheet worksheet = (Excel.Worksheet)sheets.Get_Item(1);
7
8 while (blankRowCount <=2)
9 {
40 excelRowCount++;
1
2 //This command reads cells An..En and places the results in
3 //an open-ended (dynamic) array, afoundValues:
4 Excel.Range foundRange =
worksheet.get_Range ("A" + excelRowCount.ToString(),
5 "E" + excelRowCount.ToString() );
6 System.Array afoundValues =
(System.Array)foundRange.Cells.Value2;
7
8 //Convert the two-dimensional array to a single-dimension:
9 string[] afoundCells = A120_ConvertToStringArray(afoundValues);
50

Chapter 17 - Excel and Access Page: 304


1 //Check for blank rows:
2 if (util.IsBlank(afoundCells[0]))
3 {
4 blankRowCount++;
5 continue;
6 }
7 else
8 blankRowCount = 0; //Reset the blankRow counter to
9 //keep the loop running; then
60 //fall thru...
1
2 //Do the actual work:
3 foreach (string tempString in afoundCells)
4 {
5 textBox1.Text += tempString + " ";
6 }
7 textBox1.Text += "\r\n"; //Add a crlf after each record
8
9 } //end of while-loop
70
1 //Shutdown Excel:
2 ExcelObj.Quit();
3
4 } // end of btnReadExcel
5
6
7 /// A120 accepts a passed 2D array from a get_Range set of
8 /// commands and returns a single-dimensioned string array using only
9 /// the first row of the multi-dimensioned array.
80 /// Note the Excel 'apassedValues' array is 1-based while the
1 /// returned string array is 0-based.
2
3 string[] A120_ConvertToStringArray (System.Array apassedValues);
4 {
5 //Declare and instantiate a new array with the same (width)
6 //as the passed array:
7 string[] atempArray = new string [apassedValues.Length];
8
9 for (int i = 1; i <= apassedValues.Length; i++)
90 {
1 if (apassedValues.GetValue(1, i) == null)
2 atempArray [i-1] = "";
3 else
4 atempArray [i-1] = apassedValues.GetValue (1, i).ToString();
5 }
6
7 // once the tempArray is built, return the entire string array
8 // to the calling routine:
9 return atempArray;
100}

Chapter 17 - Excel and Access Page: 305


End: Program 17.1

where:

Line 11 ExcelObj = new Excel.Application(); Instantiates the Excel object.


This step has a prerequisite, where the Excel COM object is added as a
reference.

Line 19 Excel Visible: if true, Excel loads and displays to the end-user. Line 72
closes Excel.

Line 22 Declares the Excel object; this is similar to initializing a variable with a
value.

Line 33 Opens the Excel sheet, this time using a hard-coded filename. The 15
different parameters are required but many are set to default values, such as
zero, empty-string, true or false.

Lines 35 and 36
Excel.Sheets sheets = theWorkbook.Worksheets;
Excel.Worksheet worksheet =
(Excel.Worksheet)sheets.Get_Item(1);

go hand-in-hand with the Open statement. These two lines select the first
Excel spreadsheet tab (first tab in the Workbook).

Line 38 while (blankRowCount <=2) Begins the loop. When 2 or more blank lines
(actually, column A) are found in the spreadsheet, then assume the loop has
ended. If you knew how many records to expect, you could replace the
while-loop with a for-next loop.

Line 40 excelRowCount++; Increments how many lines were processed. Because


the variable was initialized with 1, the first real iteration is at row-2; this
skips the spreadsheet's header line.

Lines 44-49
...worksheet.get_Range...
These statements read the range, from Column A through E and the final
results are deposited into a one-dimensional array. These statements always
work as a pair.

Line 52 if (util.IsBlank(afoundCells[0]))
Examines the first item in the returned array to see if it is blank; if so,
increment the blank-row-counter and re-loop. The top of the loop decides if

Chapter 17 - Excel and Access Page: 306


too many of them were found and it ends the program. If a non-blank (row)
is found, re-set the counter and start counting again. This allows for an
occasional blank line in the middle of the data.

Line 63 Begins a new loop that processes each item in the returned array. This is
where the data actually gets loaded into textBox1. See below for additional
calculations. The Date field will not be formatted correctly; see below.

Line 67 After a row is processed, slip in carriage return/line feed. Without this, all
records will string together into one long, continuous line.

Line 72 Closes Excel. Even if Excel were "invisible" (line 19), the sheet still needs
to be closed. If not closed, running a second copy of the program would
cause problems.

Limiting the Records Processed:

At each record, the program has complete access to all fields. You could, for example,
only process those records with "SALE" in the second column. Because the data is
stored in an array, this logic is easy. At line 62a, add these two statements:

62a if (afoundCells[1].ToUpper() == "SALE")


62b {
foreach (string tempString in afoundCells)...

Close the if-statement's brace at line 67a (after the \r\n).

Notice how the new if-statement isn't placed inside of the foreach loop. If you did, it
would check field-1 each time the other fields were processed (5 columns, the check
would run five times). Although the logic would work, it would be inefficient.

Speeding up textBox1:

You may notice the program runs slowly as the number of records increase. Building
textBox1 by appending is inefficient. As described in Chapter 2, it is faster to store the
(former) textBox1 appendages in a separate string and then, at the last minute, move
them to textBox1.

At line 27a, declare a new holding variable:


27a string textBox1Holding;

Then, replace lines 65 and 67 with revised commands:

63 foreach (string tempString in afoundCells)


64 {
65 textBox1Holding += tempString + " ";
66 }
67 textBox1Holding += "\r\n"; //Add a crlf after each record

Chapter 17 - Excel and Access Page: 307


Finally, outside of the while-loop, just before line 72, move the textBox1Holding value
to its final resting place.

71a textBox1.Text = textBox1Holding;

If you try to run the program now, you will get a compiler error:
Use of unassigned local variable 'textBox1Holding' at line 71a.

This is the typical nemesis found in most while-loops. The compiler fears the while-loop
might not run and when it sees "textBox1Holding" on the far-side of an equal-sign, it
complains. This is fixed by initializing textBox1Holding with an inconsequential value.
(I purposely lead you astray with this example):

27a string textBox1Holding = ""; //empty-string

Other Enhancements:

The date-fields from Excel ("1/1/2008") showed up in the textBox as "39448". Excel
represents dates as the number of elapsed days since 1/1/19003.

Convert the serial-number to a human-readable date with this code:

At line 27b, declare a new date-time variable (much like you would declare a string or
integer):

3
Interesting side-note with dates. The original Lotus 123 spreadsheet, now long-dead, incorrectly
calculated the leap-year in 1900 (2/28/1900) and Microsoft decided to keep that same error in Excel to
make date-conversions for .wks files the same. Times are represented as a decimal, number of seconds
past midnight. Saturday, March 10, 2007 9:43pm = 39151.9055.

Chapter 17 - Excel and Access Page: 308


Example: Converting SerialNumber Date to Date-Time
27b DateTime dt;

// Then, inside of the detail loop, line 64a, you could add
// logic similar to this:

dt = DateTime.FromOADate(Convert.ToDouble(afoundCells[0]));
MessageBox.Show(dt.ToString() );

where:

• "dt" is the variable name declared at the top of the routine; any name could have
been used.

• DateTime.FromOADate is an "OfficeAutomation" method.

• Excel expects all date-times to be double (real) number; it can't be an integer or a


string. 1/1/2008 is really equal to 39448.0000 (midnight, if a time was not
indicated).

• There should be logic to make sure a valid-date-number was detected in the array,
afoundCells[0].

Displaying the date properly in textBox1 is left as an end-of-chapter exercise. This


completes the Excel section.

Chapter 17 - Excel and Access Page: 309


comboBoxes based on Access Database Tables

Volatile data, such as registered guests at a hotel, current inventory and other such
information, should not be stored within the program; instead, the information must
come from an outside data-source. Typical data-sources are Access Databases or SQL
tables. This section discusses how to link a comboBox to one of these data-sources.

You will need a copy of Microsoft Access to complete this example. If you don't have
this program, you can build the example on another computer and then copy the ".mdb"
file to your computer for testing. If you have a more sophisticated database program,
such as MSSQL, you are welcome to use it. Contact your Database Administrator for
help in identifying a suitable table and for the "ODBC" connection information. Later
chapters have more details on how to retrieve information from Microsoft SQL Server
databases.

This book does not intend to teach Microsoft Access, however, the steps to build a test
database from scratch are described next. This is a rudimentary excursion into this
product and the "city" table you are about to create is simplistic, but the results will be
linked into your C# program and everything will be more-or-less automatic. If you have
an existing Access table you would like to use, you are welcome to try it.

Building a Test City Table in Microsoft Access:

A. Launch Microsoft Access.


Select "File, New, Blank Database"
(you may see a "new Blank Database" choice on the far-right window).

When prompted for a database name, change the name from "db1.mdb" to
"C:\Data\City.mdb" (This assumes you have a C:\Data folder already built).
Click "Create"

B. From the main screen, confirm "Create" is activated


Double-click "Table Design"
This opens up a "Table1" table, which looks much like a spreadsheet.

Chapter 17 - Excel and Access Page: 310


Creating Table1, adding four fields

C. In the first field, type "cityNDX"; press Tab and select "AutoNumber" from the pull-
down list. ("City Index," an invented name.) Note: Illustration shows final, saved
results; the table starts out as Table1.

D. For the remaining fields, "cityName", "cityState", "cityZipCode", select "Text" for the
Data-type field, as illustrated.

E. Assign a primary key:


"Other-mouse-click" (Right-mouse) the grey-square next to "cityNDX"
From the menu, choose Primary Key.

Chapter 17 - Excel and Access Page: 311


F. Although not required for this example, it is wise to index the field the query sorts by.
Recall the query sorts Ascending by City Name. On the top ribbon, Design tab, click
"Indexes". On a blank row, invent an index name, such as "cityName". Choose the
CityName field. Close the box when done.

G. Save the table with these steps:


Close the window by clicking the "X" on the right side.
When prompted for a table name, type "tblCity".

Chapter 17 - Excel and Access Page: 312


Build a sorted Query by following these steps:

1. From the Access top-menu, choose Create, then click the Query Design.

2. Query Design opens with a "Show Table" dialogue. In "Show Tables", double-click
"tblCity", then "Close." You are now in Query Design mode.

3. From the tblCity, click and drag the field name "cityNameNDX" to the first column
in the Design-grid.

Drag cityName to the second column.


Change the Sort to "Ascending".

Chapter 17 - Excel and Access Page: 313


Assembling the query by dragging fields

4. Drag the remaining fields "cityState" and "cityZipCode" to the next respective
columns, as illustrated. Do not set a sort-order on these fields; only set on the
CityName.

5. Create a new calculated field that assembles the city-state-zip into one field for
viewing. This combined field will be displayed in the pull-down list:

In the fifth column (to the right of cityZipCode), type this formula in the top-most
"Field" field.

cityStateZip:[cityName] & ", " & [cityState] & " " & [cityZipCode]

where:
• "cityStateZip:" names the new field with an invented name. This becomes the
name of the column. Note the colon.

• The 'formula' part of the field consists of [field names] in square brackets and
ampersands for string concatenation (Access uses an &ampersand, not pluses, to
concatenate.

Chapter 17 - Excel and Access Page: 314


6. Close the Query1 Design View by clicking the "X" on the far-right of the tabbed-bar.
When prompted for save-name, type "qryCityStateZip".

7. From the main control screen, on the left-side, double-click "tblCity", which opens
the table in a "spreadsheet view" (If opened in Design view, click the View Ribbon
button and change to "Datasheet".)

• Enter various city data (see example, below), in any order. Type the new city
name on the row labeled "(New)".
• Ignore the cityNameNDS autonumber column; this is an automatic, numeric
number.
• Invent fake Zipcodes if you don't know them.

Adding City Names

Chapter 17 - Excel and Access Page: 315


8. Close the data-entry screen (see "X" on far-right), then close Access.
The data is automatically saved.

This effectively simulates a data-table. The "City.mdb" file is a self-contained database


that contains a query "qryCityStateZip" and the database could contain other tables and
queries.

Returning to Design View:

Later, if you need to return to the Query's design views, do the following:
On the Left-Nag, click Queries (the pull-down arrow), double-click your query; then
switch to Design View.

Returning to Design View if you get lost

Chapter 17 - Excel and Access Page: 316


Attaching Microsoft Access Data to a comboBox

In this section, the table and query built above will be attached to a standard comboBox.
When the program loads, the box will retrieve all "City, State, PostalCode" values from
an external Microsoft Access table and allow the user to select a record.

The constructed Access database is called "city.mdb" and it has both an interior table,
"tblCity," and a sorted query, "qryCityStateZip." This simulates what would be found in
the real world.

Building the ODBC Call:

Before a C# program can take advantage of the data, make an "ODBC" call to the
database. This involves numerous steps with a fair amount of mouse-clicks, but the steps
are easy and can be approached in a cook-book manner.

1. From the Visual Studio (frmProcess Design view),


Select "Tools" from the top menu.
Choose "Connect to a Database"
Double-Click the "Microsoft Access Database File"

2. In the "Database file name" field, type or browse to "C:\Data\City.mdb", or as saved


from the previous section. It would be unprofessional to store the Access .mdb in the
\bin directory.

• Leave the Username (Admin) and password field blank


• Click "Test Connection"
• Click OK

Chapter 17 - Excel and Access Page: 317


Connecting to the comboBox:

3. From the toolbox, drop a new comboBox on the form.


Select the newly-dropped comboBox. Notice the little arrow on the top-right.

4. Click the Data Source pull-down (now set at "none")


Click the bottom link "Add Project Data Source"

Chapter 17 - Excel and Access Page: 318


5. A Data-connection wizard launches.
Choose Data Source Type "Database", then "DataSet"

At the Choose Data Connection prompt, it should default "Which data connection should
your application use to connect to the database?", accept the default pull-down value
"city.mdb". Click Next.

6. When prompted "The Connection you selected uses a local data file that is not in the
current project. Would you like to copy the file....". Choose No (Do not copy; you
should point to the actual database and not allow it to make a copy. The issue is this: If
copied, and your staff leaves the program active all day, it will not retrieve current values
until the application is restarted, and it is pointing to a copy).

7. When prompted to Save the Connection String to an Application Configuration File,


accept the default "[x] Yes, Save as "cityConnectionString"

Chapter 17 - Excel and Access Page: 319


8. When prompted to Choose Your Database Objects, select qryCityStateZip:

9. When Finish is clicked, control returns to the comboBox in Form Design. Make these
changes:

• Set Display Member to "CityStateZip" (this was the calculated field that shows a
combined name (e.g. Boise, ID 83700). This is the field to be displayed in the
comboBox pull-down.

• Set Value Member to "cityNDX" - store the index number (the auto-number) of the
selected City instead of storing the actual text. If the original Access database has a
change in spelling, typically to correct a mis-typed record, the combo-box will see
the change).

Chapter 17 - Excel and Access Page: 320


Typically, you should always store the underlying Auto-number (record number, index
number) when selecting records from a database; never store the (text) value of the
selected field.

The Big Picture:

Access gave each record an index-autonumber (cityNameNDX). In the example above,


Portland has a "5". But the user's pull-down list shows Portland as the third item in the
list (alphabetically – sorted by the Access Query). Even though the user sees "Portland,

Chapter 17 - Excel and Access Page: 321


Oregon (OR)", C# stores the index number 5 when the record is selected - storing it in
the Value Member field.

Contrast this with the simple comboBox built in an earlier


chapter; it used "SelectedItem" – which was the counter into the
comboBox. If you tried that now, a blank string would return;
When connected to the database, you must choose from the
"Value Member" field.

Later chapters discuss more on database design and programming, but in summary, it is
good database programming to reference a record's autoNumber instead of the actual
name. In other words, do not store the city name, instead store a pointer. With the
index, you can cross-reference that number to find any other field from the original table.

Why the run-around? Imagine your program stored the actual city name, "Portland"
from the pull-down list. Now imagine the data-entry clerk who built the original
Portland record misspelled name as "Portlan". If the actual city name were stored, there
would be hundreds of records with the mis-spelled name. Later, the clerk realized and
corrects the mistake – all future records will be spelled correctly, but the old records will
still be in error. If the program pointed to the record's number (5) rather than the text, all
records, both old and new, would see the correction immediately.

Finally, notice how the comboBox was attached to the query and not the original table.
The database has already sorted the records and there is no need to have C# sort
(comboBox1.Sorted = true).

Test the comboBox:

In button1's Click event, add the following logic, then run the program; Select a city from
the combo and click button1. Expect an error, depending on what operating system and
version of Office is installed.

Testing the Combobox


private void button1_Click (object sender, EventArgs e)
{
//Show which number was selected
MessageBox.Show(Convert.ToString(comboBox1.SelectedValue));

//Show which name was selected


MessageBox.Show("This is the cosmetic value: " + comboBox1.Text);
}

Results: "n" (the Access Auto-number of the city selected). This is not the numeric index
value of the combo-box; instead, this is the number stored by Access. You could also use
comboBox1.Text to display the selected text.

Chapter 17 - Excel and Access Page: 322


See below if you get an error "Could not find file...city.mdb".
See below if you get an error "An unhandled exception of type
'System.InvalidOperationException' occurred in System.Data.dll; the
'Microsoft.Jet.OLEDB.4.0' provider is not registered on the local machine"

Both of these errors are embarrassingly common.

Possible Error - Microsoft.Jet.OLEDB.4.0 Provider Not Registered:

"An unhandled exception of type 'System.InvalidOperationException' occurred in


System.Data.dll; the 'Microsoft.Jet.OLEDB.4.0' provider is not registered on the local
machine'

The issue is a 64-bit operating system, such as Windows 7 or Windows 8 is attempting to


launch an OLE-db connection with a 32-bit driver - especially with Office 2010 and
older. It was not until mid 2013 that Microsoft released a 64-bit version.

There are two solutions to the problem and both are acceptable.

Solution 1: Change your compiled program from 64-bit to 32-bit.

a. Click the top ribbon bar's red-box to stop the running program and return to the
editor.

b. From Visual Studio, while editing your program, choose top-menu Project, <your
project name> Properties.

c. On the left-nav, choose Build. Change "Platform Target" from "Any CPU" to "x86".
Re-run your program.

Solution 2: Download the Jet Engine OLE DB x64-bit driver, directly from Microsoft.
As of 2014.10, use this link or search for "Access Database Engine x64 redistributable
Microsoft": http://www.microsoft.com/en-us/download/details.aspx?id=13255

Possible Error - Property Value is not Valid:

Solution: Your program is still running, although it may not be visible on the Windows
task bar. From the editor's ribbon, click the red-box to stop the program.

Chapter 17 - Excel and Access Page: 323


Possible Error - Could not find city.mdb:

OLEDbException was unhandled - "Could not find file 'C:\<your program


path>\Debug\city.mdb' – even though you had clearly placed the access.mdb in
"C:\Data\city.mdb". The error flags in the Form_Load event, on this auto-generated
statement:

this.qryCityStateZipTableAdapter.Fill(this.cityDataSet.qryCityStateZip);

Visual Studio's data-connection wizard appears to be problematic. To correct the error,


change the wizard's connection string:

a) Important: Click the top-toolbar's red-square to stop the running program.

b) In Form Designer, near the bottom of the screen, other-mouse-click the "cityDataSet"
place-holding object, choose Edit in DataSet Designer.
Resolving a lost City

c) In the designer, click once to highlight the query (you must click even though it
looks highlighted; see illustration below), notice this is below the field list.

Look to the Properties window, on the far right of the screen (Properties, not events).

Expand (+) the "Connection" field


Edit the ConnectionString

Chapter 17 - Excel and Access Page: 324


Changing the .mdb file's Default Path

d) Change the ConnectionString from this:


Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\city.mdb

... replacing the bar-DataDirectory-bar with the actual path to the mdb. You may
find it easier to copy the field into Notepad for editing. For example:

Provider=Microsoft.Jet.OLEDB.4.0;Data Source="C:\Data\city.mdb"

Naturally, this is a sloppy fix. In the Form1_Load event, you could code this
statement:

qryCityStateZipTableAdapter.Connection.ConnectionString =
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source =" +
"\"C:\\Data\\city.mdb"\";

Refreshing the List:

When the comboBox was bound to the dataset, Visual Studio automatically generated a
line of code in the Form_Load event. Not surprisingly, this line reaches into the
database, fetches the records, and populates the comboBox:

Chapter 17 - Excel and Access Page: 325


Auto-generated code populates the comboBox. Move if needed.
private void Form1_Load (object sender, EventArgs e)
{
//ToDo: This line of code loads data into the qryCityStateZip
//table; moveable

this.qryCityStateZipTableAdapter.Fill
(this.cityDataSet.qryCityStateZip);
}

With this, the comboBox is only populated with data when the Form loads. What would
happen if another user added a new City entry after you loaded the form? Try this now
by running your program, then, while the program is still loaded, launch Microsoft
Access and add a new City to the table. Note that your comboBox does not refresh. If
the C# program is closed and re-launched, the list is refreshed.

A comboBox hosting volatile data requires frequent repainting to keep the data fresh.
Move the Form_Load code to another event, such as a btnRefresh module, or more
commonly to the "comboBox1_Enter" event:

Refresh the Data; Repaint the comboBox


private void btnRefresh_Click (object sender, EventArgs e)
{
//Copy the FormLoad line to this position:

this.qryCityStateZipTableAdapter.Fill
(this.cityDataSet.qryCityStateZip);
comboBox1.Refresh();
}

Performance Considerations:

• If the Access database is on a network share, the comboBox may take several
seconds to populate. Since this happens, by default, at the Form_Load event, this
causes a noticeable delay when opening the form.

• Be sure that the original Access tables are indexed properly so the query runs
efficiently. Index by City Name, as illustrated earlier.

• If the comboBox is infrequently used or if the data is volatile, populate the field on
the "comboBox1_Enter" event.

If the data is fairly stagnant and the comboBox is used frequently, load it one time
when the project loads (Form_Load).

Chapter 17 - Excel and Access Page: 326


• The Access Query already has the data sorted properly. Setting the C#
comboBox.Sorted property to True is redundant.

• It is recommended setting these two comboBox1 properties, as discussed in


Chapter 10, Forms. This allows users to type-ahead and select:

AutoCompleteSource = ListItems
AutoCompleteMode = SuggestAppend

Deleting the Data Sources:

Deleting an unwanted datasource is troublesome. Three place-keeping icons were placed


near the bottom of the form-design (cityDataSet, qryCityNameBindingSource, etc.)
when the data-source was added to the program. Also, a new folder was built in Solution
Explorer.

If you decide to delete the comboBox, you should also delete the data-source code
attached to it. The chances of dorking4 this are high and the delete needs to happen in a
specific order. If this were a real project, make sure of a backup before starting these
steps.

4
"Dorkage": A technical term. In this case, the entire project can be corrupted if the delete does not
happen in the proper order.

Chapter 17 - Excel and Access Page: 327


• Delete the bottom database-goodies, found below the form:
"cityDataSet"
"qryCityNameBindingSource"
"qryCityNameTableAdapter"

• Delete "CityDataSet.xsd" from Solution Explorer


Delete app.config from Solution Explorer

• In Server Explorer's flyout-window, Data-Connections, delete cityDataSet.


Click the refresh button near the top of Server Explorer.

• Delete the comboBox1 from the Form's design view. This does not delete the logic
in the form; it only deletes the object.

• In Code view, delete all logic referencing "comboBox1", including logic within
button1.

• In Code view, delete the "//TODO:" line and the


"this.qryCitNameTableAdapter.Fill" line that was automatically generated in the
frmProcess_Load event.

Test the newly-cleaned code by pressing F5.

Chapter 17 - Excel and Access Page: 328


Excel Exercises

A. Modify Program 17.1 (Read an Excel Sheet and display found-records in textBox1) and
make the following changes:

• Do not display the date as a Serial-number; instead:


Properly display the Excel Date (e.g. 1/1/2008)
Do not display time information

• Do not display the Qty or the unit-price


• Display the Extension calculation for (Qty * Price)
• Only process "Sale" (column B) data; discard other records.

From the sample data, the first record should look like this:
1/1/2008 SALE PANTS 11.95

Formatting dollar-figure techniques are described in a future chapter.

Hints:
• Convert the array-position to an upper-cased value before comparing to "SALE"
• The Item-Type column is the second column, addressed as afoundCells[1].

B. Continue to modify Exercise A.


Display the grand-total items sold and the grand-total extensions.

D. Modify Exercise A with an audit that makes sure a valid number (date) was passed from
column A.

E. In the Excel sheet, introduce non-valid data in column A.

2/31/2008
(blank)
(space-character)
"Bob"

F. Misspell the Excel Worksheet name and run the program. Add a try-catch to intercept
the error.

Chapter 17 - Excel and Access Page: 329


Chapter 18 - Shells - Launching Other Processes Page: 330
A Beginners Guide to C-Sharp - Volume 2
Visual Studio C# 2017
ASCII through Advanced Formatting
by Tim R. Wolf
© 2017.06.01 1.02
Table of Contents

9 Chapter 12 - ASCII Files 17


Reading ASCII (Text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
StreamReader
Priming Read
Ending the Loop and Closing Files
Using an EndOfStream Read
The "Using" Clause
ASCII File Reads with try-catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Issues with try/catch and Close
Chaining catch-statements
Completed ASCII ReadFile - Program 12.5. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Writing ASCII (text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Appending ASCII (text) Files - Advanced Open Methods. . . . . . . . . . . . . . . . . . . . 54
Preventing Append from Running Twice

9 Chapter 13 - Parsing Tab and CSV Files 65


Automatic Parsing by Delimiter (Split).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Declaring the Destination Array
foreach
Alternate Loops
.Split Limitations
CSV Files
Split Example: Phone-Numbers
Manually Parsing Comma-Delimited Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
First Field Logic
for-next Logic
Substring
Last-field Logic
Tab-Delimited Files
Parsing a Variable Number of Columns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Parsing ASCII CSV Files with Embedded Commas.. . . . . . . . . . . . . . . . . . . . . . . 105
Error Processing in CSV Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Moving ParseCSVLine to the Utility Library

9 Chapter 14 - INI Files 147


INI File Structure and Design. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
A015: Loop Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A017: Parse INI Detail Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A028: Finding the Application's Default INI Location.. . . . . . . . . . . . . . . . . . . . . 178
Command-Line Override.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Simulate a Command Line Options
Embedded Spaces in the Parameters
Command-line ini=
A029: Write Default INI when Missing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Basic INI File Read: Complete Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Using a Program Class - CL860 INI File Read. . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
CL860 Basic INI Read Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Using CL860_BasicINIRead

9 Chapter 15 - xml and App.config Files 223


xml File Structure
app.config xml.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Using (Reading) app.config
Building a Manual xml File.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Reading xml File Sequentially. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

9 Chapter 16 - Windows Registry 249


Organization of the Registry
Reading a Specific Registry Key.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Opening the Registry
Closing
Reading a Specific Registry Value
Reading Mulitple Keys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Reading All Values within a SubKey. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Multi-Line String Registry Values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Creating/Modifying Name-Value Pairs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Opening the Registry in 'Writeable' Mode
Creating Sub-SubKeys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Deleting Values and Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Deleting Entire SubTrees
Deleting the Current Key
Enumerating SubKey (folders). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

9 Chapter 17 - Reading Excel and Access 289


Reading an Excel File using a COM object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Open the Workbook
parameter list
Reading a Row
comboBoxes based on Access Database Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Attaching Microsoft Access Data to a comboBox. . . . . . . . . . . . . . . . . . . . . . . . . 317
ODBC Call
Connecting to the comboBox
Refreshing the List
Performance Considerations

9 Chapter 18 - External Programs (Shell) 333


Starting a New Process. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Launching Notepad with a Specific Filename
Building an Exit Event
Using Code to Start a New Process.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
proc.WaitForExit.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
DOS Output.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Capturing DOS output into an Array
Multiple Instances of an Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Simple Instance Test
Mutex: A Better Solution for Multiple Instances. . . . . . . . . . . . . . . . . . . . . . . . . . 355

9 Chapter 19 - Waits, Delays and Pauses 361


Poor Wait States – Not Recommended
Empty loops
current time and loop until xx seconds
Sleep events
System.Threading.Thread.Sleep (milliseconds). . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Completed wait Simulation - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Wait as a Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Using cs805_Wait
Calling Wait
Timer Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Basic Timer Code
Event Horizons
Countdown and Timer Example
Disabling the Close "X" Mid-Transaction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Intercepting a Close Event
Countdown and Timer Program - Completed. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Simple Startup Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Splash Screens.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

9 Chapter 20 - Printing 415


Printing Simple Text. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Print Layout / Print Rendering
Pixel Positions in Millimeters (x,y)
OverShooting
Printing Horizontal Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Printing Rectangles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Filling
Printing Graphics.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Printer Dialogs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Adding a Custom Printer Dialog
Print Preview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Printer Setup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Printing Text Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Font Height Calculations - Lines per Page
Page-Break Logic
GraphicsUnit.Millimeters
Setting New Margins, Landscape. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
Current Margin
Setting New (Printable) Margins
Begin and End Print Events.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

9 Chapter 21 - Formatting 469


Font Color, Font Bold. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Font Bold
Font Style Ariel
Basic String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
String.Format with Alignment.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Proportional Fonts
Numeric Formatting with String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Standard Decimals
Formatting with Alignment
Commas and Currency
Numeric Picture Clauses. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Padding Zero with Leading and Trailing Decimals
Place Holder (#)
Thousands Separator ( , )
Variable Picture Clauses (Group Separators)
Date and Time Pictures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Converting from Strings
Converting Dates
Other Date Time Properties and Methods.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
DateTime Formatting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Pictures
Display Dates, Year first
Format Class Library - cl710_Formatting.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Phone Number Formatting
Proper Names
Proper Addresses
PhoneNumberFormat Method - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
PhoneNumberFormat Module - Coding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Extension Extract
Strip Normal Punctuation
Strip "1-" Prefixes
Punctuate
Punctuating the AreaCode
"ProperNames" Formatting - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
ProperNamesFormat - Coding.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
A Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
Chapter 18 - External Shell Programs
Chapter 18 - External Programs (Shell)

C# can launch other programs with just a few lines of code. The program could launch
Notepad to edit a configuration file, launch Excel, or another C# program. When the
program is launched, it can start as a separate thread (meaning it runs independently of
the calling program) or it can launch, suspending the calling program until that task ends.
This chapter explorers these ideas.

Topics:

• Launching an external program (shell), separately from your program


• Waiting for and external program to close (WaitForExit)
• Passing filenames and other parameters
• Running Minimized, Maximized, etc.
• Keep from launching an external program twice (mutex)
• Using Process1 objects vs Code
• Running DOS programs and parsing the output

• Mutex - Stopping multiple instances of your application

Summary: Launching Notepad as a Separate, Independent Thread (Simple Call)


private void btnOpenNotepad_Click (object sener, EventArgs e)
{
System.Diagnostics.Process proc = new System.Diagnostics.Process();
proc.EnableRaisingEvents = false;
proc.StartInfo.FileName = "notepad.exe";

//Setup a text file to open, passing as an argument. Because


//no path was indicated, pull from the current directory:
proc.StartInfo.Arguments = "testfile.txt";
proc.Start();
}

Chapter 18 - Shells - Launching Other Processes Page: 333


Summary: Launching Notepad with additional parameters
private void button1_Click (object sender, EventArgs e)
{
//Launch Notepad testfile.txt maximized
//Your program continues to run; user can launch this process
//multiple times. Consider button1.enabled = false;

button1.Enabled = false; //Use proc.Exited event to re-enable

System.Diagnostics.Process procNotepad;

procNotepad = new System.Diagnostics.Process();


procNotepad.EnableRaisingEvents = true;
procNotepad.Exited += new EventHandler(ProcNotepad_Exited);

procNotepad.StartInfo.WindowStyle =
System.Diagnostics.ProcessWindowStyle.Maximized;

procNotepad.StartInfo.FileName = "notepad.exe";
procNotepad.StartInfo.Arguments = "test.txt"; //current directory

procNotepad.Start();
}

private void ProcNotepad_Exited(object sender, EventArgs e)


{
//Re-enable button1 once Notepad closes

//This event must be defined previously using the


//ProcNotepad.EnableRaisingEvents = true;

button1.Enabled = true;
MessageBox.Show("Notepad was closed");
}

Summary: Shortcut Event to open an external web page


private void button1_Click (object sender, EventArgs e)
{
System.Diagnostics.Process.Start
("http://keylinercsharp.blogspot.com/2015/10/introduction.html");
}

Chapter 18 - Shells - Launching Other Processes Page: 334


Summary: Launching Notepad as a Dependent Thread: WaitFor (Simple Call)
private void btnNotepad_Click (object sender, EventArgs e)
{
//Launching as a dependent thread - WaitForExit

System.Diagnostics.Process procNotepad =
new System.Diagnostics.Process();

procNotepad.EnableRaisingEvents = false;

procNotepad.StartInfo.WindowStyle =
System.Diagnostics.ProcessWindowStyle.Maximized;

procNotepad.StartInfo.FileName = "notepad.exe";
procNotepad.StartInfo.Arguments = "";
procNotepad.Start();

procNotepad.WaitForExit(); //Order dependent after Start

MessageBox.Show("Returned from Notepad");


}

For DOS Programs, with console-output, see the IPConfig example later in this chapter.

Chapter 18 - Shells - Launching Other Processes Page: 335


Using Mutex to Detect Multiple Application Launches
//This is in the Program.cs Module!

using System.Threading;
using System.Reflection;
using System.IO;

[STAThread]
static void Main () //in the static void MAIN() section
{
//Use a Mutex to prevent multiple instances of an application
Mutex myMutex;
bool boolmutexCreated;
string strEXEName;
string strLocation = Assembly.GetExecutingAssembly().Location;

FileSystemInfo fileInfo = new FileInfo(strLocation);


strEXEName = fileInfo.Name;

myMutex = new Mutex (true, strEXEName, out boolmutexCreated);

if (boolmutexCreated == true)
{
MessageBox.Show (strEXEName + " is already running");
Application.ExitThread();
//(note: This cannot be tested from within the compiler)
}
else
{
//This section remains the same from the original MAIN
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}

Chapter 18 - Shells - Launching Other Processes Page: 336


Starting a New Process

There are two ways for C# to launch another program. The first uses the design-view
editor and a menu to build the "process" object. A second way to build the process is to
write the calling code directly. Both methods are demonstrated, but I prefer using code-
view because the logic is exposed in code and more easily found when changes are
needed.

Setting up the Example Programs:

The example programs will launch a copy of Notepad and will optionally open an
existing document. Construct a new sample program with a button1 and button2:

button1: .Text = "Launch Notepad"


button2: .Text = "Launch Notepad and Wait"

To aid in the example, create a dummy Notepad document for testing.


From the Windows Start Menu: Start, Run, "Notepad.exe"
In the document type any small amount of text.
Save the document as "C:\Data\Test.txt". Close Notepad.

Using a Process Object:

Starting in form design view, drag a "Process" object anywhere on the form. One
Process object is needed for each task the program needs to launch.

Chapter 18 - Shells - Launching Other Processes Page: 337


1. From the new project's Design View, select the ToolBox flyout menu
Scroll to the +Components section and double-click "Process."
This adds a "process1" icon at the bottom of the Form-design screen which acts as a
place-holder and does not appear directly on the form.

2. Highlight the "process1" place-keeper. In the Properties window, rename the object
from process1 to "launchNotepad"

Giving the process an understandable name is recommended – especially if the program


has multiple processes to launch. Each process requires its own icon.

3. In the launchNotepad (a.k.a. Process1) Properties window, scroll down to the +StartInfo
section. Click the +plus to open the details.

In the FileName field, Type "Notepad.exe" (no quotes)

Notepad.exe is on the DOS Path and the executable will be found by the operating
system. Alternately, specify a fully-qualified path.

4. Double-click button1 and add this code to the button-event:

Launch the (process1) object, as configured


private void button1_Click (object sender, EventArgs e)
{
//Assuming a process1 object (launchNotepad) was built on the form;
//Launch the program using the pre-set properties:

launchNotepad.Start();
}

Chapter 18 - Shells - Launching Other Processes Page: 338


where:

• 'launchNotepad' is the process1 object built in step 2.

Testing the process-object:

The program is testable now. Press F5 to run, then click button1.


Results: Windows Notepad opens to a blank document.
Close Notepad
Close the running program

Launching Notepad with a Specific Filename:

When launching a program, such as Notepad or Excel, you almost always have a
particular document in mind. Have the process launch Notepad and open the previously-
built test file, "C:\Data\Test.txt".

Steps:

1. In design view, highlight the"launchNotepad/process1" object. This is the place-keeper


icon at the bottom of form view, not button1.

In the properties, StartInfo, add these parameters:

Arguments: C:\data\test.txt (a test document built earlier)


Filename: Notepad.exe (already set)
Windows Style: Maximized

Test the Specific Filename:

Press F5 and run the modified program; click Button1. Results: Notepad opens full
screen to the pre-typed document.

Leave Notepad running and return to your running test program.


Re-launch Notepad a second time by clicking button1.

Results: A second copy of Notepad launches (two are running). The next section has
details to prevent this.

Close both copies of Notepad.


Close your running program and return to the editor.

Exit Events and Double-Launches:

In the example above, when Notepad closes, an event can trigger, notifying your program
the deed was done. For this example, have your program announce "Notepad was

Chapter 18 - Shells - Launching Other Processes Page: 339


closed" or in a more real-world example, the code could refresh a file-list, enable another
function or other such tasks. These events can also keep users from spawning multiple
copies of other applications.

Building an Exit Event:

Return to design view and once again highlight the "launchNotepad" process object.
Then, make these three changes:

a. In launchNotepad's properties, set "EnableRaisingEvents" to True


b. Click the Events Property button (the lightning bolt)
Double-click the blank "Exited" event field

c. In the launchNotepad_Exited event, add this code:

Process.Exited Event, completed


private void launchNotepad_Exited (object sender, EventArgs e)
{
//Fire this event when the called program (notepad) closes.
//This event will not fire without the "EnableRaisingEvents"
//property

MessageBox.Show ("Notepad has closed");


}

Chapter 18 - Shells - Launching Other Processes Page: 340


Testing the Exit Event:

Press F5 to start the C# program and click Button1. Notepad opens. Close Notepad.
Results: When notepad closes, the event is detected and a prompt, "Notepad has closed"
displays.

Continue testing:

• Click Button1 again to re-open Notepad


• Minimize Notepad, leaving it opened and return to your program
• Note you can still launch multiple copies of Notepad

Comments on the Multi-Launched Notepads:

Notepad's elevator does not go to the top floor and it does not warn that a second copy of
the same text-document was launched. Other programs, such as Microsoft Excel, place
locks on the file and warn you of this type of blunder. It would be better to prevent the
problem. Here is one solution that uses the events to disable and re-enable the button.

Modify button1_Click with this logic:

Launch the (process1) and Protect the button, completed


private void button1_Click (object sender, EventArgs e)
{
//Assuming a process1 object (launchNotepad) was built on the form;
//Launch the program using the pre-set properties:

button1.Enabled = false;
launchNotepad.Start();
}

Then, modify the (process1) launchNotepad_Exited Event with the inverse:

Process.Exited Event, Enables button1, completed


private void launchNotepad_Exited (object sender, EventArgs e)
{
//Fire this event when the called program (notepad) closes.
//This event will not fire without the "EnableRaisingEvents"
//property

MessageBox.Show ("Notepad has closed");


button1.Enabled = true;
}

Chapter 18 - Shells - Launching Other Processes Page: 341


Testing the Events, confirming button1 disables:

To test, start the C# program and click button1. Minimize Notepad and return to your
program. button1 is disabled. Close Notepad and button1 is re-enabled. If the Notepad
task remains open, say the user goes to lunch or switches to another program, this event
does not fire. Notepad tasks launched from outside your program are not seen by this
event.

See later in this chapter: WaitForExit

Multiple, Separate Processes:

If your code needs to launch a second program (for example a second copy of notepad,
editing a different document), drop a second process icon (process2) into the form and
configure it as required. Then, in a separate event, such as button2_Click, add this code:

process2.Start();

Chapter 18 - Shells - Launching Other Processes Page: 342


Using Code to Start a New Process

Instead of using a Process1 object from the Toolbox, the same can be accomplished in
code. Although this method is more verbose, you may find it more useful because all
settings are exposed in code, making it easier to search and change (property settings are
hidden behind objects and can be difficult to find).

Not surprisingly, the code method is similar to object method. This launches Notepad
and opens file test.txt. Note how the Proc_NotepadExited event is built manually as a
second procedure

The Process1 object, from above, is not needed with this method and should be
deleted before writing the code for this section.

Program 18.1: Launching Notepad via Code, recommended


private void button1_Click (object sender, EventArgs e)
{
//Launch Notepad testfile.txt maximized
//Your program continues to run; user can launch this process
//multiple times. Consider button1.enabled = false;

button1.Enabled = false; //Use proc.Exited event to re-enable

System.Diagnostics.Process procNotepad;

procNotepad = new System.Diagnostics.Process();


procNotepad.EnableRaisingEvents = true;
procNotepad.Exited += new EventHandler(ProcNotepad_Exited);

procNotepad.StartInfo.WindowStyle =
System.Diagnostics.ProcessWindowStyle.Maximized;

procNotepad.StartInfo.FileName = "notepad.exe";
procNotepad.StartInfo.Arguments = "test.txt"; //current directory

procNotepad.Start();
}

private void ProcNotepad_Exited(object sender, EventArgs e)


{
//Re-enable button1 once Notepad closes

//This event must be defined previously using the


//ProcNotepad.EnableRaisingEvents = true;

button1.Enabled = true;
MessageBox.Show("Notepad was closed");
}

Chapter 18 - Shells - Launching Other Processes Page: 343


comments:

• A process object (from the toolbox) is not needed.

• In the example, procNotepad.EnableRaisingEvents is set to "true". The "Exited"


event will be used to disable button1 (disabling itself) and it will be re-enabled when
Notepad closes in the Exited event.

• The Exited event requires the += syntax:


procNotepad.Exited += new EventHandler(procNotepad_Exited)

This appends the Exited-event to the existing event handler and the details of this are
not particularly important; just be aware this is how you attach the event.

• The procNotepad_Exited event (bottom half of the example code) was built by hand,
typing the method, as illustrated above. The event would be orphaned (non-
functional without the += new EventHandler statement.

• In the example, "procNotepad.StartInfo.Arguments" specifies a non-path'ed


filename. With this, the program looks in the current (DOS) directory for the
"test.txt" file.

For testing, put the test notepad file, "test.txt", in the project's " ...\debug\bin"
directory. Because a path is not specified, the program defaults to the current
directory - that is, the directory where the .exe lives. Once compiled for final
production, the text file would have to live in the program's starting directory, where
ever that may be. If logic allows, it is best to use a fully-qualified path.

• The user is prevented from launching Notepad multiple times by the button1.Enabled
and Exited logic. See an alternate method in the "WaitFor" event (next section).

Chapter 18 - Shells - Launching Other Processes Page: 344


proc.WaitForExit

In the earlier example, button1 (launchNotepad), disabled itself, keeping the user from
clicking the button twice. When Notepad later closes, the procNotepad_Exited event re-
enables the button. However, during the intervening events, your program is still alive
and users can do other non-button1 tasks while Notepad is active.

There are times when the program needs to call another program and wait for that
program to finish before continuing. For example, you may call another program that
generates an output file and this may take several seconds or several minutes to write. In
this instance, the original program needs to stop all processing and wait for the other
program to complete. To accomplish this, use a "WaitForExit."

For this example, Notepad will be used as the down-stream program. All logic is the
same except for the line marked with an asterisk (do not type the asterisk in your code):

Program 18.2: Launching Notepad with "WaitForExit", completed


private void button1_Click (object sender, EventArgs e)
{
//Launch Notepad testfile.txt maximized and waitfor
//the event to complete

System.Diagnostics.Process procNotepad;
procNotepad = new System.Diagnostics.Process();

//No need to use the EXITED event


//No need to disable button1
procNotepad.EnableRaisingEvents = false;

procNotepad.StartInfo.WindowStyle =
System.Diagnostics.ProcessWindowStyle.Maximized;

procNotepad.StartInfo.FileName = "notepad.exe";
procNotepad.StartInfo.Arguments = ""; //No document name was passed

procNotepad.Start();

//"WaitFor" Order-dependent setting; after the Start


* procNotepad.WaitForExit()

MessageBox.Show ("The process finished");


}

comments:

• WaitForExit must be after the procNotepad.Start statement

• Notice the WaitForExit clause does not require the EnableRaisingEvents and button1
was not disabled; there is no need, except for cosmetic reasons.

Chapter 18 - Shells - Launching Other Processes Page: 345


• In this example, no filename was passed so Notepad opens a blank document.

• A MessageBox was added to aid in testing and diagnostics. It displays only after the
called Notepad was closed. In this area you might run a screen/panel refresh or a
log-entry.

Testing:

Launch your program and click (button1) to open Notepad.


Minimize notepad and click inside your running C# program. Notice you cannot activate
the window and you can not perform any other function within your program.

Results: Because of the WaitForExit, your program is disabled until Notepad is closed.
While your program is waiting, it does not occupy significant CPU resources.

File Cache Waits:

If your code launches another routine, such as a database extract, and that routine writes
an output file needed by your program, the file may not be ready even though the
WaitForExit returned control. Beyond the control of your program, the physical file may
be delayed in cache before it is committed to disk. In other words, the operating system
may have said the file is ready, but often the drives, especially server drives, are set to
have a short delay before writing – disk cache.

Although it may not seem so, cache enhances drive performance and without going into
details, be aware that your file may not arrive for one or two seconds after the original
write events. If your program immediately tries to open and process the newly-minted
file, it may not be available. Almost always, your code must hesitate a second or two
before continuing. If not, file-open errors may occur.

The next chapter discusses WAIT and TIMERS. In that chapter, a new function will
solve this problem: utilWait.wait(2);

Chapter 18 - Shells - Launching Other Processes Page: 346


DOS Output

Often over the years I have had need to run console (DOS) programs and often needed to
parse the results of the command. For a simple example, the DOS command "ipconfig"
can report the workstation's IP address. For the fun of it, parse the results.

This section describes how to run a DOS command and how to capture the results of the
command-line. C# makes this a pleasure to work with.

Note: This is an esoteric topic and is not needed to understand


this chapter, but the techniques can be useful when launching
DOS utilities.

Manually Running IPConfig:

As an example of the technique, the DOS command demonstrated in this example runs
the "IPCONFIG" command, which displays various TCP/IP network settings. The
output of this program will be captured by the C# program. To run the command by
hand, follow these steps:

A. Start, Run, "cmd" (no quotes) or Windows-R, "CMD"


Press Enter to run. This brings you to a DOS Command-line interpreter, also known as
"the DOS Prompt".

B. At the "C>" C-Prompt, type "ipconfig" and press enter


The resulting DOS screen shows information of interest, including the workstation's
IP Address (these examples assume your computer is on a network)

Chapter 18 - Shells - Launching Other Processes Page: 347


Sample DOS IPConfig

C. Type "exit" to close the DOS window.

Note: if you Start, Run, "ipconfig" (not using 'cmd'), the program runs and immediately
closes before you have a chance to read the results.

Capturing DOS output into an Array:

The Goal: In a C# program, run this same command, attached to a process (see previous
"Using Code to Start a New Process"). The DOS-screen results will be
captured into an array, where each line can be examined individually. These
examples were tested in Windows 7 and Windows 8.x.

Starting in a sample program (or start a new project), place a button on the form (e.g.
button2). Add this logic to the button_click event:

Program 18.3: Parse DOS IPConfig, completed

private void button2_Click (object sender, EventArgs e)


{
string[] aworkingIPConfig;
string stroutput; //Temporary holding area

Chapter 18 - Shells - Launching Other Processes Page: 348


int ireturnCode = 0; //Set a DOS-ErrorLevel return code

//Setup a standard Process called "runIPConfig"


System.Diagnostics.Process runIPConfig =
new System.Diagnostics.Process();

runIPConfig.EnableRaisingEvents = false;
runIPConfig.StartInfo.UseShellExecute = false; //DOS Requirement
runIPConfig.StartInfo.RedirectStandardOutput = true; //DOS Req.

runIPConfig.StartInfo.FileName = "ipconfig.exe";
runIPConfig.StartInfo.CreateNoWindow = true; //DOS Requirement
runIPConfig.StartInfo.Arguments = ""; //No DOS arguments needed

runIPConfig.Start();

//Step2: Run the IPConfig command and output results to array

output = runIPConfig.StandardOutput.ReadToEnd(); //DOS


runIPConfig.WaitForExit(); //Order Dependent

//Step3: Capture the returncode from the IPCONFIG command


//Later, test against ireturnCode==0
//(You may need to declare ireturnCode higher in the program)
ireturnCode = runIPConfig.ExitCode;

//Comment/Uncomment this line for full-screen Diagnostics:


MessageBox.Show("Diagnostics: " + "\r\n" + stroutput);

//Step4: Parse the messy DOS commands...


//Each line contains a leading carriage-return-linefeed
//Use a Split to break the (screen) into individual lines, breaking
//on the crlf. This split is limited to one-character and is
//parsed in two stages:

aworkingIPConfig = output.Split('\r');

//Now remove the \n part of the crlf for all lines with data...
for (int i = 0; i < aworkingIPConfig.Length; i++)
{
if ((aworkingIPConfig[i]+"").TrimStart().Length > 0)
{
//Truncate the first character '\n' using a simple mid-str
//(The cl800 utility libraries would have been easier with
//awrokingIPConfig[i] = util.Midstr(aworkingIPConfig[i],1);

aworkingIPConfig[i] = aworkingIPConfig[i].
Substring(1,
(aworkingIPConfig[i].Length) - 1);

//More diagnostic code:


//Display each found line
MessageBox.Show(aworkingIPConfig[i];
}
}

Chapter 18 - Shells - Launching Other Processes Page: 349


end Program 18.3: Parse DOS IPConfig

where:

• Setup for a DOS Process is nearly the same as the Notepad examples earlier in this
chapter. A few additional lines were added, as seen in the listing above.

• //Step 2 takes the results of the IPConfig command and puts the "standard output"
into a string. Later, the string is ".Split" along carriage-return/linefeeds into an array,
called "aworkingIPConfig".

• Each line in the array has CRLF (\r\n). Over the next several statements, they are
split with the \r and then the \n is removed in a separate steps (Split commands only
work with single characters). The result is a populated array with all the detail items,
one line per row.

Testing IPConfig:

Press F5 to run the program; click (button2).

Results: Assuming both Diagnostics lines are un-commented, a full-DOS-like screen


displays, followed by each non-blank line in the array.

From here, it is relatively easy to manipulate each line with a util.ParseKeyValue,


breaking on a ":colon" to retrieve individual values.

Chapter 18 - Shells - Launching Other Processes Page: 350


Multiple Instances of an Application

Users, either by accident or design, can launch a program multiple times and depending
on the data, this can be dangerous. With some applications, such as a word processor or
web browser, multiple copies are not a concern. But with others, such as an inventory or
payroll system, it could be risky and could put the underlying data at risk.

Because of this, it is often necessary to detect whether a copy of your program is already
running and if so stop loading the second copy. This turns out to be surprisingly
complicated. The problem is not as simple as detecting the same executable twice
because with Windows you have to worry about multiple users on the same workstation
in these situations:

• Standard Windows User-switching.


• Remote-desktop connections (Terminal Services), where multiple users can be
connected to the computer at the same time.
• Citrix servers where there may be 50 other people trying to run your application
simultaneously, all on the same CPU and each with a different user-ID.

This clouds the decision on whether to allow multiple instances of an application.

This section discusses the various methods for detecting multiple copies, from the easy
to the more complex.

Important Notes:

j None of the examples below can be tested while using the Visual Studio Editor. To
test, you must run the compiled EXE, found in the Project's \bin\Debug folder.

Locate the \bin\Debug folder by looking in Solution Explorer and highlighting the
Project's name (WindowsApplication1, or as illustrated below). In properties, note
the Project folder in Properties. This is where the \bin\debug folder can be found.
Use Windows Explorer to browse to the directory and find the compiled .exe.

j The .exe is not up-to-date until compiled. After making any code changes for this
section, Press F6 (Build Solution). Alternately, press F5 to compile and run the
program, then immediately exit the running program – this is enough to re-write the
.EXE.

j Then, using Windows Explorer, tunnel to the bin\debug folder and double-click the
.exe to launch the program. Double-click the .exe a second time to run a second
copy. Note two running copies on the taskbar.

Chapter 18 - Shells - Launching Other Processes Page: 351


Setting Up an Example Program:

The routines in this section can be written and tested in either a new or existing project.
All code lives within the "Form1_Load" event. While in the form's designer view,
double-click anywhere on the form's title-bar or background; taking you to the
Form_Load method.

Simple Instance Test:

This is the simplest test for checking to see if your program is already running. The
routine detects the program's Process Name (task manager) and if already running,
simply exits, with an optional message.

Benefits to this design:


• Easy to code and implement.
• This works well for most computers where there is only one user at-a-time logged
into the workstation.

Drawbacks:
• Multiple Users on same workstation (Terminal Services, Citrix, XP User Switching)
will be blocked from running the program and there is nothing they can do until the
first person exits.

• Do not use this method if the code runs on a Citrix Server. (However, for some
processes, such as an Interface Engine or other unattended process, this may be a

Chapter 18 - Shells - Launching Other Processes Page: 352


perfect way to keep the program from running twice. Of course, programs in this
category should be compiled and run as a Windows Service.)

• There is a (rare) possibility that two instances of your application could launch at the
same time and would not be detected by this method.

In the program's Form_Load event, add this logic:

Simple Instance Testing for Single-User Applications


private void Form1_Load (object sender, EventArgs e)
{
//Simple Instance Testing; Do not use for most Citrix applications

System.Diagnostics.Process currentProcess =
System.Diagnostics.Process.GetCurrentProcess();
string currentProcessName = currentProcess.ProcessName;

if (System.Diagnostics.Process.GetProcessByName
(currentProcessName).Length > 1)
{
MessageBox.Show(currentProcessName + " is already running");
Application.ExitThread();
}

where:

• "System.Diagnostics" is a prefix for the .Process and .GetProcessByName methods.


If a "using System.Diagnostics" were used, the prefix could be dropped.

• Logic to switch to the already-running application requires a more sophisticated


approach; details in a few pages.

Testing the Simple Instance Solution:

To test the program, follow these steps.

1. Make your code changes, press F6 to Rebuild (Compile but not Run).

2. Using the steps previously outlined, locate the bin\Debug folder and the compiled EXE.

Double-click the EXE to run the application. Slide the application's title bar to a
different location incase your logic does not work properly (keeping the second copy
from opening exactly over the top of the first).

3. Double-click the EXE a second time.

Results: "<your process> is already running" and the second copy of the application
does not fully open.

Chapter 18 - Shells - Launching Other Processes Page: 353


If two programs were launched within milliseconds of each other, or by two
simultaneous users (in a telnet, citrix, or terminal server session), this method may not
detect the simultaneous loads properly.

Chapter 18 - Shells - Launching Other Processes Page: 354


Mutex: A Better Solution for Multiple Instances

A better solution to keep your code from executing twice is to use a construct called a
"Mutex". At the risk of oversimplification, a Mutex looks for a unique ID in the system
and uses this to prevent other threads (processes) from running. Mutex stands for
"Mutual Exclusion." There are many different ways to use a Mutex, here is a design I
like to use.

Benefits:
On single-user workstations, the mutex stops a single user from running multiple copies
or on a Citrix server, limiting each user to only their one copy. Optionally, it can be set
to allow only one copy on (a Citrix server), regardless of the number of users.

Drawbacks:
The code is placed in an odd location (at least as far as this book is concerned)
Relies on the EXE name instead of a GUID (unique code); this is mostly acceptable.
It can malfunction in rare conditions where the first running copy is closing during the
second's launch.

Building the Test Mutex:

Use a new project or an existing project. If you have previous (Form_Load) code to
detect multiple instances, remove that code before continuing. Then, follow these steps.
Notice the unusual location for the code.

1. In Solution Explorer, double-click "program.cs"

This takes you to the "true" entry point of your program (this is not Form1's class or the
Form_Load event). If you have written console apps, you will recognize the "Main"
starting point.

Chapter 18 - Shells - Launching Other Processes Page: 355


2. Add the following code to the top of the module

Required for Mutex


using System.Threading;
using System.Reflection;
using System.IO;

3. Inside of "static void Main ()"


Add this logic:

Using a Mutex to Control Multiple Instances, completed

[STAThread]
static void Main ()
{
//Use a Mutex to prevent multiple instances of an application.
//Requires using statements:
//using System.Threading;
//using System.Reflection;
//using System.IO;

//Declare the mutex:


Mutex myMutex;

bool boolmutexCreated;
string strExeName;
string strLocation = Assembly.GetExecutingAssembly().Location;

FileSystemInfo fileInfo = new FileInfo(strLocation);


strExeName = fileInfo.Name;

myMutex = new Mutex(true, strExeName, out boolmutexCreated);

if (boolmutexCreated == false)
{
//Already running...

MessageBox.Show (strEXEName + " is already running");


//myMutex.ReleaseMutex(); //Not recommended
Application.ExitThread();
}
else
{
//This code remains the same from the original
//Launch for first time
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

//Change this to match your entry form's main Name:


Application.Run(new Form1());
}
}

Chapter 18 - Shells - Launching Other Processes Page: 356


Mutex, end

Testing the Mutex:

You cannot test from within the compiler. To test, use the same steps as before: First,
compile (F6). Next, locate the EXE in the bin\debug directory and launch twice.

Citrix Servers:

For Citrix, Windows multi-user sessions, TSE and other such environments, change this
statement. (You may also consider appending a User-ID to the strExeName.)

Limiting to One Single Instance in a Multi-User Environment


//For Citrix/TSE and other multi-user environments, use
//this switch to limit the application to one total occurrence

myMutex =
new Mutex(true, "Global\\"+strExeName, out mutexCreatedSW);

Other Notes:

Microsoft has other notes about using Mutexes:


http://msdn.microsoft.com/en-us/library/system.threading.mutex.aspx

Chapter 18 - Shells - Launching Other Processes Page: 357


Exercises

A. Have a C# program launch a copy of Microsoft Excel, opening any .XLS file of your
choosing (e.g. C:\data\Wksheets\test.xls). Be sure the spreadsheet is within a path.

Hint: backslashes are reserved characters.

B. Have the program in Exercise A display any message once Excel is closed. e.g. "Excel
has closed."

C. In Program 18.3: Parse DOS IPConfig, remove all diagnostic code.


Have the program display the found IPAddress and Subnet mask in textBoxes on Form1;
only show the addresses.

Chapter 18 - Shells - Launching Other Processes Page: 358


A Beginners Guide to C-Sharp - Volume 2
Visual Studio C# 2017
ASCII through Advanced Formatting
by Tim R. Wolf
© 2017.06.01 1.02
Table of Contents

9 Chapter 12 - ASCII Files 17


Reading ASCII (Text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
StreamReader
Priming Read
Ending the Loop and Closing Files
Using an EndOfStream Read
The "Using" Clause
ASCII File Reads with try-catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Issues with try/catch and Close
Chaining catch-statements
Completed ASCII ReadFile - Program 12.5. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Writing ASCII (text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Appending ASCII (text) Files - Advanced Open Methods. . . . . . . . . . . . . . . . . . . . 54
Preventing Append from Running Twice

9 Chapter 13 - Parsing Tab and CSV Files 65


Automatic Parsing by Delimiter (Split).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Declaring the Destination Array
foreach
Alternate Loops
.Split Limitations
CSV Files
Split Example: Phone-Numbers
Manually Parsing Comma-Delimited Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
First Field Logic
for-next Logic
Substring
Last-field Logic
Tab-Delimited Files
Parsing a Variable Number of Columns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Parsing ASCII CSV Files with Embedded Commas.. . . . . . . . . . . . . . . . . . . . . . . 105
Error Processing in CSV Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Moving ParseCSVLine to the Utility Library

9 Chapter 14 - INI Files 147


INI File Structure and Design. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
A015: Loop Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A017: Parse INI Detail Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A028: Finding the Application's Default INI Location.. . . . . . . . . . . . . . . . . . . . . 178
Command-Line Override.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Simulate a Command Line Options
Embedded Spaces in the Parameters
Command-line ini=
A029: Write Default INI when Missing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Basic INI File Read: Complete Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Using a Program Class - CL860 INI File Read. . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
CL860 Basic INI Read Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Using CL860_BasicINIRead

9 Chapter 15 - xml and App.config Files 223


xml File Structure
app.config xml.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Using (Reading) app.config
Building a Manual xml File.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Reading xml File Sequentially. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

9 Chapter 16 - Windows Registry 249


Organization of the Registry
Reading a Specific Registry Key.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Opening the Registry
Closing
Reading a Specific Registry Value
Reading Mulitple Keys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Reading All Values within a SubKey. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Multi-Line String Registry Values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Creating/Modifying Name-Value Pairs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Opening the Registry in 'Writeable' Mode
Creating Sub-SubKeys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Deleting Values and Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Deleting Entire SubTrees
Deleting the Current Key
Enumerating SubKey (folders). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

9 Chapter 17 - Reading Excel and Access 289


Reading an Excel File using a COM object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Open the Workbook
parameter list
Reading a Row
comboBoxes based on Access Database Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Attaching Microsoft Access Data to a comboBox. . . . . . . . . . . . . . . . . . . . . . . . . 317
ODBC Call
Connecting to the comboBox
Refreshing the List
Performance Considerations

9 Chapter 18 - External Programs (Shell) 333


Starting a New Process. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Launching Notepad with a Specific Filename
Building an Exit Event
Using Code to Start a New Process.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
proc.WaitForExit.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
DOS Output.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Capturing DOS output into an Array
Multiple Instances of an Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Simple Instance Test
Mutex: A Better Solution for Multiple Instances. . . . . . . . . . . . . . . . . . . . . . . . . . 355

9 Chapter 19 - Waits, Delays and Pauses 361


Poor Wait States – Not Recommended
Empty loops
current time and loop until xx seconds
Sleep events
System.Threading.Thread.Sleep (milliseconds). . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Completed wait Simulation - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Wait as a Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Using cs805_Wait
Calling Wait
Timer Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Basic Timer Code
Event Horizons
Countdown and Timer Example
Disabling the Close "X" Mid-Transaction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Intercepting a Close Event
Countdown and Timer Program - Completed. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Simple Startup Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Splash Screens.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

9 Chapter 20 - Printing 415


Printing Simple Text. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Print Layout / Print Rendering
Pixel Positions in Millimeters (x,y)
OverShooting
Printing Horizontal Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Printing Rectangles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Filling
Printing Graphics.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Printer Dialogs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Adding a Custom Printer Dialog
Print Preview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Printer Setup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Printing Text Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Font Height Calculations - Lines per Page
Page-Break Logic
GraphicsUnit.Millimeters
Setting New Margins, Landscape. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
Current Margin
Setting New (Printable) Margins
Begin and End Print Events.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

9 Chapter 21 - Formatting 469


Font Color, Font Bold. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Font Bold
Font Style Ariel
Basic String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
String.Format with Alignment.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Proportional Fonts
Numeric Formatting with String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Standard Decimals
Formatting with Alignment
Commas and Currency
Numeric Picture Clauses. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Padding Zero with Leading and Trailing Decimals
Place Holder (#)
Thousands Separator ( , )
Variable Picture Clauses (Group Separators)
Date and Time Pictures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Converting from Strings
Converting Dates
Other Date Time Properties and Methods.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
DateTime Formatting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Pictures
Display Dates, Year first
Format Class Library - cl710_Formatting.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Phone Number Formatting
Proper Names
Proper Addresses
PhoneNumberFormat Method - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
PhoneNumberFormat Module - Coding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Extension Extract
Strip Normal Punctuation
Strip "1-" Prefixes
Punctuate
Punctuating the AreaCode
"ProperNames" Formatting - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
ProperNamesFormat - Coding.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
A Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
CH20 - Printing
Chapter 20 - Printing

Printing in C# is noticeably different than most other languages because it requires up-
front work and every mundane detail must be managed. Items such as font-heights to
calculations on the number of lines on a page, to the exact (x,y) coordinate position for
text must be computed. In exchange for the tediousness, you have complete control over
the print job – more control than most other languages.

C# uses a "System.Drawing" library for all printing, including text and graphics. The
basic design is to declare a PrintDocument (which acts like a virtual sheet of paper) and
then attach what I call a "layout" routine. Within the layout, assemble all the items
needed for printing. Once assembled, issue a .Print command.

The steps are not particularly complicated but do require careful setup.

Topics:

• Printing simple text


• Print Layout (rendering) logic
• Pixel positions (x,y) at 100dpi
• Using e.Graphics.PageUnit = GraphicUnit.Millimeter;
• Printing horizontal lines and boxes
• Printing graphic logos (BMP, JPG, etc)
• Printer Dialog information
• Using Print Preview dialog
• Printer Setup dialog
• Printing multipage (ASCII Text Files and others); headers and footers
• Font Height calculations
• Problems with priming reads and recursive calls to Layout
• Programmatically setting margins, landscape
• Begin and End print events

Chapter 20 - Printing Page: 415


Overview:
Generally Required Print Variables
using System.Drawing;
using System.Drawing.Printing;

namespace Printing
{
public partial class Form1 : Form
{
//Declare class-level PrintDocument:
PrintDocument printStuff = new PrintDocument();
PrintPreviewDialog viewStuff = new PrintPreviewDialog();

Font printFont;

Intermediate Routine Called by both Preview and PageSetup


private void A700_Print1 (bool printPreviewSW, bool printPageSetupSW)
{
//Format the page layout:
printStuff.PrintPage += new PrintPageEventHandler (layout1);

if (printPreviewSW == true)
{
PrintDialog printSetup = new PrintDialog();
printSetup.AllowSomePages = true;
if (printSetup.ShowDialog(this) == DialogResult.OK)
{
printStuff.Print();
return;
}
else
return;
}

if (printPreviewSW == true)
{
viewStuff.Document = printStuff;
viewStuff.ShowDialog();
}
else
{
//Optional: Hide the PrintDialog box:
PrintController stdPrintController =
new StandardPrintController();

printStuff.DocumentName = "Print1 Document Test";


printStuff.Print();
}

Chapter 20 - Printing Page: 416


Actual Layout Called by A700_Print1 (via btnPrint)
private void layout1 (Object sender, PrintPageEventArgs e)
{
Font printFont = new Font("Courier New", 11);
e.Graphics.PageUnit = GraphicsUnit.Millimeter;

//Text
e.Graphics.DrawString ("stuff to print", printFont,
Brushes.Black, 19.4F, 19.4F);

//Horizontal Line
Pen myPen = new Pen(Color.Grey, 0.2F);
e.Graphics.DrawLine(myPen, 19.4F, 44F, 194.4F, 44F)

//Graphic
Image myImage = Image.FromFile("C:\\data\\Test.bmp");
e.Graphics.DrawImage (myImage, 190.0F, 8.0F);
}

Chapter 20 - Printing Page: 417


Printing Simple Text

This example's goal is to print the contents of textBox1 directly to the printer. Start a
new project (named "Printing") and add the following objects:

Name: textBox1 (Default) Text: "Dogs and Cats" (any text)


Button: btnPrint1 Text: "Print"
Button: btnPrint2 Text: "Print More"

Program 20.1: Printing Simple Text

1. At the top of the program, add this using statement:

Required "using-Statement" for all Printing Routines


using System.Drawing;
using System.Drawing.Printing;

If you are writing a Console application, expand Solution Explorer's tree,


opening "References". Add a standard reference for "System.Drawing"

2. Create a class-level (form-level) variable, "printStuff".


printStuff is a "PrintDocument" data-type and because it is being declared at a higher
scope, the variable is visible to all methods within the program. This object contains all
of the methods necessary for printing:

Chapter 20 - Printing Page: 418


Declare a Class-Level (Form) variable for the Printing Object
namespace Printing
{
public partial class Form1 : Form
{
//Declare a class-level variable "printStuff":
PrintDocument printStuff = new PrintDocument();

3. For educational reasons, two separate printing routines are being demonstrated, one
attached to btnPrint1 and the second to btnPrint2. This will show how two pages can
overlay each other before being committed to paper.

• Stub-in Print1 and Print2's initial code by double-clicking both buttons in design
view, creating two separate click events.

• For both btnPrint1_Click and btnPrint2_Click, type the following statements:

...Each button Calls the Printing Logic


private void btnPrint1_Click (object sender, EventArgs e)
{
//Requires using System.Drawing and using System.Drawing.Printing;

printStuff.PrintPage += new PrintPageEventHandler (layout1);


printStuff.DocumentName = "Print1 Document Test";
printStuff.Print(); //Print now
}

private void btnPrint2_Click (object sender, EventArgs e)


{
//(This could be flawed; see text)
printStuff.PrintPage += new PrintPageEventHandler (layoutMore);
printStuff.DocumentName = "Print2 Test";
printStuff.Print(); //Print now
}

Each button does three things:


a) Sets up a Formatting Event ("layout1" or "layoutMore")
b) Gives the Windows Print Spooler a document
c) Issues the actual print request

where:

• btnPrint1_Click appends ( += ) a new PrintPageEventHandler, arbitrarily named


"layout1", into the PrintDocument variable "printStuff".

The append "+=" is required by the statement.

• The actual layout work happens in another method - not here (See Layout1)

Chapter 20 - Printing Page: 419


• Each layout should be given a DocumentName so it looks pretty in the Windows
print spooler:

• Of interest, printStuff.PrintPage is an event, not a method:

j This is subtle. An event-trigger is built on the fly (think about the lighting-bolt
icon on a button event) and like all events, it waits patiently for a trigger before
launching. In other words, although it was written first, it does not run, instead,
it waits for a .Print method. When called, it runs "asynchronously," as a separate
thread.

To be more concrete, at the moment of printing, when printStuff calls its own
.Print method, the event "layout1" starts. You will see more on this in a few
moments.

Print Layout / Print Rendering Logic:

Think of the two button events (layout1 and layoutMore) as Print rendering routines or
as print-layout commands:

printStuff.PrintPage += new PrintPageEventHandler (layout1);


printStuff.PrintPage += new PrintPageEventHandler (layoutMore);

Chapter 20 - Printing Page: 420


These are the routines that format the printed page with all of the text and other graphic
elements needed to make the page look as intended.

4. Manually build the print rendering logic, starting with btnPrint1's "layout1".

At a convenient location, manually type the function name ("private void layout1...") in
any open area in the code. As usual, avoid the two closing braces near the bottom of the
program and be sure the routine is outside of another method's braces.

Notice the signature line, which contains unusual parameters, and as always, they are
case-sensitive.

btnPrint's Layout1 Logic


private void layout1 (Object sender, PrintPageEventArgs e)
{
//Requires: using System.Drawing and using System.Drawing.Printing

string textToPrint;
textToPrint = textBox1.Text + "\r\n";
textToPrint += "line 2" + "\r\n";
textToPrint += "line 3";

Font printFont = new Font ("Courier New", 11);


e.Graphics.DrawString(textToPrint, printFont, Brushes.Black, 0,0);

//(See below for e.Graphics.PageUnit = GraphicsUnit.Millimeter;)


}

where:

• layout1's name is arbitrary. Notice how btnPrint1 called layout1 via the
PrintPageEventHandler.

• For this example, layout1 appends three separate strings into a new string,
"textToPrint", and then uses the passed "e.Graphics.DrawString" method to build the
print statements.

The DrawString method requires 5 parameters: The text to print; the font; color; and
a 2-Dimensional page location, in this case, starting at zero pixels from the top-left
corner of the paper. The location, 0,0 is described in more detail later, along with a
way to change the measurements to millimeters.

• You can have multiple DrawString commands within the routine or you can
assemble "textToPrint" as one large string.

• layout1 does not actually print – it just sets up the printing format.
printStuff.Print is the command that actually does the printing and this module
does not get called until that event.

The code looks like this:

Chapter 20 - Printing Page: 421


5. Next, create btnPrint2's printing layout (Print More's), which follows the same basic
logic as the layout1 routine. The layout was given an arbitrary name "layoutMore".

btnPrint2: Layout Routine "printMore"


private void layoutMore (Object sender, PrintPageEventArgs e)
{
//This routine is problematic if run after btnPrint

string textToPrint;
textToPrint = "yaba-daba-doo";

Font printFont = new Font ("Ariel", 14);


e.Graphics.DrawString (textToPrint, printFont, Brushes.Black, 0,0);
}

Chapter 20 - Printing Page: 422


Testing btnPrint:

The initial test will print correctly but a problem will be uncovered when a second print
is sent. Do the following:

• Confirm the computer has a default printer attached.


• Press F5 to run the program.
• Click btnPrint1 (Print).

Results: "cats and dogs" (or what ever was in textBox1) with a second and third line with
carriage-return/linefeeds. The text is printed correctly in the upper-left corner of the
paper.

Cats and dogs


line 1
line 2

* Without closing the program, click "Print More".

Results: A new (second) page is printed where "Yaba-daba-doo" (or what ever text you
hard-coded in printMore) overlays the original three-line print statement from btnPrint1
– even though btnPrint1 was not executed in the second print.

This is an order-dependent problem. Because the variable printStuff 's scope is broad
enough to survive the two button-events, the previous values remained. Since both
printed at position (0,0), they overlaid each other. Think of this in terms of a laser-
printer page-layout, where the entire page is formed as one image before being sent to
the printer.

Correcting btnPrint2's Positioning:

Correct Print2's printout by shifting its location from (0,0) to a new position on the page.
To save a sheet of paper, and in order to demonstrate other features, Print1 will be set to
format the page but it will leave the actual printing to button2. Be sure to close the
previously-running program before continuing with these steps:

Chapter 20 - Printing Page: 423


6. In btnPrint1_Click, comment-out the .Print method, leaving the PrintPage layout
statement intact. This allows button1 to place text into the page layout but does not
actually submit the print-job to paper:

printStuff.PrintPage += new PrintPageEventHandler (layout1);


//printStuff.Print(); //Comment out this line in btnPrint1_Click

7. In the "layoutMore" method, change the location from (0,0) to (0,70), where 70
represents 70 pixels:

e.Graphics.DrawString (textToPrint, printFont, Brushes.Black, 0,70);

Testing Results: "yaba-daba-doo" prints below the original text. Only btnPrint2
executes the .Print method.

(x,y) Pixel positions are explained in more detail in a moment.

Correcting btnPrint2's "Prior" Data:

In the examples, btnPrint2 showed how two routines could overprint each other.
btnPrint2's intent was purposely not clear. Is it a separate print job or does it augment
the previously printed text? If the intent was to be a separate print job, leave Print1's
"printStuff.Print" in place, acting as a form-feed.

However, because btnPrint2 shares the same PrintDocument object (printStuff), the
layout from Print1 survives long enough to impact Print2. This can be corrected in one
of two ways:

The first is to overwrite the page with white before loading more data.
At the beginning of Print2, issue this command:
e.Graphics.Clear(Color.White);

This is a somewhat clumsy way to "erase" the prior data before populating the (new)
page but it works if Print1 and Print2 are trying to share the same variable.

The second method is less reprehensible: If the two are intended to be separate print
jobs, create a separate PrintDocuments variable (e.g. printStuff2).

Pixel Positions in Millimeters (x,y):

The previous examples showed how text can be positioned by specifying a pixel
position, expressed in a traditional algebraic coordinate system (x,y).

Chapter 20 - Printing Page: 424


Of course, the big question is how big is a pixel and does it change depending on the
dots-per-inch of the output device? It turns out that pixels are scaled at 100 dots-per-inch
and they are somewhat of a nuisance to work with. They change position based on the
output device – e.g. 300 dpi printer vs a 600dpi printer..

You can instruct the compiler to use a different ruler, such as millimeters and the
compiler will make all the proper translations. Use the PageUnit property:

Setting Page Units to millimeters - Recommended


e.Graphics.PageUnit = GraphicsUnit.Millimeter;

Modify layout1's e.Graphics.DrawString command, replacing (0,0) with (19.4F, 19.4F),


noting these are floating point values (F):

Using e.Graphics.PageUnit = GraphicsUnit.Millimeter; partial


private void layout1 (Object sender, PrintPageEventArgs e)
:
:
Font printFont = new Font("Courier New", 11);

//Modify the units from Pixels to Millimeters (Floating Point):


e.Graphics.PageUnit = GraphicsUnit.Millimeter;

e.Graphics.DrawString (textToPrint,
printFont, Brushes.Black,
19.4F, 19.4F);

Chapter 20 - Printing Page: 425


Results: btnPrint prints the three-lines of text 19.4mm (apx. 1 inch) from the top and left
edge of the paper.6

where:

• The letter "e.", as in "e.Graphics", is from layout1's signature and it is traditionally


used but the variable could be renamed from 'e.' to any other name.

• e.Graphics.PageUnit can use other scales, such as Points (1/72 inch), Display (1/100
inch), inches, and others. Millimeters are easy to work with, especially if you have a
metric ruler. Measurements such as 15mm are easier to work with than 38/64".

• In the example code above, 19.4F (millimeters) is approximately 1 inch and was
chosen to demonstrate fractional (Floating Point decimal) values. With decimals you
must explicitly specify a Floating point number (19.4F); note the "F".

Neglecting the "F" results in "<object> has some invalid arguments. Argument
'<x>': cannot convert from 'double' to 'float'.

• When coding the e.Graphics.PageUnit and GraphicsUnit.Millimeter, don't forget the


letter 's' in the word Graphics.

OverShooting the Page:

If a specified position is off the page, C# truncates or discards the results with no errors
or warnings. For example, an offset of (0, 295)mm prints at 11.5" on an 8.5x11" sheet of
paper; nothing appears on the page and it may appear as-if a blank-page formfeed.

Organizing Your Print Jobs:

Appending text to a large, single string (see textToPrint, above) only works with the
simplest of printing needs and it assumes that each line of text fits within the page
margins. Although a string can hold literally thousands of characters, the design lacks
control over the printing logic. For example, imagine a printed invoice with header and
detail sections, boxes, lines, and subtotals.

From a programming-point-of-view, it is best to break printing details into separate


processes. First, and most obvious, the printing logic should probably live in its own
class (Chapter 8), but beyond that, the printing routines can be broken-down into smaller
constituent steps with any of the following ideas:

6
Note: laser-printers cannot physically print within a roughly 6mm margin on all edges of the paper:
19.4mm + 6mm = 25.4mm = 1.0 inches. C# is aware of these margins and bases all of its calculations on
the printable area – not on the physical paper.

Chapter 20 - Printing Page: 426


• (btnPrint) can call multiple layout routines, as in:
printStuff.PrintPage += new PrintPageEventHandler(custAddressLayout);
printStuff.PrintPage += new PrintPageEventHandler(graphicLineLayout);
printStuff.PritnPage += new PrintPageEventHandler(invoiceDetails);
printStuff.Print(); //print the physical page

• or (layout1) can have multiple e.Graphics.DrawString methods:


:
Font printFont = new Font("Courier New", 10);
e.Graphics.DrawString (textToPrint, printFont, Brushes.Black,10,10);
textToPrint = "new stuff to print";
e.Graphics.DrawString(textToPrint, printFont, Brushes.Black, 10, 40);

• Calling other methods from within a (layout) routine, can also help orgainize the
printing routines.

Chapter 20 - Printing Page: 427


Printing Horizontal Lines

Lines and boxes, as well as filled shapes, can be drawn on a printed form with the
following commands. These require some setup but are generally easy to do. Accurately
positioning the lines require basic addition and subtraction and a metric (millimeter)
ruler is helpful.

Setting up the Line and Box Examples:

Program 20.2: Adding Graphic Lines and Boxes

The previous section's btnPrint1 code will be used for these examples and the code is
repeated here. In the btnPrint1_Click routine, un-comment the "printStuff.Print"
statement, that was commented in the previous example. btnPrint2 will be ignored.

Previous btnPrint1 Code, repeated


using System.Drawing.Printing;

namespace Printing
{
public partial class Form1 : Form
{
//Declare a class-level variable "printStuff":
PrintDocument printStuff = new PrintDocument();

public Form1 ()
{
InitializeComponent();
}

private void btnPrint1_Click (object sender, EventArgs e)


{
printStuff.PrintPage += new PrintPageEventHandler (layout1);
printStuff.Print(); //Return the Print now routine
}

private void layout1 (Object sender, PrintPageEventArgs e)


{
string textToPrint;
textToPrint = textBox1.Text + "\r\n";
textToPrint += "line 2" + "\r\n";
textToPrint += "line 3";

Font printFont = new Font ("Courier New", 11);


e.Graphics.PageUnit = GraphicsUnit.Millimeter;
e.Graphics.DrawString
(textToPrint, printFont, Brushes.Black, 0,0);

//Place Line Drawing logic here:


}

A horizontal lines is specified with a pair of coordinate-system points. A pair of


numbers mark the start of the line and a second pair mark the ending point (two points

Chapter 20 - Printing Page: 428


determine a line). The coordinates are given as (x, y) where x=Horizontal axis (in from
the edge) and y=Vertical axis (down from the top).

Thus, two points determine a line (19.4, 44) and (194.4, 44):
The line is horizontal because both y-axis are the same.

looks like this

Setting Up the Example:

For this example, use btnPrint1's "layout1"(see previous section) to print a thin
horizontal line below the three text lines, using the code repeated above. Here are the
measurements, which may look complicated but really only involve simple subtraction:

Chapter 20 - Printing Page: 429


j When printing on a sheet of paper it is easiest to have a metric ruler (millimeters) to
help calculate the line's position and length.

For mechanical reasons, Laser Printers do not print on the outside edges of the paper.
All measured positions from the edge of the paper need to be increased by 6mm,
depending on the printer. In the illustration above, none of the measurements start at the
outside-edge of the paper – all move in 6mm. Thus, to print with a roughly 3/4inch
margin, take 9.4 + 6mm = .79" from the paper's edge.

1. Begin by establishing a "pen" (Pens can be solid, dashed, etc.; they can have different
colors and thicknesses).
In layout1, near the bottom of the routine, add this statement:

Defining a Pen
:
//Place Line Drawing Logic here
//Define a "pen" for drawing the lines
Pen myPen = new Pen (Color.Black, 0.2F);

where:

• myPen is an invented name with a zero-point-2mm (very thin).


The statement instantiates a new Pen object

2. Draw the line with an e.Graphics.DrawLine method:

Drawing a Line
:
Pen myPen = new Pen (Color.Black, 0.2F);
e.Graphics.DrawLine(myPen, 19.4F, 44F
194.4F, 44F);

where:

• The 'e.' is passed from layout1's signature line.

• All dimensions are in millimeters because of layout1's earlier


e.Graphics.PageUnit = GraphicsUnit.Millimeter;

• The first two numbers mark a single Begin point: (x1, y1), and the second pair marks
the End Point:(x2, y2). Most developers would write this statement on one line.

• The end point is 194.4mm from the zero-point margin. This is not a measurement of
the line's length – only where the end-point lives. The position 194.4mm is a
coincidence, with no relation to the 19.4 measurement.

Chapter 20 - Printing Page: 430


• The length of the line is calculated as:
194.4 - 19.4 = 175mm.

Keep in mind from the left-edge of the paper, the end-point is 175mm + 6mm.

• Horizontal lines have the same y-axis ( , h) ( ,h)


Vertical lines would have the same x-axis: (v, ) (v, )

• The key thing to remember about lines is they are always defined as two points; you
do not specify the length. Contrast this with a rectangle, described next.

• The line is not physically drawn until btnPrint1_Click executes printStuff.Print.

Testing:

Press F5; then click btnPrint1.


Results: Text and a horizontal line should print on the page.

Chapter 20 - Printing Page: 431


Printing Rectangles

Rectangles are built similarly to lines with one exception: Instead of specifying the
EndPoint, mark the width and depth.

Continuing with the same code as above (btnPrint1 and "layout1"), add this statement
after the previously-built horizontal line.

Drawing a Rectangular Box


:
Pen myPen = new Pen (Color.Black, 0.2F);
e.Graphics.DrawLine(myPen, 19.4F, 44F,
194.4F, 44F);

//Drawing a Box using the previously built Pen object (LxW):


e.Graphics.DrawRectangle (myPen, 19.4F, 47F,
175F, 25F);

where:

• The rectangle is anchored 3mm below the horizontal line: 44mm + 3 = 47mm at
point (19.4, 47)

• Do not specify ending points for a rectangle, instead, specify a width and depth. In
the example, the rectangle is 175mm wide, 25mm deep. This is inconsistent
compared to lines.

e.Graphics.DrawRectangle (myPen, 19.4F, 47F, 175F, 25F);

• All dimensions are in millimeters because of layout1's earlier


e.Graphics.PageUnit = GraphicsUnit.Millimeter;

Chapter 20 - Printing Page: 432


Filling Rectangles:

There is no method for filling a DrawRectangle Box but you can draw another rectangle
using a FillRectangle method. To demonstrate that the FillRectangle is separate from the
first rectangle, draw this new rectangle 2mm down and to the right of the original box:

Drawing an unrelated Filled Box


:
Pen myPen = new Pen (Color.Black, 0.3F);
e.Graphics.DrawLine(myPen, 19.4F, 47F,
194.4F, 44F);

//Drawing a box using the previously built Pen object:


e.Graphics.DrawRectangle (myPen, 19.4F, 47F,
175F, 25F);

//Draw a filled box, unrelated to the first:


e.Graphics.FillRectangle (Brushes.Gray,
21.4F, 48F,
175F, 25F);

where:

• The filled rectangle has no relationship to the first rectangle. This is a separate
object.

• For this example, the box was shifted 2mm: 19.4mm + 2mm = 21.4mm – shifting
the anchor point down and to the right of the original box. Notice the width and
depth are the same.

• To fill the original rectangle, draw the filled rectangle on top of the first. You may
want to account for first box's line thickness by making the Filled rectangle slightly
smaller than the box or by first printing the Fill, followed by the outside box.

If you have been following the examples, the final results should look like this:

Chapter 20 - Printing Page: 433


Printing Graphics

Continuing with the btnPrint examples and "layout1," print a graphic image on the same
page as the text, lines and boxes from the previous pages.

For this example, use any BMP, JPG or TIF image, but a small image is recommended
for testing. A graphic can be created using MSPaint by following these instructions:
Start, Run, MSPAINT.exe. Select menu choice Image, Attributes. Set the Width and
Height to 40x40 pixels. Draw or paste something interesting. Select File, SaveAS and
save the file in a known location.

1. In the "layout1", near the bottom of the module, declare an "Image" object and using the
"FromFile" method and then attach a graphic. (See code below).

2. Use "e.Graphics.DrawImage (myImage, x, y);" to position the graphic. Following the


code examples from above, the graphic positions are set in millimeters.

Printing a Graphic Image, partial code


private void layout1 (Object sender, PrintPageEventArgs e)
{

:
//Previous code (printing text, lines and boxes)
//lives here...

e.Graphics.PageUnit = GraphicsUnit.Millimeter;

//Print a BMP or JPG graphic at this upper-left-corner location:


Image myImage = Image.FromFile ("C:\\Data\\test.bmp");
e.Graphics.DrawImage (myImage, 180.0F, 8.0F);
}

where:

• btnPrint calls the layout1 module, as described in previous sections and that code is
not repeated here

• The Image position is specified with a simple x,y coordinate and this marks the
upper-left corner of the graphic. The graphic prints in its entirety and does not scale
to smaller sizes.

• BMP, JPG and TIF files are supported

Chapter 20 - Printing Page: 434


Printer Dialogs

When a page prints, Windows displays a default print Dialog, notifying the user that a
print job has been released to the Windows Print Spooler. This is adjustable through
some rather wordy but easy-to-use commands.

Adding a Custom Printer Dialog:

In btnPrint1_Click, add lines 9, 10, and 11. (See the previous section for the original
program):

Building a Custom Print Dialog, completed


1 private void btnPrint1_Click (object sender, EventArgs e)
2 {
3
4 printStuff.PrintPage += new PrintPageEventHandler (layout1);
5
6 //Build a custom Print Dialog box using an already
7 //defined PrintDocument printStuff:
8
9 PrintController stdPrintController =
new StandardPrintController();
10 PrintController prntControllerMsg =
new PrintControllerWithStatusDialog (stdPrintController,
"My job is printing");
1 printStuff.PrintController = prntControllerMsg;
2
3 //Send the document to the printer:
4 printStuff.DocumentName = "Print 1 Document Test";
5 printStuff.Print();
6 }

Chapter 20 - Printing Page: 435


where:

• The underlined variable names are arbitrary. Notice how they relate to each other.

To keep Windows from displaying the box (for silent printing), use only lines 9 and 11,
making a minor change to line 11:

Disabling the Print Dialog, option


:
9 PrintController stdPrintController =
new StandardPrintController();
10 //
11 printStuff.PrintController = stdPrintController;
:

Chapter 20 - Printing Page: 436


Print Preview

Adding a Print Preview function to your routine is relatively easy but keep this one
cardinal rule in mind: You can only Print Preview items that have passed through a
layout routine, or in the vernacular of C#, it must hit a PrintPageEventHandler, such as
"layout1".

Setting up a Print Preview Example:

Typically a Print Preview button, checkbox, or other such mechanism triggers the
preview. Continuing with the program used in previous sections, do the following:

1. Add a new button to Form1:


"btnPreview" with text "Preview"
Double-click the button to stub-in the method's code.
Leave the method empty for the time being.

2. Near the top of Form1's class definition, add a PrintPreviewDialog variable next to the
already-existing PrintDocument variable. This acts almost as-if it were another
PrintDocument type:

Create a PrintPreviewDialog variable: viewStuff


public partial class Form1 : Form
{
PrintDocument printStuff = new PrintDocument();
PrintPreviewDialog viewStuff = new PrintPreviewDialog();
:

Chapter 20 - Printing Page: 437


How to Pass a PrintPreview = true:

Ideally, the new btnPreview can call the same routine used by btnPrint1. By doing this,
they can share the same PrintDocument variable and the same Layout1 routine. To make
this work, btnPrint1 must accept some kind of flag that shows whether to use a Print
Preview.

This will cause a minor issue with Print1's Click routine. Consider btnPrint1's signature
line:

Practically speaking, btnPrint1's Click event signature cannot be changed; it is not easy
to change the system-generated Click event with a new signature line and yet there is still
a need to pass a value, such as "PrintPreview = true".

3. Resolve this problem by moving all of btnPrint1_Click's logic to a new function. This
can be done with a cut-paste:

• Below btnPrint1_Click's closing brace, manually build a new function called


A700_Print1, where the name A700 is somewhat arbitrary.

private void A700_Print1 (bool printPreviewSW)


{
//Cut and Paste all of btnPrint1_Click logic into here
}

• Then, Cut all of the logic in btnPrint1_Click (leaving the opening and closing
braces); Paste the code into the new A700 module.

Note how A700's signature line accepts a newly-invented boolean value called
"printPreviewSW"

4. Modify the now-empty btnPrint1_Click event with this command:

private void btnPrint1_Click (object sender, EventArgs e)


{
//Print with no Print Preview:
A700_Print1(false)
}

Chapter 20 - Printing Page: 438


5. Make the same changes to the previously-built btnPreview_Click with this similar
command:

private void btnPreview_Click (object sender, EventArgs e)


{
//Preview the Print job:
A700_Print1(true)
}

These changes allow either the Preview or Print routine to call the same A700_Print1
function. In other words, both btnPrint and btnPreview call a newly-built A700 routine.
That routine accepts a true/false Preview switch, allowing you to ignore the original
button's signature line. Next, add the logic to engage the Print Preview.

Adding the Print Preview Logic to A700_Print1:

Continue the work above by modifying the A700_Print1 routine. Recall that A700
contains the same printing logic that was formerly in btnPrint1_Click. Now add an if-
statement checking to see if the PrintPreviewSW is true or false.

6. Tie both buttons together into the A700_Print1 routine by using these new statements:

Chapter 20 - Printing Page: 439


A700_Print1: PrintPreview Logic, completed
private void A700_Print1 (bool printPreviewSW)
{
//Format the Page:
printStuff.PrintPage += new PrintPageEventHandler(layout1);

if (printPreviewSW == true)
{
//Show the preview but do not print:
viewStuff.Document = printStuff;
viewStuff.ShowDialog();
}
else
{
//Print normally

//Hide the Print Dialog box (optional):


PrintController stdPrintController =
new StandardPrintController();
PrintStuff.PrintController = stdPrintController;

printStuff.DocumentName = "Print1 Document Test";


printStuff.Print();
}

where:

• The call to PrintPageEventHandler (layout1) is the same as before. This means that
either a Print or Preview can share the same event. Remember, the page must be
formatted prior to the PrintPreview. If not, Print Preview shows a blank page.

• If printPreviewSW (switch) is true, point the existing PrintDocument "printStuff" to


the "viewStuff" variable. Then, viewStuff.ShowDialog, which is similar to a
MessageBox.Show command.

:
viewStuff.Document = printStuff;
viewStuff.ShowDialog();

Ultimately the preview logic hits the "else" and skips around the printing routines. If
the user wanted to print from the Preview screen, they can click the conveniently-
provided printer-icon button in the preview screen, illustrated below.

• If printPreviewSW is false, the entire Preview is bypassed and the normal printing
happens as before.

• The routine relies on this class variable, declared earlier in this section:
PrintPreviewDialog viewStuff = new PrintPreviewDialog();

Chapter 20 - Printing Page: 440


Testing Print Preview:

Press F5, then click btnPreview. Results: (text from previous examples as well as
assorted lines and boxes). Note the printer button in the upper-left corner.

Close the Print Preview and return to Form1.


Click btnPrint (Print1)
Results: Same fields should print as before. The printout should look remarkably similar
to the Preview.

Chapter 20 - Printing Page: 441


Printer Setup Dialog

To further complicate the example, it would be nice if the user could select alternate
printers or change the page-orientation, margins, etc. This is accomplished by giving
them access to the Windows's Page Setup Dialog box. For this example, a "Page Setup"
button could call this Dialog:

Setting the Stage for This Example:

This example piggy-backs on the A700_Print1 logic built in the previous section.
btnPrint and btnPrintPreview's logic was intercepted and redirected to a common routine
in A700_Print1.

Since the compiler-built btnPrint and btnPreview signatures were not easily changed, the
program needed an alternate way to pass a "Print Preview" switch to the (layout)

Chapter 20 - Printing Page: 442


routines. By having both buttons call a common A700_Print1 routine, other parameters
could be passed through the new signature line. In this example, a "true" or "false" was
passed, indicating if a Print Preview were required.

But now, there is a need for a second switch: "boolean Printer Page Setup SW". It is
easy enough to add this A700_Print1's signature line:

With A700's expanded signature, which allows two parameters (Preview, PageSetup),
the user can click either Print Preview or Page Setup before actually printing the job.

Preparing for Printer Setup's Dialog:

To keep the example simple, a new button btnPageSetup will be added to the Form. In
real life, this would probably be attached to the File, PrintSetup menu (File Menus were
not used in these examples).

Chapter 20 - Printing Page: 443


1. On the Form, add a new button, "btnPageSetup" with a Text value "Page Setup"
Stub-in the code by double-clicking the button, then complete the routine with this line
of code:

btnPageSetup, completed
private void btnPageSetup_Click (object sender, EventArgs e)
{
//Prepare to call the PageSetup Dialog.
//Note how two signatures are passed:
//printPreview = false
//pageSetup = true

A700_Print1 (false, true);


}

2. Modify the pre-existing A700_Print1 signature line, adding the new parameter:

A700_Print1's New Signature Line


private void A700_Print1 (bool printPreviewSW, bool printPageSetupSW)
{
: etc.

3. Because A700_Print1's signature was changed, other calls to this routine also need a
second item in their parameter's list. This can be done by overloading but it is easiest to
add a second parameter to the existing calls. Since neither of the previous routines call
the PageSetup logic, append a comma-false (,false) to each, acting as a required place-
holder:

private void btnPrintPreview_Click (object sender, EventArgs e)


{
A700_Print1 (true, false);
}

Chapter 20 - Printing Page: 444


Do the same for the previously-written btnPrint_click:

private void btnPrint_Click (object sender, EventArgs e)


{
A700_Print1 (false, false);
}

4. The only thing left to do is to insert the logic that opens the PageSetup dialog box. Much
like the PrintPreview routine, the PrintPageEventHandler(layout1) must be specified
before calling PageSetup. Insert this new code, shown in bold, after the layout1
statement and before the PrintPreview code:

Logic to open the Page Setup Dialog


private void A700_Print1 (bool printPreviewSW, bool printPageSetupSW)
{
//Format the Page:
printStuff.PrintPage += new PrintPageEventHandler (layout1);

//PageSetup Logic is here:


if (printPageSetupSW == true)
{
PrintDialog printSetup = new PrintDialog();
printSetup.AllowSomePages = true;
if (printSetup.ShowDialog(this) == DialogResult.OK)
{
//If user clicked OK, go ahead and print using layout1
//but don't fall into other routines such as printPreview
printStuff.Print();
return;
}
else
{
//user clicked Cancel; again,
//don't fall into other routines...
return;
}

//From here down is the original logic in the A700 module:


if (printPreviewSW == true)
:

where:

• If a "true" were passed from the "Page Setup" button, execute this new block of
code; otherwise, fall through to the previously-written printPreview and normal
Printing routines.

• If a PageSetup is demanded, declare a new PrintDialog object named "printSetup";


this follows along the same type of design seen with PrintPreview.

• The printSetup.AllowSomePages = true; statement is required. This essentially


gives the dialog permission to open.

Chapter 20 - Printing Page: 445


• When the user clicks OK in the print dialog box, run the printStuff.Print method, the
same as in previous examples. Notice immediately after printing, you must exit the
routine with a "return;". If you don't, the logic continues further down into the
A700 module, where it ultimately encounters the normal printing logic (logic
without a PageSetup or PrintPreview). Without the "return", the document would
print twice; once for the PageSetup OK and once for the normal logic.

• Finally, if the user clicked PageSetup's "Cancel", exit the entire A700 module with
no other action.

Chapter 20 - Printing Page: 446


Printing Text Files

Use C# to print an ASCII text file to the printer (this could also be a multi-lined textbox).
This example is complicated because the length of the printed file may extend past the
end of the page. The program needs a mechanism to force a page break.

This section explores some incorrect code with a minor flaw in


the logic. Study the examples carefully. As a reminder,
completed versions are shown in shaded code boxes.

Setting up the Example:

Program 20.3: Printing ASCII Text Files

For this example,

• add a new button to the Form, "btnPrintFile"

• Using Windows Notepad (Start, Run, Notepad.exe"), type a long, but narrow
document of at least 80 lines of text, which works out to a little more than one page.
Place a carriage-return after each line and use short, one or two word (even
nonsense) phrases for each line. With this, the line-widths will fit well within the
page-margins, which is a requirement for this example.

• In the text document, consider manually numbering the lines to confirm the page-
breaks occur properly. For example:

This is a test
1 asdf
2
4
3 (etc.) thru
:
80 lines

• Save the file as "C:\Data\Test.txt"

The logic to open and read the text file with a Priming Read is
taken directly from Chapter 12, ASCII Text Files, however, the
logic, while fundamentally sound, has a flaw when used in this
context. Please follow these examples carefully.

Chapter 20 - Printing Page: 447


Printing an External ASCII Text File:

Follow these steps to open and process the ASCII file:

1. At the top of the form's class definition, create four class-level (Form) variables that hold
a new PrintDocument, Font, a StreamReader Object and a pageCount. These are next to
the PrintDocument "printStuff" declarations built with previous examples:

Required Form-Level Variables


public partial class Form1 : Form
{
//From previous examples (Not used with ASCII printing):
PrintDocument printStuff = new PrintDocument();
PrintPreviewDialog viewStuff = new PrintPreviewDialog();

//In support of the FilePrint routines:


PrintDocument pd = new PrintDocument();
Font printFont;
StreamReader myAsciiFile;
int pageCount = 0;
:

Notice how the Font and StreamReader are both declared with variable names here but
are not assigned values. With this, they can be used in multiple functions and methods (a
broader scope).

Add a "using System.IO" statement, supporting the ASCII file-read:

Required using Statements for Printing and ASCII Files


using System.IO;
using System.Drawing.Printing;

2. In form design, double-click the new button, btnPrintFile.


This stubs-in the new method. Add this code:

Chapter 20 - Printing Page: 448


btnPrintFile_Click: The Calling Click Event
1 private void btnPrintFile_Click (object sender, EventArgs e)
2 {
3 string strFileName = "C:\\data\\test.txt";
4 printFont = new Font("Courier New", 11);
5
6 //Create the StreamReader object; already defined:
7 myAsciiFile = new = new StreamReader (strFileName);
8
9 //Setup the object and the Layout Routine:
10 //"pd" was declared above
1 pd.PrintPage += new PrintPageEventHandler (printFileLayout);
2 pd.Print();
3
4 //Close the Reader object:
5 if (myAsciiFile != null)
6 MyAsciiFile.Close();
7 }

where:

• The ASCII file is assigned a file name and opened at line 7


myAsciiFile = new = new StreamReader (strFileName);

• As a reminder, the "pd" PrintDocument was defined at the top of the program using
this statement:
PrintDocument pd = new PrintDocument();

• Line 11 builds the PrintFileLayout (the same idea as "layout1" in the previous
examples), which is called as an event when ever pd.Print executes.

pd.PrintPage += new PrintPageEventHandler (printFileLayout);


pd.Print();

Other events (Begin and EndPrint) will also go here as the examples progress.

3. Build the printFileLayout event:

In code view, move the editing cursor to a convenient location below the
btnPrintFile_Click module. As usual, be sure to pick a blank line after the other
methods. Avoid the two closing braces at the bottom of the program.

Manually type a new function-header and its opening and closing braces:

private void printFileLayout (object sender, PrintPageEventArgs ppeArgs)


{

Pay particular attention to the routine's signature line. On a whim, I used "ppeArgs"
instead of "e." ("PrintPageEventArgs").

Chapter 20 - Printing Page: 449


4. Declare these variables at the top of the Layout routine:

private void pritnFileLayout (ojbect sender, PrintPageEventArgs ppeArgs)


{
float linesPerPage = 0;
float yPos = 0;
int lineCount = 0;

string strReadLine = null;


float leftMargin = ppeArgs.MarginBounds.Left;
float topMargin = ppeArgs.MarginBounds.Top;

Graphics gobj = ppeArgs.Graphics;


:

Note they are a mixture of floating-point (decimal), integer, strings, and graphics. You
will also find a page-count increment, which helps emphasize this routine is called as
each page is printed. Continue with this code at line 20:

printFileLayout: Required Variables, initial coding


1 private void printFileLayout
(object sender, PrintPageEventArgs ppeArgs)
2 {
3 //Note: This Event is called as each page is printed
4 pageCount++;
5
6 float linesPerPage = 0;
7 float yPos = 0;
8 int lineCount = 0;
9
10 string strReadLine = null;
1
2 //Fetch the paper margins from the PrintPageEventArgs:
3 float leftMargin = ppeArgs.MarginBounds.Left;
4 float topMargin = ppeArgs.MarginBounds.Top;
5
6 //Establish a "graphics" object for font-calculations:
7 Graphics gobj = ppeArgs.Graphics;
8
9 //Calculate the lines per page:
20 linesPerPage = ppeArgs.MarginBounds.Height /
PrintFont.GetHeight (gobj);
:

Font Height Calculations - Lines per Page:

The crucial issue is how many lines to print on a page. As you had seen with the graphic
lines and boxes, if something extends past the margin it is truncated without warning and
text is no exception. This means you need to calculate the number of ASCII text lines
available on a page and this number can be found with a simple division using the font's
height and the total height of the paper, minus margins. All values are readily available.

Chapter 20 - Printing Page: 450


Line 17, above, establishes a variable"gobj" ("graphic object" or any other name) and
assigns it as type "Graphics". This gives access to various graphic properties on the
printed page, including font heights and margins.

17 Graphics gobj = ppeArgs.Graphics;

With this, linesPerPage can be calculated by dividing the page height by the height of the
font. Since Courier New, 11 points was selected as the default font (see
btnPrintFile_Click), the number is about 52 lines per page, as calculated by this
statement:

20 linesPerPage = ppeArgs.MarginBounds.Height /
printFont.GetHeight (gobj);

Iterating Through the Page:

Ponder for a moment where you are in the program. The "printFileLayout" event
triggers on each printed page: page-1, page-2, etc. and btnPrintFile_Click is the calling
routine. So far, there is no logic that retrieves each line and actually does the printing.
This same logic needs to watch the line count, making sure it does not overshoot the
page.

All of this is tied together in this part of the printFileLayout routine and, as you will see,
there are two surprises in the page-logic. Continue with this preliminary code:

Chapter 20 - Printing Page: 451


printFileLayout: Print Detail Lines, continued and preliminary
:
20 linesPerPage = ppeArgs.MarginBounds.Height /
printFont.GetHeight (gobj);
1
2 //Tentative: Read the File and iterate through the page,
3 //printing each detail. Assume line width fits within margins.
4
5 strReadLine = myAsciiFile.ReadLine(); //Tentative:Priming Read
6
7 while (lineCount < linesPerPage && strReadLine != null)
8 {
9 //Calculate the starting position for the printed line:
30 yPos = topMargin + (lineCount * printFont.GetHeight(gobj));
1
2 //Print the detail line:
3 gobj.DrawString (strReadLine, printFont, Brushes.Black,
leftMargin, yPos,
New StringFormat());
4
5 //Tentative: Move to the next line:
6 strReadLine = myAsciiFile.ReadLine();
7 lineCount++;
8 }
9
40 //If more detail lines exist, print another page:
1 if (strReadLine != null)
2 ppeArgs.HasMorePages = true;
3 else
4 ppeArgs.HasMorePages = false;
5
6 }

Chapter 20 - Printing Page: 452


where:

• As a reminder, the printFileLayout routine needs these supporting statements:


using System.Drawing.Printing
using System.IO

Also, these two Form-level variables were declared near the top of the program:
Font printFont;
StreamReader myAsciiFile;

• The ASCII file is opened in the button-click event and the priming read follows at
line 25; all of this was described in the ASCII-File chapter. After loading the first
record (so it survives the while-loop test), each line processed and a new line is
loaded into the buffer before re-looping. As you will see in a moment, this
introduces a subtle flaw in the program.

Detail-Line Printing Logic:

The loop travels through each line of the file until it finds a null-end-of-file record and it
also sports an additional test that watches the line-count; if it exceeds the allowed
number on a page, the loop ends:

strReadLine = myAsciiFile.ReadLine(); //Tentative Priming Read

while (lineCount < linesPerPage &&


strReadLine != null)

This leaves no obvious way to finish processing the file. If either an end-of-file marker
(null) or a page-break occurs, the loop ends. This is one of the surprising parts of the
printFileLayout event and it will be discussed in a moment.

The loop details are also interesting, especially at Line 30. This statement takes the
current line-number and calculates (in pixels) where the next printed detail line appears
by essentially shifting a cursor-location on the paper:

yPos = topMargin + (lineCount * printFont.GetHeight(gobj));

It does this by taking the current line number times the Font.Height and then adds that
result to the top margin. As the line-counter grows, the newly-printed line's "Y-axis
Position" creeps down the page. The values of this calculation are only important to the
computer; all you need to understand is the results are the Y-position of the next printed
line.

• The next line, 33, prints the current strReadLine, as described at the top of the
chapter. (Be sure to print with the same font that was used in the calculations.)

• The remaining two lines, 36, 37, increment the line-counter and read the next detail
line before re-looping. Both of these can trigger an end of the loop. The line-
counter could exceed the calculated lines-per-page or strReadLine could encounter a
null-record, indicating end-of-file:

Chapter 20 - Printing Page: 453


27 while (lineCount < linesPerPage &&
strReadLine != null)
8 {
9 //Calculate the starting position
30 yPos = topMargin + (lineCount * printFont.GetHeight(gobj));
1
2 //Print the detail line:
3 gobj.DrawString(strReadLine, printFont, Brushes.Black,
leftMargin, yPos, new StringFormat());
4
5 //Tentaive: Move to next line:
6 lineCount++;
7 strReadLine = myAsciiFile.ReadLine();
8 }

Handling the Page-Break Logic:

So far, the logic makes sense, but you should sense a problem with the page-break logic.
Ultimately, the routine needs to print all of the detail lines from the ASCII text file but
the while-loop ends when the number of printed lines exceed the page-limit and there is
no obvious way to finish the file.

printFileLayout: Print Detail Lines, preliminary ending routines


:
24 //Tentative: Priming Read
5 strReadLine = myAsciiFile.ReadLine(); //Priming Read
6
27 while (lineCount < linesPerPage &&
strReadLine != null)
8 {
9 //Calculate the starting position
30 yPos = topMargin + (lineCount * printFont.GetHeight(gobj));
1
2 //Print the detail line:
3 gobj.DrawString(strReadLine, printFont, Brushes.Black,
leftMargin, yPos, new StringFormat());
4
5 //Tentative: Move to next line:
6 lineCount++;
7 strReadLine = myAsciiFile.ReadLine();
8 }
9
40 //If more lines exist, print another page:
1* if (strReadLine != null)
2 ppeArgs.HasMorePages = true;
3 else
4 ppeArgs.HasMorePages = false;
5
6 } //End of the printFileLayout Event

Here is the trick. Line 41 checks to see if the end-of-file has been reached. If not, it sets
a property for the printFileLayout Event: HasMorePages = true.

Chapter 20 - Printing Page: 454


This tells the printFileLayout event to re-execute until no more detail lines are found in
the original ASCII file. This is admittedly somewhat of a programming trick, making the
loop implicit rather than explicit. It works like this: Since the ASCII file was not closed,
the ASCII file's record pointer is still positioned at the last-record-read and when the
routine calls itself a second time, it simply continues with the same while-loop as before.

Initial Testing with Flawed Results:

Press F5 to run the program, then click button PrintFile.

Results: (80-lines; 2-pages) should print at the printer. Confirm that each line printed
by examining the ASCII file's manually-typed line numbers 1,2,3... 80. A surprising
result should be that line (53 - possible variation) did not print.

The Problem with Priming Reads and Recursive Calls:

When the C#-compiler sneakily re-executes the printFileLayout event (almost like a
recursive call) it introduces two flaws in the logic which have not been experienced in
prior loops.

First, the statements that calculate the Left and Top Margins re-calculate with each page.
This is a minor inefficiency which is easily solved by moving this logic higher-up in the
program (at the expense of a class-level/form-level variable). This is a trivial issue and it
can be ignored.

More importantly is a flaw introduced by the priming read logic, especially at line 37
(the "read the next record" line). When the lines-per-page is exceeded, the loop ends
prematurely, but the next record was already read at line 37. When the routine is re-
executed, the priming read at line 25 obliterates the record at line 37. This problem is
caused by the layout routine being called multiple times against the same open file.

If the ASCII record pointer were pointing at text-line 52 (bottom of page 1)


and then the same routine were called again, the next-record Read statement
(at line 37) is over-written by the new iteration of the priming read. This
means line 53 is skipped. On longer reports, the first-line of every new page,
past page-1, is lost. You can see this in the test results.

This was never a problem in previous chapters because the loop was only called one time

The Solution:

Re-write the while-loop in a more succinct fashion by deleting the priming-read


statement at line 25 and the next-record-read at line 37 and replacing the while-loop with
a more sophisticated control:

Chapter 20 - Printing Page: 455


printFileLayout: Re-iterated while-loop with Embedded Priming Read, completed
:
25 strReadLine = myAsciiFile.ReadLine(); //Priming Read
26
27 while (lineCount < linesPerPage &&
(strReadLine = myAsciiFile.ReadLine()) != null)
28 {

:
37 strReadLine = myAsciiFile.ReadLine();
:

This combines the priming read and the next-record read into one line of code and many
developers use this style. Even on the first-read, the very act of testing the while-loop
causes the strReadLine clause to execute, populating the variable. Then, with each
iteration, the test causes another read.

This design was not described in the past because it is not


intuitively obvious about how it works and this statement is more
difficult to understand:

where:

• The variable strReadLine is first populated with data, then compared against null.
The parenthesis force this order:

• Without the parenthesis, the null comparison has essentially two equal-signs and it
confuses the compiler with this error: Operator '&&' cannot be applied to operands
of type 'bool' and 'string'.

By writing the loop in this manner, the problem of re-executing


the same module is solved and the records are processed
correctly. Why the scenic route to this solution? For beginning
programmers, this is a complicated statement that has many
nuances. Also, this proves the value of careful testing. This

Chapter 20 - Printing Page: 456


does not mean to imply that Priming Reads are flawed; only
when recursively called.

GraphicsUnit.Millimeters:

For this example, the GraphicsUnit in millimeters was not specified, leaving the default
pixel ruler. It could be explicitly mentioned with this statement:

ppeArgs.Graphics.PageUnit = GraphicsUnit.Pixel; //not Millimeter;

Sadly, in this case, millimeters causes problems (or at least complexities) because the
MarginBounds and font-size commands only return results in pixels.

j For this reason, when working with text and font-heights, I recommend staying with
Pixels; other routines can switch to millimeters when positioning other graphic
elements.

Completed ASCII File Print Routine:

Program 20.4: Printing an ASCII File with Page Numbering

This is the completed ASCII File print routine including additional logic to print page
numbers at the bottom of each page. Note changes at lines 27, 33 and 40.

printFileLayout: With PageBreaks and PageFooters, completed


See also btnPrintFile_Click, above, for related logic

1 private void printFileLayout


(object sender, PrintPageEventArgs ppeArgs)
2 {
3 //Note: This Event is called as each page is printed
4 pageCount++;
5
6 float linesPerPage = 0;
7 float yPos = 0;
8 int lineCount = 0;
9
10 string strReadLine = null;
1
2 //Fetch the paper margins from the PrintPageEventArgs:
3 float leftMargin = ppeArgs.MarginBounds.Left;
4 float topMargin = ppeArgs.MarginBounds.Top;
5
6 //Establish a "graphics" object for font-calculations:
7 Graphics gobj = ppeArgs.Graphics;
8
9 //Calculate the lines per page:
20 linesPerPage = ppeArgs.MarginBounds.Height /
PrintFont.GetHeight (gobj);

Chapter 20 - Printing Page: 457


1
2 //Read the File and iterate through the page,
3 //printing each detail. Assume line width fits within margins.
4
5 strReadLine = myAsciiFile.ReadLine(); //Tentative:Priming Read
6
7 while (lineCount < (linesPerPage - 2) &&
(strReadLine = myAsciiFile.ReadLine()) != null)
8 {
9 //Calculate the starting position for the printed line:
30 yPos = topMargin + (lineCount * printFont.GetHeight(gobj));
1
2 //Print the detail line:
3 gobj.DrawString (strReadLine, printFont, Brushes.Black,
leftMargin, yPos,
New StringFormat());
4
5 //Count the detail line and reloop:
6 strReadLine = myAsciiFile.ReadLine();
7 lineCount++;
8 }
9
40 //For fun: Print page-numbers at bottom of page:
1 gobj.DrawString ("Page: " + pageCount,
printFont, Brushes.Gray,
leftMargin,
yPos + printFont.GetHeight(gobj) * 2,
new StringFormat());
2
3 //If more detail lines exist, print another page:
4 if (strReadLine != null)
5 ppeArgs.HasMorePages = true;
6 else
7 ppeArgs.HasMorePages = false;
8 }

End: printFileLayout

Chapter 20 - Printing Page: 458


Setting New Margins, Landscape

If you have been following along with the previous examples, the printed test results had
generous margins. On my laser printer, Program 20.4 printed with a default left-margin
of 25mm plus an average 6mm non-printable, for a total margin of 31mm (1.25 inches)
which is probably wider than most people would like.

In this section, use code to change the page-layout settings for the left and top margins as
well as switching to landscape. You will find that changed margins and page-lengths
still calculate their lines-per-page properly. This section continues with Program 20.4,
developed on the previous page.

The next section displays a Page Setup Dialog box.

Investigate the Current Margin Setting:

The current leftMargin is calculated in Pixels and mere human beings are not privy to the
actual value. Do the following to find the value:

1. Near the top of Program 20.4's "printFileLayout," locate the topMargin calculation and
place a breakpoint on this line (line 14, previous page) . To insert a breakpoint, click in
the grey margin while in code view. The breakpoint is one line below the leftMargin –
where the leftMargin is the line of interest:

2. Run the program and click btnPrintFile.

• The compiler runs up to the topMargin statement and then stops


• Click once on the Visual Studio Titlebar to focus on the editor
• Hover the mouse over the "leftMargin" variable
• The pop-up shows the current default value: 100.0 (pixels)

3. Click the red-square on the toolbar to stop debugging (or press Shift-F5), returning to the
editor.

Chapter 20 - Printing Page: 459


Setting New (Printable) Margins:

The best place to change a PrintDocument's margin is immediately after it is created.


Notice this is in the btnPrintFile_Click routine and not within the printLayout module;
this way it is set one time and not with each page. The code is straight-forward:

Setting Default Margins and Orientation


private void btnPrintFile_Click (object sender, EventArgs e)
{
string strFileName = "C:\\data\Test.txt";
printFont = new Font("Courier New", 11); //declared above

//Create the StreamReader Ojbject:


myAsciiFile = new StreamReader (strFileName); //declared above

//(Later: These move to a pd.BeginPrint routine)


pd.DefaultPageSettings.Margins.Left = 30; //Pixels
pd.DefaultPageSettings.Margins.Top = 30;
pd.DefaultPageSettings.Landscape = true;

where:

• The PrintDocument name was named "pd" and was defined at the top of the program

• The Margin ruler is still using the default pixels (not millimeters)

• The printFileLayout routines properly calculate the printable margins and the lines-
per-page even though the paper is rotated landscape

• Later in this chapter, much of this code can be moved to the BeginPrint event

Testing the New Margins:

Run the program and click Print File as before. Results: Landscape printing with
narrower margins and different page-breaks. Because of the Landscape, this likely
prints on three sheets of paper.

Chapter 20 - Printing Page: 460


Begin and End Print Events

Buttons and other Form objects can have Enter and Exit events (got and lost focus), and
so too can Print events. BeginPrint and EndPrint events fire just before the first page
prints and after the last page and these events are useful for segregating setup and
disposal routines, making code more organized and manageable. There is not a
BeginPage/EndPage event.

Which Variables Can Move?

The btnPrintFile_Click routine opens an ASCII text file, sets new margins, defines a
printLayout event and prints the file. Below is the btnPrintFile_Click method, as it
existed from previous examples. Should any of these lines be moved into a BeginPrint
routine? How can you tell which can move and which must stay?

Current btnPrintFile_Click before using pd.BeginPrint, repeated


private void btnPrintFile_Click (object sender, EventArgs e)
{
string strFileName = "C:\\data\Test.txt";
printFont = new Font("Courier New", 11); //declared above

//Create the StreamReader Ojbject:


myAsciiFile = new StreamReader (strFileName); //declared above

pd.DefaultPageSettings.Margins.Left = 30; //Pixels


pd.DefaultPageSettings.Margins.Top = 30;
pd.DefaultPageSettings.Landscape = true;

pageCount = 0;

pd.PrintPage += new PrintPageEventHandler(printFileLayout);


pd.Print();

//Close the Reader:


if (myAsciiFile != null)
MyAsciiFile.Close();

The rule is this: If a variable is defined at a higher scope, its logic can often be moved to
another routine. Knowing this, these can be moved into a BeginPrint method:

printFont
myAsciiFile
pd.DefaultPageSettings (various)
pageCount

Additionally, since strFileName is defined and used only with the myAsciiFile "open"
statement, it can follow the moved logic.

Chapter 20 - Printing Page: 461


Defining BeginPrint and EndPrint:

The btnPrintFile routine constructed a PrintPage "layout" event and this event fired when
a .Print method was called. Using similar syntax, a BeginPrint and EndPrint event can
be declared.

1. Directly above the pd.PrintPage statement, repeated here, create two new events:

:
//Define the BeginPrint and EndPrint events here:
pd.BeginPrint += new PrintEventHandler(printFileBegin);
pd.EndPrint += new PrintEventHandler(printFileEnd);

pd.PrintPage += new PrintPageEventHandler(printFileLayout);


:

where:

• Both BeginPrint and EndPrint are new Events that fire when ever a .Print is
executed. Notice how these point to a new method name, much like the
"printFileLayout" routines described earlier.

The resulting btnPrintFile_Click routine, when all is done, will be much smaller. In the
code listed below, the strikeout text will be cut and pasted into new routines; wait a
moment before cutting the statements.
Current btnPrintFile_Click Before Using pd.BeginPrint Logic
private void btnPrintFile_Click (object sender, EventArgs e)
{
//This code can be moved into the BeginPrint routines:
string strFileName = "C:\\data\Test.txt";
printFont = new Font("Courier New", 11); //declared above

//Create the StreamReader Ojbject:


myAsciiFile = new StreamReader (strFileName); //declared above

pd.DefaultPageSettings.Margins.Left = 30; //Pixels


pd.DefaultPageSettings.Margins.Top = 30;
pd.DefaultPageSettings.Landscape = true;

pageCount = 0;

//Define the BeginPrint and EndPrint events here:


pd.BeginPrint += new PrintEventHandler(printFileBegin);
pd.EndPrint += new PrintEventHandler(printFileEnd);

pd.PrintPage += new PrintPageEventHandler(printFileLayout);


pd.Print();

//Close the Reader (this can move to the EndPrint routine):


if (myAsciiFile != null)
MyAsciiFile.Close();

Chapter 20 - Printing Page: 462


2. As with other layout routines, locate a safe location in the program where you can
manually create two new methods, where the two method names agree with the signature
lines above:

private void printFileBegin (object sender, PrintEventArgs peaArgs)


{
//Manually stub-in the BeginPrint event
}

and

private void printFileEnd (object sender, PrintEventArgs peaArgs)


{
//Manually stub-in the EndPrint event
}

3. Cut and paste the first group of strikeout text into the printFileBegin module. The final
results will look like this and includes a MessageBox to help visualize where you are in
the program:

printFileBegin, completed
private void printFileBegin (object sender, PrintEventArgs peaArgs)
{
//Initialize variables in preparation for the real print:

MessageBox.Show("Begin Printing"); //Diagnostic MessageBox

string strFileName = "C:\\data\Test.txt";


printFont = new Font("Courier New", 11); //declared above

//Create the StreamReader Ojbject:


myAsciiFile = new StreamReader (strFileName); //declared above

pd.DefaultPageSettings.Margins.Left = 30; //Pixels


pd.DefaultPageSettings.Margins.Top = 30;
pd.DefaultPageSettings.Landscape = true;

pageCount = 0;
}

4. Cut and paste the second group of strikeout text into the printFileEnd module:

printFileEnd, completed
private void printFileEnd (object sender, PrintEventArgs peaArgs)
{
//This event runs after the last page prints
MessageBox.Show("End of Printing"); //Diagnostic MessageBox

//Close the StreamReader:


if (myAsciiFile != null)
MyAsdciiFile.Close();
}

Chapter 20 - Printing Page: 463


5. Confirm the original btnPrintFile_Click looks like this:

btnPrintFile_Click, completed
private void btnPrintFile_Click (object sender, EventArgs e)
{
//Setup the main ASCII File Print, now much smaller:

pd.BeginPrint += new PrintEventHandler (printFileBegin);


pd.EndPrint += new PrintEventHandler (printFileEnd);

pd.PrintPage += new PrintPageEventHandler (printFileLayout);


pd.Print();
}

where:

• BeginPrint and EndPrint must be defined at the button's click event or else the code
would never execute. Some beginning programmers mistakenly put the Begin and
End definitions within the "printFileBegin" routine.

• In order to move other logic, such as the printFont, myAsciiFile and pageCount to
other routines, those variable scopes must be high-enough in the program to still be
active.

Testing the New Logic:

Press F5 to run the program and then click on the Print File button.

Results: A MessageBox announcing a begin-print, 2 to 3 pages printed, followed by an


"ending" MessageBox. All margins, fonts, etc., are set properly and the logic is clearly
segregated into Begin, middle and End routines.

Chapter 20 - Printing Page: 464


Exercises

A. Program 20.3 / 20.4 (btnPrintFile: Printing a multi-page ASCII text file) has a bug. If
you press btnPrintFile twice in a row, without closing the running program, the Page
Count is wrong. Fix the problem with a single, well-placed line of code.

B. In Program 20.4, make these cosmetic changes

• Print a thin horizontal line above the "Page: nn"


• Center the "Page: nn" between the margins.
• Make the last-page "Page: nn" prints properly at the bottom of the paper rather than
after the last line in the ASCII file.

Chapter 20 - Printing Page: 465


Chapter 21 - Formatting Numbers and Text Page: 466
A Beginners Guide to C-Sharp - Volume 2
Visual Studio C# 2017
ASCII through Advanced Formatting
by Tim R. Wolf
© 2017.06.01 1.02
Table of Contents

9 Chapter 12 - ASCII Files 17


Reading ASCII (Text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
StreamReader
Priming Read
Ending the Loop and Closing Files
Using an EndOfStream Read
The "Using" Clause
ASCII File Reads with try-catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Issues with try/catch and Close
Chaining catch-statements
Completed ASCII ReadFile - Program 12.5. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Writing ASCII (text) Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Appending ASCII (text) Files - Advanced Open Methods. . . . . . . . . . . . . . . . . . . . 54
Preventing Append from Running Twice

9 Chapter 13 - Parsing Tab and CSV Files 65


Automatic Parsing by Delimiter (Split).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Declaring the Destination Array
foreach
Alternate Loops
.Split Limitations
CSV Files
Split Example: Phone-Numbers
Manually Parsing Comma-Delimited Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
First Field Logic
for-next Logic
Substring
Last-field Logic
Tab-Delimited Files
Parsing a Variable Number of Columns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Parsing ASCII CSV Files with Embedded Commas.. . . . . . . . . . . . . . . . . . . . . . . 105
Error Processing in CSV Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
Moving ParseCSVLine to the Utility Library

9 Chapter 14 - INI Files 147


INI File Structure and Design. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
A015: Loop Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
A017: Parse INI Detail Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
A028: Finding the Application's Default INI Location.. . . . . . . . . . . . . . . . . . . . . 178
Command-Line Override.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Simulate a Command Line Options
Embedded Spaces in the Parameters
Command-line ini=
A029: Write Default INI when Missing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Basic INI File Read: Complete Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Using a Program Class - CL860 INI File Read. . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
CL860 Basic INI Read Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Using CL860_BasicINIRead

9 Chapter 15 - xml and App.config Files 223


xml File Structure
app.config xml.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Using (Reading) app.config
Building a Manual xml File.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
Reading xml File Sequentially. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239

9 Chapter 16 - Windows Registry 249


Organization of the Registry
Reading a Specific Registry Key.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Opening the Registry
Closing
Reading a Specific Registry Value
Reading Mulitple Keys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Reading All Values within a SubKey. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Multi-Line String Registry Values. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Creating/Modifying Name-Value Pairs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Opening the Registry in 'Writeable' Mode
Creating Sub-SubKeys. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Deleting Values and Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Deleting Entire SubTrees
Deleting the Current Key
Enumerating SubKey (folders). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

9 Chapter 17 - Reading Excel and Access 289


Reading an Excel File using a COM object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291
Open the Workbook
parameter list
Reading a Row
comboBoxes based on Access Database Tables. . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Attaching Microsoft Access Data to a comboBox. . . . . . . . . . . . . . . . . . . . . . . . . 317
ODBC Call
Connecting to the comboBox
Refreshing the List
Performance Considerations

9 Chapter 18 - External Programs (Shell) 333


Starting a New Process. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
Launching Notepad with a Specific Filename
Building an Exit Event
Using Code to Start a New Process.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
proc.WaitForExit.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
DOS Output.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Capturing DOS output into an Array
Multiple Instances of an Application. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
Simple Instance Test
Mutex: A Better Solution for Multiple Instances. . . . . . . . . . . . . . . . . . . . . . . . . . 355

9 Chapter 19 - Waits, Delays and Pauses 361


Poor Wait States – Not Recommended
Empty loops
current time and loop until xx seconds
Sleep events
System.Threading.Thread.Sleep (milliseconds). . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Completed wait Simulation - Recommended. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Wait as a Class Library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Using cs805_Wait
Calling Wait
Timer Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Basic Timer Code
Event Horizons
Countdown and Timer Example
Disabling the Close "X" Mid-Transaction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Intercepting a Close Event
Countdown and Timer Program - Completed. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Simple Startup Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
Splash Screens.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

9 Chapter 20 - Printing 415


Printing Simple Text. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
Print Layout / Print Rendering
Pixel Positions in Millimeters (x,y)
OverShooting
Printing Horizontal Lines. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
Printing Rectangles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
Filling
Printing Graphics.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Printer Dialogs.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Adding a Custom Printer Dialog
Print Preview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Printer Setup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Printing Text Files.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Font Height Calculations - Lines per Page
Page-Break Logic
GraphicsUnit.Millimeters
Setting New Margins, Landscape. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
Current Margin
Setting New (Printable) Margins
Begin and End Print Events.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

9 Chapter 21 - Formatting 469


Font Color, Font Bold. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473
Font Bold
Font Style Ariel
Basic String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
String.Format with Alignment.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Proportional Fonts
Numeric Formatting with String.Format.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Standard Decimals
Formatting with Alignment
Commas and Currency
Numeric Picture Clauses. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Padding Zero with Leading and Trailing Decimals
Place Holder (#)
Thousands Separator ( , )
Variable Picture Clauses (Group Separators)
Date and Time Pictures. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Converting from Strings
Converting Dates
Other Date Time Properties and Methods.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
DateTime Formatting. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Pictures
Display Dates, Year first
Format Class Library - cl710_Formatting.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Phone Number Formatting
Proper Names
Proper Addresses
PhoneNumberFormat Method - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505
PhoneNumberFormat Module - Coding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Extension Extract
Strip Normal Punctuation
Strip "1-" Prefixes
Punctuate
Punctuating the AreaCode
"ProperNames" Formatting - Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
ProperNamesFormat - Coding.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
A Beginner's Guide to C-Sharp - ABGC
Published by Tim R.Wolf, © 2017
CH21 - Formatting
Chapter 21 - Formatting

This chapter describes how to format and punctuate numbers and text using a variety of
techniques. Numbers can be formatted with dollar-signs, comma-separators, and fixed
decimal points. Text and numbers can be printed at set fixed widths, justified to the right
or left. Date-formatting and date-arithmetic are also covered in detail.

This chapter also covers these two interesting topics:


• Formatting phone numbers - with Area-codes, prefixes and extensions.

• "Proper" (Initial uppercase for names and addresses) with appropriate punctuation.
For example, "Bob VanNuys", "Thomas Jones, Jr." and "123 N. Elm St." - all can be
cased and punctuated automatically, regardless of how typed.

Topics:

• Review: Setting FontStyle.Bold, New Font("Ariel")


• Using String.Format with {0} position markers
• String.Format with alignment {0,15}, {0,-15}
• String.Format with multiple columns

• Simple Numeric Formatting


• Numeric Picture Clauses {0:$#,#0.00; ($#,#0.00);-0-}

• DateTime.Now
• DateTime Parsing from Strings
• DateTimeParsing from Numeric Values
• DateTime .ToString, .Hour, etc.
• DateTime Picture Clauses {0:dd/MM/yyyy}

• Building a Format Class Library


• formatting.PhoneNumberFormat (with complete source code)
• formatting.ProperNamesFormat (with complete source code)

Chapter 21 - Formatting Numbers and Text Page: 469


Summary:

Below is a summary of many of the most commonly-used formatting techniques. Each


are covered in greater detail within the chapter and this list is not meant to be
comprehensive.

Font Summaries
Changing a Font Color: two methods displayed:

Using a text color (Recommended):


textBox1.ForeColor = Color.Red;

Using a Red-Green-Blue (RGB) Numeric value:


textBox1.ForeColor = Color.FromArgb (255,0,0); //Red

textBox1.Font = new Font(textBox1.Font, FontStyle.Bold);


textBox1.Font = new Font
(textBox1.Font, FontStyle.Bold | FontStyle.Underline);

Changing Fonts:
textBox1.Font = new Font("Ariel", FontStyle.Regular);

Basic String and Numeric Formatting


textBox1.Text = String.Format ("item {0} has {1} units", var1, var2);

String.Format("{0,-15}", var0); Left justified, 15 spaces wide

String.Format("{0,-15} {1,6}", var0, var1);


Two columns; left and right
just.

Basic Numeric Formatting:

Format Clause Results using "-1223.1451F"

"{0,15}" -1223.1451 15 chars wide, Right justified


"{0,-15}" -1223.1451 15 chars wide, Left justified
"{0,f2}" -1223.15 Fixed number, 2 decimals,
rounded
"{0,f6}" -1223.145100 Fixed number, 6 decimals, padded
"{0,15:n2}" -1,223.15 n for 'numbers' with commas
"{0,15:c2}" ($-1,223.15) c for 'currency'
"{0,15:c2}" ($-0.15) using -.15F; note leading zero.

Chapter 21 - Formatting Numbers and Text Page: 470


Numeric Picture Clauses using "1223.1451F"
"{0:0.000000}" 1223.145100 Padded zeroes on Right
"{0:.00}" .15 note no leading zero
"{0,15:0.00}" 1234.15 15 chars wide, 2 decimals,
round.
"{0:0}" 1224 No decimals, rounded
"{0:000000}" 001223 Prepended zeroes, no decimal

"{0:#.###}" 1223.145 Place-holder; no padding with


zeroes.

"{0:#,#0.00}" 1,223.15 Thousands comma-separator, two


decimals. (0.15).

"$" + String.Format ("{0:#,#0.00}")


$1,223.15 Thousands with literal "$"
($0.15)

"{0:$#,#0.00; ($#,#0.00);-0-}"
$1,223.15
($1,223.15)
-0- Variable pictures for positive,
negative and zero values

Date and Time


DateTime dtValue = DateTime.Now;
strDate = DateTime.Now.ToShortDateString(); //mm/dd/yyyy

DateTime dtValue = DateTime.Parse("3/24/1981 2:23:11");

DateTime dtValue = new DateTime (1981, 03, 24); //Numeric


DateTime dtValue = new DateTime
(Convert.ToInt32 ("1981"),
Convert.ToInt32("03"),
Convert.ToInt32("24"));

dtValue.ToLongDateString() //"Tuesday, March 24, 1981"


dtValue.ToShortDateString() //"3/24/1981"
dtValue.ToString ("MM/dd - hh:mm tt") //"3/24 7:23 AM"

String.Format ("{0:dd/MM/yyyy}", dtValue);


dtValue.ToString ("dd/MM/yyyy");

Adding / Subtracting Date-Times:


dtValue.AddDays(15)
dtValue.AddDays(-15)

Chapter 21 - Formatting Numbers and Text Page: 471


Using the cl710_Formatting.cs Class Library
The "PhoneNumberFormat" and the "ProperNameFormat" are not native to C# and
are developed in a Formatting Class Library, as described in the text.
PhoneNumberFormat:

strNewPhoneNumber = formatting.PhoneNumberFormat
(strOriginalPhoneNumber,
strFormatStyle,
strDefaultAreaCode,
boolRequireAreaCode,
boolReturnBlankOnError);

where: FormatStyle = "AM1", "AM2", "EU" (American or European)

ProperNameFormat

strNewText = formatting.ProperNamesFormat
(strOriginalText, "NAME")

where the RecordType can be: "NAME", "ADDRESS" or "OTHER"

Chapter 21 - Formatting Numbers and Text Page: 472


Font Color, Font Bold

Starting with a font's color, programmatically change the font color with either of these
two statements. The first technique uses a system color variable and the second specifies
precise Red-Green-Blue color numbers.

Although setting a textBox to Bold or changing its color is not


strictly a formatting concern, readers may expect to find this
information in this chapter. Is repeated here, from Chapter 10,
Forms.

Use one or the other syntaxes:

Example: textBox Foreground Color (Font Color)


private void button1_Click (object sender, EventArgs e)
{
//Changing a Font Color: two methods displayed:

//Using a text color (Recommended):


textBox1.ForeColor = Color.Red;

//Using a Red-Green-Blue (RGB) Numeric value:


textBox1.ForeColor = Color.FromArgb (255,0,0); //Red
}

The system-defined color scheme is expansive:

Chapter 21 - Formatting Numbers and Text Page: 473


Font Bold:

Programmatically setting a font (Bold) is messier. This syntax fails:

//These syntaxes fail...


textBox1.Font = Font.Bold; //Fails
textBox1.Font.Bold = true; //Fails

The correct syntax for a Bolded font requires instantiating a new font before it can be set.
Note the details of the command: it sets the New font to the existing 'textBox1.Font', and
then changes the FontStyle to Bold:

Example: textBox Font Bold


private void button1_Click (object sender, EventArgs e)
{
textBox1.Font = new Font(textBox1.Font, FontStyle.Bold);
}

Font Style Bold and Underline:

Example: textBox Font Bold and Underline


private void button1_Click (object sender, EventArgs e)
{
//Note the OR symbol:

textBox1.Font = new Font


(textBox1.Font, FontStyle.Bold | FontStyle.Underline);
}

Chapter 21 - Formatting Numbers and Text Page: 474


FontStyle is an "enumerated" type, which means the options are "additive". The author
does not know why Microsoft chose a split-vertical bar as the delimiter.

Font Style Ariel:

Change the font to Arial, regular:

Example: textBox Font Style / Font Change


private void button1_Click (object sender, EventArgs e)
{
textBox1.Font = new Font("Ariel", FontStyle.Regular);
}

See also, Chapter 10, Forms. The next section begins more proper formatting
statements.

Chapter 21 - Formatting Numbers and Text Page: 475


Basic String.Format

Strings can be assembled by concatenating text using the +plus sign. For example,
assume two values: an item-name and a quantity with an expected output of "Item
Airplane has 127 units in stock". The code could be constructed in this manner:

string stritemName = "Airplane";


int iquantity = 127;

textBox1.Text =
"Item " + stritemName + " has " + iquantity + " units in stock";

In the assembled string, care was taken to insert extra spaces, especially near the variable
names, e.g. "Item(space)" and "(space)units in stock".

"String.Format" is an alternate way to assemble the final string. With this method, the
output string is one continuous literal, requiring only one pair of quotes and no +pluses.
{place holders} live where the variables would normally go. Consider this new
textBox1.Text statement:

Example: String.Format with a parameter list


private void button1_Click (object sender, EventArgs e)
{
string stritemName = "Airplane";
int iquantity = 127;

//Output: "Item Airplane has 127 units in stock":

* textBox1.Text = String.Format
("Item {0} has {1} units in stock", stritemName, iquantity);
}

where:

• {0} and {1} are placeholders for the variables listed at the end of the statement.
stritemName is first in the list and is represented by"{0}" and {1} is the second
variable, iquantity.

• In the quoted string, notice the embedded spaces, which are typed more naturally.

Chapter 21 - Formatting Numbers and Text Page: 476


Possible Errors:

You may experience these errors as you typed the code:

Compiler Error "Invalid expression term '{'.


Solution: Missing quotes for the quoted string. Even if a single number is being
displayed (without any other text), the quotes are still needed. Consider this minimalist
example:

string.Format ("{0}", value); //quotes still required

Runtime Error "Format Exception was unhandled" along with "Index (zero based) must
be greater than or equal to zero and less than the size of the argument list".
Solution: You forgot the comma-separated parameter list (e.g. stritemName, iquantity).

Chapter 21 - Formatting Numbers and Text Page: 477


String.Format with Alignment

Within the braces, a {parameter} can be allotted a fixed number of spaces and then
justified right or left. For example, parameter {0} can be assigned 15 spaces and right
justified by using {0,15}. The same value could be left-justified using {0,-15}, with a
minus-sign.

Example: String.Format with Justified Text


private void button1_Click (object sender, EventArgs e)
{
string stritemName = "Airplane";

//Output a 15-character-wide field, justified Right.


//Use -15 for Left justfication:

textBox1.Text = String.Format ("'{0,15}'", stritemName);


}

Chapter 21 - Formatting Numbers and Text Page: 478


where:

• {0} is the place holder for the first and only field, strItemName. "{0,15}" means it
gets 15 character positions. Using a fixed-space font (Courier new) in textbox1
makes this abundantly clear.

• Use {0,15} justifies to the Right-side; plus-signs not allowed.


Use {0,-15} for Left justification, minus required.
Think -/+ Left/Right

• The 'tic-marks help illustrate the field width for these examples and are not required.

String.Format Alignment with Multiple Items (Columns):

More commonly, lists often have multiple values displayed in columns. For simplicity,
consider these two itemNames and quantities. As a reminder, the textBox is using a
CourierNew fixed-width font and the box is multi-lined. With this, columns line up
nicely:

Chapter 21 - Formatting Numbers and Text Page: 479


Example: String.Format with Multiple Columns; Mixed text and numbers
private void button1_Click (object sender, EventArgs e)
{
string stritemName1 = "Airplane";
string stritemName2 = "Car";
int iquantity1 = 127;
int iquantity2 = 1;

//Format both items and their quantities in a nice columnar view:

textBox1.Text =
String.Format ("{0,-15} {1,6}", stritemName1, iquantity1) +
"\r\n" +
String.Format ("{0,-15} {1,6}", stritemName2, iquantity2);
}

where:

• Note the two variables, {0} and {1} and each has a size parameter {0,-15} and {1,6}.
This takes some practice to read.

• The first column is left-justified with 15 spaces (-15) and the second column is right-
justified with 6 positions. The alignment depends on a fixed-space font (courier-
new) in the display area.

• If the printed itemName were longer than 15 characters, it would still print,
extending into the next "column" and pushing the numeric value out of alignment.

• The quantity values automatically convert to string and do not need an explicit
"Convert.ToString". You could use this parameter list:
..., stritemName2, iquantity2.ToString()

• {0,-15} is often easier to use than PadLeft and PadRight.

Aligning strings with this method may not work well with decimal-pointed numeric
values. In these instances, use more advanced picture clauses, described later in this
chapter.

Proportional Fonts:

As an aside, if the example above used a proportional font (Arial, Times New Roman,
etc), the columns would not line up properly and this is difficult to resolve in code
(hence the reliance on a courier-new, non-proportional font):

Chapter 21 - Formatting Numbers and Text Page: 480


The next several sections deal with numeric formatting.

Chapter 21 - Formatting Numbers and Text Page: 481


Numeric Formatting with String.Format

C# has a variety of other String.Format "picture clauses" which can help format numbers
with decimal points, commas, negative values and currency symbols. The syntax takes a
moment to understand.

Standard Decimals:

The simplest, and probably the most common picture clause can display a numeric value
with decimals. Values will round and it can pad trailing zeroes, as needed.

Use this generic statement to display a value with two decimal places. As before, brace-
{zero} represents the first place holder in a comma-separated list of variable names and a
"picture" clause can follow. In this case, the :f2 indicates two fixed decimals on a
floating-point number:

String.Format ("{0:f2}", somefloating-point-variable-name)

For example, this code takes the number 3.1451 and displays it as "3.15", noting the
automatic rounding:

Example: fixed-decimals (:f2 = 2 positions)


private void button1_Click (object sender, EventArgs e)
{
//Print numeric value with two decimals; note rounding.

float fvalue = 3.1451F; //Floating point number

textBox1.Text = String.Format ("{0:f2}", fvalue);


}

where:

• The decimal value is declared as type "float" (also known as a "Single" - capital S).
Floating point literals are suffixed with an "F". See Chapter 5.

• In the example, even though the list contains a single number with no intervening
text, a string is still expected by the String.Format command. Because of this, even a
lonely numeric place-holder value must be enclosed in quotes; "{0:f2}".

• 0:f2 displays the number with two decimal points.


0:f6 displays as 3.145100, padded with zeroes.

Chapter 21 - Formatting Numbers and Text Page: 482


• Text can be added to the string by typing the literals within the quotes. For example,

textBox1.Text = String.Format ("My number is: {0:f2}", fvalue);

Formatting with Alignment:

As before, text and numbers can be combined into a single String.Format and the results
can be aligned. For example, the floating point number, 3.1451F, can be combined with
the text "Fixed Points". The strings can be assembled with the text embedded in the
literal or prepended with a plus.

Both of these statements return the same results:

where:

• "{0,15:f3}" displays the first numeric-parameter {0} at 15 characters wide with three
decimals. Note the mixture of commas and colons.

Chapter 21 - Formatting Numbers and Text Page: 483


Basic Formatting with Commas and Currency:

Extend the example using a floating point value of "-1223.1451F" (negative), along with
the formatting type "n" for numbers or "c" for currency. See also "Variable Picture
Clauses," later in this section:

Format Clause Results


"{0,15:n2}" -1,223.15 n for 'numbers' with commas
"{0,15:c2}" ($-1,223.15) c for 'currency'

Other Fomatting Types:

Other formatting types are available and these are listed here but not detailed. In each
remember the "{0" indicates the first variable in the parameter-list; use "{1" for the
second parameter, etc.:

{0:d} Whole Numbers Explodes if decimals are passed; Consider using :f0
{0:e} Scientific Notation 1.223e+03
{0:g} General Numeric 1223.1541 with no commas or other accouterments
{0:x} Hexadecimal 1223d = 4c7h (whole numbers only)

Chapter 21 - Formatting Numbers and Text Page: 484


Numeric Picture Clauses

Picture clauses give precise control over the number's formatting. With a picture clause
you can show commas, decimal points and decimal positions, along with dollar-signs,
dashes and other literals, all without affecting the original numeric value.

With most of these examples, assume a floating point number with 4 significant digits,
unless otherwise indicated. With the various picture clauses, digits may be rounded or
truncated.

Testing Picture Clauses:

Test numeric picture clauses by adding this code to a button1 Click Event. A test
variable, 'fvalue' can be populated with any floating-point number. Display the results in
TextBox1 or in a MessageBox.Show using a String.Format along with a picture clause.
Replace "<picture clause here>" with examples from the next page:

Use this code for Examples


private void button1_Click (object sender, EventArgs e)
{
//Use this code to test most of the following picture clauses:
//Don't forget "String.Format" is required
//as also are quotes

Single fvalue = 1223.1451F;


textBox1.Text =
String.Format ("{0: <picture clause here>}", fvalue);
}

Padding Zero with Leading and Trailing Decimals (0):

The "0" character can be used in a picture clause as a place-holder. The zero represents
any digit. If the position does not contain a value, pad with zeroes. Both sides of the
decimal can be padded, giving leading or trailing zeroes.

String.Format ("{0: 0.000000}", 0.43F);

For example, a picture of "0.000000" represents a standard fraction with 6 decimal


places. .43 displays as "0.430000", while "0000.00" displays as "0000.43". Don't forget
the Format statement's first place holder (position zero) uses a "{0:", which can be
confusing to look at. The colon separates the {place-holder} from the picture.

Chapter 21 - Formatting Numbers and Text Page: 485


As before, other text (literals) can be inserted into the clause:

String.Format ("The answer is {0: 0.000000} hurray!", 0.43F);

Continuing with the Example:

On the right-side of the decimal, zeros can limit longer fractions by rounding and
truncating. On the left-side, one or more zeroes can represent digit positions but these
are not truncated and the number can grow into the positions. Consider these picture
examples:

"{0:0.000000}" 1223.145100 Padding zeroes on Right-side; similar to


:f6. The zero on the Left-side represents
a minium of one displayed digit, if zero.
For example the fraction ".1451F"
display with leading zero (0.1415) due to
the 0.00000. Larger numbers (1223) are
fully displayed.

"{0:.00}" .15 This picture clause does not have a zero


to the Left of the decimal (ignore the
placeholder). With pure fractions, such
as, ".1451F", a leading zero would not
display.

"{0,15:0.00}" " 1223.15" {0,15 represents a 15-character field,


aligned to the right and padded with
spaces. Two decimals, rounded are
specified. See the previous section for

Chapter 21 - Formatting Numbers and Text Page: 486


more examples on the (15-character)
alignment.

"{0:0}" 1223 Starting with the same 1223.1451, this


picture does not specify a decimal so the
fraction is rounded (as needed), then
truncated. Fo another example: 1224.98
displays as 1225.

"{0:000000.000000}" 001223.145100 Padding both left and right of decimal.


Note leading and trailing zeroes. Larger
numbers, greater than 10x5, display
without padding leading zeroes .

"{0:000000}" 001223 Prepend leading zeroes for things like


partnumbers (Original Numeric Value
= 1223. Note: Numeric picture clauses
cannot be used with string data, such as
"Dog". If padding a string, use PAD.)

"{0:00.00}" 1223.15 Rounds to two decimals on the right-side.


The left side prints with leading zeros for
01, 02 ... 09; after that, numbers print
normally.

Example: "3.1451F" prints as 03.15.

123.15 prints as 123.15 with no leading


zeroes because the three leading digits
extend past the picture clause's 2 left
positions..

"0.1451F" prints as 00.15.

"{0:0-00.00}" 12-23.15 Inserts a literal (-) in front of the second


position. Any literal(s) can be used.
Possibly useful for part-numbers.

For phone numbers, see a much better


text routine described later in this
chapter. Part-numbers, phone-numbers,
and the like should be stored as strings
and should not be inside of numeric
picture-clauses.

"{0:0 0 0.00}" 12 2 3.15 Literals can include embedded spaces.

Chapter 21 - Formatting Numbers and Text Page: 487


Place Holder (#):

A pound-sign "#" works similarly to the "0" place-holder, except if the digits do not
exist, they are not filled with (leading or trailing) zeroes. See the "Thousands" section
for more details on this type of picture clause.

"{0:#.###}" 1223.145 Similar to the "0" place holder but this


does not pad with zeroes if the digit does
not exist.

If the original number were 1223.1, and


the picture was #.###, it would print as
1223.1 – regardless of the number of
decimals specified in the picture.
Contrast this with 0.000. Rounding still
occurs.

"{0:#.###}" 43.1 The number "43.1F" displays as "43.1"


even though three trailing ".###"'s were
specified. Since the digits did not exist
in the original value, they were ignored
by the #..

"{0:#.###}" 43.009 The number "43.0089F" displays as


"43.009", rounded.

If you really wanted to truncate, truncate


first, before inserting into the picture
clause.

Thousands Separator ( , ):

Use a comma in the picture clause to mark the thousands position. The comma must be
placed between two place-holding characters; typically "#,#.00" or more rarely, "0,0.00".
Dollar-sign characters can be added to the picture clause and are treated as a literal, with
no special significance.

In general, when dealing with larger numbers and dollar figures:


• Digits to the left of the decimal are pictured with pound-signs
• Digits to the right of the decimal are pictured with two zeroes

Placing both zero and pound-signs is contrary to most examples


that I have seen published and on the web – but you want filled-
zeroes to print on the decimal side.

Chapter 21 - Formatting Numbers and Text Page: 488


"{0:#,#.00}" 1,223.15 "#,#" (common picture clause) Places a
comma at the thousands position with
fixed decimals to the right.

Even though only two positions are


indicated on the left, larger numbers are
correctly assumed because this is the
nature of the "#"-sign picture symbol.
No need to indicate each position, as in
#,###,###. In other words, "#,#.00"
correctly prints 1,234,567.00. See also
{0:c} (currency).

"{0:#,#.##}" 43.1 (Right-side #'s not recommended) For


example, using "43.1F": Even though
two pound-signs are specified after the
decimal, only one digit displays when the
decimal is not fully-formed (43.1 vs
43.12). Usually you should use
{0:#,#.00}.

"{0:$#,#.00}" $1,223.15 The dollar-sign is treated as a literal and


is simply prepended. This can cause
cosmetic problems, as shown in the next
example.

"{0:$#,#.00}" -$1,223.15 (Flawed example) Note the negative-


dollar when the value is negative. This is
not standard nomenclature and may not
be what you wanted. See "Variable
Picture Clauses," below.

"$" + String.Format ("{0:#,#.00}"


$-1,223.15 In order to fix the previous example,
prepend a literal string "$" before
assembling the String.Format, making
the dollar-sign first. A better solution
can be found in "Variable Picture
Clauses," below.

"{0:000000,0.00}" 0,001,223.15 (Non-sense, but can be done) Zero-


padding instead of #'s, with commas.

Chapter 21 - Formatting Numbers and Text Page: 489


j Variable Picture Clauses (Group Separators):

Positive, negative and zero numbers may have differing formatting needs.

Within the picture clause, three variations can be specified for positive, negative and
zero number and these are the types of picture clauses most likely used in financial
transactions. After the {0: position indicator, three separate pictures can be built with
each sub-clause separated by a semi-colon.

"{0:$#,#0.00; ($#,#0.00);-0-}" Positive numbers first;


Negative second;
Zero is third.

Note the negative picture has both dollar-


signs and parenthesis as literals.

The third parameter could be a literal,


such as "zero" or "*" (do not use quotes).
When testing clauses like this, also
consider small numbers and zero values.
Here are some example formatted
numbers:
$1,223.15
($1,223.15)
-0-

$1.15 Using 1.15F


($1.15) Using -1.15F

$0.43
($0.43) Using 0.43F. Note leading zero with
poundsigns for other leading digits
(#,#0.00)

Other Picture Clauses:

"{0:0%}" 43% Percentages: Takes the original number


(e.g. 0.43F); multiplies by 100 and
appends a percentage-sign.

"{0:0,.00}" 1.23 Scaling with a comma: Scales the


original number (1223.1541F) by 1,000.
This could be useful for converting
"Megabytes" to "Gigabytes" or 1,000,000
to 1,000M. Place the comma next to the
period. This is an esoteric picture;
consider more explicit ways, such as a

Chapter 21 - Formatting Numbers and Text Page: 490


previous step that divides the number by
1000.

"{0:(000) 000-0000}" (208) 376-1234 This is, of course nonsense, because why
would anyone store a phone number as a
numeric value? Besides, this picture
clause is impractical: The number
2,083,761,234 is large and will not fit
into a "Single" floating point number; if
you try, the value truncates and displays
as (208) 376-1000; use a "double" type.
Better yet, see the later sections in this
chapter for Phone Number formatting.

Chapter 21 - Formatting Numbers and Text Page: 491


Date and Time Pictures

Date and times can also be formatted with picture clauses and they use their own set of
formatting characters. Before the dates can be formatted, the values must be in a
DateTime-type – these picture clauses will not work for numeric or string values.

This section reviews how to convert textual and numeric values


into date-time formats and more details can be found in
Chapter 5.

Dates and Times

Try as hard as you would like, C# does not work well with dates in any other format than
the "DateTime - type, which is different than a string, integer or float types. However,
when working with Dates, you often need to display the "DateTime" type as a string, as
in "Sunday, August 2, 2010" and conversely, you often need to take stringed-dates, such
as "08/02/2010" and convert them to Dates (the type) and then back to another format.
This section covers these scenarios.

Converting Dates to String:

DateTime variables are declared as type, DateTime and assigned an arbitrary and
invented name, such as "dtValue". These examples use the current date/time and convert
them to Strings.

Excel users will have an uncontrollable urge to use "Now( )".


Drop the parenthesis if you want this to work in C#:

Example: Getting the Current DateTime, completed


public void button1_Click (object sender, EventArgs e)
{
//Techniques for displaying the current date-time in
//a multi-lined textBox

DateTime dtValue = DateTime.Now; //Current DateTime; no parens.

string strDate = DateTime.Now.ToShortDateString(); //mm/dd/yyyy


string strTime = DateTime.Now.ToShortTimeString(); //HH:MM AM

textBox1.Text = "The Date Now: " + dtValue.ToString() + \r\n";


textBox1.Text +=
"A Short Date: " + dtValue.ToShortDateString() + "\r\n";
textBox1.Text +=
"A Short Time: " + DateTime.Now.ToShortTimeString();

Chapter 21 - Formatting Numbers and Text Page: 492


In the example, a string, such as "strDate" is created and populated with a convert to
string (.ToShortDateString).

Results:

Converting from Strings:

On the other side, dates can arrive as strings and may need to be converted to a DateTime
variable before they can be re-formatted or used in calculations (such as adding 30 days).
These examples assume US Date formats. See the DateTime.Parse overloads for other
geographic information.

The following examples take various date strings and converts them to a DateTime-
variable, which was given an invented name, dtValue. This variable name is not reserved
but is often used by programmers. The command "DateTime.Parse" does all the work.
In the examples, run each possibility by replacing 'strDTValue_Long' with any of the
other examples, pressing F5 each time to test.

Chapter 21 - Formatting Numbers and Text Page: 493


Examples: Converting from Strings to DateTime, completed
private void button1_Click (object sender, EventArgs e)
{
//Convert Stringed-Dates (various) to DateTime variables.
//Then, foolishly, convert them back to strings.

DateTime dtValue;

//Convert these 'strings' into dtValues:


string strDTValue_Long = "3/24/1981 2:23:11";
string strDTValue_LongAM = "03/24/1981 2:23:11 PM";
string strDTValue_SimpleDate = "3/24/1981";
string strDTValue_PaddedDate = "03/24/1981";
string strDTValue_ShortYear = "03-24-81";

//Replace each of the date variables below;


//Each will convert properly
//(A try-catch for invalid dates is recommended but not shown)

dtValue = DateTime.Parse (strDTValue_Long);


textBox1.Text = "Converted Date: " + dtValue.ToString();

dtValue = DateTime.Parse (strDTValue_LongAM);


textBox1.Text = "Converted Date: " + dtValue.toString();

//etc. for each test string...


}

comments:

• Each of the stringed-dates properly converts to a true DateTime variable using the
DateTime.Parse method. This includes those dates formatted with slashes and
dashes.

• To test each possiblity in the example code above, replace the underlined
'strDTValue_Long' with another variable's name, such as 'strDTValue_SimpleDate'.
Press F5 to run and see the results in textBox1.

• If a string did not have a "time", 12:00am is assumed.

• Two-digit years break the century at "30": Years 00-29 assume 2000; 30-99 assume
1900. (Information current as of 2009 with dotNet 2.0 libraries. See also MSDN
documentation for "Set Century" and "Rollover Years.")

• Times are assumed as 24hour clock unless AM/PM specified. Time-strings cannot
contain periods, as in "A.M.".

• Although not illustrated, you should always use a Try-Catch around the .Parse
commands. If not protected, non-sensical dates, such as "03/47/1981" will cause
grief.

Chapter 21 - Formatting Numbers and Text Page: 494


Converting Dates from Given Numeric and Stringed Values:

If presented with separate numeric or string values for the Year, Month and Day, convert
with these statements. For example, to convert from three separate numeric values
(1981, March (03), 24th), use this statement:

dtValue = new DateTime (1981, 03, 24);

where three numeric parameters, passed within the parenthesis. If the three values are
strings, convert each part to an Int32 prior.

Example: Converting from Numeric to DateTime, completed


DateTime dtValue; //Declare the temporary variable

//To Convert from three numeric values to DateTime:


dtValue = new DateTime (1981, 03, 24);

//To Convert from three string values to DateTime:


dtValue = new DateTime (Convert.ToInt32 ("1981"),
Convert.ToInt32 ("03"),
Convert.ToInt32 ("24"));

See Chapter 5 for more details on Converting to Integer.

Chapter 21 - Formatting Numbers and Text Page: 495


Other Date Time Properties and Methods

These properties and methods work directly on any DateTime variable and do not have
to pass through a String.Format or other picture clauses (see the next several sections for
more information).

For each of the items listed below, assume a standard DateTime variable, arbitrarily
named "dtValue", and assume it has already been populated with a legitimate date using
one of the methods above.

General .ToString:

With any DateTime variable, use the ".ToString( )" method to get an immediate,
formatted result.

Results: All examples assume this date


dtValue.ToString() String: "3/24/1981 7:23:09 AM"

The results can be customized with a "picture" clause or the results can be placed in a
String.Format command, both of which are discussed in the next section:

dtValue.ToString("MM/dd - hh:mm tt")


String: "3/24 7:23 AM"

Other DateTime Properties:

Properties
dtValue.Day Integer: Day of the month
dtValue.DayOfWeek String: Day of week: e.g. "Tuesday". See also
String.Format for other variations (Tue).
dtValue.DayOfYear Integer: Number of days into the year (1-365)

dtValue.Hour Integer: Hour


dtValue.Minute Integer: Minute
dtValue.Month Integer: Month (1-12)

Methods:
dtValue.ToLongDateString() String: "Tuesday, March 24, 1981"
dtValue.ToLongTimeString() String: "7:23:09 AM"
dtValue.ToShortDateString() String: "3/24/1981"

Example: MessageBox.Show (dtValue.ToongDateString());

Chapter 21 - Formatting Numbers and Text Page: 496


DateTime Formatting

DateTimes have an impressive list of formatting options. With a specified picture clause
you can choose the layout of the date-time string by listing each component with
individual codes. For example, "MM" represents the month (01=January), "MMM"
represents "JAN" and "MMMM" is 'January' spelled out. Picture values are case-
sensitive, with differences between values like "mm" (minutes) and "MM" (month).

Keep in mind the formatting only works against DateTime-type variables and the results
are always stringed. (See also the .Values method, which returns a numeric result). Use
this logic for testing:

Use this code for Examples


private void button1_Click (object sender, EventArgs e)
{
//Use this code to test most of the following picture clauses:
//Don't forget "String.Format" is required
//as also are quotes

DateTime dtValue = new DateTime (1981, 03, 24);

textBox1.Text =
String.Format ("{0: <picture clause here>}", dtValue);

//Example: String.Format ("{0:dd/MM/yyyy}", dtValue);


//Alternately: dtValue.ToString("dd/MM/yyyy");
}

Most of these examples use the String.Format command, which was described at the top
of this chapter. As a reminder, a place-holder is used for each variable, where the first
variable is at position {0} and a second variable, if present, would be at position {1}.
The place-holders live within a quoted string and the variables are listed at the end of the
statement, in a comma-separated list.

Within the {place holder}, use a colon to mark where the picture clause begins. For
example, to display the day-of-the-week for a particular day, use a picture clause "ddd"
(lower-case).

textBox1.Text = String.Format ("Today is {0:ddd}", dtValue);


or
textBox1.Text = "Today is " + dtValue.ToString ("ddd");

Chapter 21 - Formatting Numbers and Text Page: 497


resulting in "Today is Tue" ("dddd" would be Tuesday).

Date Format Pictures:

These examples use "3/24/1981 7:23:09 AM" for most of their dates and times. All
picture components are case-sensitive.

Picture Desc Example Results Comments

Days and Dates

d Short Date {0:d} 3/24/1981 No leading zeroes

D Long Date {0:D} Tuesday, March 24, 1981


Monday, March 02, 1981

dd Day {0:dd} 24 Leading zeroes


ddd Short Day Name {0:ddd} Tue
dddd Long Day Name {0:dddd} Tuesday

MM Month {0:MM} 03 Leading zeroes


MMM Short Month {0:MMM} Mar
MMMM Long Month {0:MMMM} March
M MonthDay {0:M} March 24 Leading zeroes
March 02

Year

yy 2-Digit Year {0:yy} 81 Leading zeroes


yyyy 4-Digit Year {0:YYYY} 1981
Y YearMonth {0:Y} March, 1981

Time

h Hour/12 {0:h:mm} 7:23 No leading zero


hh 2-Digit Hour/12 {0:hh} 07 (1-12) Leading zeroes
HH 2-Digit Hour/24 {0:HH} 07 (00-23) Leading zeroes
mm 2-Digit minutes {0:mm} 23 Leading zeroes
ss 2-Digit seconds {0:ss} 09 Leading zeroes
tt AM/PM {0:tt} AM
zz Timezone Offset {0:zz} -06 MST
zzz Timezone Offset {0:zzz} -06:00 MST, Full display

t Short Time {0:t} 7:23 AM No leading Zeroes


T Long Time {0:T} 7:23:09 AM Long = Seconds

Chapter 21 - Formatting Numbers and Text Page: 498


f Full Date Time {0:f} March 24, 1981 7:23 AM
F Full Date Time {0:F} March 24, 1981 7:23:09 AM
g Default Date T. {0:g} 3/24/1981 7:23 AM
G Default Date T. {0:G} 3/24/1981 7:23:09 AM

r RFC1123 Date {0:r} Tue, 24 Mar 1981 7:23:09 GMT


s Sortable Date {0:s} 1981-03-24T07:23:09, year first

u Sortable Universal Local


{0:u} 1981-03-24 07:23:09Z
U Sortable Universal GMT (Does not work properly in VS2008)

(see also Chapter 5 for similar information)

Assembled Picture Clauses:

If an exact picture cannot be found, different formats can be assembled using individual
components of a date and time. For example, assume you wanted this type of result,
which includes the words "My date" and a non-standard punctuation, including a hyphen
on the -AM:

Non-standard Date example: "My date 03/24:1981 07:23 -AM"

Example Assembled Picture Clause, completed


//Results in: My date 03/24:1981 07:23 -AM

textBox1.Text =
String.Format("My date {0:MM/dd:yyyy hh:mm -tt}", dtValue);

//Alternately:
textBox1.Text = dtValue.ToString("MM/dd:yyyy hh:mm -tt");

where:

• The dtValue must be of type DateTime and is marked as position {0:<picture>}.

• The entire string is enclosed in a single set of quotes. The free-form text "My date"
is part of that same string.

• Note "MM"=Month while "mm"=minutes. Parameters are case-sensitive

• Spacing after the Year and after the Minutes was manually typed in the picture. The
slashes, colons and hyphens are simply literals and there is nothing special about
them; any character could be used.

Chapter 21 - Formatting Numbers and Text Page: 499


Alignment and Spacing:

Date and Time picture clauses can also be aligned, as described near the top of this
chapter. For example, to pad 15 spaces and align the day to the Right, use this statement:

textBox1.Text = String.Format("My date {0,15:ddd}", dtValue);

Results: "My date Tue";

Display Dates, Year first:

A common request is to display the date and time, year first.

//Year-first with 12-hour clock


//2012-03-12 05:21 PM
textBox1.Text = String.Format("{0:yyyy-MM-dd hh:mm -tt}", dtValue);

//Year-first with 24-hour clock:


//2012-03-12 17:21
textBox1.Text = String.Format("{0:yyyy-MM-dd HH:mm}", dtValue);

Chapter 21 - Formatting Numbers and Text Page: 500


Format Class Library - cl710_Formatting

C# does not have built-in routines to handle formatting for such things as

• Phone Numbers
• Proper Names
• Street Addresses

In this section, build a new class library with routines to handle these items. As with all
class libraries, write these one time and then link into any program, as needed. These are
some of the handiest (and smartest) routines you will ever write.

These formatting routines could be written in the existing


cl800_Util library, but this means whenever the Utility libraries
are used, the Formatting routines tag along, even if the program
does not need them. Also, I don't consider "phone number
formatting" a "utility" and it seems mis-placed in the cl800
library. For these reasons, use a separate library.

Phone Number Formatting:

The "PhoneNumberFormat" module can take almost any phone number, no matter how
badly formatted, and return the number formatted in your favorite style. For example,
"(208/383-1234 x456" could be returned as "208.383.1234 x456". These routines were
designed for United States numbers but could easily be modified to other preferences.

Proper Names:
Proper Addresses:

The formatting library also needs to handle two types of names, Proper Names and
Addresses. The "ProperNamesFormat" routine can take most proper names, such as
"dR john q smith", and return a correctly punctuated and capitalized string,
"Dr. John Q. Smith".

Street addresses, such as "123 n elm st ste 14a" are returned as "123 N
Elm ST, STE 14A". This is controlled by a flag on the Proper Names function.

These are complicated modules that will take time to build. However, with the given
step-by-step instructions, you will learn new skills in dissecting a problem and in
building robust and useful routines.

Chapter 21 - Formatting Numbers and Text Page: 501


cl710 "Formatting" Class Library:

The new formatting routines need a place to roost and a one-time formatting class library
will be built with these steps (from Chapter 8). This will only take a few minutes to
construct:

1. Launch a fresh (second) copy of Visual Studio and select Create New, Project.

2. In the Project Types section, choose Visual C# "Windows" and "Class Library"

3. In the Name field, type: "ns710_Formatting"

4. In the Location field, type or browse to "C:\Data\Source\CommonVS" (or other


location of your choosing. Ideally, place this next to the cl800_Util library).
Do not check "Create directory for solution"
Click OK to build the solution.

5. In Solution Explorer, change the default class name, "Class1.cs" by:


Other-mouse-clicking Class1.cs and selecting Rename.
Rename the class to "cl710_Formatting.cs" (you must use the .cs extension)
When prompted "Would you also like to perform a rename in this project and all
references...", choose Yes.

The class is nearly ready to accept code but it will need to use the cl800_Utility library.

ns710_Formatting (cl710_Formatting) relies heavily on the


cl800 utility library. It is assumed and nearly required that you
built this from Chapter 8.

Linking the Needed cl800 Utilities:

The formatting routines will use functions such as IsBlank, LeftStr and MIDStr and
because of this, it needs to link the cl800_Util library. This is in addition to possibly
being linked into your main program (Form1). This is not a duplication of code; the
compiler is smart enough to know the same library is already in memory and reuses the
code.

6. Link the cl800_Util library into the new Formatting library using these steps. However,
because the new formatting library does not have a "Form," you will have to manually
build a Class Constructor, detailed below. See also Chapter 8.

a. Select "ns710_Formatting" in Solution Explorer


b. "Other-mouse-click" and choose, Add Existing Item
c. Browse to C:\Data\CommonVS\ns800_Util
Highlight "cl800_Util.cs"
Click button-Add's pull-down menu, choosing "Add as Link"

Chapter 21 - Formatting Numbers and Text Page: 502


7. At the top of ns710_Formatting's code, add this "using" statement:

Required 'using' statement


using ns800_Util.cs;

8. Declare the utility library with this statement: "cl800_Util util;", which is placed
underneath the cl710 class name:

Declare the utility library


:
using ns800_Util;

namespace ns710_Formatting
{
public class cl710_Formatting
{
cl800_Util util; //Declare here

// <Class constructor goes here>

// <Phone number routines go here>


}

9. Manually build a "Constructor"

The util library needs to be instantiated, but since there is not a Form Constructor,
cl800_Util cannot be declared in manner you have in past. To work around this,
manually create a Class Constructor.

In cl710's code, near the top, manually type a new "method" that has the same name as
the Class, but do not include a "void" or other variable in the function. Then, within the
new Constructor, instantiate the util library as you have in the past. The constructor
(must be typed by hand) is in bold, below:

Chapter 21 - Formatting Numbers and Text Page: 503


Manually build the constructor (bolded)
public class cl710_Formatting
{
cl800_Util util; //Declare here

public cl710_Formatting()
{
//This is Formatting Class's Constructor
util = new cl800_Util();
}

// <Phone number routines go here>


:

10. Close and save the new class library (File, Close Solution).
Then close the extra copy of Visual Studio and return to your original test program.

This completes the base layout for the formatting class library and it is now ready to
accept code. The new methods will be written from within the Form1 test program.

Chapter 21 - Formatting Numbers and Text Page: 504


PhoneNumberFormat Method - Overview

Consider these U.S. phone numbers, some of which are poorly formatted or poorly
typed. You can probably imagine other combinations:

American-1 and American-2 Formatting European Style


383-1234 383.1234
3831234
2083831234
93831234
(208)383-1234 or 208-383-1234 (208)383.1234
(208) 383-1234 or 208 383-1234 (208) 383.1234
(1208) 383-1234 or 1208-383-1234 1208.383.1234
208-383-1234 208.383.1234
208/383-1234 208/383.1234
383-1234 x1234 383.1234 x1234
1800-383-1234 1800.383.1234
1-(800)-383-1234 1.800.383.1234
1 800 383-1234 1 800 383.1234
1234
x1234
011 61 2 xxxx xxxxxx (US International outbound numbers)

The routines demonstrated in this section can take all of these numbers and reformat
them into a style of your choosing. You should find this is an interesting routine to write
and it goes well beyond most other formatting seen on the Internet.
"PhoneNumberFormat" can handle all types of phone numbers, including International
numbers, In-State LongDistance, mis-punctuated, and phone extensions.

Formatting "Numeric" Phone Numbers:

You may have considered the simplistic approach of using numeric picture clauses to
format phone numbers. Consider this numeric picture, derived from the front of this
chapter; this assumes a "numerically-stored" phone number:

("{0:(000) 000-0000}", dblPhoneNumber)

It is unrealistic to store phone numbers as a numeric value because the numbers are large.
For example, (208) 383-1234 would be stored as 2,083,831,234, which requires a
double-wide integer. Secondly, phone numbers are not numeric beings and real-data
usually contains punctuation and other non-numeric formatting characters. Because of
this, a simple numeric picture clause, no matter how appealing, does not work.

Additionally, numeric phone numbers would have to be re-formatted each time they are
displayed and this is inefficient. Because of these reasons, phone numbers should be
stored as strings and should be formatted one-time when saved.

Chapter 21 - Formatting Numbers and Text Page: 505


Formatting a phone number is a complicated task with numerous steps. To make the
programming easier, the steps need to be broken into smaller, more digestible modules.

The PhoneNumberFormat routines will be placed in the new cl710 class library, much
like the cl800_Util routines and the function will be "public," visible to other programs
and classes, but because of its complexity, it will call five other "private" functions. The
private functions only need to be seen by the main formatting routine.

A high-level schematic of the five sub-routine calls:

An Overview of the "Call":

The phone number formatting routine accepts a moderately-long list of parameters and
because all are required, there are no overloads, making for a wordy signature. Since
this routine is likely to be used in other programs, it will be placed in the
cl710_Formatting class library. The new routine will be named "PhoneNumberFormat".
The call has the following required parameters:

Chapter 21 - Formatting Numbers and Text Page: 506


strPhoneNumber The original phone number as typed by the end-user.
strFormatStyle American or European-styled punctuation
strDefaultAreaCode Default Area-Code; helps in formatting
boolRequireAreaCode Force default Area-Codes if missing in 7-digit phone
numbers
boolReturnBlankOnError If a bad phone number, return blank or the original.

When completed, Form1's Phone Number Formatting "call" will look like this schematic
(see actual coding steps in the next section; these are variablized parameters):

where:

• formatting.PhoneNumberFormat is from the newly-built cl710 class library.


"formatting" is the class name's alias (much like "util" is an alias for cl800_Util).
"PhoneNumberFormat" is a method of class "formatting."

• Five parameters are passed into the PhoneNumberFormat method and all are
required, although not all need to be populated.

The PhoneNumberFormat method is a "public" method, visible to other classes,


including Form1. This public module will call other, soon-to-be-built private modules,
which are only visible to the Format modules.

phoneNumber_StripNormalPunctuation
phoneNumber_StripExtension
phoneNumber_StripLongDistancePrefix
phoneNumber_Punctuate
phoneNumber_AreaCodePunctuation

The names give an indication on how the logic will work when called by the main
routine. Out of convention, the Public method is named "PhoneNumberFormat", with a
capital "P" while the private modules begin with a lower-cased initial letter.

Chapter 21 - Formatting Numbers and Text Page: 507


Phone Number Logic:

It would be horribly difficult to reverse-engineer what end-users type into a phone-


number in order to see if the style were correct. Instead, this routine strips all
punctuation from the number, even if properly formatted, and then re-assembles the
number. For example, assuming you liked dots as punctuation (European style):

(208) 383-1234 becomes 2083831234 which becomes 208.383.1234


383-1234 becomes 3831234 which becomes 208.383.1234
(with an optional default area-code)

The soon-to-be-built routine can output numbers in one of three different styles, which I
call the American-1, American-2 or the European style. Choose which style you want
when calling the module:

American-1 (208) 383-1234 Includes (area-code) parenthesis and hyphens.


American-2 208-383-1234 Uses only hyphens
European 208.383.1234 Uses dots instead of hyphens

When assembling the final, punctuated number, the program needs to accommodate each
of the styles and it needs to be conscious of default area-codes and how In-State Long
Distance is handled. These preferences are controlled by a list of parameters passed into
the routine.

The logic to disassemble the phone number is surprisingly involved and needs to be run
in this order:

1. US outbound International numbers (US, starting with "011") are immediately


returned, as-typed, because there is no rhyme or reason when formatting
international numbers. Substitute your country's International Prefix, as needed.

2. If the number contains an "@", assume this is an email address and immediately
return unmodified. Reasons for this are discussed later.

3. If the number has an extension (e.g. "x1234", "x Bob's Cell"), strip the extension or
comment and hold for safe-keeping; then continue with the rest of the formatting
logic. The routine is expecting an 'x' as an extension or can use the x as a comment,
as in "x Home Phone").

With this, phone numbers can be stored as:


208.383.1234 x4567 or
208.383.1234 xHome Phone

4. With the remaining characters, strip all normal punctuation (parenthesis, dashes,
dots, slashes, spaces, pound-signs). If other non-numeric data remains, return the
original string unchanged and assume this is not a valid phone number or return an
empty-string, flagging the number as an error. This behavior is controlled by one of
the passed parameters; see step 7.

Chapter 21 - Formatting Numbers and Text Page: 508


5. If the number begins with a long-distance prefix of "1", as in 1-space, 1-dash,
1-period, or "(1208)" - area-code, strip and hold the digit so it does not mess with the
area-code formatting. Reason: Some U.S. states have "In-State" long distance, where
a "1" is used for some e.g. (208) numbers, and not for other (208) numbers.

Similarly, strip leading 9's which are used for outside lines in most business
exchanges and are not part of the true phone number.

6. If the remaining digits are less than 7 characters long, assume this is a local
extension (typed without the "x") and return to the calling routine, unmodified. Do
this after stripping explicit extensions (e.g. 65543 x Robert's desk) and after
removing punctuation. You may want to consider this an error and this possibility
can be checked separately, however, for internal PBX phone numbers, this would be
an allowable number.

7. Look at the remaining digits and possibly report invalid phone numbers (those with
non-numeric values), returning to the calling routine, depending on preferences.

8. If the number survives these edits re-punctuate the number using your preferences,
American or European. Re-punctuation is a series of substrings with a new delimiter
(dashes or dots). Append a default area-code, as needed.

The length of the surviving digits helps determine what type of phone number
remains:

12345 = Local extension


3831234 = Local phone number "3-4" (3 digit prefix, 4-digit suffix)
2083831234 = Area-Code Number "3-3-4"

Re-attach Long Distance Prefixes ("1"), if appropriate, from Step 5.

9. Re-append extensions from Step 3: "208.383.1234 x1234".

10. Return the newly-formatted number to the calling routine

Chapter 21 - Formatting Numbers and Text Page: 509


PhoneNumberFormat Module - Coding

The phone number formatting logic and other routines will be placed in the cl710 class
library. From earlier in this section, link the newly-built but still empty cl710 library into
your test program (e.g. Form1). Link this in the same fashion as cl800_Util.

Important: Before starting, confirm you have closed and saved the cl710_Formatting
class built earlier.

Linking cl710_Formatting and cl800_Util Libraries:

In your test program, Form1, follow these steps to use the formatting library:

1. In Form1's Solution Explorer, Add (link) the "cl710_Formatting.cs" library. This is the
library stubbed-in from a few pages earlier.

a. Other-mouse-click "WindowsApplication1" (your test program),


Select Add, Existing Item.

Chapter 21 - Formatting Numbers and Text Page: 510


b. Browse to C:\Data\Source\CommonVS\ns710_Formatting
Single-click/select cl710_Formatting.cs
Click the pull-down menu on the Add button, Choose "Add as Link"

c. Also link in cl800_Util.cs, using similar steps. Do this even though cl800 was linked
into the formatting class. The result, two linked in libraries.

2. At the top of Form1's code, add these "using" statements:

Required 'using' statements


using ns800_Util;
using ns710_Formatting;

3. Declare, then instantiate the libraries in your program (Form1).

After "public partial class Form1: Form", declare the libraries, giving them a friendly
name: "util" and "formatting". Then instantiate the library either in the public Form1
Constructor or in button1_Click. See bolded code, below:

Chapter 21 - Formatting Numbers and Text Page: 511


Declare and Instanitate the new Formatting Library
using ns800_Util;
using ns710_Formatting;

namespace WindowsApplication1
{
public partial class Form1 : Form
{
cl800_Util util;
cl710_Formatting formatting;

public Form1 ()
{
InitializeComponent();
util = new cl800_Util();

//Instantiate the cl170_Formatting library here, or


//below in button1_Click. Do one or the other
formatting = new cl710_Formatting();
}

private void button1_Click (object sender, EventArgs e)


{
//If only needed by this method, instantiate the new
//formatting library here. Or, if needed in numerous
//locations throughout your program, instantiate next
//to cl800. Only choose one of these designs.

formatting = new cl710_Formatting();


:
}

The new Formatting library is ready to use. The next sections will jump back and forth
between Form1 and the cl710_Formatting.cs modules, as code is added. While doing
this, remember that the utility libraries are called like this:

util.IsBlank()

while the Formatting routines will be called like this:

formatting.PhoneNumberFormat()

where "formatting." is used as the class library alias name. I would have preferred the
word "format.", but this is a default C# class name that should not be over-rode by your
code. How can you tell if the class name "format." is in use? In any module, type
"format. (dot)" and see if the pop-up help shows anything.

The next section begins building a new Phone Number formatting module. This will
require a half-dozen minor routines, where the main one is a "public" and the others will
be "private," only visible to the formatting class.

Chapter 21 - Formatting Numbers and Text Page: 512


Begin Coding in cl710:

1. Begin writing the new "formatting.PhoneNumberFormat" module.

In Solution Explorer, double-click the 'cl710_Formatting.cs' module, opening code view.


You will see the stubbed-in code (cl800 links) written previously. The Tabs along the
top row of the editor allow you to hop between this library and Form1.

2. (In cl710_Formatting.cs) Scroll to an open location to add the new module, probably just
below the cl710_Formatting Constructor. Begin typing the "public string
PhoneNumberFormat" module name, then add the following code, with a longer-than-
normal signature:

Chapter 21 - Formatting Numbers and Text Page: 513


Initial Setup for util.PhoneNumberFormat, in progress
public string PhoneNumberFormat
(string strPassedPhoneNumber,
string strFormatStyleFlag,
string strDefaultAreaCode,
bool boolRequireAreaCode,
bool boolReturnBlankOnError)
{
string strnewFormattedNumber; //Return this result
string strextensionHold = ""; //x-Extension holding area x1234
string strlongDistancePrefixHold = "";
string strinternationPrefix = "011";

strFormatStyleFlag = strFormatStyleFlag.ToUpper();

//Move to a working area:


strnewFormattedNumber = strPassedPhoneNumber.Trim();

: <more code goes here>


}

comments:

• This will be the main formatting routine and it will ultimately call other sub-routines.
Be sure to place this code in cl710's library and not in Form1. Notice it is a public
method.

• The signature line contains the parameters required by the routine and these match
the "call" parameters. Parameters were typed on separate lines for readability. Note
the mixture of string and boolean values. The variables are all invented names.

• Since the original string may need to be returned (in the event it fails to format),
make a new home for the resulting string by declaring "strnewFormattedNumber".
This will also be the original working-value for most of the routines.

• The InternationalPrefix = "011" is the U.S. outgoing International-call dialing prefix


and is used to identify non-standard phone numbers. This is a hard-coded value but
could be variablized.

• Uppercase the passed FormatStyleFlag (AM1, AM2, EU) for easier testing. This is
an invented code; any naming scheme could be used.

AM1 / or AM = Standard American Format: (208) 383-1234


AM2 = Simplified American Format: 208-383-1234
EU = European Format: 208.383.12347

7
I am on a mission to make the European phone-number format the new US standard.

Chapter 21 - Formatting Numbers and Text Page: 514


• One of the first steps trims and assigns the original phone number to this working
area. The Trim makes subsequent logic easier to manage because they will not have
to worry about errant leading and trailing spaces.

3. On a blank line directly above "public string PhoneNumberFormat (...)", type triple-
slashes (///) and add this documentation. For a routine as complicated as this one, with
this many parameters, comments like this are almost required. Fill in each field's
parameters, as indicated:

Necessary Comments for PhoneNumberFormat's Signature Line


/// <summary>
/// Reformats a passed phone number; replacing all original
/// punctuation with the preferred punctuation. Returned string
/// is formatted per passed preferences.
/// </summary>
/// <param name="strPassedPhoneNumber">Original passed phone number"
/// <param name="strFormatStyleFlag">Use "AM" for (American-dashes),
/// "AM2" (with dashes, no parens),
/// "EU" European (dots) </param>
/// <Param name="strDefaultAreaCode">"208" - prefix is prepended to
/// all 3-4 phonenumbers; leave blank to ignore </param>
/// <param name="boolRequireAreaCode">prefix the default area-code
/// on all 7-digit numbers; ignored if no default Area-code</param>
/// <param name="boolReturnBlankOnError">If true, return blank values
/// if an invalid phone number is detected, otherwise, return the
/// original phone number </param>
public string PhoneNumberFormat (... etc.)
:

Note: A complete code listing can be found at the end of this


section. Source code is also available directly from the Author.

Early Exit with International Numbers:

The signature lays the groundwork for the rest of the routines.

public string PhoneNumberFormat


(string strPassedPhoneNumber,
string strFormatStyleFlag,
string strDefaultAreaCode,
bool boolRequireAreaCode,
bool boolReturnBlankOnError)

The first step, outlined at the start of this section, accounts for International Calls. If the
passed phone number begins with "011" (the hard-coded International Prefix for U.S.),
immediately exit and return the phone number, as typed. International numbers cannot
be formatted properly without knowing each country's rules and each country appears to
be different. Similarly, if the 'phone number' contains an @-sign, assume this is an email

Chapter 21 - Formatting Numbers and Text Page: 515


address and return. You may wish to treat this as an error. The bold code shows these
tests:

(Reminder: This code is in the cl710_Formatting library and not in Form1.)

Phone Number Formatting: Early Exit Logic, in progress


public string PhoneNumberFormat
(string strPassedPhoneNumber,
string strFormatStyleFlag,
string strDefaultAreaCode,
bool boolRequireAreaCode,
bool boolReturnBlankOnError)
{
string strnewFormattedNumber; //Return this result
string strextensionHold = ""; //Temporary holding for x1234
string strlongDistancePrefixHold = "";
string strinternationalPrefix = "011";

strFormatStyleFlag = strFormatStyleFlag.ToUpper();

//Move to working area:


strnewFormattedNumber = strPassedPhoneNumber.Trim();

//Early exits
if (util.IsBlank (strnewFormattedNumber))
return "";

//Unconditionally return email addresses:


if (strnewFormattedNumber.Contains("@"))
return strnewFormattedNumber;

//See if this is an internat call; if so, return unconditionally


if (util.LeftStr(strnewFormattedNumber, 3) ==
strinternationalPrefix)
return strnewFormattedNumber;

//x-Extension Extract goes here...

//For testing, add this temporary line now:


return "Pending Formatting: " + strnewFormattedNumber;
}

comments:

• The routine returns email addresses and international numbers unmodified.

• util.IsBlank and util.LeftStr are cl800_Utility routines from Chapter 8 and these
libraries should already be in-place from earlier in the chapter, when the class library
was originally built; see earlier in this chapter for information. The "util" prefix is
required.

• Notice the"Pending" statement at the end of the module. This is testing and
diagnostic code and during debugging, it will help you tell the routine ended.

Chapter 21 - Formatting Numbers and Text Page: 516


Testing:

It is never too early for testing. Return to (Form1) and add these statements, illustrated
below, to button1_Click.

A. In Form1, confirm the cl710_Formatting.cs Library was instantiated, probably in the


constructor
formatting = new cl710_Formatting();
:

As a reminder, this gives the Class Library an aliased name "formatting. (dot)", similar
to "util.".

B. In "button1_Click", declare a string, strTestPhoneNumber and assign a test phone


number.

string strTestPhoneNumber = "(208) 383-1234";

C. Call the new "formatting.PhoneNumberFormat" routine. As you type the word


"formatting. (dot), notice the popup help:

Type all of the parameters, as illustrated below. This makes for a busy signature line and
it may be easier to put each parameter on a separate line. A comma is required after each
and strings are in quotes. There are also opening and closing braces, which is typical for
any method-call. Use this for all testing.

Chapter 21 - Formatting Numbers and Text Page: 517


Button1_Click Testing Logic, completed

where:

• A working variable, strtempvalue holds the results for the duration of the button-
click.

• textBox1.Text, and the various switches, are passed into the


'formatting.PhoneNumberFormat' routine.

• For this example, "AM2" is the requested format flag. American-2 uses hyphens
with no parenthesis around area codes. 208-383-1234.

• "208" is the default (example) area code used with 3-4 phone numbers: "383-1234".
This field is not being used yet. If you did not want to pass a default area-code, an
empty-string ("" quote-quote) is required to fill the position.

• "true" – force a default area-code, if missing from 7-digit phone numbers (e.g.
"383-1234" becomes "208-383-1234" when formatted. If a default area-code was
not sent, this parameter becomes false, no matter how set.

• "true" – return a blank if an invalid phone number is found ("383-Q123") or, if


"false", return the original phone number, as-typed, un-modified.

Testing:

Run an initial test, testing the early exits on International numbers and email addresses.
Launch the program and type "011-208-383-1234" in textBox1 and click button1. It
should return unmolested. Similarly test an email address (@-sign). Normal phone

Chapter 21 - Formatting Numbers and Text Page: 518


numbers will return with a "pending" notice while the remainder of the code is
completed.

Extension Extract:

The next step is to extract optionally-typed extensions. If the original phone number
contains a manually-typed extension, as in
"383-1234 x456" or
"383-1234 xBob's phone"

(where the extension could be used as a comment), remove the extension and store in a
holding area (strextensionHold, previously defined). Extensions must have a space-x as
the delimiter.

Use a substring (util.MidStr) to copy the value. At the same time, when the extension is
identified, move the remainder of the phone number into a working area,
strnewFormattedNumber. Later, after all formatting is complete, the extension will be
appended, unaltered, to the final string.

Striping the extension will take several lines of code, so write them in their own
subroutine, making this the first of several "private" modules needed by the
PhoneNumberFormat routine.

Returning to the cl710 module, make this call at the "//x-Extension Extract Goes Here"
comment (a complete code listing of the PhoneNumberFormat module can be found at
the end of this section).

Phone Number Format: The Call to StripExtension, in progress


public string PhoneNumberFormat ( <various parameters went here> )
{
: <other code here>
:

//x-Extension Extract goes here: Call subroutine by ref


phoneNumber_StripExtension
(ref strnewFormattedNumber, ref strextensionHold)

//For testing:
return "Pending formatting: " + strnewFormattedNumber;
}

where:

• "phoneNumber_StripExtension" is a new cl710_Formatting function that will be


written in a moment and the compiler will complain until then.

• Because the called routine needs to modify two separate values (the held-extension
and the originally-passed phone number), pass the two values using a "By

Chapter 21 - Formatting Numbers and Text Page: 519


Reference" call. "By Ref" means that routine can modify the variables directly,
without worrying about the variables falling out of scope. By Ref was covered in
Chapter 7.

• Because of the By Ref call, the called-function does not need to "return" a value –
and this is why the "call" does not have an equal-sign, assigning the results to a
different value; values are updated directly.

phoneNumber_StripExtension:

Write the "phoneNumber_StripExtension" module. The logic is straight-forward: If a


space-x is found, lob everything from that position onwards.

a. Along the editor's top row of tabs, return to the cl170_Fomatting.cs module.

b. Locate a blank line after PhoneNumberFormat's closing brace and create a new
function called "phoneNumber_StripExtension" (or click the red-underline in the
calling-statement to stub-in the new module)

c. Type or change the new function into a "private void" – meaning it does not return a
value to the calling routine (hence the " call" did not have an equal-sign).

d. Type the parameter names, prefixing with the "ref" keyword (aka "by ref").

e. Complete the remainder of the phoneNumber_StripExtension logic using normal


string-parsing:

Chapter 21 - Formatting Numbers and Text Page: 520


phoneNumber_StripExtension, completed
private void phoneNumber_StripExtension
(ref string strNewFormattedNumber,
ref string strExtensionHold)
{
//Working function for the PhoneNumberFormat routine.
//Removes any extensions from a phone number (383-1234 x4567)
//Locate first space-x (x 4567 or xBob's); parse and save
//back to the original phone number and ExtensionHold
//Return both values byRef

int idelimFront;

idelimFront = strNewFormattedNumber.ToLower().IndexOf(" x");


if (idelimFront > -1)
{
//An extension was found; parse both x and truncate orig.
strExtensionHold =
util.MidStr (strNewFormattedNumber, idelimFront + 1).Trim();
strNewFormattedNumber =
util.LeftStr (strNewFormattedNumber, idelimFront).Trim();
}
}

where:

• The new function is "private", visible only to the cl710_Formatting library.

• The function is "void", meaning it does not return a value to the calling routine.
Instead, values are returned via "ref".

• idelimFront locates the " x" extension (required leading space).

• Once the extension is parsed, strNewFormattedNumber over-writes itself with the


new, shorter string, minus the extension, and strExtensionHold holds the extension
for later use.

Possible Compiler Error: "Use of unassigned local variable 'strExtensionHold'.


Solution: In the calling PhoneNumberFormat, initialize the strExtensionHold variable
with an empty string. It must be populated (allocated) before it can be used in a "by ref."

string strextensionHold = "";

Testing:

The new module, with the current code, is testable now. From button1_Click, send a
sample phone number: "(208) 383-1234 x5678". Results: only the phone number is
displayed when the value is returned. The extension is held in a temporary variable and
will be returned in a later step. If desired, place a breakpoint and use the debugger to
spy on the stored values.

Chapter 21 - Formatting Numbers and Text Page: 521


Strip Normal Punctuation:

After removing a possible extension, the working phone number


(strnewFormattedNumber) was re-painted with a shorter version. With the remaining
digits, strip all normal punctuation, which will hopefully leave a string of numeric digits.

The module could use the util.StripNonNumerics, but in this case it might be wiser to
only strip normal phone-number formatting characters, leaving other characters for
further testing. By leaving unexpected characters, such as "383-Q422", this can be later
flagged as an end-user error. Because this step requires several lines of code, this logic
moves into its own routine.

Calling "phoneNumber_StripNormalPunctuation":

In cl710's main "PhoneNumberFormat" routine, add a new statement calling a


"phoneNumber_StripNormalPunctuation", illustrated below in bold:

Phone Number Formatting: Call to StripNormalPunctuation, in progress


public string PhoneNumberFormat ( <various parameters here> )
{

//x-Extension Extract
phoneNumber_StripExtension
(ref strnewFormattedNumber, ref strextensionHold);

//Strip normal non-numeric punctuation from phone number:


strnewFormattedNumber =
phoneNumber_StripNormalPunctuation (strnewFormattedNumber);

//For testing:
return "Pending Formatting: " + strnewFormattedNumber;
}

Function: phoneNumber_StripNormalPunctuation:

In cl710, after 'PhoneNumberFormat's' closing brace, create a new function,


"phoneNumber_StripNormalPunctuation'. This will also be a private string function.
Complete the module with this code:

Chapter 21 - Formatting Numbers and Text Page: 522


phoneNumber_StripNormalPunctuation, completed

private string phoneNumber_StripNormalPunctuation


(string strPassedPhoneNumber)
{
//Strip all phone-number-ish punctuation, such as parenthesis,
//dashes, spaces, dots, etc; hopefully leaving just numbers.
//(am not using util.StripNonNumerics because we want to leave
//other letters A-Z, so the number can be flagged as invalid.
//This logic was taken from Chapter 6/7: Functions

char[] acpassedNumber; //Original phone number


string strstrippedNumber = ""; //Assembled number

if (util.IsBlank(strPassedPhoneNumber))
return "";

//Convert the passed number into a character array


//where ac=arracy-character
acpassedNumber = strPassedPhoneNumber.ToCharArray();

//Loop through each character:


foreach (char foundchar in acpassedNumber)
{
//Examine each character; discarding punctuation; leaving
//all others (0-9, a-z)
if (foundchar == ' ' ||
foundchar == '(' ||
foundchar == ')' ||
foundchar == '-' ||
foundchar == '.' ||
foundchar == '/' ||
foundchar == '#' ||
foundchar == '*' ||
foundchar == '+' ||
foundchar == ',' ||
foundchar == '\\')
{
//Punctuation char; discard and loop to next position
}
else
{
//The found character is a keeper; stack like chord-wood.
//It probably is 0-9, but can be an unexpected character;
//leave unexpected characters in place for later error-
//checking...
strstrippedNumber = strstrippedNumber + foundchar;
}
}

return strstrippedNumber;
}

phoneNumber_StripNormalPunctuation, end

comments:

Chapter 21 - Formatting Numbers and Text Page: 523


• This logic was pulled directly from Chapter 6/7's "StripNonNumeric" function and
was changed to accommodate the needs of this routine. Only 'phone-numberish'
punctuation is being removed, other characters survive. A later test will be made to
see if any non-numerics (a-z) survived. If you do not need to test for invalid phone
numbers, this routine could be replaced with a simpler call to util.StripNonNumeric.

• From the Call you can see it expects a string to be returned and the returned string
overwrites the original value, replacing it with the stripped version. This could have
also been called as a "by ref" function, but because it only modified a single variable,
a standard parameter-pass was used.

Testing:

This is testable now. From Form1's button1_Click, send any punctuated phone-number
through the routine. (Note: Extensions are still missing and they will be added in a later
step).

Results - Try these original and returned strings:

Original Expected Return


(208) 383-1234 2083831234
208/383-1234 2083831234
383.1234 3831234
383-Zebra 383Zebra
1-800-383-1234 18003831234

Strip "1-" Prefixes:

With the returned phone number, strip any long-distance prefixes that may have been
typed: 1-space, 1-dash, 1-period, and "(1" [as in (1208)]. Because of In-State
LongDistance concerns, the "1" may need to be preserved, but more often-than-not, it
can be discarded. Since the punctuation was removed in the previous step, this is a fairly
simple substring, but there are a few rules it needs to be aware of.

If the remaining phone number is less than 7 digits long, this is likely an extension
(without an "x"), as in "1456". Since phone numbers shorter than 7 digits can not
possibly be a long-distance phone number, the "1" needs to survive. For longer phone
numbers, the only other concern is with International phone numbers, where the
International Calling Prefix could be for another country, such as "1xx" -- but because of
the calling prefix ("011") was passed as a parameter and these numbers were already
removed from consideration, this is not a worry. Note that no US area-codes begin with
a "1".

When the completed number is formatted and re-assembled, the "1" will be put back into
its proper place, provided it matches the passed "default" area-code. In other words, if
your default area-code is "208", then the "1208" is preserved in case this is an In-State

Chapter 21 - Formatting Numbers and Text Page: 524


LongDistance number. Otherwise, it is considered fluff (1-800, 1-313, etc.) and is
discarded.

Calling the "phoneNumber_StripLongDistancePrefix" subroutine:

Immediately after the call that strips punctuation (shown below), add a call to a new
routine that conditionally strips "1" prefixes. The new routine needs to modify both the
original phone number and it needs to populate the strLongDistancePrefixHold variable,
defined at the top of the PhoneNumerFormat routine. Because of this, call two of the
variables as "By Ref". This code is in the cl710 Library and it calls another soon-to-be-
written cl710 module:

Phone Number Formatting: Call to Strip LongDistance Prefix (#1), in progress


public string PhoneNumberFormat ( <various parameters here> )
{
:

//Strip normal non-numeric punctuation from phone number:


strnewFormattedNumber =
phoneNumber_StripNormalPunctuation (strnewFormattedNumber);

//Strip long-distance #1 prefixes:


//(strnewFormattedNumber is also modified)

phoneNumber_StripLongDistancePrefix
(ref strnewFormattedNumber,
strDefaultAreaCode,
ref strlongdistancePrefixHold);

//For testing:
return "Pending Formatting: " + strnewFormattedNumber;
}

where:

• The call to "phoneNumber_StripLongDistancePrefix" has a mixture of by-ref and


non-by-ref variables. Only pass those variables that need to be modified as "ref"

Function: phoneNumber_StripLongDistancePrefix:

This is a fairly straight-forward routine and like the others in this section, it is called as a
"private void". Add this code in the cl710_Formatting.cs library.

Chapter 21 - Formatting Numbers and Text Page: 525


phoneNumber_StripLongDistancePrefix, completed

private void phoneNumber_StripLongDistancePrefix


(ref string strNewFormattedNumber,

string strDefaultAreaCode,
ref string strLongDistancePrefixHold)
{
//If present, remove and hold the 1- prefix (1-800), returning
//the stripped phone number and the "1" by Ref.
//Only store the "1" if the prefix matches the default Area-code,
//otherwise, discard.
//This routine expects de-punctuated phone numbers and it expects
//that no area-code begins with the number 1 (108, 117).
//Examples if default=208: 1208=Keep "1", 1800,1313=Discard "1"

if (util.IsBlank(strNewFormattedNumber))
return;

//If the passed phone is too short, assume an extension


if (strNewFormattedNumber.Length < 7)
return;

if (util.LeftStr(strNewFormattedNumber, 1) == "1")
{
if (util.MidStr(strNewFormattedNumber, 1, 3)
== strDefaultAreaCode
//Mid-string starting positions are base-0; lengths base 1
strLongDistancePrefixHold = "1";
else
strLongDistancePrefixHold = "";

//Unconditionally return the phone number, minus the "1"


strNewFormattedNumber = util.MidStr(strNewFormattedNumber,1);
return;
}
else
//Existing phone number does not being with "1"
return;
}

phoneNumber_StripLongDistancePrefix, end

Testing:

From Form1's "button1_Click", pass these stringed phone numbers through the
"formatting.PhoneNumberFormat" routine. As a reminder, here is the button1_Click
call:

Chapter 21 - Formatting Numbers and Text Page: 526


private void button1_Click (object sender, EventArgs e)
{
string strtempvalue = "";

strtempvalue = formatting.PhoneNumberFormat
(textBox1.Text,
"AM",
"208",
true,
true);
}

where:

• An "AM" (American-style) format will be used (not written yet)


• a default Area-Code "208",
• Prefix default area-code on all 7-digit numbers ("208")

It is always a good idea to test intermediate routines as they are written. When testing
with the default area-codes and other settings described, pass these phone numbers, with
these expected results.

Test Number Results


1-208-383-1234 2083831234 (with a hold on "1", assuming default 208)
1(208)383-1234 2083831234 (with a hold on "1")
1413-383-1234 x4567 4133831234 (discard "1" and a hold on extension)
12345 12345 (assumed to be an extension)

Note: The returned phone numbers are still unformatted. Long-distance prefixes and
extensions will be re-united in the next steps.

Report Errors:

If boolReturnBlankOnError was set to True, examine the remaining digits to see if the
phone number is valid. Because this step is run after the punctuation and extensions
were removed, a simple util.IsNumbers check is all that is needed. Use this code to test
for valid phone numbers. The test is only allowed if boolReturnBlankOnError is
engaged. This acts as an early exit:

Chapter 21 - Formatting Numbers and Text Page: 527


PhoneNumberFormatting: Checking for Valid PhoneNumbers, in progress
public string PhoneNumberFormat (<various parameters>)
{
:

//Check if the remaining number is numeric. If returned errors


//are requested, return a blank value.
//Otherwise, let the formatting routine look at this:

if (boolReturnBlankOnError= = true &&


util.IsNumbers (strnewFormattedNumber) == false)
return "";

//For testing:
return "Pending Formatting: " + strnewFormattedNumber;

Punctuate the remaining Digits:

Finally, now that the phone number has been stripped of all punctuation and other
variable information, look at the remaining lengths to determine how it should be
formatted. For example, a 7-digit phone number, could be punctuated in these steps:

Steps to Formatting...
208/383 1234 x456 Original number as typed by end-user
3831234 Original number, stripped, minus the extension
383-1234 Initial American Format
(208) 383-1234 Appended Default Area-Code
(208) 383-1234 x456 Re-applied Extension

Before formatting, make one last check to ensure the remaining phone number is not an
extension, such as 4567 (e.g. x4567 - where the "x" was not typed). Allow up to 6 digits.
If there are 7 or more digits, pass the phone number into a new module that actually does
the formatting. This module will use the phone number's length to determine the
activities needed. Here is the call to the last sub-routine:

Chapter 21 - Formatting Numbers and Text Page: 528


PhoneNumberFormat: Call to Punctuate, completed
Calls the punctuation routines

This is the completed module, that was shown above in pieces. The last several
paragraphs, marked in bold deal with the current text.

public string PhoneNumberFormat


(string strPassedPhoneNumber,
string strFormatStyleFlag,
string strDefaultAreaCode,
bool boolRequireAreaCode,
bool boolReturnBlankOnError)
{
//Format the passed phonenumber using the indicated style,
//
// AM1=(American), AM2=American-2, EU=European
// DefaultAreaCode for 7-digit phone numbers; may be blank
// boolRequireAreaCode (for 7-digit numbers) T/F
// boolReturnBlankOnError if mal-formed, return a blank or the
// originally-passed phone number T/F

string strnewFormattedNumber;
string strextensionHold = "";
string strlongDistancePrefixHold = "";
string strinternationalPrefix = "011";

strFormatStyleFlag = strFormatStyleFlag.ToUpper();

//Trim and move to working area:


strnewFormattedNumber = strPassedPhoneNumber.Trim();

//Early-Exits:
if (util.IsBlank (strnewFormattedNumber))
return "";

//Unconditionally return email addresses:


if (strnewFormattedNumber.Contains ("@"))
return strnewFormattedNumber;

//If international prefix; return un-edited number:


if (util.LeftStr(strnewFormattedNumber, 3) ==
strinternationalPrefix)
return strnewFormattedNumber;

//x-Extension Extract
//Move the remainder of the orig.number to a working area
//Note the call by ref
phoneNumber_StripExtension
(ref strnewFormattedNumber,
ref strextensionHold);

//Strip normal, non-numeric punctuation; leave other non-numerics


//for later (validity) testing; note mixture of ref's
strnewFormattedNumber =
phoneNumber_StripNormalPunctuation (strnewFormattedNumber);

//Strip long-distance #1 prefix; also modify strnewFormattedNumber


phoneNumber_StripLongDistancePrefix

Chapter 21 - Formatting Numbers and Text Page: 529


(ref strnewFormattedNumber,
strDefaultAreaCode,
ref strlongDistancePrefixHold);

//Check if the remaining number is numeric. If returned-errors


//are requested, return a blank value, otherwise, let the
//formatting routine look at the digits

if (boolReturnBlankOnError == true &&


util.IsNumbers (strnewFormattedNumber) == false)
//Non-numeric data detected and errors are requested...
return "";

//If the remaining number is long enough, format per the user's
//preferences, using the length as a key indicator:

if (strnewFormattedNumber.Length > 6)
{
//Finally, punctuate the phone number and repaint the
//working value. Pass everything you need to punctuate:
strnewFormattedNumber = phoneNumber_Punctuate
(strnewFormattedNumber,
strFormatStyleFlag,
strDefaultAreaCode,
strlongDistancePrefixHold,
strextensionHold,
boolRequireAreaCode,
boolReturnBlankOnError);
}
else
{
//Return the original number, unmodified: extension without x
return strPassedPhoneNumber.Trim();
}

//(No longer testing)


return strnewFormattedNumber;
}

phoneNumberFormat - Call to Punctuate, completed

where:

• The working variable, "strnewFormattedNumber" is about to be re-written with a


final, punctuated string.

• Pass everything you need to punctuate the now-naked phone number; area code
preferences, styles, held-extensions, and other switches are all sent into this one last
routine. This is a lengthy parameter list.

• The last statement was changed to return the final, newly-formatted string.

Chapter 21 - Formatting Numbers and Text Page: 530


phoneNumberPunctuate:

Finally, write the logic that re-punctuates the phone number. Everything up to this point
'cleaned' the phone number, getting it into its simplest form. From here, the program
looks to see if it is a 7 or 10-digit phone number and punctuates accordingly.

Here is the completed subroutine, as called by the main cl710_Formatting module:

phoneNumberPunctuate, completed
This is the routine that actually punctuates the phone number...

This is the completed punctuation routine and it relies on five other modules preceding
this function. This module is called by cl710_PhoneNumberFormat.

private string phoneNumber_Punctuate


(string strNewFormattedNumber,
string strFormatStyleFlag,
string strDefaultAreaCode,
string strLongDistancePrefixHold,
string strExtensionHold,
bool boolRequireAreaCode,
bool boolReturnBlankOnError)
{
//The received phonenumber is likely all digits 2083831234
//By this stage, all "1" prefixes, x-extensions have been removed
//and all standard punctuation and international numbers have been
//removed. Re-assemble based on Length

string strpunctuation;
string strcurrentAreaCode;
string strprefix;
string strsuffix;

//Consolidate-simplify the AM Format Styles:


strFormatStyleFlag = strFormatStyleFlag.ToUpper();
if (strFormatStyleFlag == "AM")
strFormatStyleFlag = "AM1";

//Decide the punctuation now; dashes or dots


//Make this the punctuation character, used everywhere
if (strFormatStyleFlag == "AM1" ||
strFormatStyleFlag == "AM2")
strpunctuation = "-"; //Use hyphens
else
strpunctuation = "."; //Assume European dots

//As a courtesy, strip leading "9" form all 8-digit phone numbers;
//This would be a "9" prefix for a PBX
if (strNewFormattedNumber.Length == 8 &&
util.LeftStr(strNewFormattedNumber, 1) == "9")
{
//Strip the leading 9, making this a 7-digit number
strNewFormattedNumber = util.MidStr(strNewFormattedNumber ,1);
}

Chapter 21 - Formatting Numbers and Text Page: 531


//Decide to use either the DefaultAreaCode or the areacode passed
//in from the original phone number. (The next step punctuates the
//areacode clause)
switch (strNewFormattedNumber.Length)
{
case 7:
//Parse the xxx-prefix and xxxx-suffix
strprefix = util.LeftStr(strNewFormattedNumber, 3);
strsuffix = util.MidStr(strNewFormattedNumber, 3);

//Set a defaultAreaCode, if appropriate:


if (boolRequireAreaCode == true &&
util.IsFilled(strDefaultAreaCode))
strcurrentAreaCode = strDefaultAreaCode;
else
strcurrentAreaCode = ""; //Force null
break;

case 10:
//Parse the xxx-areacode, xxx-prefix, xxxx-suffix
strcurrentAreaCode = util.LeftStr(strNewFormattedNumber, 3);
strprefix = util.MidStr(strNewFormattedNumber, 3,3);
strsuffix = util.MidStr(strNewFormattedNumber, 6);
break;

default:
//This is not a valid or expected phonenumber length.
//Decision: Should this be an error/blank or should the
//routine decide to format what it has? For now...
//Return a blank if authorized:

if (boolReturnBlankOnError == true)
strNewFormattedNumber = "";
else
{
//Leave the passed-number as is
}
return strNewFormattedNumber; //early-exit

} //End-of-switch statement

//Punctuate the AreaCode:


//The returned value from this function is perfectly punctuated
//as "(208) ", "1(208) ", "(208) ", "208-", "208." or ""
//(The function is written below)
strcurrentAreaCode = phoneNumber_AreaCodePunctuate
(strcurrentAreaCode,
strFormatStyleFlag,
strDefaultAreaCode,
strLongDistancePrefixHold,
strpunctuation);

//Now you are ready to format the final number and by this stage
//it is easy because all decisions have been made.
//Unconditionally prepend the AreaCode, prefix then suffix with
//the understanding the AreaCode may be null or blank. If the
//AreaCode is populated, it has all dashes, dots or spaces, as
//required by the passed preferences.

strNewFormattedNumber = strcurrentAreaCode +
strprefix + strpunctuation + strsuffix;

Chapter 21 - Formatting Numbers and Text Page: 532


//Return the newly-punctuated phone number
//Append the x-Extension, even if empty. Note the parenthesis:
return (strNewFormattedNumber + " " + strExtensionHold).Trim();

phoneNumber_Punctuate, end

where:

• This is not ready for testing. An additional module (below) is still needed.

• Several tricks were employed. Using the FormatStyleFlag (AM1, AM2, or


European), make a one-time decision on what the punctuation is going to be: dashes
or dots and store this in a variable called "strpunctuation". Later routines can use
this variable rather than re-interpreting which of the three choices it was originally.
This saves a lot of if-statements.

• Near the top of the module, it parses the area-code and if not found, it uses the
DefaultAreaCode or (blank), depending on how "boolRequireAreaCode" is set. By
placing this logic, one-time at the top, it doesn't need to be repeated later in the 7 and
10-digit routines.

• Punctuating the AreaCode ("phoneNumber_AreaCodePunctuate") was written as


another sub-routine so it didn't clutter this module. See the map below for
schematic. When this routine completes, it returns a fully-punctuated area-code,
with parenthesis, dashes or dots, depending on preferences.

• Later, the punctuated AreaCode is prepended to the rest of the phoneNumber. Even
though it might be an empty field, it is always prepended to the phone number; this
saves a lot of "if-statements" and other formatting-style questions from cluttering the
other parts of the routine.

• Finally, as the last step in the routine, the held-Extension is re-attached


(383-1234 x456) unconditionally. Even if the held-Extension is blank it is still
appended, leaving an extra space. A Trim will take care of the added baggage.

Chapter 21 - Formatting Numbers and Text Page: 533


Punctuating the AreaCode:

phoneNumber_Punctuate is a large routine and it would have been twice as large if it had
to decide, by itself, how to punctuate the AreaCode field. Because AreaCode logic was
fairly complicated, it was spun-off into its own module.

The goal of this function is to properly punctuate the AreaCode field, giving parenthesis,
dashes or dots, as needed. The AreaCode is being punctuated as an island, independent
of the rest of the formatting logic. If the AreaCode is not available, there is the
possibility it will return an empty-string.

Because the AreaCode's punctuation is so intertwined with the preferences on


formatting, almost every parameter passed into the main routine needs to be passed a
second time into this module. Looking above, at phoneNumber_Punctuate, look at the
"Call" to the AreaCode module, and at the signature-line, below.

phoneNumber_AreaCodePunctuate, completed

This is the AreaCode punctuation routine, which is called by the


phoneNumber_Punctuate module. Do not confuse this with either the
"phoneNumber_Punctuate" or the main cl710 "PhoneNumberFormat" module.

private string phoneNumber_AreaCodePunctuate


(string strcurrentAreaCode,
string strFormatStyleFlag,
string strDefaultAreaCode,
string strLongDistancePrefixHold,
string strpunctuation);
{
//Punctuate the AreaCode with parenthesis and preppended long-dist.
//prefixes, as needed.
//When done, you will have
// "1(208) ", "(208) ", "1208-", "208-", "208.", or ""

//The AreaCode was populated correctly in the calling module.


//However, it may have passed a legitmate blank:
if (util.IsBlank(strcurrentAreaCode))
return "";

//Compare the current AreaCode with the defaultAC; if the same,


//return with the AreaCode and a LongDistancePrefix (1), if typed
//by the user originally:
if (strcurrentAreaCode == strDefaultAreaCode &&
util.IsFilled(strLongDistancePrefixHold))
{

//The AreaCode matches the default and they passed a "1"


if (strFormatStyleFlag = "AM1")
//Use Parenthesis around the AreaCode; note the space, which
//acts as a punctuation: "(208)_383-1234":
strcurrentAreaCode =

Chapter 21 - Formatting Numbers and Text Page: 534


strLongDistancePrefixHold +
"(" + strcurrentAreaCode + ")" + " ";
else
//Assemble the area-code without parenthesis
//e.g. "208." or "208-"
strcurrentAreaCode =
strLongDistancePrefixHold +
strcurrentAreaCode + strpunctuation;
}
else
{
//The found AreaCode did not match the defaultAreaCode
//(This cannot be an instate-Longdistance number; drop the "1",
//if it was passed orginally by the end-user)
//Assemble found AreaCode with parenthesis or not, punctuation
//was pre-decided.

if (strFormatStyleFlag = "AM1")
//Use parenthesis around the AreaCode:
strcurrentAreaCode = "(" + strcurrentAreaCode + ")" + " ";
else
//Do not use parenthesis; note the punctuation:
//e.g. "800." or "800-"
strcurrentAreaCode = strcurrentAreaCode + strpunctuation;
}

//Return the fully-punctuated AreaCode:


//By this stage,the phone number is fully-edited
return strcurrentAreaCode;

phoneNumber_AreaCodePunctuate, end

Putting this all together:

When looking at the code above, it is easy to get lost. Keep these thoughts in mind: All
of the PhoneNumber modules are written within the cl710_Formatting.cs class library.
Remember, code examples documented in grey text boxes (or surrounded by grey boxes)
are 'completed' code.

Not counting the class library itself, you will be building six separate routines, including
the main PhoneNumberFormat." All are needed to support the formatting logic.

The cl710 "PhoneNumberFormatting" modules were a complicated series of routines,


with many inter-related steps, but the results are ready for final testing. The completed
routine is part of the cl710_Formatting Class Library and it can be linked into any future
program. The method is robust and can handle any imaginable U.S. phone number and
with minor changes, it can probably accommodate non-US numbers. You will never
have to write a phone number routine again.

Chapter 21 - Formatting Numbers and Text Page: 535


Follow these steps for testing.

A. In Form1's, button1_Click, add this test code:

private void button1_Click (object sender, EventArgs e)


{
//Accept test phone numbers from TextBox1

textBox1.Text = "(208) 383-1234 x1234";


string strtempvalue = "";

strtempvalue = formatting.PhoneNumberFormat
(textBox1.Text,
"EU",
"208",
true,
true);

if (util.IsBlank(strtempvalue))
MessageBox.Show("'" + textBox1.Text + "' is invalid");
else
MessageBox.Show("Reformatted number is " + strtempvalue);
}

B. Press F5 to run the program,


Type a badly-formatted phone number in textBox1 and click button1.

Chapter 21 - Formatting Numbers and Text Page: 536


Run the tests multiple times, passing each of following phone numbers through
textBox1.

Example Test Phone Numbers:

After setting a Default AreaCode in button1_Click's call, try these example phone
numbers for testing. You can imagine other combinations.

3831234
383-1234
38-31234
2083831234
(208)3831234
(208) 383.1234
12083831234
(1208)383-1234 x456
011 34 332.11.1344

Exercise:

See the Exercises at the end of the chapter for an interesting way to test all possibilities.

This completes the phone number routines.

Chapter 21 - Formatting Numbers and Text Page: 537


"ProperNames" Formatting - Overview

Continuing with the same formatting module, cl170, format Person and Company name
fields, as well as street addresses. If you ever needed to process data-files that arrived in
ALL CAPS, this routine can repair the data and it can repair data typed by end-users on
data-entry screens. I have found this to be one of the most useful modules I have ever
written. It is easy to teach it new rules if your needs differ.

For example, consider these names and addresses:

Should be
randy b jones jr Randy B. Jones Jr.
123 n. elm street 123 N Elm ST

Programs such as Microsoft Access, often have an "@Proper" function, which converts a
field to "initial caps," where "randy b jones" becomes "Randy B Jones", but I have found
them to be too simplistic. C# does not have this capability, but even if it did, the design
fails to account for words like "and", "the", "inc" and others.

Consider these phrases. In each case, consider why simple @Proper would fail:

Original, mis-typed Correct Proper @Proper failings and comments


randy b jones Randy B. Jones Needs a period at Middle Initial
randy b jones jr Randy B. Jones Jr. Punctuated
randy b Jones Iii Randy B. Jones III "The Third" (@Proper would "Ill")
RAndy b Jones lll Randy B. Jones III Incorrectly typed with "L's" "LLL"

bob o'donnel Bob O'Donnel Apostrophes; D needs capitalized


Mary smith-jones Mary Smith-Jones Hyphenated; Jones needs caps
Dr John Q Smith, dds Dr. John Q. Smith, DDS DDS, DMD, DDSDMD, etc.
Drs smith & jones Drs. Smith & Jones

Bob And Sons inc Bob and Sons, Inc. lower-cased "and" + ", Inc."
the ribbon co The Ribbon Co. Colorado "CO" or "Company"?
aaa Automotive Repair AAA Automotive Common "TLA"8
at systems AT Systems "At"
pizza at the station Pizza at the Station lower-cased 'at' and 'the'
EZ carpet EZ Carpet Common TLA

123 n. elm st. apt 1a 123 N Elm ST Apt 1A


p.o. Box 123a PO Box 123A Caps on numbers; PO, PMB, etc.
123 nw elm st ste 15a 123 NW Elm ST, STE 15A
st johns ln Saint Johns LN "Saint"
st. johns st. Saint Johns ST "Saint", Street

8
"TLA" Three Letter Acronym.

Chapter 21 - Formatting Numbers and Text Page: 538


With all these exceptions, is an @Proper routine possible? Yes. This next routine
handles all of these issues and once written, they can be called any time.

The Goal:

In this section, write a routine that correctly sets "proper" capitalization and punctuation
that will work for a vast majority of Names, Addresses and Titles. This new module is
more effective than a standard VB @Proper. While no "proper" routine is perfect, the
code demonstrated here has been remarkably accurate and it has been tested on literally
hundreds of thousands of names and addresses.

The ProperNamesFormat module is longer, but less complicated than the previously-
written PhoneNumberFormat routine. Most of the length comes from "exceptions to the
rule." While most words can be set for "Initial Caps," exceptions such as "NYC", "Co.",
"and", "the", and others, must be treated differently and this takes the bulk of the code.

The exception logic is almost always the same. Each parsed word is passed into the
routine and converted to a variable called "tWord" – a temporary word. As you can see,
the exception logic is very simple:

:
case "M.D.":
case "MD":
tWord = "MD";
boolModified = true;
break;

case "MR.":
case "MR":
tWord = "Mr.";
boolModified = true;
break;
:

Summary: Using ProperNameFormat:

Here is how the new module will be called:

strNewText = formatting.ProperNamesFormat (strOriginalText, "NAME")

where a hard-coded switch, "NAME", tells the routine what kind of string it is repairing.
Other 'RecordTypes' (the second parameter) are
"NAME",
"ADDRESS" or
"OTHER".

Chapter 21 - Formatting Numbers and Text Page: 539


The Design for ProperNamesFormat:

The program's design is simple: Take a phrase, such as


"123 n. maple-grove st, apt 106a" or even
"123 n mAplE-GRove St, aPT 106a"

and parse the individual words into an array:


123
n
maple
grove
st
apt
106a

Note how hyphenated words are considered independently. Each word is processed
through the "upper-case" routine. No matter how badly-typed, it will resolve to this:

"123 N Maple-Grove ST, Apt 106A"

Each-word's Processing Includes:

Take each word and check to see if it has an exception. If not, upper-case the initial
letter and lower-case the remainder.

• If the last word in the phrase, and it is two letters long, force to all upper-case
• If the word starts with a number, upper-case (e.g. 123a to 123A)
• If the word is a single-character, upper-case, with some exceptions, especially if not
the first word.
• If the word is a single-character and it is name, upper-case with period (Q.)
• Special consideration given to names such as "O'Donnel" and "McFarland"

The module behaves slightly differently with first, middle or last words and it behaves
differently if the passed value is an "Address", "Person's name", controlled with
parameters.

Writing ProperNamesFormat:

Note: The "ProperNames" formatting routine lives in the cl710


Format class library, built in the previous section and it heavily
uses Chapter 8 (cl800_Util Libraries). If you have not already
written these modules, contact the Author for an electronic copy.

Consider how the new module is going to be called before writing the actual routine. In
this case, a test-program, "Form1," will take what ever is in TextBox1 and will place a
corrected value into TextBox2 and it will assume it is an address field:

Chapter 21 - Formatting Numbers and Text Page: 540


Sample Call to cl710's "ProperNamesFormat", completed
private void button1_Click (object sender, EventArgs e)
{
//link in the formatting Class Library:
formatting = new cl710_Formatting(); //Built earlier

//Take what was typed in TextBox1, correct the punctuation and


//return the results into TextBox2

textBox2.Text =
formatting.ProperNamesFormat (textBox1.Text, "ADDRESS");
}

where:

• formatting.ProperNamesFormat is a method in the cl710_Formatting Class Library.


cl710 was instantiated with an aliased-name "formatting" (similar to 'util.MidStr').

• textBox1 (a data-entry field in Form1) is being sent to the formatting routine.

• "ADDRESS" (or "NAME" or "OTHER") can be sent as a parameter to help control


what type of formatting is being applied.

• The results are placed in textBox2.

Writing ProperNamesFormat will take the remainder of the


chapter. After the introduction, the source-code is displayed
without the normal step-by-step details because of space-
restrictions. You will find there is nothing unusual about the
logic and if you have followed along with the earlier chapters,
nothing will be surprising.

Chapter 21 - Formatting Numbers and Text Page: 541


ProperNamesFormat - Coding

1. In your test program, link the cl710_Formatting library, as described at the front of this
chapter and also, as detailed in the "PhoneNumberFormatting." If you have been
following along through this chapter, this is already done. Summary:

In Form1, Add (link) cl710_Formatting.cs


using ns710_Formatting;
cl710_Formatting formatting;
formatting = new cl710_Formatting();

2. In Solution Explorer, double-click on "cl710Formatting.cs" to open code view.

3. In a blank area (in cl710 – not Form1), create a new method called
"ProperNamesFormat" and have it accept two passed parameters in the signature:

public string ProperNamesFormat


(string strPassedPhrase, string strRecordType)
{
//Converts first-character of each word to uppercase and remaining
//characters to lowercase, with numerous exceptions.
//If the phrase begins with an asterisk (*), leave the phrase
//exactly as-typed.
//Do other cleanup, such as multiple interior spaces, etc.
//Usage: ProperNamesFormat (string, "type") where
// RecordType = "ADDRESS", "PERSON", "OTHER"

4. Complete the main module. The top-level routine parses each word into an array, then
calls other routines. You will notice it keeps track of the first and last words in the array
and it keeps track of the "previous word." After that, complete three other modules, all
documented below.

Author's note: This routine was loads of fun to write and it


works really well.

Chapter 21 - Formatting Numbers and Text Page: 542


cl170_Formatting: "ProperNamesFormat", completed

This is the completed main section for "ProperNamesFormat" and this is the "public
string" routine, visible to Form1. Type this code now:

public string ProperNamesFormat


(string strPassedPhrase, string strRecordType)
{
//Converts first-character of each word to uppercase and remaining
//characters to lowercase, with numerous exceptions.
//If the phrase begins with an asterisk (*), leave the phrase
//exactly as-typed.
//Do other cleanup, such as multiple interior spaces, etc.
//Usage: ProperNamesFormat (string, "type") where
// RecordType = "ADDRESS", "NAME", "OTHER"

int tWordCount = 0;
string tWord; //Currently-processed word
string tPrevWord = "";
string tWordDelimiter = ""; //A space or a (^)=Hyphen
string firstWordFL; //"FIRST", "MID", "LAST"
string tFinalAssembly = ""; //The resulting string, assembled

string[] aSeparateWords; //The array of words

strRecordType = strRecordType.ToUpper();

if (util.IsBlank(strPassedPhrase))
return "";

//Early Exit
if (util.LeftStr(strPassedPhrase, 1) == "*")
{
//User has specified to leave this text exactly as typed;
//truncate *
//Example: *enLight --> enLight

return util.MidStr(strPassedPhrase, 1).Trim(); //Return as typed


}

//Prep field by Trimming off leading spaces (Unconditional) and


//setting to Lowercase
strPassedPhrase = strPassedPhrase.ToLower().Trim();

//Hyphens are a problem because they are needed by the split


//(forming a two separate words), but the split removes the hyphen
//making re-assembly a problem. Substititue hyphens with special
//character for safe keeping; let downstream routines remove;
//thus, smith-jones
//becomes smith-^jones

//Remove dual-hyphens (messes up split routine); rare.


strPassedPhrase = strPassedPhrase.Replace("--", "-");
strPassedPhrase = strPassedPhrase.Replace("-", "-^");
//(Matches tWordDelimiter)

//This logic parses each word into an array.

Chapter 21 - Formatting Numbers and Text Page: 543


//Each word is sent to a Ucase routine that looks for exceptions.
//The returned word is assembled into 'tFinalAssembly' and the
//next word is sent downstream. The routine needs to keep track
//if the word delimiter is a space or a hyphen.

aSeparateWords = strPassedPhrase.Split(' ', '-');


foreach (string strfoundWord in aSeparateWords)
{
//re-assign found word to a new temp variable
tWord = strfoundWord;
if (util.LeftStr(tWord, 1) == "^") //Look for hyphen (e.g. ^)
{
//Found a hyphenated word; remove ^-marker
//Mark word-delimiter as a hyphen; otherwise use a space
tWordDelimiter = "-";
tWord = util.MidStr(tWord, 1); //Truncate first char '^
}
else
tWordDelimiter = " ";

//Count the words found...


tWordCount++;
if (tWordCount == 1)
firstWordFL = "FIRST";
else
{
if (tWordCount == aSeparateWords.Length)
firstWordFL = "LAST";
else
firstWordFL = "MID";
}

//Uppercase/Proper the found-word


if (cl753_CheckPrevWordForNumerics(tPrevWord) == true)
tWord = tWord.ToUpper();
else
{
//Conditionally ucase using this routine:
//Attach to previously found words and append the
//preceding delimmiter
tWord = cl751_UCaseWord
(tWord, tPrevWord, firstWordFL, strRecordType);
}

//Some tWord begin with a leading space (e.g. " Inc")


tFinalAssembly =
tFinalAssembly.TrimEnd() + tWordDelimiter + tWord;
tPrevWord = tWord;

} //end of the foreach loop

//Look to see if the last word is two characters long; if so,


//unconditionally UCase
tFinalAssembly =
cl752_ConditionallyUcaseLastWord(tFinalAssembly);

//Finally, return the results to the calling routine:


return tFinalAssembly.Trim(); //Return the function with it's value

ProperNamesFormat, end

Chapter 21 - Formatting Numbers and Text Page: 544


Module cl751_UCaseWord:

ProperNamesFormat calls 'cl751_UCaseWord' and this routine does most of the actual
work and contains the bulk of the code. Most of the logic within this module is self-
similar and easy to write.

Notice this is a "private string" routine, meaning it is only visible to the cl710 Class
Library. It accepts a single-passed word and returns the word properly cased and
formatted. Here is the completed code with inline comments:

cl710_Formatting: private routine: cl751_UCaseWord, completed

This routine is called by the main ProperNameFormat module and this routine upper-
cases and punctuates individual words, as passed. This is a lengthy routine but much of
the logic consists of the same three lines.

private string cl751_UCaseWord


(string tWord,
string tPrevWord,
string FirstWordFL,
string strRecordType)
{
//This routine works on the individual word, as passed through tWord
//Shift the first character of the word to uppercase....

string tSubString;
bool boolModified = false;

//Check for O'Leary, O'Brian, D'Alsendrios, O'Conner, etc.


if (util.MidStr(tWord, 1, 1) == "'")
{
tWord = util.LeftStr(tWord, 2).ToUpper() +
util.MidStr(tWord,2,1).ToUpper() +
util.MidStr(tWord, 3);
return tWord;
}

//Some words, such as VanHalen or vanHalen can not be distinguished in


//this routine. Abandon any editing of names which are ambiguously
//cased.
//E.g. von Newman, le Chance
if (strRecordType != "NAME")
{
//Do not hesitate on Von's, etc., send them through the routine
}
else
{
//Strict = false; use more finese'
tSubString = util.LeftStr(tWord, 3).ToUpper();
if (tSubString == "VON" ||
tSubString == "LE ")

Chapter 21 - Formatting Numbers and Text Page: 545


{
return tWord; //Return unmodified
}
}

switch (tWord.ToUpper())
{
//Reminder: This routine only deals with single-words
//All tests are against upper-case

case "&ASSOC":
case "&ASSOC.":
tWord = "& Assoc.";
boolModified = true;
break;

case "A":
if (strRecordType == "NAME" ||
FirstWordFL == "FIRST" ||
FirstWordFL == "LAST")
{
//Always uppercase lonely letter "A"'s
tWord = "A";
boolModified = true;
}
else
{
//Ignore the word (leaving as typed) and return
//"Bobby Caught a Fish"
}
break;

case "AT":
if (FirstWordFL == "FIRST")
tWord = "AT";
else
tWord = "at";
boolModified = true;
break;

case "AA":
//Famous TLA
tWord = "AA";
boolModified = true;
break;

case "AAA":
//Famous TLA
tWord = "AAA";
boolModified = true;
break;

case "ABC":
//Famous TLA
tWord = "ABC";
boolModified = true;
break;

case "ADDC":
tWord = "ADDC";
boolModified = true;
break;

case "AND":
tWord = "and";

Chapter 21 - Formatting Numbers and Text Page: 546


boolModified = true;
break;

case "APT":
case "APT.":
//Force to a consistent punctuation with mixed case and periods
tWord = "Apt";
boolModified = true;
break;

case "ATTN":
case "ATTN:":
tWord = "Attn:";
boolModified = true;
break;

case "AVE.":
case "AVE":
tWord = "Ave";
boolModified = true;
break;

case "BLVD":
case "BLVD.":
tWord = "Blvd";
boolModified = true;
break;

case "BY":
tWord = "by";
boolModified = true;
break;
case "BX":
tWord = "BX";
boolModified = true;
break;

case "CIA":
//Famous TLA
tWord = "CIA";
boolModified = true;
break;

case "CO":
case "CO.":
//Doesn't get confused with Colorado (CO) because the State Code
//edit is checked in a different area for two digits before it is
//sent for Proper
//e.g. Statecode logic:
// "if Len=2 the UCASE, else, send to Proper"

if (strRecordType == "ADDRESS")
{
//Leave as typed
}
else
{
tWord = "Co.";
boolModified = true;
}
break;

case "CT":
case "CT.":
//cant win with this one:

Chapter 21 - Formatting Numbers and Text Page: 547


//if Address could be court or CT connecticut
//Recommend not sending through state-codes
if (strRecordType == "ADDRESS")
tWord = "Ct";
else
tWord = "Ct";
boolModified = true;
break;

case "CV":
//No idea what this is
tWord = "CV";
boolModified = true;
break;

case "DBA":
case "D.B.A.":
tWord = "DBA";
boolModified = true;
break;

case "DR":
case "DR.":
if (strRecordType == "ADDRESS")
tWord = "DR";
else
tWord = "Dr.";
boolModified = true;
break;

case "DCP":
//Famous TLA
tWord = "DCP";
boolModified = true;
break;
case "DJ":
tWord = "DJ";
boolModified = true;
break;
case "DOJ":
//Famous TLA
tWord = "DOJ";
boolModified = true;
break;

case "DRS":
case "DRS.":
//Doctors
tWord = "Drs.";
boolModified = true;
break;

case "DMD":
tWord = "DMD";
boolModified = true;
break;
case "DMDS":
tWord = "DMDS";
boolModified = true;
break;
case "DDS":
tWord = "DDS";
boolModified = true;
break;
case "DDSPC":

Chapter 21 - Formatting Numbers and Text Page: 548


tWord = "DDSPC";
boolModified = true;
break;
case "DSS":
tWord = "DSS";
boolModified = true;
break;

case "EL":
tWord = "El";
boolModified = true;
break;

case "EZ":
tWord = "EZ";
boolModified = true;
break;

case "FAGD":
tWord = "FAGD"; //physician title
boolModified = true;
break;

case "FBI":
//Famous TLA
tWord = "FBI";
boolModified = true;
break;

case "FE":
//Sante Fe
tWord = "Fe";
boolModified = true;
break;

case "FL":
case "FL.":
//Floor
if (FirstWordFL == "LAST")
tWord = "FL";
else
tWord = "Fl";
boolModified = true;
break;

case "FM":
tWord = "FM";
boolModified = true;
break;

case "FOR":
tWord = "for";
boolModified = true;
break;

case "II":
case " LL ":
tWord = "II";
boolModified = true;
break;
case "III":
case " LLL ":
tWord = "III";
boolModified = true;
break;

Chapter 21 - Formatting Numbers and Text Page: 549


case "IV":
case " LV ":
tWord = "IV";
boolModified = true;
break;

case "INC":
case "INC.":
//Check if the previous word already had a comma: John Smith, Inc
if (util.RightStr(tPrevWord, 1) == ",")
tWord = "Inc";
else
tWord = ", Inc";
boolModified = true;
break;

case "JD":
tWord = "JD";
boolModified = true;
break;

case "JFK":
//Famous TLA
tWord = "JFK";
boolModified = true;
break;

case "JR.":
case "JR":
tWord = "Jr.";
boolModified = true;
break;

case "LN":
case "LN.":
if (strRecordType == "ADDRESS")
tWord = "LN";
else
tWord = "LN";
boolModified = true;
break;

case "LLC":
tWord = "LLC";
boolModified = true;
break;
case "LTD":
case "LTD.":
tWord = "Ltd";
boolModified = true;
break;

case "M.D.":
case "MD":
tWord = "MD";
boolModified = true;
break;

case "MR.":
case "MR":
tWord = "Mr.";
boolModified = true;
break;
case "MRS.":
case "MRS":

Chapter 21 - Formatting Numbers and Text Page: 550


tWord = "Mrs.";
boolModified = true;
break;

case "MLK":
//Famous TLA
tWord = "MLK";
boolModified = true;
break;

case "NY":
//tamous TLA
tWord = "NY";
boolModified = true;
break;

case "NYC":
//Famous TLA
tWord = "NYC";
boolModified = true;
break;

case "OF":
tWord = "of";
boolModified = true;
break;

case "OMS":
tWord = "OMS";
boolModified = true;
break;

case "PA":
tWord = "PA";
boolModified = true;
break;

case "PHD":
case "P.H.D.":
tWord = "PHD";
boolModified = true;
break;

case "PL":
case "PL.":
if (strRecordType == "ADDRESS")
tWord = "PL";
else
tWord = "Pl.";
boolModified = true;
break;

case "P.O.":
case "PO":
case "POB":
case "P.O.B.":
tWord = "PO";
boolModified = true;
break;
case "P.M.B":
case "PMB":
tWord = "PMB";
boolModified = true;
break;

Chapter 21 - Formatting Numbers and Text Page: 551


case "PT":
case "PT.":
if (strRecordType == "ADDRESS")
tWord = "PT";
else
tWord = "Pt.";
boolModified = true;
break;

case "RD":
case "RD.":
if (strRecordType == "ADDRESS")
tWord = "RD";
else
tWord = "Rd.";
boolModified = true;
break;

case "RE":
case "RE:":
tWord = "RE:";
boolModified = true;
break;

case "RMA":
case "RMA:":
tWord = "RMA:";
boolModified = true;
break;

case "SC":
tWord = "SC";
boolModified = true;
break;

case "SQ":
case "SQ.":
tWord = "SQ";
boolModified = true;
break;

case "SR.":
case "SR":
tWord = "Sr.";
boolModified = true;
break;

case "ST":
case "ST.":
if (strRecordType == "ADDRESS")
{
if (FirstWordFL == "FIRST")
tWord = "Saint";
else
tWord = "ST";
boolModified = true;
}
else
{
tWord = "St.";
boolModified = true;
}
break;

case "STE":

Chapter 21 - Formatting Numbers and Text Page: 552


//Suite
//See additional comma-cleanup at the bottom of the A750 routine
if (FirstWordFL == "FIRST")
tWord = "STE";
else
{
//tWord = ", Ste" //This is how I'd like it
//Check if the prevword already had a comma: John Smith, Inc
if (util.RightStr(tPrevWord, 1) == ",")
tWord = "STE";
else
tWord = ", STE";
}
boolModified = true;
break;

case "THE":
//If "THE" is the first word, convert to "The"; otherwise, leave
//as typed
if (FirstWordFL == "FIRST")
tWord = "The";
else
tWord = "the";
boolModified = true;
break;

case "TO":
if (FirstWordFL == "FIRST")
tWord = "TO";
else
tWord = "to";
boolModified = true;
break;

case "TO:":
tWord = "TO:";
boolModified = true;
break;

case "US":
case "U.S.":
tWord = "US";
boolModified = true;
break;

case "USA":
case "U.S.A.":
tWord = "USA";
boolModified = true;
break;

case "VFW":
tWord = "VFW";
boolModified = true;
break;

case "N":
case "N.":
tWord = "N";
boolModified = true;
break;

case "S":
case "S.":
tWord = "S";

Chapter 21 - Formatting Numbers and Text Page: 553


boolModified = true;
break;

case "E":
case "E.":
tWord = "E";
boolModified = true;
break;

case "W":
case "W.":
tWord = "W";
boolModified = true;
break;

case "NW":
case "N.W.":
tWord = "NW";
boolModified = true;
break;
case "NE":
case "N.E.":
tWord = "NE";
boolModified = true;
break;
case "SW":
case "S.W.":
tWord = "SW";
boolModified = true;
break;
case "SE":
case "S.E.":
tWord = "SE";
boolModified = true;
break;

case "1ST":
case "1ST.":
//Force to a consistent punctuation including lcase and periods
tWord = "1st";
boolModified = true;
break;
case "2ND":
case "2ND.":
tWord = "2nd";
boolModified = true;
break;
case "3RD":
case "3RD.":
tWord = "3rd";
boolModified = true;
break;
case "4TH":
case "4TH.":
tWord = "4th";
boolModified = true;
break;
case "5TH":
case "5TH.":
tWord = "5th";
boolModified = true;
break;
case "6TH":
case "6TH.":
tWord = "6th";

Chapter 21 - Formatting Numbers and Text Page: 554


boolModified = true;
break;
case "7TH":
case "7TH.":
tWord = "7th";
boolModified = true;
break;
case "8TH":
case "8TH.":
tWord = "8th";
boolModified = true;
break;
case "9TH":
case "9TH.":
tWord = "9th";
boolModified = true;
break;
case "10TH":
case "10TH.":
tWord = "10th";
boolModified = true;
break;

default:
//All other words, which do not fall into the above exceptions:
//Shift the first character to uppercase; leave the remaining
//word as typed
tWord = util.LeftStr(tWord, 1).ToUpper() + util.MidStr(tWord, 1);

//Do not flag as modified; this is the default modification


//boolModified = True
break;
}

//Final Exceptions and Cleanup

//Process after all other rules are looked at.


//Place here in the off-chance that another rule may be added in
//the future;
if (boolModified == true)
{
//Leave as Modified: Already modified with another rule
}
else
{
//Check to see if this is a two-charcter ("ADDRESS" type)
//AK, AL, AK. CT., etc.
//If so, uppercase both characters; only do this if not
//previously modified
if(strRecordType == "ADDRESS")
{
if(tWord.Length == 2 ||
(tWord.Length == 3 && util.MidStr(tWord, 3, 1) == "."))
tWord = tWord.ToUpper();
}

//Check to see if the word begins with a # or number; if so,


//unconditionally ucase. Examples: apt "123a" "#14b"
if(util.LeftStr(tWord, 1) == "#" ||
util.IsNumeric(util.LeftStr(tWord, 1)))
tWord = tWord.ToUpper();
}

//Check to see if this is a person's initial:


//Only do this for A-Z values; not for &. or +. or #. etc.

Chapter 21 - Formatting Numbers and Text Page: 555


if (strRecordType == "NAME" && tWord.Length == 1)
{
char twordChar = Convert.ToChar(tWord);
if (twordChar >= 'A' && twordChar <= 'Z' ||
twordChar >= 'a' && twordChar <= 'z')
tWord = tWord + ".";
}

return tWord; //Return with the final word

cl751_UCaseWord, end

Minor UCase Subroutines:

UCase calls two minor subroutines and the code for these is listed below with internal
comments.

cl752_ConditionallyUCaseLastWord in a phrase, completed


private string cl752_ConditionallyUcaseLastWord
(string strpassedPhrase)
{
//If the last word in the passed phrase is 2 characters long,
//ucase unconditionally
//In Mid-string, look for the word " of " and if found, see if
//the next word is two characters long...

int ihpos;
string strtemp;
string strfront;

ihpos = strpassedPhrase.LastIndexOf(" ");


if(ihpos > 0)
{
strtemp = util.MidStr(strpassedPhrase, ihpos + 1);
if(strtemp.Length == 2)
{
//This is the last word in the phrase and it is two
//characters long
strfront = util.LeftStr(strpassedPhrase, ihpos);
strpassedPhrase = strfront + " " + strtemp.ToUpper();
return strpassedPhrase;
}
}
return strpassedPhrase;
}

This next routine compares the previously-UCased word to see if it is a reason to force
the next word to upper-case. For example, "STE (suite) B1" should always be ucased
because of the word "Ste.". This is a "private bool" function.

Chapter 21 - Formatting Numbers and Text Page: 556


private bool cl753_CheckPrevWordForNumerics(string tPrevWord)
{
//Check to see if the word should be unconditionally uppercased.
//Typically after words like "Apt 103A", Ste AA, etc.
//This routine looks at the *previous* word to decide
//returns a T/F
if (tPrevWord.ToUpper() == ", STE" ||
tPrevWord.ToUpper() == "STE" ||
tPrevWord == "#" ||
tPrevWord.ToUpper() == "APT" ||
tPrevWord.ToUpper() == "APT.")

//Unconditionally ucase all letters in the next word


return true;
else
return false;
}

Calling Notes:

formatting.ProperNamesFormat punctuates both proper names and addresses, depending


on what switch was passed through the calling signature. In your program, the calls may
look similar to this:

pnlPersonName.Text =
formatting.ProperNamesFormat(pnlPersonName.Text, "NAME");

pnlStreetAddress.Text =
formatting.ProperNamesFormat(pnlStreetAddress.Text, "ADDRESS");

pnlCityName.Text =
formatting.ProperNamesFormat(pnlCityName.Text, "OTHER");

You would only pass those fields which needed this type of editing into the
ProperNames routine.

Chapter Conclusion:

This chapter demonstrated how to format numbers, words and phrases, using a variety of
picture-clauses and with more sophisticated logic. The PhoneNumber and ProperNames
methods will take time to build but you should find them useful in a variety of different
situations. Once placed in a Class Library, they can be used in other programs and this
code will never need to be written again.

Chapter 21 - Formatting Numbers and Text Page: 557


Exercises

A. As an exercise, modify the PhoneNumberFormat program (Form1) with these additional


fields (illustrated). Pass the values of the fields into the Formatting routines, simulating
various program preferences.

B. Create a simple data-entry screen that prompts for


Name
Address1
Address2
City
State
Zip
HomePhone
Work Phone

Set ProperNames, using the cl710 Class Library as users type the data. Assemble each
term into a completed Address block and display either on screen or in a MessageBox

For example:
Dr. John Q. Smith DDS
1500 SW Front ST, STE 123A
Santa Fe, NM 83322-1234
Work Phone: 800.383.1234

where:

• Address2 was not displayed because no data was entered.


• HomePhone was not displayed because of no data
• Some lines were passed a RecordType of "NAMES" and other lines "ADDRESS"

C. Extra Credit
Using Excel or Notepad, create a tabbed-delimited table with a variety of name and
address information. Save the file.

Chapter 21 - Formatting Numbers and Text Page: 558


Have C# open the file and process each record, correcting for ProperNames. Re-write
the results to a new (ASCII) file. Drive this from button1_Click.

Chapter 21 - Formatting Numbers and Text Page: 559


Chapter 21 - Formatting Numbers and Text Page: 560
Appendixes
Appendix A - Compiler Error Messages

Alphabetic Listing

This is an alphabetic listing of various compiler messages with likely solutions. These are
from Visual Studio 2005, SP1 through VS 2014.

Errors and warnings are sorted alphabetically. Search by the first non <variable> word.
e.g. "Argument '2': cannot convert from 'double' to 'float' will be found under "cannot..."
Messages such as "The type arguments..." will be under "The"; Messages that begin with
punctuation ("; expected") are listed first.

Additional Comments on Error Messages:


If you have repaired a mis-typed line in your program but the same error message
continues to show, try the following from the editor window: Select menu choice: "Build,
Rebuild Solution".

"; expected" (semi-colon expected)


Also: Invalid Expression Term "."

Symptoms:
The compiler normally shows exactly where a semi-colon is expected and when you get this
error it is normally flagged at the very end of a line. If the compiler shows it in the middle
of a line, it can get confusing.

Problem:
This incorrectly typed command would show an expected missing semi-colon at the
Convert.ToString phrase.:
MessageBox.Show Convert.ToString(loopCounter); //missing paren

Solution:
In this MessageBox example, note that the MessageBox.Show phrase was incorrectly typed;
it is missing a set of parenthesis. This confuses the compiler like something awful. The
correct syntax is:

MessageBox.Show (Convert.ToString(loopCounter));

Problem
In this incorrectly typed command, the word "if" is 'misspelled' with a capital "I" instead of
a lower-cased "if":
If (IsBlank(testString)) //Capital "If" is wrong

A110: Database problems, various

See "A Network-related or instance-specific error occurred while establishing a connection


to SQL Server"

A constant value is expected

Common Error Messages and Solutions Appendix A: 3


Possible Solution:
In a 'switch' statement, a 'case' is using a variable instead of a hard-coded literal or a
constant. Replace the case <variableName> with case "quoted-string" or number.

A list that is this enumerator is bound to has been modified. An Enumerator can only be used if the list
does not change. (Sic)

Symptoms:
Attempting to delete an item from an array, comboBox, listBox, etc, while in the middle of
a foreach loop.

Issue:
You cannot delete an array-item while in the midst of a foreach loop.
Mark the item's position (counter) – typically in another temporary array and use a separate
loop to remove them, after the first loop completes.

A local variable named 'e' cannot be declared in this scope because it would give a different meaning to
'e', which is already used in a 'parent or current' scope...

Symptoms:
The top of the module, typically button1_Click, already has an 'e', as in "EventArgs e" and
you probably have a try-catch that also uses "(Exception e)"

Recommendations:
See button1_Click's signature line and compare it with the catch statement's signature lines

Change the "(Exception e)" to "(Exception e2)" with corresponding changes to e2.Message.
Or consider moving (most) of the logic from button1_Click to its own routine: e.g.
A100_Process(); which won't have an 'e' in its declaration.

A Namespace does not directly contain members such as fields or methods.

Possible Solution:
Do not define variables or methods (functions) above the form level; form-class level.
If you are trying to make a "global" variable, see Chapter 7.

A Network-related or instance-specific error occurred while establishing a connection to SQL Server

Symptoms:
While opening a form that uses SQL server resources.

Solution:
Confirm that the SQL Server is running and you have rights to the database.
If the SQL Server is running locally, on the LocalHost, confirm the Microsoft SQL Server
Services are started. From Windows, Start-Run, "Services.msc"; Confirm SQL Server
(SQLExpress) is started

An Error has occurred while establishing a connection to the server.


When connecting to SQL Server 2005/2008, ... (this failure may indicate) ...

Common Error Messages and Solutions Appendix A: 4


SQL Sever does not allow remote connections.
Could not open a connection to SQL Server.

See SQL: An Error has occurred while establishing a connection to the server....

An object of a type convertible to 'string' is required.

Issue:
"return" is not returning the correct 'type'.

Solution:
Examine the method's signature line to see if it returns a string, integer or other type of
object. The corresponding return statement(s) within the module must also return that same
'type'.

For example, a string function needs to return a <string> value.


It cannot return (nothing)

example code:
private string myFunction()
{
if (util.IsBlank(mystring))
return mystring;

Meanwhile, a void function can only return (nothing):

private void myFunction2()


{
stuff
return;

An object reference is required for the nonstatic field....

This is a generic error that generally means the compiler cannot find the variable or an
associated class was not instantiated.

Possible Solution:
If the variable or method in question is in a different Class, do one of the following:
a) Declare the variable as "public" or "internal" and instantiate the class within your
Form/Class using the "new" keyword. See Chapter 6, External Class Libraries, for
details.

b) Declare the variable as "public static" <string> or

c) If the method is in error, consider declaring the method as "public static...." or better
yet, "internal static" as in

internal static string B000_INILoad.B021_DiscoverINIFileName();


//returns string

Possible Solution:

Common Error Messages and Solutions Appendix A: 5


If calling a class.method within another 'sub-Class' (see Chapter 6; where cl800_Util was
placed within the PayrollTools.cs Class), there may not be a "constructor" in the new
(payroll) class and because of this, the (cl800_Util) declaration may not have a place to run.

Move the declaration into another method, directly above the Instantiation.
In simpler terms, move "cl800_Util util;" just above the line "util = new cl800_Util();"

Possible Solution:
If you have just switched a variable from a local variable to a "public static" variable, re-
compile the program using menu Build, Rebuild Solution.

Possible Solution:
Misspelled or wrong case variable name.

Possible Solution:
Especially when using a (Form's) properties. Do not use the current Form's name (it was not
instantiated within itself); instead, use "this."

ProgramGlobal.IformLeftPos = frmA000Form.Left;
ProgramGlobal.IformLeftPos = this.Left;

Note: You could also simply use "... = Left;", which is considered too vague for
most people even though the code would work.

ArgumentoutOfRangeException was unhandled

Argument out of range exception (s) are always due to an array being unalloacted, un-
available or a value [x] within square-brackets was using a larger number than the size of
the array. This always indicates a logic or counting problem and often the problem happens
at the end of a loop, where you over-shoot by one position. Remember, arrays are base-0; a
ten-item array's last position is [9].

See also "Index out of range"

If manipulating SQL data, statements, such as:


dataGridView1.Columsn[0].xxxxx = "yyyy"
may indicate the SQL Server service has not started on your development machine or the
remote SQL server is not available.

In any case, array arithmetic should be protected with a try-catch (if using a for-next loop or
are addressing [addresses] directly. Consider using a for-each loop, if logic is appropriate.

Argument '1': cannot convert from 'object' to 'string'


Also: The best overload method match for '<form(parameter)>' has some invalid arguments.

Issue:
The parameter you are trying to send is something other than a <string>; often the results
are an object-type or a "collection".

example:
frmA031CategoryAdd addCat = new frmA031CategoryAdd
(dataGridView1.SelectedRows[0].Cells[0].Value);

Common Error Messages and Solutions Appendix A: 6


...SelectedRows[0].Cells[0].Value is not necessarily a string. You can test this
by adding a .ToString() method and placing a debugging breakpoint at the statement. While
debugging, hover the mouse before the ".ToString()" method to see the value is missing
quotes – indicating it is not a string.

Possible Solution:
Convert it to a string using one of these two techniques:
... (dataGridView1.SelectedRows[0].Cells[0].Value.ToString());
... ("" + dataGridView1.SelectedRows[0].Cells[0].Value);

Argument '1' must be passed with the 'ref' keyword


Also: The best overloaded method match for '<class>(ref string, string)' has some invalid
arguments.

Solution:
When using "By Reference" (ref), both the calling and the called functions need the 'ref'
keyword. C# requires this for documentation purposes.

Example:
appendDefaultAreaCode (ref myPhoneNumber, locationDefaultAreaCode)

private void appendDefaultAreaCode


(ref myPhoneNumber, locationDefaultAreaCode)
{

Array creation must have array size or array initializer

Issue:
Sometimes you can declare an open-ended array with a simple statement, such as:
string [] afoundFields;
but if the array is used inside of a loop (while-statement), C# often requires that the array be
initialized with a starting value or by declaring a fixed array size. This is incase the while
statement never runs and downstream commands may panic.

Solution:
Initialize the array with an item count. Consider over-allocating.
string [] aFoundFields = new string [100];

Array does not have that many dimensions

Possible Solution:
Assuming a single-dimension array (a linear array),
aArrayName.GetUpperBound(0);

where (integer 0) is the first dimension of the array, "aArrayName".

Presumably you used aArrayName[x,x], when the array only had one dimension,
aArrayName[x].

'<btnClose>' is a 'field' but is used like a 'method'

Common Error Messages and Solutions Appendix A: 7


<btnClose> is a field but is used like a method

Summary:
Typed as btnClose()
Should be typed as an Event: btnClose_Click(null, null);

See also: "is a field but is used...."

Build Failed - with no compiler error messages

Likely solutions - Do all:


Close Visual Studio
Using Windows Explorer, open the Project's folder; delete all "*.suo" files
Re-Launch VS; select top-menu View, Output Window
Rebuild solution.

If still an error, look in the output Window. (See top-menu, View, Output)

Related: See "The type or namespace name 'Tasks'....

Cannot access a closed registry key

Symptoms:
Usually while performing a .GetValue(stringName)

Solution:
Move the RegKey.Close command below the GetValue statements. If the GetValues are in
a loop, be sure the Close is after the loop.

Cannot Assign to '<string name>' because it is a 'foreach iteration variable'

Symptoms:
Attempting to manipulate an array-element from within a foreach loop.

Issue:
Within a foreach loop, you cannot modify or change the values used by the foreach loop.
More to the point, you cannot transform or change the array's internal elements with a
foreach loop.

Solutions:
If you are merely trying to change the value of the array's element, move the value to a
secondary (intermediate) temp-string. Consider this example, with particular attention on
strtempString:

foreach (string strtempString in aTestArray)


{
//Note: strtempString cannot be manipulated directly
//within the loop

//Use an intermediate value to manipulate


string tstring = strtempString;

Common Error Messages and Solutions Appendix A: 8


tstring = tstring.ToUpper();
}

If your intent is to actually change the value(s) of the items in the array, you cannot use a
foreach loop. Instead, use a for-next loop.

for (int ii = 0; ii <= aTestArray.Length - 1; ii++)


{
//This actually modifies the values in the array...
aTestArray[ii] = aTestArray[ii].ToUpper();
}

Note the loop runs to the Array's length, minus-1 – a base-0 calculation
See the Array Chapter, "Transforming Array Elements" for more details.

Cannot connect to <Server> (SQL Server Management Studio)

Symptoms:
When attempting to launch SQL Server Management Studio

Possible Solution:
Are the services (Start, Run, Services.msc) "SQL Server" started?

<argument>: cannot convert from 'double' to 'float'

Solution:
A numeric parameter must be specified as a floating point number "F"
e.g.
Pen myPen = new Pen (Color.Black, 0.3) should be
Pen myPen = new Pen (Color.Black, 0.3F)

Cannot convert method group '<various: GetLength, etc>' to non-delegate type 'int'. Did you intend to
invoke this method?

Possible solution:
ilastHighlighted = myFiles.GetLength();

Did you remember the closing parenthesis?

Cannot Convert method group '<name>' to non-delegate type 'bool'. Did you intent to invoke this
method?

Possible Solution:
if using an implied comparison in an if-statement:

if (A100_SomeMethod_ThatReturns_Bool)
{
//Incorrect, missing ()
}

you forgot the parenthesis after the method name. Instead:

if (A100_SomeMethod_ThatReturns_Bool() )

Common Error Messages and Solutions Appendix A: 9


{
//corrected with ()
}

if (A100_SomeMethod_ThatReturns_Bool() == true)
{
//optional
}

Cannot currently modify this text in the editor. It is read-only

Solution:
Close the running program before attempting to modify either the code or the design-view.
You cannot edit while the program is running.

Either close the running VS program (your program) or in the Visual Studio Editor (ISE),
click ribbon-bar "Red Square" icon to abruptly close your program.

Cannot convert null to 'System.DateTime' because it is a non-nullable value type

Issue:
A DateTime method is attempting to return a null value to the calling module when only
"DateTimes" are allowed. This often happens in a try-catch error condition.

Solution:

Change the signature line from


private DateTime A100_MethodName()

to a "nullable" DateTime, where the <brackets> are required.

private Nullable <DateTime> A100_MethodName()


{
try
{
//Do stuff here
}
catch
{
return null;
}
}

Test in the calling routine using


if (returnedVariable.HasValue)

where the HasValue method only operates on items with a nullable data-type. See below for
more information on this.

Optionally, in the case of this examle's DateTime value, you could also use this command,
bypassing the Nullable solution: return DateTime.MinValue;

Solution:

Common Error Messages and Solutions Appendix A: 10


Change the original DateTime value to a "nullable" variable by using a question-mark in the
declaration.

:
DateTime? dtValue = (some date/time or null if not available);

With this, the downsteam function can return a null, if it has the need to do so.

:
if (dtValue.HasValue)
return dtValue.Value;
else
return null;

<procedureName> cannot declare instance members in a static class

Symptoms:
Usually when building a new method or function near the "static class program" / "static
void Main" class – the main driving procedure for your program. You have tried to use a
"private void <functionName>" within a "static" class.

Solution:
Consider changing
private void <functionName> to
private static void <functionName>

Cannot implicitly convert type 'int' to 'string'

Symptoms:
Code is trying to display a text message, a MessageBox, assign a text label, or assign a text
field with both text (string) data and numeric data. The numeric data refuses to cooperate.

Solution:
Use Convert.ToString on any numeric fields (or other non-string data-types) before moving
them or concatenating them to another string [field].

MessageBox.Show ("variable 'i' is set to this value: " + Convet.ToString(I));


textBox1.Text = "The number is equal to " + Convert.ToString(valueA));

also: <variableName>.ToString();

Cannot implicitly convert type 'long' to 'int' (are you missing a cast?)

Possible Solution:
Examine the return values of the command you are using. It likely is returning a 'long'
value, not an integer. The error will be flagged deep within the code, but it is the function's
(method's) signature line where you may need to make the fix.

For example:
private int A630_ReturnFileLength (string strpassedFileName)

Common Error Messages and Solutions Appendix A: 11


and: return fi.Length; <error complains here

but the fix may be changing the "int" to "long" on the Signature line.
Change "private int ..." to "private long ..."

CS0029
Cannot implicitly convert type 'string' to 'System.Windows.Forms.Label'

Solution:
Be sure to use a ".Text" when populating a label.
For example, assigning a blank string to a label:

Incorrect: lblmyField = "";


Correct: lblmyField.Text = "";

Cannot implicitly convert type 'System.Data.CommandType' to 'SystemLdata.Sqlclient.SqlCommand'

Issue:
Missing method name ".CommandType"

example code:
SqlCommand refCategoryCMD = new SqlCommand("RecordCategoryDelete");
refCategoryCMD = CommandType.StoredProcedure; //In error

Solution:
refCategoryCMD.CommandType = CommandType.StoredProcedure;

Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a
cast?)

Symptoms:
You are using a string array and attempting to assign a value to another text field.

For example:
lblDisplay.Text = aNames[1]; //fails
MessageBox.Show(aNames[1]); //fails

Solution:
Convert to String prior to assigning. This can be done explicitly or implicitly:

lblDisplay.Text = aNames[1].ToString();
lblDisplay.Text = (string)aNames[1];
MessageBox.Show("" + aNames[1]);

Common Error Messages and Solutions Appendix A: 12


CS0029
Cannot implicitly convert type 'string' to 'bool'
Cannot implicitly convert type 'int' to 'bool'

Symptoms:
In an "if" or other conditional.

Likely solution:
Did you use a required double-equal in the conditional?
if (testString = "Smith") vs
if (testString == "Smith")

Cannot implicitly convert type 'string[*,*]' to 'string[]'

Likely explanation: A multi-dimensioned (dynamically-sized) string array was declared in a


higher scope using and later dimensioned with actual sizes:

string [,] aCollectionNames; //Declared at a higher scope

and then later, in a different method, initialize with a fixed size, as in:

aCollectionNames = new string [15,4]; //Dimensioned

The author had this error after several mistyped array definitions. But once the array was
declared, as described above, the error persisted. Finally, after selecting menu "Build,
Rebuild Solution"; the problem went away.

Cannot implicitly convert type 'string' to 'System.Windows.Forms.TextBox'

Likely Solution:
You neglected the ".Text" appendage.

For example:
Incorrect:
textBox1 = textBox1 + Convert.ToString(<variable>);

Correct:
textBox1.Text = textBox1.Text +
Convert.ToString(<variable>);

For example:
MessageBox.Show("'" + pnlCategoryCode + "'");
vs
MessageBox.Show("'" + pnlCategoryCode.Text + "'");

Cannot implicitly convert type 'System.DateTime?' to 'System.DateTime'. An explicit conversion exists


(are you missing a cast?)

Issue: You are using a Nullable <DateTime> and since a Null is allowed, you must re-
convert to the same type. This seems redundant in code because the called function may
already be returning a Date Time. Re-cast the returned value:

Common Error Messages and Solutions Appendix A: 13


Example Syntax:

DateTime dtfileDate;
dtfileDate = (DateTime)A700_ReturnFileCreateDate(textBox1.Text);

See also "Cannot convert null to 'System.DateTime' because it is a non-nullable value


type" and consider a "nullable" declaration or cast (e.g. DateTime? dtValue;)

See Chapter 24 for further discussions.

Cannot Insert an explicit value into a timestamp column (SQL)

Issue:
SQL data field was defined as 'Timestamp' but C# code is trying to insert a Date. Change
the SQL field definition to a date-time or date format.

Cannot use local variable '<variable>' before it is declared

Likely Solution:
Declare (and possibly initialize) the variable before using:
string myString = "";
if (myString = "House")

Also, you can see this message if an if-statement, either above or below the first error has a
mis-spelled "Else" (vs "else") or missing parenthesis. This may take a while to locate in
large modules.

Possible (and likely) Solution:


In the function or method, is the last "return" statement mis-spelled, as in capital-R-Return?
or is the "return" clause otherwise mal-formed.

Changes are not allowed while code is running...

Solution:
Your program is still running from your last compile (F5 / Run). Locate the program on the
task bar and close before attempting to run it again. Alternately, from the Editor, press
Shift-F5 to force-close the program.

Command Line Arguments not parsed; Command Line Arguments ignored

By default, Visual Studio will not allow passed command line arguments, even
though the Start Options are set in the Project's properties.

Symptoms:
The program will behave as if no command-line arguments were passed, especially
if you compile a Release version of the program. Make this additional change in
the program:

Common Error Messages and Solutions Appendix A: 14


In Project Properties, Security, click [x] Enable ClickOnce Security Settings.
Then click "This is a partial trust application".
Click the Advanced button
Unclick "Debug this application with the selected permission set"
Click OK
Click "This is a full trust application"

Alternately: In Project Properties, left-nav, click Security.


Uncheck "Enable ClickOnce Security Settings".

Control cannot fall through from one case label ('case "<label>":') to another

Solution:
In a 'switch' statement, a "case" statement is missing a break; command, as in

case "Green":
<do stuff here>
break;
case "Red":
<do other stuff here>
break;

<formanme> does not contain a constructor that takes 1 arguments

Solution:
The call, typically on btnFormName_Click, instantiates a new form, as in:
frmA031CategoryMaint catMaint = new frmA031CategoryMaint("");
catMaint.InstanceRef = this;
catMaint.ShowDialog();

where it is passing one parameter, in this case, null, typed as ("").

But in frmA031's constructor, at

public frmA031CategoryMaint()
{

(this example) does not show any parameters.

The method's signature line must match the calling statement's (values). The two must
match the same count of parameters.

<DataGridView> does not contain a definition for Cells and no extension method Cells accepting a first
argument....

See below: "does not contain a definition for 'Cells'....

CS1061

Common Error Messages and Solutions Appendix A: 15


'<Classname>' does not contain a definition for "<MethodName>' and no extension method
'<MethodName>' accepting a first argument of type '<ClassName>' could be found
(are you missing a using directive or assembly reference)

For example: 'MainProgram' does not contain a definition for "A000_Base' and no extnsion
method 'A000_Base' accepting a first argument of type 'MainProgram' could be found (are
you missing a using directive or assembly reference)

Likely solution:
In another class (e.g. MainProgram.cs), you have not yet created or have misspelled a
method called "A000_Base".

<formname> does not contain a definition for <event such as 'checkBox1_CheckChanged'>


<formname> does not contain a definition for <'textBox1_TextChanged'>

Symptoms:
This is an Event problem where the original Event's code was either deleted or renamed in
Code View, but the pointer to the event was not changed in the Event Properties.

Solution(s):
There are two ways to correct this error. Either is acceptable.

1. Double-click the error and the editor will take you to the [Form1.Designer.cs] class;
and as scary as this may look, delete the entire highlighted line.

2. Or, open the <event> properties (Lightning Bolt) for the control in question and delete
the event information from the property screen. For example, if this were a
textBox1_TextChanged event, delete the detail-text after the (lightning-bolt) event.
Doing so still leaves the "textChanged" code, orphaned, in the program. It should be
deleted by hand.

Common Error Messages and Solutions Appendix A: 16


<System.Windows.Forms.DataGridView> does not contain a definition for Cells and no extension
method Cells accepting a first argument....

Solution:
In the foreach clause, did you use "DataGridViewRows" (and not just "DataGridView")?

foreach (DataGridView currentRow in dataGridView1.SelectedRows)


MessageBox.Show("Selected: " + currentRow.Cells[0].Value;

Entering Break Mode failed for the following reasons: Source file <server-drive....form.cs> does not
belong to the product being debugged.

Cause:
A previous project was moved from a server-drive to a local disk.
Reference paths still point to the (old) server location.

Solution:
With Visual Studio 2005 or above, select menu Build, Clean Solution followed by Build,
Rebuild Solution.

With Visual Studio Express, these menu choices may not be present. Do the following:
a. Close the Visual Studio Project
b. Using Windows Explorer, locate the solution; delete the "bin" and "obj" sub-
directories. Re-open the Solution and the problem should be fixed.

error CS0234: The type or namespace name 'Tasks' does not exist in the namespace
'System.Threading'

Error visible in the View, Output pane.


See error message "The Type or namespace name 'Tasks'

ExecuteNonQuery: Connection Property has not been initialized.

When using a Stored Procedure and attempting a SAVE or INSERT (ADD) operation.
Missing a connection clause with the SqlCommand. For example:

Incorrect:
SqlCommand refCategoryCMD =
new SqlCommand("RecordCategoryUpdate");

Corrected:
SqlCommand refCategoryCMD =
new SqlCommand("RecordCategoryUpdate", refCategoryConn);

where "refCategoryConn" was the connection defined earlier in the routine, as in:
string strConnection = "Data Source = <servername\\SQLExpress;" +
"Initial Catalog = <database name>;" +
"User ID=<sa>; Password = <password>";
refCategoryConn = new SqlConnection(strConnection);

Common Error Messages and Solutions Appendix A: 17


<method name> hides inherited member 'SystemWindows.Forms.<object>' Use the new keyword if
hiding was intended.

example message: Form1.left(string, char)' hides inherited member


'system.windows.forms.control.left'.

Cause: The name of your function/procedure/method is the same as a built-in name. e.g., if
you built a function called "Left". This is a new warning, starting with Visual Studio 2010.

Solution:
This error can be ignored. But consider renaming your function. For example, instead of
"Left", use "LeftStr". In general, single-word functions, such as Left, Mid, Right should not
be used.

Field '<name>' is never assigned to, and will always have its default value null (warning)

Possible Solution:
A variable was declared but was never set equal to anything. The 'variable' does not need to
be a normal variable, it could be a class name. Consider this example when declaring an
external class library with the "new" statement either commented or not typed in the proper
location:

clSiteGlobals SiteGlobals;
//SiteGlobals = new clSiteGlobals();

IDE1006 Naming rule violation: These words must begin with upper case characters: <button1_Click>

This is an informational message. Rename the procedure or method, shifting the first
character to upper-case. This is to follow recommended naming standards for cross-
platform programs.

Identifier Expected

Possible Solution:
When declaring a function, are all the parameters in the parameter list prefixed with a data-
type? Missing "string", "int", etc.

Incorrect: static bool IsNumeric(passedString)


Correct: static bool IsNumeric(string passedString)

'<btnClose>' is a 'field' but is used like a 'method'


<btnClose> is a field but is used like a method

Likely Solution:
You are calling a button-event from another location but forgot or mis-typed the event-
name.
btnClose ("", null); //Incorrect – not just the btn name
btnClose_Click ("", null); //Corrected: _Click was missing

Index Out of Range (DataGridView)

Symptoms:

Common Error Messages and Solutions Appendix A: 18


Commands that use a column index position, such as

dataGridView1.Columns[0].HeaderText = "SEQ";
dataGridView1.Columns[1].Width = 55;

must be written after the statement that populates the actual grid. See Chapter 27 for
examples.
GetMyData(strSelectString)

Confirm the SQL Server (SQLExpress) services are running (Services.msc) or the remote
server is available.

See also: ArgumentoutOfRangeException was unhandled

Index was outside the bounds of the array

Symptoms:
A generated error, usually from a try-catch, where array operation attempted to access a
point not in the array – usually one position beyond the end of the array [max n + 1].

If you are not looping through the array and are directly accessing the array (e.g. variable
[n]), then likely the array was not populated with data; especially with a previous .Split
command.

Possible Diagnostics:
If using a foreach loop, place a debug break point at the top of the loop and monitor the
loop. If you suspect the error is (1000 records) into the loop, add this diagnostic logic to the
program and break within the if-statement:

foreach ....
{
if (recordCount > 999)
MessageBox.Show
("Reached suspected error; put break point here");
// <regular processing here>>
}

If using a ".Split" and a subsequent command accesses a variable-field [n] directly, likely
the split found an empty record and had nothing to split into the array. After the split, check
for blank records before executing the (parsing) logic within the loop.

Symptoms:
If you are processing a CommandLine (arguments list), what happens when no command-
line arguments are passed? If you reference aargs[1], the program would abend. Consider
this statement:

if (aargs.Length >= 2 && aargs[1].ToUpper() == "/DIAG")

where the double-ampersand is absolutely required in the test.

Index was outside the bounds of the Array (SQL)


Also: Index was out of Range

Symptoms:

Common Error Messages and Solutions Appendix A: 19


After executing a SQL statement, such as a ExecuteReader.

Possible Solutions:
Confirm the SQL Service is running (Start, Run, Services.msc; look for MSSQL/SQL
Server).

Confirm you have the proper "strConnection" (Data Source=<Franken8>)

Confirm the SQL SELECT statement (an assembled string) includes the field-name you
need and it is punctuated with appropriate commas and spaces, especially within the
assembled string.

Are you trying to reference a [column] position before a DataGridView was populated?

Invalid Expression Term ',' (plus "; expected") when using a picture clause

Likely symptoms:
You are using a picture clause (with a String.Format).

Solution:
Did you forget the words "String.Format ("?

Invalid Expression Term '{' (when using a picture clause)

Solution:
String.Format requires a string, even if a single numeric variable is being formatted.
Encompass the phrase with quotes:
textBox1.Text = String.Format ({0:dddd}, dtValue); //Incorrect
textBox1.Text = String.Format ("0:dddd}", dtValue); //Correct

Invalid Expression Term "."


See "; expected".

Invalid Expression term 'else' and ";expected"

Possible Solutions:
The "if" clause above the errored line requires braces for the "then" section. Sections with
more than one command require braces to group them.

if (valueA == valueB)
{
<stuff>
<more stuff>
}

Possible Solutions:
Does the if-statement-clause have an unneeded semicolon on the if-clause itself? Remove
the semicolon.

InvalidCastException was unhandled

Common Error Messages and Solutions Appendix A: 20


See Error: Unable to cast object of type 'System.Windows.Forms.TextBox' to type
'System.IConvertible'.

Invalid Column Name '<field>' (SQL Read)

Symptoms:
An Invalid Column Name <field name> during a SQL Read or SQL ExecuteReader and the
field name is obviously right, when examined in SQLServer Management Studio.

Possible Solution:
The assembled strSQLstmt (the SELECT statement) is mal-formed, usually a space is
missing in a quoted string, especially on the last field-name, just before the FROM clause.
Set a breakpoint at the ExecuteReader and examine the IntelliTrace. For example, in this
illustration, notice how the "FROM" is crammed next to the field "Comment":

Invalid token '{' in class, struct, or interface member declaration

This message generally means the compiler is confused about an opening or closing brace
or there is a mis-placed semi-colon that confuses where the compiler expects a brace.

Possible Solution:
You have a semicolon at the end of an if-statement; while-loop or for-next-loop or remove
an unnecessary semi-colon from the end of a statement:

Common Error Messages and Solutions Appendix A: 21


private void xxxxx (object sender, EventArgs e); (bad semicolon)
while (loop stuff); (bad semicolon)
if (condition); (bad semicolon)

Possible Solution:
There is a statement or group of statements typed after the module's closing brace. Make
sure all your code is above the closing brace (e.g. above button1_Click's closing brace).

Possible Solution:
Check to make sure that all opening braces have a closing brace and all braces are lined-up
properly. Especially near the end of the program/namespace.

Invalid token 'string' in class, struct, or interface member declaration

Likely solution:
In a statement, such as:

public string SomeMethodNameHere()

where "string" is flagged as an error.


Be sure the word "public" (private, etc.) is lower-case. Not "Public".

Login Failed for user <xxxx>. Reason: Server is in script upgrade mode. Only the administrator can
connect at this time. Error 18401.

Solution:

The SQL Server service just started and the engine is updating tables. Wait a few minutes
and try the SQL connection again.

<method> is inaccessible due to its protection level

Solution:
The method in the Class Library are "private".
Set to either:
"public" if the Class is instantiated, or if the class is in the same namespace as the calling
routine.

Set to "public static" if the Class is not instantiated and it is in another namespace.

MessageBox is a 'type' but is used like a 'variable'

Solution:
You forgot to use a dot-method with the command.
For example: MessageBox.Show (...)
where the .Show was missing

Must declare the scalar variable "@<variable name".

Common Error Messages and Solutions Appendix A: 22


Symptoms:
During a btnSave event, when using replaceable parameters

Solution:
A field was used in the UPDATE/INSERT statement but it was not defined with an
"AddWithValue" clause. For example:

refCategoryCMD.Parameters.AddWithValue
("@NonRequiredField",
util.StripSQLinjections(pnlNonRequiredField.Text));

Also check the SQLstmt in two places (once for INSERT and once for EDIT), making sure
the field-name is listed, and within the parenthesis of the field list:

strSQLstmt = "INSERT INTO refCategory " +


"(RecordCategoryCode, RecordCategoryDesc, DeleteInhibit, NonRequiredField) " +
"Values ( @CategoryCode, " +
"@CategoryDesc, " +
"@CheckBox, " +
"@NonRequiredField )";

strSQLstmt = "UPDATE refCategory SET " +


"RecordCategoryCode = @CategoryCode, " +
"RecordCategoryDesc = @CategoryDesc, " +
"DeleteInhibit = @CheckBox, " +
"NonRequiredField = @NonRequiredField " +
"WHERE (RecordCategorySeq = '" + strEditPassedRecordSeq + "')";

Newline in constant

Likely Solution:
You are appending a "\" backslash character in a string, probably to build a directory-path.
Backslash is a reserved character. Use double-backslashes to represent a single backslash.

serverName + "\" + directoryName //error


serverName + "\\" + directoryName //corrected

No overload for method '<method name>' takes 0 arguments

Likely solution:
Typically with a button or other on-screen event, such as a button, notice the signature line
of the button; there are two parameters. For example, btnSomething_Click(object
sender, EventArgs e). When calling an event like this, make your call in this fashion:

btnSomething_Click(null, null);
Passing a null value for each item in the signature line.

No overload for method '<method name>' takes '1' arguments

Solution:

Common Error Messages and Solutions Appendix A: 23


You are attempting to pass <1> parameter via the signature line to a function that either
needs more than one value or no values. In other words, the two signature lines need to
match, parameter-to-parameter. Double-click the error to locate the invalid call statement.
Then locate the routine it is calling; once found, look at its signature line (the items in
parenthesis; they must match).

Non-invocable member 'System.IO.FileInfo.Length' cannot be used like a method


Non-invocable member 'System.Windows.Forms.Control.Text' cannot be used like a method

Solution:
You are attempting to use a 'property' as-if it were a method. In other words, remove the
trailing parenthesis. For example:
fi.Length ( ); //is incorrect; use instead:
fi.Length;

or: pnlMsg.Text ("some text in quotes here"); //incorrect; use instead:


pnlMsg.Text = "some text here";

Object reference not set to an instance of an object


Use the "new" keyword to create an object instance.

This is a generic error that can be hard to resolve.

Solution:
Generally it means something is mis-spelled.

For example: RegKey.GetValue("ApplicationzzzName").ToString()


has a mis-spelled parameter.

Solution:
You are calling another method, in another library, without having first instantiating the
object. For example, when using the cl710_Formatting.cs library's "ProperNames"
function, you may need to declare the library either at the top (Class level) or within the
current function (e.g. button1_Click):

formatting = new cl710_Formatting();


then: formatting.ProperNames(<stuff>);

Solution:
You have declared a variable, such as a string, an array, a number, but have not initialized it
to a value; the variable still contains nulls.

For instance, a string declared but not initialized:


string myName;

if (myName.Length == 5) ... Generates this error

For instance:
string [] aMyArray;

with: aMyArray[1] = "Dog" will generate the error.

Common Error Messages and Solutions Appendix A: 24


In each case, initialize the variable with a non-null value before using it in any equation or
comparison. With an array, instantiate the value with the "new" keyword:

Only assignment, call, increment, decrement, and new object expressions can be used as a statement.

Symptom 1:
In a for-next statement you have mis-keyed one of the three required phrases. For example,
this statement has an error in the first phrase:
for(i; i <= 10; ++i)

Possible Solution:
you can't use a simple variable in the first part of the phrase; it must have an assignment
clause. The statement correctly typed is:
for(i=1; i <= 10; ++i)

Possible Solution:
A method, such as
sr.Close(); or
A180_ClearDateEntryFields();
was typed without opening and closing parenthesis.

Operator '&' cannot be applied to operands of type 'string' and 'string'

Solution:
Use the "+" symbol to concatenate strings. You used to be a Visual Basic programmer,
weren't you?

Operator '&&' cannot be applied to operands of type 'bool' and 'string'

Possible solution:
In a complex if-statement or while-loop clause, would an extra set of parenthesis help?

while (lineCount < linesPerPage &&


( strReadLine = myAsciiFile.ReadLine() ) != null)

Operator '==' cannot be applied to operands of type 'string' and 'method group'
Operator '==" cannot be applied to operands of type 'method group'
Operator '+' cannot be applied to operands of type 'string' and 'method group'

Likely Solution:
You forgot a "( )" after a function name.
"ToString" vs "ToString()" is commonly missed.

For example:
If using a method, such as .ToLower; as in ...textB.ToLower
did you remember the required parenthesis, as in: textB.ToLower()
if (textB.ToLower == "dog") //incorrect
if(textB.ToLower() == "dog") //correct

For example, also in a written method call:


if (A027_CheckPreviousCount == 15) //incorrect
if (A027_CheckPreviousCount() == 15) //correct

Common Error Messages and Solutions Appendix A: 25


Operator '>=' cannot be applied to operands of type 'string' and 'string'

Symptoms:
You are using a > or < conditional when comparing two strings, as in:
if (testString >= "Brown")

Solution:
You can't use >, < operators against two strings. This is different than (VB). Instead, see
string.Compare(string1, string2, T|F);
string.CompareOrdinal(string1, string2);

Operator "||" cannot be applied to operands of type 'int' and 'int'

Symptoms:
You are using an equal sign in an if-statement; you need double-equals for the comparison.
e.g.
if (passedPhoneNumber.Length = 7 || passedPhoneNumber.Length = 8)

should be:
if (passedPhoneNumber.Length == 7 || passedPhoneNumber.Length == 8)

CS0019
Operator '+' Cannot be applied to operands of type 'TextBox' and 'TextBox'
Operator '+' cannot be applied to operands of type 'System.Windows. Forms.TextBox'

Solution:
You forgot to include the object's (field) dot-property after the object's name.

Consider this code:


label1.Text = textBox1 + textBox2;
label1.Text = textBox1.Text + textBox2.Text;

CS0642
Possible mistaken empty statement

Symptom:
On an if-statement, while, or for-next statement

Likely Solution:
Although this is a warning, it is most likely a true error. Do you have a superfluous
semicolon after an if-clause, for-next, or other loop statement?
Remove the semicolon and let the next line (or the next set of braces) act as the end-of-line.

if (stringA == stringB); // <- Remove this semicolon


{

Warning: Possible unintended reference comparison; to get a value comparison, cast the left hand side
to type string

Common Error Messages and Solutions Appendix A: 26


Symptom:
When comparing a string array to a string:
if (alSomeArray[iposition] == "some fixed string")
The ~ warning appears after runtime, not during editing

Solution:
Do one of the following by casting explicitly or implicitly:

if (alSomeArray[iposition].ToString() == "some fixed string")


if ((string)alSomeArray[iposition] == "some fixed string")

Note: The error will only clear after run-time; it will not clear during the editing session
(VS2010).

Property of indexer '<class.variable>' cannot be assigned to – it is read only.


Property or indexer '<class variable>' cannot be assigned to - it is read only.

Solution:
If this is in an if-statement, did you remember to use double-equals (==)?

Solution:
In the "get/set" routines, typically in a Global External Class Library, there is not any logic
for the "set". If your intention is to make a read-only variable, either remove the logic in
your program that is trying to set the variable's value (e.g. myName = "Smith") or add an
empty-set routine, which ignores the myName = Value statement. Fixing the error is
preferable.

Property Value Not Valid (Dialog box)


Property Not Valid

Solution:
Your program is still running while trying to edit the source code. Close your running
program before changing the code or an object's property (e.g. Click the editor's ribbon
icon: "Red Square").

Send Error Report / Don't Send "Please tell Microsoft about this problem"

Symptom:
When you ctrl-alt-Break your program and your program may be in an infinite loop or
otherwise crashed. Microsoft sees this as a problem and offers to send a diagnostic error
report to Redmond. This message is annoying and should be disabled.

Solutions:
Click "Don't Send," then make this registry key change to your workstation.

Start/Run/Regedit
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PCHealth\ErrorReporting. Dword
Value: DoReport, 0 = Don't Send.

Common Error Messages and Solutions Appendix A: 27


SQL: An Error has occurred while establishing a connection to the server. When connecting to SQL
Server 2005/2008, ... (this failure may indicate) ... SQL Sever does not allow remote
connections. Could not open a connection to SQL Server.

Possible Solutions:
1) If you are connecting from a remote computer: In Microsoft's Surface Area
Configuration tool (SQL 2005), confirm that "Local and remote connections" is
selected. Choose TCP/IP

See: "C:\Program Files\Microsoft SQL Server\90\Shared\SqlSAC.exe"

2) If you are connecting from a remote computer: Consider starting the Windows Service:
SQL BROWSER

3) Does the SQL Express Server have a software Firewall installed? For example, if using
Windows XP SP2 with Windows Firewall, open the Firewall's control panel; click
Exceptions: Add this program: sqlserver.exe. Also add SQLbrowser (udp port 1434).

Other Solutions:

4) You are using the wrong server name in the connection string.

With SQL Server 2008:


string strConnection =
"Data Source = Franken8;" +
"Initial Catalog=Address;" +
"User ID=sa;Password=<yourpassword>";

With SQL Server 2005:


string strConnection =
"User ID=sa;Initial Catalog=Address;Data Source=FRANKEN8\\SQLEXPRESS";

or use ...Data Source=LOCALHOST\SQLEXPRESS If a local database

This can be especially true if you have moved your application from one computer to
another (when in development) and your Development SQL Server also moved. The
author had this problem when moving from a Desktop to a Laptop.

5) You are using the wrong Catalog (database name). e.g. from Chapter 16 "Address"

6) And of course, the wrong username and or password. If the password is encrypted; did
you decrypt it prior to executing the command?

SQL: Login Failed for user <xxxx>. Reason: Server is in script upgrade mode. Only the administrator
can connect at this time. Error 18401.

Solution:

The SQL Server service just started and the engine is updating tables. Wait a few minutes
and try the SQL connection again.

Common Error Messages and Solutions Appendix A: 28


Static member '<namespace.class.variablename>' cannot be accessed with an instance reference;
qualify it with a type name instead.

Solution:
Was the External Class Instantiated (with a "new" keyword)?
If so, remove the "static" modifier from the variable's declaration and use the "new"
variable's name as a variable prefix.

e.g. SiteGlobals = new clSiteGlobals ();


then: MessageBox.Show (SiteGlobals.CompanyName)

If the External Class was not Instantiated (using Quick and Dirty Global Variables), prefix
the variable name using the physical Class Name, as seen in Solution Explorer. Do not use
the "new" keyword.

For example:

Use:
MessageBox.Show(<namespace name.> clSiteGlobals.CompanyName);

'String' does not contain a definition for 'length' and no extension method 'length' accepting a first
argument of type 'string' could be found (are you missing a directive or an assembly
reference?)

Solution:
Capitalize the .Length property, as in:

switch (strfoundString.Length)
{
:
}

'System.Configuration.ConfigurationSettings.AppSettings' is obsolete: 'This method is obsolete, it has


been replaced by System.Configuration!
System.Configuration.ConfigurationManager.AppSettings (depricated)

Symptoms:
When attempting to use an app.config file and
MessageBox.Show (ConfigurationSettings.AppSettings ["<variable name>"]);
And yet, the statement still works properly, except for a compiler warning.

Solution:
In Solution Explorer, References, add a Reference to .NET, System.Configuration.dll
Then change the call statement to
MessageBox.Show (ConfigurationManager.AppSettings ["<variable name>"]);
See the App.Config chapter for full details.

'System.DateTime.Now' is a 'property' but is used like a 'method'

Solution:
Remove the parenthesis from the .Now. This is not Excel.

Common Error Messages and Solutions Appendix A: 29


= DateTime.Now; //Not DateTime.Now()

System.FormatException: 'Input string was not in the correct format.'

Likely solution:
You are attempting to Convert.ToInt32(textBox1.Text) and the textBox was empty or
contained non-numeric values, such as a hyphen or other character.

This is a run-time error that should have a try-catch clause.

System.InvalidCastException: 'Unable to cast object of type 'System.Windows.Forms.TextBox' to type


'System.IConvertible'.'

You will see this message if a textBox or other string field is blank and is trying to be
converted to a numeric value - Visual Studio 2012 and older.

You will also see this if Convert.ToInt32(textBox1.Text) - where the .Text property is
missing.

Note: This is a run-time error (an unhandled exception) and your program has crashed.
Provided this is not a syntax error, consider a try-catch block.

System.Windows.Forms.TextBox - various:

Issue: Likely missing ".Text" appendage.


See:
Cannot implicitly convert type 'string' to 'System.Windows.Forms.TextBox'

CS1061
'System.Windows.Forms.<field>' does not contain a definition for 'text'

Possible Solution:
Usually this means you mis-typed or more likely, mis-capitalized a field's property value.

Consider:
Field1.text (with a lower-cased .text) vs
Field1.Text

Other properties exhibit similar type messages when mis-spelled.

CS1061
'MainProgram' does not contain a definition for "A000_Base' and no extnsion method 'A000_Base' accepting
a first argument of type 'MainProgram' could be found (are you missing a using directive or
assembly reference)

Likely solution:
In another class (e.g. MainProgram.cs), you have not yet created or have misspelled a
method called "A000_Base".

'System.Windows.Forms.MessageBox' is a 'type' but is used like a 'variable'.

Common Error Messages and Solutions Appendix A: 30


Solution:
You forgot to use a method: MessageBox.Show (
e.g., you forgot the ".Show"

This is incorrect: MessageBox("Hello World");


Corrected: MessageBox.Show("Hello World");

The best overload method match for '<form(parameter)>' has some invalid arguments

See "Argument '1': cannot convert from 'object' to 'string'.

The best overloaded method match for 'string.PadRight(int,char)' has some invalid arguments

Solution:
When PadLeft or PadRight, the pad-fill is a character, not a string. Delimit a single
character with tic marks, not quotes.
e.g. strtestValue.PadRight(ipadLength, '*');

The best overload method for 'System.Windows.Forms MessageBox.Show(string)' has some invalid
arguments.

Possible Solution:
MessageBox.Show must have a string as the first item in the "show list". For example:

MessageBox.Show (comboBox1.SelectedItem); //errors; not really a string.


MessageBox.Show (Convert.ToString(comboBox1.SelectedItem)); //works

You can also trick the method by appending an empty-string before the first object being
displayed. Often, the object is converted to a string automatically, but the MessageBox
doesn't know this. Force it to get past the compiler by putting an empty-string in the front:
MessageBox.Show ("" + comboBox1.SelectedItem);

Another way around this problem is to use a ".ToString()" method. For example, with this
RegistryKey example (snippet):

MessageBox.Show (RegKey.GetValue("ApplicationName").ToString());

The class name '?' is not a valid identifier for this language

Likely Solution:
Close all frm (forms), then close and re-open the solution. It appears the development
environment can get confused, especially if you have been deleting methods.

The current project settings specify that the project will be debugged with specific security settings

Symptoms:
When using File-IO functions or Command-Line arguments (and others)

Select Project, Properties, Security


Uncheck the "Enable ClickOnce Security Settings"

Common Error Messages and Solutions Appendix A: 31


The Insert Statement conflicted with the Foreign Key constraint <key name>... (SQL)

Symptoms:
When attempting a SQL Insert where the main table has a relationship with a sub-table. For
example, adding a new NAMES record, pointing to Record Category = 1, when using:
tblNamesCMD.Parameters.AddWithValue("@RecordCategorySeq", 1);

Problem:
@RecordCategorySeq = 1
Looking in the RecordCategory table, there is not a record with a value "1"

The left-hand side of an assignment must be a variable, property or indexer.

Example Problem Statement:


if (String.Compare (strReadLine, null) = 0)

Possible Solution:
With if-statements, use a double-equal signs (not single) when comparing values; as in:
if (String.Compare (strReadLine, null) == 0)

The name 'ConfigurationManager' does not exist in the current context.

Solution:
See "System.Configuration.ConfigurationSettings.AppSettings' is obsolete: "

Common Error Messages and Solutions Appendix A: 32


CS0103
The name '<variable>' does not exist in the current context.

Symptom 1:
You have not declared the variable using "string", "int", "float", etc, as in:

string myString;
int aNumber;

Or you have declared the value as "myString" but used the variable later as "mystring"
(case-sensitive).

Or you declared the variable in another (routine or module) and that declaration is outside
the scope of your current routine/method/module. A variable was declared in another
construct (such as within a for-next loop) and that construct has ended.

Consider the integer i, which is declared as part of the for-next loop but was used in a
MessageBox outside of the loop; in this case, variable "i" was out of scope and cannot be
used.

for (int i=1; i <= 10; ++i)


{
<stuff to do>
}
MessageBox.Show ("Variable i = " + Convert.ToString(i));

Solutions vary. Check variable declarations.

Symptom 2:
Error: The name '<FixedSingle>' does not exist in the current context.
You are setting a Property incorrectly, such as
textBox1.BorderStyle = FixedSingle

Possible Solution:
The item on the Right-side of the equal sign may need to be prefixed with a property name,
as in:
textBox1.BorderStyle = BorderStyle.FixedSingle

Note: BorderStyle requires a "using System.Windows.Forms;" statement or you can fully-


qualify the name, as in:
textBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle

Possible Solution:
If the 'name' is a keyword-like name:
The name 'IsNumeric' does not exist.... do you need a Class Library prefix, such as:
util.IsNumeric?

The type arguments for method 'System.Array.Resize<T>(ref T[], int)' cannot be inferred from the
usage. Try specifying the Type Arguments explicitly

Issue:

Common Error Messages and Solutions Appendix A: 33


Array.Resize only works with single-dimensioned arrays. You cannot resize multi-
dimensioned arrays; you cannot resize List<T> arrays.

Solution:
Manually copy existing array to a new, larger array -- but you will have problems that the
new array will have a different name. There does not seem to be a good solution to this
problem.

The type or namespace name 'boolean' could not be found (are you missing a using directive or an
assembly reference?)

Consider this called function, which returns a boolean:


static boolean IsBlank(string passedString)

Should be typed as:


static bool IsBlank(string passedString)

C# is inconsistent in how one should spell boolean. When used in a function, use "bool".

The type or namespace name 'CurrentUser' | 'Local Machine' does not exist in the namespace
'Registry' (are you missing an assembly reference?)

Symptoms:
When attempting to read a specific registry key from the Windows Registry

Solution:
Confirm you have a "using Microsoft.Win32;" at the top of the program.
Then use this prefix in the RegistryKey command:

RegistryKey RegKey =
Microsoft.Win32.Registry.LocalMachine.OpenSubKey
(@"Software\Test");

The author is unsure why the "Microsoft.Win32.Registry" prefix is required when a "using"
statement is in place.

More generally:
You are missing a 'using' statement (e.g. using System.Management;).
if this does not resolve the problem, often you can add a new "Reference" (in Solution
Explorer). The name will usually be the same ("System.Management").

The type or namespace name 'DllImport' could not be found (are you missing a using directive or an
assembly reference?)

Possible Solution:
Add these two statements at the top of the (DllImport) class:

using System.Collections;
using System.Runtime.InteropServices;

Note "DllImport" is spelled with lower-cased -el's Dll's

Common Error Messages and Solutions Appendix A: 34


The type or namespace name 'Return' could not be found...

Solution:
Spell "return" with a lower-case 'r'.

If a Void function:
return;

If a non-Void function:
return (some-variable);

The type or namespace name 'single' could not be found (are you missing a using directive or an
assembly reference?)

Solution:
With floating point numbers,
Use Single (with a capital S) or "float" instead.

Unlike "int", Single does not have a shorter alias. Many programmers prefer "float".

The type or namespace name 'StreamWriter' / 'StreamReader' / 'WriteLine' could not be found (are
you missing a using directive or an assembly reference?)

Likely solutions:
Confirm "using System.IO;" near the top of the program.

Confirm you are using the variable name on the WriteLine method.

For example, this is incorrect:


System.IO.WriteLine("my text to write");

Use this:
myreviewFile.WriteLine...

The type or namespace name 'Tasks' does not exist in the namespace 'System.Threading'

You are likely using one of the Wait methods and System.Threading.Tasks is only available
in dot net 4.0 and higher.

Solution:
In the project, select top-menu "Project, Project Properties".
Change the target framework from .NET Framework (3.5) to version 4.0 or newer.
Re-compile.

The type or namespace 'Windows' does not exist in the namespace 'System' (are you missing an
assembly reference?) File: cl800_Util.cs

Likely solution:

Common Error Messages and Solutions Appendix A: 35


You are writing a Console application and have imported the cl800_Util library. cl800's
"Wait" routines call a System.Windows.Form module -- which Console applications do not
allow.

Recommended Solution:
Delete cl800_Util from Solution Explorer and re-add as a "Copy" (not as a link). Once
added, locate the WAIT routines and remove them from the cl800_Util library. Because
cl800 is copied, you are only damaging this program's local version. If you write a lot of
console applications and wish to continue using cl800, move the WAIT logic into its own
library.

Note: The text was changed to reflect this need. All Wait routines were moved into their
own class library. You would see this message if you tried to combine them contrary to
what the book recommends.

Related Solutions:
Console applications cannot call any "Windows-like" method. For example,
MessageBox.Show will not work in a console application. Adding a "using
System.Windows.Forms" defeats the purpose of a console application.

There were build errors. Would you like to continue and run the last successful build?

Symptoms:
When you compile (F5) your newly-written program.

Solutions:
Select checkbox "Do not show again" and click No. In other words, you would never want
to run the previous version of your code (before newly introduced bugs; you really want to
see the current bugs).

If you had already checked yes, see Tools, Options, "Projects and Solutions", "Build and
Run". Set "On Run, when build or deployment errors occur" to "Do not Launch".

Unable to cast object of type 'System.Int32' to type 'System.String' (SQL ExecuteRead)


Unable to read record (SQL)

Symptoms:
When attempting to read a SQL record.

Solution:
When assembling the SELECT statement (strSQLstmt), the "WHERE" clause's record
number (e.g. usually a SEQuence number), must be enclosed in tic-marks. For example:

Incorrect:
:
"WHERE NameSeq = " +
strEditPassedNameSeq;

Corrected:
:
"WHERE NameSeq = " +
"'" + strEditPassedNameSeq + "'";

Common Error Messages and Solutions Appendix A: 36


See also:
Invalid Column Name (SQL)

Unable to cast object of type 'System.Windows.Forms.TextBox' to type 'System.IConvertible'. When


casting from a number, the value must be a number less than infinity.

Likely Solution:
A Convert.To phrase is missing a dot-property

Consider this flawed for-next loop fragment:


for (int loopCounter = 1;
loopCounter <= Convert.ToInt32(textBox2); ....

The "Convert.ToInt32( )" does not point to a particular property.

It should read
Convert.ToInt32(textBox2.Text)

I bet you used to be a VB programmer.

Unable to Read Record (SQL)


See Invalid Column Name (SQL)
See Unable to cast object of type 'System.Int32' to type 'System.String' (SQL)

Unrecognized escape sequence

A string was found with a "\" (backslash) character. This is a reserved character needed for
"escape sequences." If you need a backslash character in a string (typically for a file-
name\path), double-up the backslashes, as in: "C:\\data\\filename.ext"

\t = tab
\r = carriage return
\n = newline
\r\n = crlf
\\ = backslash
\' = tic
\" = quote

See Appendix Special Characters for details.

Use of unassigned local variable <"myInteger"> | <"myString">, etc

Possible Solution:
In your declarations, usually at the top of your routine, a variable, such as

int <myInteger>; or string <myString>

was declared but not initialized with an explicit value. Or you attempted to use a variable
on the right-side of an (equals) statement when it has not yet been populated by another
statement earlier in the code.

Common Error Messages and Solutions Appendix A: 37


Or, the variable was declared, but because of logic, was never assigned a value before being
used in another statement or calculation.

The variable needs an initial value, either explicitly or programmatically. Remember,


declaring a variable does not initialize it..

Another likely scenario is the variable was not initialized and a "while" loop was going to
set the value but the loop never ran (or more likely, the compiler thought the loop had a
possibility of never running).

Recommendations:
Consider using this type of syntax:

int myInteger;
myInteger = 0

or declare and initialize on the same line, as in:


int myInteger = 0;

Possible Solution:
The variable was declared in another module and has fallen out of scope.

Visual Studio cannot start debugging because the debug target <your project name\bin\debug> is
missing. Please build the project and retry, or set the OutputPath and AssemblyName
properties appropriately to point at the correct location for the target assembly.

Solution:
The Program must compile at least one time without errors or you will see this message.
Delete or comment-out the line causing a compiler error.
Run the program again (even if the program does nothing but display the form)
Close the running program and re-introduce the errors. This error should go away.

Solution:
Immediately after starting any new project, press F5 to compile the first empty-screen.
Then immediately close the running program and begin your coding work.

Solution (untested):
Select Menu: Project, Properties.
Go to "Build"; check the "Output" section at the bottom
Browse to your project's main directory/path, choosing Bin\debug"; this is where the actual
exe/dll lives.

When casting a number, the value must be a number less than infinity...

See
Error: Unable to cast object of type 'System.Windows.Forms.TextBox' to type
'System.IConvertible'.

Common Error Messages and Solutions Appendix A: 38


Appendix B - Compile and Distribution

This section discusses how to compile and distribute an .EXE.

Background:

When developing and testing a program, pressing F5 (top menu Debug, Start
Debugging) compiles the program, writes a temporary executable, and then
launches that .EXE as a separate task on the Windows task bar.

On the disk, Visual Studio builds a Debug folder in the Project's directory and in
there you will find a compiled .EXE and other support files – but only the .EXE is
needed for distribution. If you compile for "Release" (described below), a new
directory, "Release" is populated similarly.

The debug version (the .exe) contains code overhead that helps you test and
develop and this version is about 10 to 15% larger than a release version.
Although you can distribute the debug version to end-users, it is not recommended.

How to Compile and Distribute EXEs Appendix B: 39


Cheap and Easy EXE Distribution:

Follow these steps to compile your program as a stand-alone executable and to


give it a formal version number and release date. The resulting .EXE is not a full-
fledged, installable program, but it can be manually distributed (without a setup
routine) and the executable can be run from a server or from a thumb-drive, etc.
This method works well in corporate environments.

1. Open your Visual Studio solution as you would normally.

2. Select top-menu Project, (project name) Properties.

a. In the Project Properties screen, click the left-nav "Application" tab.


Change the "Startup object" from "not set" to your program's main routine,
often <ProgramName.Program>.

b. Click button, "Assembly Information".

Type a short description for the program and fill out the company, copyright,
etc.
Manually set an assembly version (version number) and a file version. The
GUID is a random number, which you should leave as-is.

c. On the left-nav, select "Build".


Change the top Configuration menu from "Debug" to "Active (Release)"
Recommend leaving the platform at "Active (Any CPU)".

3. Close the Properties tab and return to the editor.


On the top ribbon, change from "Debug" to "Release".

How to Compile and Distribute EXEs Appendix B: 40


4. Build the final code by choosing top-menu "Build", then "Build <your project's
name>"

5. Once built, use Windows File Explorer to open the project's "bin\Release" folder
(for example: C:\data\Proj\VS\FileManipulation\bin\Release)

The file (e.g.) FileManipulation.exe is distributable to end users or can be


positioned on a server. The file version is visible from Windows File Explorer.

How to Compile and Distribute EXEs Appendix B: 41


Virus Risks:

EXE files placed on a file server / file share, like all executables, are susceptible to
being infected by viruses. Be sure the EXE is in a read-only directory and your
development staff does not have write-access to the EXE or any DLL's in this
directory. This includes you. Use a service account, from a secured workstation,
when updating shared executables.

Protecting executables from viruses is a real-world


problem experienced by the author. A helpdesk employee's
machine was infected and they in-turn infected a variety of
executables on a main login-script server. As each
workstation logged in, they all were infected. It wasn't
until the next day the company's virus signatures were
updated. The Author now believes it is safer to distribute
executables locally, on each workstation. This makes
house-wide infections less-likely but is more problematic
when updating.

How to Compile and Distribute EXEs Appendix B: 42


EXE Icons

The taskbar and shortcut icon will be a default Visual Studio icon and your
program deserves better. Unfortunately, you will have to create, buy or steal your
own icon. Of the three techniques, one of them requires a bit of artistry and it is,
of course the most fun.

Obviously, thieving an icon is reprehensible and can get confusing if your program
shares the same icon as another. To help, Microsoft provides a free library of
icons, which can be found in the Microsoft Visual Studio Image Library,
downloadable at this link:

http://msdn.microsoft.com/en-us/library/ms246582.aspx

As of 2015.01, this is a downloadable .zip file. Extract all


files in the archive, then tunnel to
ImageLibrary\Actions\ICO. Other ICO libraries are near-
by.

The number of (Application) icons is limited, but the number of toolbar icons is
expansive. Regardless, it provides a good starting point, especially if you want to
draw a complete set of icons, with more on this in a moment.

Icon Files:

Icon files (.ico) are peculiar because they contain multiple images, at different
resolutions and different color depths. A fully-populated icon has these images
embedded:

16 x 16, 4-bit color


16 x 16, 8-bit color
16 x 16, 32-bit color

32 x 32, 4-bit color

How to Compile and Distribute EXEs Appendix B: 43


32 x 32, 8-bit color
32 x 32, 32-bit color

48 x 48, 32-bit color

256 x 256, 32-bit

To do an icon properly, you need a full-fidelity version at 256 x 256 pixels and
another at 48 x 48 pixels, followed by progressively smaller and less-detailed
versions. You will have poor results if you take a full-sized version and attempt to
scale it down to the smaller sizes; color shading and pixellation will occur and it
is beyond the scope of this book to describe the intricacies. As you will learn,
there is an art to creating icons.

Editing Icon .ICO files

Contrary to popular belief, you cannot create icons with most photo editors and
you can't edit them properly with MSPaint (it only sees one icon within the file).
There are ways to draw an icon locally, saved as a PNG, and then upload to a
website for ico conversion, but these are generally limited to one size, one icon.

As a free solution, Microsoft recommends this web-based editor. It will not build
the larger Windows-8 style tiles, but it is generally workable:

www.xiconeditor.com:
http://msdn.microsoft.com/en-us/library/gg491740%28v=vs.85%29.aspx

Using Visual Studio to Edit Icons

Amazingly, Visual Studio, the editor, can also edit ico (icon) files, but it comes
with infuriating limitations, only editing the 16 and 32 pixel icons. And it does not
seem to give full control over color pallets.

With the editor, you can view, but not modify 48 and 256-pixel icons. Also, it
does not appear capable of building a new icon file – it only works against existing
ones, but this restriction is easily worked around.

Even with these limitations, it is worth a moment to explore. Do the following:

A. Because you cannot create a new ico file with Visual studio, you must begin your
work with an existing icon. Locate a larger, full-fidelity icon, one with multiple
icons within the file. For example, from the downloaded ImageLibrary (see
above):

How to Compile and Distribute EXEs Appendix B: 44


ImageLibrary\Objects\ico\ActiveServerPage(asp)_11272.ico

Or search C:\Windows for any *.ico files.

Protect the original file by copying the .ico to a temporary location before editing.
I recommend creating an "Images" folder within your project and storing icon and
clipart files there.

B. From any Visual Studio Project, select File, Open. Tunnel to and open the .ico file
and it will open in a tabbed-window, next to your code and form designs. The left-
nav shows each of the different sizes. Note the editing ribbon bar is only available
on 16 and 32-pixel icons.

Example Icon in Visual Studio, showing multiple sizes

Once edited, close the tab and save the changes.

Attaching Icon Files to your Project:

The icon needs to be attached in two locations: One for the file system and a
second for the running program.

How to Compile and Distribute EXEs Appendix B: 45


1. Open the top-menu, Project, (your project's name) Properties. Select left-nav
"Application". In the Resources section, set the icon file by browsing to the icon
file. Illustrated "ActiveServerPage(asp)_11272.ico". Once selected, File Explorer
will show this as the EXE's default icon and it will automatically pick the
correctly-sized icon.

2. Return to the Form Editor (Form1, Design View). In the form's properties, locate
the "Icon" setting. Browse to the same .ico file and select. Again, the editor will
choose the properly-sized icon from within the file.

How to Compile and Distribute EXEs Appendix B: 46


3. Re-compile the program for the changes using F5 or F6-re-build. The icon will
show in File Explorer, on the program's (form's) title bar, on the Task bar, and on
any desktop shortcuts created. The icon file appears as a resource in Solution
Explorer.

How to Compile and Distribute EXEs Appendix B: 47


Creating Publishing / Distribution Packages

The "cheap and easy EXE" distribution method described above is my favorite
way of distributing a compiled program, but you can build a setup.exe that
automatically installs the software and builds an un-install routine. The benefits of
this design are:

• Setup.exe installs your program in a location of your choosing


• Adds an un-install to the Control Panel's "Add Remove Software" (Programs
and Features).
• It builds a desktop icon for the current user (In Windows 8, adds a tile to the
All Apps menu.

For example, from Windows 8's Control Panel, Programs and Features:

and from the Tile screen:

Building a Distribution Package:

1. Create a Release version of your application, as described earlier in this chapter,


then close the project.

How to Compile and Distribute EXEs Appendix B: 48


2. If this is the first time building an MSI package, you must install a Microsoft-
recommended InstallShield ("InstallShield Limited Edition for Visual Studio" - a
free product from a third-party).

Flexera Software
http://learn.flexerasoftware.com/content/IS-EVAL-InstallShield-Limited-Edition-V
isual-Studio

From the web site, download and follow the instructions. Once downloaded and
installed, close and restart Visual Studio.

3. Re-launch Visual Studio, selecting


New Project, Other Project Types, "Setup and Deployment"
Choose the "InstallShield Limited Edition Project" template

For the Location, type a path that is near but separate from your original Visual
Studio Solution. For example: C:\data\Source\FileManipulation\ (Deployment),
where "Deployment" is the recommended name. As usual, I recommend leaving
Create Directory for the solution and letting the "Name" become the actual
directory.

The new solution opens into an Install Shield Wizard with a row of buttons/icons
showing each step, starting with "Application Information." This is called the
Project Assistant and if you get lost, look in Solution Explorer and double-click the
Project Assistant

How to Compile and Distribute EXEs Appendix B: 49


4. In the InstallShield wizard's first step, "Application Information", fill out your
company name, web-address, and version number (not illustrated).

5. In Step 2/Icon 2 – "Installation Requirements", choose any restrictions you may


have, such as only installable on Windows 7 or newer and choose any required
software, typically Microsoft.Net Framework version 4.x. The screen is self-
explanatory.

6. In "Application Files", rename the default [ProgramFilesFolder] from


"InstallShield" to "MyCompany" or "MyApplication". This becomes the default
installation folder.

In the right-hand Name section, "other-mouse-click" and browse to your Release


version and add your final compiled EXE to the list. Add any additional INI files,
Readme.txt, etc, in this same location.

How to Compile and Distribute EXEs Appendix B: 50


7. In the "Application Shortcuts" section, choose where you want icons built,
typically the Start Menu (In Windows 8 this is the All Programs tile screen).

8. In the "Installation Interview" section, choose options as needed. Typically:

No - Do not display license agreement


No - Do not make users type their company or username
Yes - Allow users to modify the Installation Location
Yes - Allow the user to launch the application after install

9. Finally, select top-menu "Build", "Build Solution". Note that this is not part of the
Wizard steps. This completes the MSI build.

10. In Windows Explorer, tunnel to ...\Express\CD_ROM\DiskImages\Disk1

This is your Deployment directory – not your original program solution!

Results:
Note the Setup.exe, Setup.INI and .MSI file.

This entire directory can be positioned on a Share, CD, thumb-drive, etc. and is
ready for use.

Testing:

Launch Setup.exe and allow the program to install.

How to Compile and Distribute EXEs Appendix B: 51


Results:
• In this example, installation arrives at "C:\Program Files
(x86)\MyCompany\*.exe"
• Note desktop icon (if selected)
• If Windows 8, note the All Programs Tile installed as "Launch (your program
name)"
• In Control Panel, Programs and Features (Add Remove), you can un-install.

Possible Warning:

Warning: -7235: InstallShield could not create the software identification tag
because the Tag Creator ID Setting in General Information View is empty.
ISEXP: Warning.

Solution:

This is a warning and can be ignored with no harm. It suggests files required for
automatic inventory scanning are not in place and implies a corporate install. As
of 2014.03, this design is not in wide-spread use.

The warning can be resolved in one of two ways.

1. Disable the Inventory feature: In the Deployment Package's solution, Solution


Explorer. Tunnel to "Orgainize your Setup", "General Information". Scroll
down to the Use Software Identification Tag. Set Use Tag = no

2. Or Enable the Software Inventory by filling out the fields in the "Software
Identification Tag" section.

In Solution Explorer, tunnel to "General Information"

How to Compile and Distribute EXEs Appendix B: 52


Complete these fields:
Tag Creator Name: Your business name
Tag Creator ID: (See below to generate)
– example: regid.2009-04.com.yourBusinessName

Generating a Creator Tag:

Go to this site:
Magnicomp Software Tag Maker (free)
http://www.magnicomp.com/cgi-bin/mcswtagmaker.cgi

Complete the online form and generate an XML tag file.


Download and store the Tag file in your deployment's root directory.

My tag file was named this way:


2009-04.com.keyliner\regid.2009-4.com.keyliner.examplefilemanipulation_13
96226547.swidtag

a. In your Package's "Application Files" section, add the tag file, so it installs
at the same level as your .EXE program.

b. A second copy of the tag file must also be copied, using "Application
Files": (example file name):

%PROGRAMDATA%\2009-04.com.keyliner\regid.2009-4.com.keyliner.e
xamplefilemanipulation_1396226547.swidtag

Note: The Vendor's Generated Tag webpage will have the exact link and
name you should use – cut and paste.

Your MSI package must also build the Program Data directory:
"%PROGRAMDATA%\2009-04.com.keyliner"

where %PROGRAMDATA% value is a Windows system environment


variable. On Windows Vista and later the value is usually
C:\ProgramData

Recompiling:

If your original program is pulled for maintenance or enhancements, you must


rebuild the Release version *and* rebuild the Deployment version. Remember,
your source code and the deployment solution are two different Visual Studio
projects.

There are other features, such as automatic updates when version numbers change.
This is beyond the scope of this chapter.

How to Compile and Distribute EXEs Appendix B: 53

You might also like