Programming in Visual Studio 2017 C# - Combined PDF
Programming in Visual Studio 2017 C# - Combined PDF
Programming in Visual Studio 2017 C# - Combined PDF
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.
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.
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.
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#?:
The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.
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.
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:
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.
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.
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 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:
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:
"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,
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:
"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")
if (EndofFileSwitch)
if (EndofFileSwitch == true)
"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:
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
int loopCounter;
loopCounter = 15;
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:
Not all developers use prefixes and some are violently opposed to them.
Legal Stuff:
Visual Studio, Excel, Access, Visual Basic, Microsoft SQL, SQL Express, are Microsoft
Products with all of their copy-rights and trademarks
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:
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:
• 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"):
• 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:
• 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.
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:
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.
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."
• 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 "+").
3. Drag a second Button, "button 2" to your form. This will be used in a later example.
Click and drag the new objects, maneuvering them until they appear similar to the
illustration below:
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.
"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.
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.
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.
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.
A. Run the code by pressing the keyboard's F5 key (or pressing the green Start arrow on the
top tool bar).
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:
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.
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.
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.
valueA = 3;
valueB = 2;
3. Run the program by pressing F5. Click button1 to run the event.
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.
When reading a line of code like this, read it from the inside-out – starting at the inner-
most parenthesis:
• "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.
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
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:
4. Press F5 and attempt to run the program. The compiler should immediately error with
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.
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:
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:
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.
"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.
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.
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.)
• 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:
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".
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 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.
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]".
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:
valueA = 3;
valueB = 2;
label1.Text = ConvertToString (valueA = valueB);
}
The editor will be angry and a number of errors will show in the error list.
3. Attempt to run the new code by pressing F5. Notice the compiler errors:
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)
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.
"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.
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.
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:
Double-click (or drag) each object from the ToolBox flyout and arrange them to look
something like the illustration below.
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.
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.
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.
Because the string literals "Cat" and "Dog" were hard-coded, the two textBox fields
were not used in this part of the example.
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.)
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.
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:
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]")
Using the same properties list, lock a field so users cannot edit - forcing them to accept
the defaults.
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:
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.
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.
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
E. Add another label, "label4", leaving it with its default name and default text value. This
will hold the answer, as generated by 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
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"
B. From the Visual Studio "Getting Started" landing page, note the Recent section.
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.
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
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.
do-loop, Overview
int iloopCounter = 0;
do
{
//Always executes at least one time;
//note semicolon on while-statement
//Looping backupwards, 10 to 1:
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.
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).
• 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:
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.
"while" loops are probably the most common and most useful looping command. They
have the following characteristics:
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.
The first example will loop 10 times, printing some text each time. Later examples
expand this idea by introducing counters and line-breaks.
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.
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.
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."
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.
}
}
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:
MessageBox.Show("Done");
}
where:
• 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, }
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.
Some developers code their braces in this fashion, which is also acceptable:
Results: textBox1 shows "hihihihihihihihihi" along with a simple dialogue "Done". Note
10 concatenated strings.
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.
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.
Definitions
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.
Hungarian Notation is now frowned up, but is used in this book to help
illustrate and clarify the variable's purpose.
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.
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:
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:
• "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.
After ten iterations, the loop will have appended ten "hi"'s, giving: "hihihihihihihihihihi".
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.
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.
= 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.
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.
++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
This new syntax will cause problems if both styles are mixed on the same line. For
example, this statement is an infinite loop:
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:
MessageBox.Show("Done");
}
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 ";
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".
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?
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.
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.
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).
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.
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.
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.
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.
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.
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.
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 }
:
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.
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:
• Alternately, click inside of the editor's workspace (again, ignoring the looping
program). From the top menu, select Debug, "Break All".
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).
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
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.
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.
• 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.
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:
// 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.
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.
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.
On line 8, append a space after the "Convert.ToString". The space is inserted at each
iteration loop and is part of textBox1 assembly.
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.
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.
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-
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).
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:
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 }
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.
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.
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".
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.
"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.
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.
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:
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:
Results: textBox1 contains: "1 2 3 4 ... 10", where each loopCounter is converted to a
text-string and then appended to textBox1.
for-next loops are composed of three separate phrases and each phrase handles either the
beginning, middle or end of the loop.
• 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.
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".
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:
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:
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.
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.
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.
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:
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).
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."
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:
- 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:
where:
• 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.
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.
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":
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":
where:
• the increment is by twos. Because of this you must use the older-styled increment
"i = i + 2" instead of "i++".
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"
MessageBox.Show ("Blastoff!");
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".
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."
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.
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.
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.
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.
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)"
Exercise:
Modify the example program, exchanging the hard-coded <= 10 with a converted
textBox2.Text. Detailed steps are documented next.
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:
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.
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.
(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.)
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();
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.
if (iloopCounter == 7)
continue;
The next chapter discusses if-statements in more detail but here are the important points:
if (iloopCounter == 13)
continue;
textBox1.Text = textBox1.Text +
Convert.ToString(iloopCounter) + "\r\n";
}
}
Confirm that the output shows all numbers, 1 - 20, skipping 7 and 13.
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.)
if (iloopCounter == 13)
continue;
if (iloopCounter == 15)
break; //Numbers 16-20 never run
textBox1.Text = textBox1.Text +
Convert.ToString(iloopCounter) + "\r\n";
}
}
• 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.
:
int iloopCounter = 0;
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.
:
if (iloopCounter == 7)
{
iloopCounter++; //A duplicate increment is needed
continue;
}
:
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; ...)".
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:
Now change where integer iloopCounter is declared, moving it into the loop definition:
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.
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.
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:
The goal of the first nested-loop example is to generate a short 7x5 multiplication table
where
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,
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.
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.
}
}
where:
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....
}
}
}
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.
:
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
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.
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):
This logic is more useful than in multiplication tables. Arrays and other table work
often uses this type of logic.
• 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:
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:
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.
2. Following the multiplication table example, add an outside "row" loop with a nested,
inside "col" loop.
}
}
}
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.
Here is the code for the completed program. Note little changed from the previous
multiplication table example:
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.
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:
• 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:
In the code above, after the loop, look for a comment: "\\ Trunk logic goes here".
Add this logic at that location:
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:
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.
"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":
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
• 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.
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.
and
• 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.
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 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:
• 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.
• You cannot use the tempString to modify the values in the original source array.
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
:
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.
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.
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.
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:
Overview:
if-statement Summary
if (valueA == valueB)
{
//"Then" statements;
}
else
{
//optional "else" statements;
}
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).
"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.
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.
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}.
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:
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)).
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:
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.
if (valueA == valueB)
{
textBox1.Text = "ValueA and B are equal";
}
else
{
textBox1.Text = "ValueA and B are not equal";
}
}
Common Mistakes:
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:
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:
//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.
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.
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:
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:
or more simply:
}
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.)
Like the loop commands, there are no closing semicolon on the if-statement itself, but
out of habit, you may type one.
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";
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.
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:
Fix the problem by adding braces around those two lines of code on the if-side.
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:
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.
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:
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..."
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.
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.
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
}
}
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.
//Again, the variables are set one way, but the example if-
//statement will test for a different value
}
}
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.
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.
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.
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:
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.
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:
if (myColor == "Blue")
theAnswerIs = "Blue 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.
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
:
if (valueA > valueB)
valuex = valueA;
else
valuex = valueB;
MessageBox.Show ("The larger number is " + valuex);
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:
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:
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?
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:
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:
if (myColor == "Blue")
theAnswerIs = "Blue is my color";
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:
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);
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.
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.
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.
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.
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.
case 10:
:
The myColor switch example had two labels next to each other (Black and Red); this
acts as an "OR":
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#.
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
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.
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.
myColor = "RED";
myColor = myColor.ToLower(); //Now 'red'
switch (myColor)
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":
:
• 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.
switch (myColor.ToLower().Trim())
myColor = myColor.ToLower().Trim();
switch (myColor)
switch (myColor)
{
case strMainColor: //Illegal to use a variable here
case red:
• 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.
switch (valueA)
{
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,
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.
:
case "yellow":
theAnswerIs = "Yellow is the color";
break;
case "pink":
break; //execute no logic
}
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.
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.
Exercise:
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"
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.
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:
if (myColor == "Red")
goto bypassLogic;
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.
• 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?
• 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.
Basic Syntax:
theAnswerIs =
(valueA >= valueB) ? "Answer is A" : "It is B";
MessageBox.Show (theAnswerIs);
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:
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.
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.
• 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".
Hard-code the test variables at the top of the routine using these statements. For
simplicity, pretend they are input by end-users:
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?
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";
string prefix = "Mr."; //Change this variable to other possibilities for testing
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.
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.
There are many possible solutions to these problems. Here are some suggestions:
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;
}
switch(strPrefix.ToUpper())
{
case "MR.":
case "MR":
strPrefix = "Mr."; //Force a known value regardless!
break;
case "MRS.":
case "MRS":
strPrefix = "Mrs.";
break
}
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!");
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.
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
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.
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.
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
See SQL: An Error has occurred while establishing a connection to the server....
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'.
example code:
private string myFunction()
{
if (util.IsBlank(mystring))
return mystring;
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.
c) If the method is in error, consider declaring the method as "public static...." or better
yet, "internal static" as in
Possible Solution:
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.
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].
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.
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);
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);
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)
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];
Possible Solution:
Assuming a single-dimension array (a linear array),
aArrayName.GetUpperBound(0);
Presumably you used aArrayName[x,x], when the array only had one dimension,
aArrayName[x].
Summary:
Typed as btnClose()
Should be typed as an Event: btnClose_Click(null, null);
If still an error, look in the output Window. (See top-menu, View, Output)
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.
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:
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.
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.
Symptoms:
When attempting to launch SQL Server Management Studio
Possible Solution:
Are the services (Start, Run, Services.msc) "SQL Server" started?
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();
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 ()
}
if (A100_SomeMethod_ThatReturns_Bool() )
if (A100_SomeMethod_ThatReturns_Bool() == true)
{
//optional
}
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.
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:
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:
:
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;
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>
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].
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)
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:
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]);
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")
and then later, in a different method, initialize with a fixed size, as in:
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.
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 + "'");
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:
DateTime dtfileDate;
dtfileDate = (DateTime)A700_ReturnFileCreateDate(textBox1.Text);
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.
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.
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.
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:
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;
Solution:
The call, typically on btnFormName_Click, instantiates a new form, as in:
frmA031CategoryMaint catMaint = new frmA031CategoryMaint("");
catMaint.InstanceRef = this;
catMaint.ShowDialog();
public frmA031CategoryMaint()
{
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....
CS1061
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".
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.
Solution:
In the foreach clause, did you use "DataGridViewRows" (and not just "DataGridView")?
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'
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);
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.
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
Symptoms:
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.
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:
Symptoms:
Possible Solutions:
Confirm the SQL Service is running (Start, Run, Services.msc; look for MSSQL/SQL
Server).
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 ("?
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
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.
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":
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:
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.
Likely solution:
In a statement, such as:
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.
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.
Solution:
You forgot to use a dot-method with the command.
For example: MessageBox.Show (...)
where the .Show was missing
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:
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.
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.
Solution:
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;
Solution:
Generally it means something is mis-spelled.
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):
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:
string [] aMyArray;
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.
Solution:
Use the "+" symbol to concatenate strings. You used to be a Visual Basic programmer,
weren't you?
Possible solution:
In a complex if-statement or while-loop clause, would an extra set of parenthesis help?
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
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);
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.
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.
Warning: Possible unintended reference comparison; to get a value comparison, cast the left hand side
to type string
Solution:
Do one of the following by casting explicitly or implicitly:
Note: The error will only clear after run-time; it will not clear during the editing session
(VS2010).
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.
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.
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
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.
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.
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.
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)
{
:
}
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.
Solution:
Remove the parenthesis from the .Now. This is not Excel.
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.
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:
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
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".
The best overload method match for '<form(parameter)>' has some invalid arguments
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:
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)
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"
Possible Solution:
With if-statements, use a double-equal signs (not single) when comparing values; as in:
if (String.Compare (strReadLine, null) == 0)
Solution:
See "System.Configuration.ConfigurationSettings.AppSettings' is obsolete: "
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.
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
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:
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?)
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;
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.
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:
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".
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 + "'";
Likely Solution:
A Convert.To phrase is missing a dot-property
It should read
Convert.ToInt32(textBox2.Text)
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
Possible Solution:
In your declarations, usually at the top of your routine, a variable, such as
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.
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
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'.
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.
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.
5. Once built, use Windows File Explorer to open the project's "bin\Release" folder
(for example: C:\data\Proj\VS\FileManipulation\bin\Release)
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.
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
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:
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.
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
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.
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):
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.
The icon needs to be attached in two locations: One for the file system and a
second for the running program.
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.
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:
For example, from Windows 8's Control Panel, Programs and Features:
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.
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
9. Finally, select top-menu "Build", "Build Solution". Note that this is not part of the
Wizard steps. This completes the MSI build.
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:
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.
2. Or Enable the Software Inventory by filling out the fields in the "Software
Identification Tag" section.
Go to this site:
Magnicomp Software Tag Maker (free)
http://www.magnicomp.com/cgi-bin/mcswtagmaker.cgi
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"
Recompiling:
There are other features, such as automatic updates when version numbers change.
This is beyond the scope of this chapter.
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
"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.
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
Right Strings:
<string>.Substring (<string.Length> - 10).Trim()
//Right 10 Chars.
Mid Strings:
<string>.Substring (<start position base-0>,
<number of characters>)
<string>.Substring
(delimiterFrontNDX base-0 + delimiterFront.Length,
<number of characters>)
if (Char.IsNumeric('<single-byte char-value>'))
if (Char.IsNumeric("string value", #-of-characters))
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.
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 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();
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.
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 (" \\\\ "):
This can get you in trouble if you make a mistake. Consider this flawed string:
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.
Carriage-Return-Line-Feeds CRLF:
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.
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:
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:
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:
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.
This code demonstrates how to convert from 'A' to the number ASCII 65 and back. Note
the new type of declarations, "char" and "byte".
MessageBox.Show(myInt.ToString());
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.
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.
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:
or
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.
".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.
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:
strassembledPartNumber =
string.Concat(strpartNumberPrefix, iPartNumber);
MessageBox.Show (strassembledPartNumber);
strassembledPartNumber =
strpartNumberPrefix + Convert.ToString(iPartNumber);
MessageBox.Show (strassembledPartNumber);
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.
MessageBox.Show(strpartNumberPrefix + floatingPartNumber);
MessageBox.Show
Results: "N123.456"
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:
This example compares two similar part numbers, one with an uppercase prefix and the
second with a lowercase prefix - "N1030" and "n1030":
if (strtextA == strtextB)
MessageBox.Show("they are equal");
else
MessageBox.Show("they ain't equal");
}
Case-Insensitive 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())
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)
• 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.
0 if equal
+1 if textA comes alphabetically before textB
-1 if textB comes alphabetically before textA
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:
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.
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.
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).
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:
• 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:
"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"
Results: <stuff to do if not equal>. This is only a case-sensitive compare, where "n" and
"N" are not equal.
string.CompareOrdinal:
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).
string.CompareOrdinal( ), complete
private void button1_Click (object sender, EventArgs e)
{
//string.CompareOrdinal compares ASCII values,
//not by a Dictionary Compare
int result;
iresult = string.CompareOrdinal(strtextA, strtextB)
where:
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:
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
}
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:
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");
}
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.
where:
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).
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.
• 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
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.
<string>.ToUpper( )
<string>.ToLower( )
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.
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.
strtestA = strtestA.ToUpper();
comments:
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?
• 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:
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.
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.
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:
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.
<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.
Example Setup:
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:
where:
• 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".
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, '*');
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:
string strPrintedString =
("$" + fDollarAmount.ToString()).PadLeft(10, '*');
MessageBox.Show(strPrintedString);
}
("$" + fDollarAmount.ToString())
• The same effect could be achieved in this fashion, reading left-to-right. But I like the
first method because the order is explicit:
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, '*');
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 .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:
where:
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:
<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:
Results in 'Space Ghost '. (Trailing spaces survived; printed with tic-marks).
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.
In order to Trim with other characters, two new concepts are introduced: The 'char'
variable and "arrays."
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:
//Method 1
//(Will fail if leading spaces are present)
//(Will fail if the 'c' is lower-cased)
strstrippedPartNumber = strpartNumber.Trim('C');
MessageBox.Show(strstrippedPartNumber);
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
strstrippedPartNumber =
strpartNumber.TrimStart('C').ToUpper(); //fails
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.
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").
Results: "1030" with the prefixes and leading and trailing spaces removed.
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).
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:
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.
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.
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.
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.
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
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.
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')
{
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.
Consider this simplistic example, which examines a character 'A' to see if it is numeric.
This version only looks at a single character:
if (char.IsNumber(testChar))
{
MessageBox.Show
("This is numeric: " + testChar.Convert.ToString());
}
else
{
MessageBox.Show ("This is not numeric");
}
}
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":
if (char.IsNumber(strTest, 2))
MessageBox.Show ("The first three digits are numeric");
else
MessageBox.Show ("The first three digits are not numeric");
where:
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:
char.IsNumber requires an index and to test the entire length, take the calculated
length minus one: char.IsNumber ("123 Elm", 7 - 1 = 6)
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");
}
}
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"
The ".Replace" method replaces any string with another string and the general syntax is:
<string-variable>.Replace
(<search-original-string>, <new replacement-string>)
MessageBox.Show(strtest.Replace("Robert", "Bob");
}
where:
• Search and Replace strings are case-sensitive and the search must match exactly.
• 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.
• 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:
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.
Consider this code, which converts a string "123" to a numeric value, then adds 10, then
another 8:
iNumber = Convert.ToInt32(strInput);
MessageBox.Show (Convert.ToString(iNumber));
}
where:
MessageBox.Show
("Intermediate answer: " + (Convert.ToInt32(strInput)+10);
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 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):
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":
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:
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.
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.
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>))
There are several states of "emptiness," each of which is different. Here is a string
variable in various states of declaration:
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.
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 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.
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.
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.
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.
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)
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.
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.
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.
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.
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:
if (imyNumberVariable.HasValue)
imyNumberVariable = imyNumberVariable + 100;
else
{
//You cannot use this variable
}
Null-Conditional Operator:
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:
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.
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.
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.
Using the techniques described in the previous sections, the null and .Length test can be
combined into one semi-complicated statement:
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:
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.
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.
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.
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.
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
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:
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.
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 ".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.
".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.
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:
if(delimFoundNDX >= 0)
{
MessageBox.Show("equal found at position: " +
Convert.ToString(delimFoundNDX));
}
else
{
MessageBox.Show("no equals found");
}
}
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
• 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.
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.
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).
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:
delimFoundNDX =
strinputLine.ToLower().IndexOf("fees",0);
where:
• The numberic found index position is stored in the integer variable delimFoundNDX.
This is a base-zero count.
delimFoundNDX = strinputLine.ToLower().IndexOf("fees",0);
The shift changes the value only for the duration of the test; the original inputline is
un-changed.
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.
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.
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(
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".
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:
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.
<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.
or more generally:
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.
<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.
if(strinputLine.Contains("="))
{
<do true stuff>
}
where:
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.
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.
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:
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.
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.
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>);
From above, .SubString(4, 4) begins its count at position zero. The count looks like this:
Example Programs:
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:
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::
Using this input string, "Name = Smith, John" the left-most 10 characters are found
with this statement:
Results: 'Name = Smi'. Even though the substring starts at base-zero, the length uses
base-1.
where:
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.
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.
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:
strfoundHeader =
strinputLine.Substring (0, delimFoundNDX).Trim();
where:
• 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());
• 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 ").
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.
A Matter of Style:
Original:
int delim1FoundNDX;
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:
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.
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.
strfoundString =
strinputLine.Substring (strinputLine.Length - 10);
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.
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.
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.
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:
strfoundString = strinputLine.Substring(delimFrontNDX);
where:
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:
where:
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...".
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.
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.
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();
if (delimFrontNDX >= 0)
{
// delimiter was found; locate the fullname
// using a variable-length delimiter:
strfoundString = strinputLine.Substring
(delimFrontNDX + strdelimFront.Length).Trim();
2. Change strdelimFront from "=" to "Database" (making the word "Database" an eight-
character delimiter)
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.
• 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.
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".
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.
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:
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()
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:
"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:
• 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.
In general:
Note: This is not a perfect mid-string command because this example only considers
single-character delimiters. Longer delimiters are discussed shortly.
Detailed Explanation:
Begin by declaring variables that hold the front and back index positions. Then, capture
the two delimiter's locations, each using an IndexOf.
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:
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
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:
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.
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.
foundLastName = inputLine.Substring
(5 + 1,
12 - 5 -1)
.Trim();
Results in "Smith"
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.
int idelimFrontNDX;
int idelimBackNDX;
strfoundLastName = strinputLine.Substring
(idelimFrontNDX + 1,
idelimBackNDX - idelimFrontNDX -1)
.Trim();
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.
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.
where:
• 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.
• 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 "=".
• Line 14 finds the backside delimiter's position, starting its search after the frontside
delimiter: note the idelimFrontNDX + (the 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...
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.
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:
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.
//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:
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):
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.
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.
7. "Mid-string" the last name with this statement, literally copied from the section above.
The math is already done:
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.
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:
string strfoundHeader;
string strfoundLastName;
string strfoundFirstName;
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);
MessageBox.Show
("The person's " + strfoundHeader + " is: " +
strfoundFirstName + " " + strfoundLastName);
}
else
{
MessageBox.Show ("Unable to parse - bad delimiters");
}
}
Exercise:
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.
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.
B. Write a routine that looks at any path/filename and appends a closing backslash.
For example:
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. 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
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.
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.
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.
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#?:
The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.
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.
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:
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.
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:
For reference, here is an overview on how to use the soon-to-be-built Utility library:
18
The reason for the name 'CL800' is explained at the end of the chapter.
• StripSlashes
• StripTrailingComments
• StripLastCharacter
• VerifyYN
• StripNonNumerics
• StripNonNumerics, preserving decimals and signs
• ParseBetweenDelimiters
• ParseKeyValue, ParseKeyName
• IsNumeric (T/F)
• IsNumbers (T/F)
• StripDuplicateCharacters
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).
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.
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.
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.
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.
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:
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.
a. From the Start Page, select "More Project Templates,"or optionally, File, New
Project)
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."
c. When prompted "You are renaming a file. Would you like to perform a rename in
this project of all references....", choose "YES"
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.
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.
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" 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.
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.
• 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.
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.
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.
c. Instead of clicking the "Add" button, choose "Add As Link" from the pull-down next
to the button.
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.
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.
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
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
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.
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).
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.
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.
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.
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:
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;
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.
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":
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( ).
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
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.
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).
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.
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.
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.
For the experience, create a PayrollTools class within any existing project by doing these
steps.
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.
• 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.
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."
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.
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.
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.
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:
where:
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:
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.
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.
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
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.
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
}
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.
• 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
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.)
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
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":
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.
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.
where:
• 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:
class PayrollTools
{
CL800_Util 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.
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
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.
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.)
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).
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.
Confirm the results in Windows Explorer by opening the Project's source directory, and
tunneling to the bin/Release directory.
Note "NS800_Util.dll"
Follow these steps to use the DLL. For this example, start with a new Project, adding a
button1 object for an initial test.
Browse to bin\release\NS800_util.dll.
7. In your new Project, follow the same steps as described earlier in this chapter:
d. Use the module by typing "util."; note the available methods appear, along with
the \\\ (triple-slash) comments you may have typed.
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.
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.
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.
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.
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.
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.
Consider the following pseudo-code, where A710 begins its work. Notice how it might
call a dozen other smaller routines:
//Early Exit
If (boolUserAuthorizedToPrint == false)
return;
MessageBox.Show
("Printing completed: Last Check Number = " + iCount);
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.
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);
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.
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
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:
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.
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
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
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.
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.
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:
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
//*******************************************************
button logic
//*******************************************************
Menu logic
//*******************************************************
//Initialization routines
A010 ... A090
//*******************************************************
//Main Logic:
A100 ... A999
//*******************************************************
Miscellaneous, un-numbered routines
(especially small program utility functions)
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";
}
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.
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.
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.
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.
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.
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.
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#?:
The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.
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.
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:
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.
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.
Topics:
• 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
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:
• 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.
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.
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.
In the same example notice the string "loopString" was declared inside the loop –
declared with the word "string":
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.
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.
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
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:
Stylistically, consider initializing the loop's numeric value when an extended scope is
needed, making your intent abundantly clear:
int i = 0;
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.)
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,
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.
• 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.
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.
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.
• 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.
• 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.
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.
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.
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";
In conjunction with "public", a second modifier, "static", makes a variable "global." I like
to call these 'quick and dirty' global variables.
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.
If the global variable is defined and used in form1, use the variable as you would
normally:
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);
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.
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.
B. Add two buttons to Form1, changing the button widths to show the text, as needed.:
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.
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.
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:
The compiler complains. Form2 cannot see Form1's variables, no matter how you try to
prefix them; strCurrentFormName is not in scope.
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.
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:
21
Paul Knoll, a famous Structured Programming instructor, was fond of saying quick and dirty
programming tricks are always quick and always dirty.
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.)
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.
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:
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.
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.
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:
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.
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.
A. From Solution Explorer, other mouse-click the project's name. In the illustration above,
you would highlight the bolded "WindowsApplication1":
B. The new class appears in the tree, without a "shortcut" icon, indicating the file is within
this project.
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.
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.
• 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:
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.
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"
When prompted "Would you also like to perform a rename in this project and all
references...", choose Yes.
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.),
Notice Solution Explorer's icon, showing a shortcut, indicating the library is external to
this project:
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";
}
}
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.
• The "static" modifier allows you to address the CompanyName variable without
instantiating the class with a "new" keyword.
"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;
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.
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.
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.
a) In Form1.cs,
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.
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.
• 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.
CLSiteGlobals SiteGlobals;
If you instead decided to use "public static" variables, preface strCompanyName with
the physical class's name instead of the instantiated name:
• 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
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.
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:
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.
MessageBox.Show (SiteGlobals.strCompanyName);
}
Form2 instantiates its own copy of the class into Form2. To prove this, run the program,
then follow these testing steps:
• 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)
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.
where:
• Formerly, in older Visual Studio 2015 and older, the syntax for the Set looked like
this:
get { return privstrCompanyName; }
set { privstrCompanyName = value; }
B. Link in the External Class Library; see previous section for setting up an external class:
C:\Data\Source\CommonVS\CLSiteGlobals.cs
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.
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.";
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.
set
{
}
}
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.
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);
With this design, global variables are protected from inadvertent changes because there is
no "set". This is a recommended technique for handling "global" variables.
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.
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.
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
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:
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:
With our without the passed-variable rename, the results are the same – the original value
is changed.
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:
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:
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.
A. Within Form1, create these form-level variables, populated as follows. These should only
be defined one time in your program:
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.
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.
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.
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.
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.
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#?:
The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.
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.
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:
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.
"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:
• Progress Bars
• monthCalendar; date processing, date arithmetic
• Hiding controls on the Form
• menuStrips and Menu Bars
• ToolTips
• Horizontal lines, label tricks; transparency
• Tab-order
• Build the initial project/Solution, saving into a dedicated directory on the C: drive.
BtnClose Event
this.Close();
Application.Exit();
1. In Solution Explorer,
Link class library CL800_Util (See Chapter 8 for details)
Other Summaries:
textBox1.BorderStyle = Borderstyle.FixedSingle;
Font Settings
Changing a Font Color: two methods displayed:
Changing Fonts:
textBox1.Font = new Font("Ariel", FontStyle.Regular);
comboBox, summary
Recommended Settings:
Sorted = True
MaxDropDownItems = n
AutoCompleteSource = ListItems
AutoCompleteMode = SuggestAppend
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
Setting:
cbxCheckBox1.CheckState = CheckState.Checked;
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
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
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.
• Uncheck [ ] Warn user when the project location is not trusted (for storing code on a
file server).
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:
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.
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'".
1. Close any previously opened Visual Studio projects (File, Close Solution); no need to
save.
• 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)
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.
• 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:
When starting a new project, rename the form before working on the code.
There are three places to rename:
5a. In Solution Explorer, locate Form1.cs (as an aside, this is the physical file's name, as
stored on the disk).
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)
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.
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"
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:
where:
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.)
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.
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.
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.
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:
Detailed steps:
Click the pull-down menu on the "Add" button and select "Add as Link".
Note the new CL800 shortcut item in Solution Explorer.
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.
using NS800_Util;
3. At "public partial class FrmProcess : Form", declare the new utility class with
CL800_Util util;
public FrmProcess()
{
InitializeComponent ();
* util = new CL800_Util(); //instantiate the class
Press F5 to run and test the form. Note the Minimize and Maximize buttons and sizing.
Click Close to return to the editor.
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.
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:
this.Close();
Application.Exit():
}
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
}
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.
* 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.
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).
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);
}
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.
Testing:
You may have need for a form to auto-minimize (rather than closing).
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.
(Property) MaxLength = n
PasswordChar = *
ReadOnly = false
Multiline = true | false
Event(s) Click
Enter
Leave
TextChanged
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.
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.)
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.
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.
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.
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:
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.
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:
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.
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:
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.
Run the program (F5). Notice you can see and change textBox1 ("Smith"). Click button1
and attempt to change the same text again.
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:
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.
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:
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.
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:
//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:
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:
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.
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.
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:
2. Add a second textBox below the first (or change the previous example's textBox2 to a
non-multi-lined textBox); see illustration.
• 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":
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.
textBox1.Text = textBox1.Text.ToUpper();
label1.Text = ""; //You are no longer in textbox1...
}
A. Press F5 to run the program. Note, textBox1 is not the active control; the form has
"focus."
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.
boolLastNameModified = true
}
Press F5 to run. Type new text in textBox1. Note label1, which simulates a lookup
routine.
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.
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,
Add this code, which looks at each keystroke and discards all numeric entries (see later
for code that only accepts numeric values):
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.
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".
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 ().
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:
• 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.
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:
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:
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:
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.
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.
Comments:
• KeyPress events only deal with character data; never string data. For this reason, tic-
mark delimiters are used.
• 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.
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.
1. In design view, click on the Form's background or click the title-bar to highlight the entire
form.
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):
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";
}
}
}
• Notice the signature line's "KeyEventArgs e"; this is the currently-pressed keystroke.
Testing:
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.
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.
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:
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.
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.
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
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]
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"
if (listBox1.SelectedIndex == -1)
//A blank line was selected
Cautions:
SelectedIndexChanged may fire multiple times if the
keyboard is used, rather than the mouse.
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.
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.
this.Show();
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:
• Items are added in the order typed, unless later sorted. These city names, in a non-
sorted order are needed for later examples.
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.
1. AutoCompleteSource = ListItems
2. AutoCompleteMode = SuggestAppend
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.
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.
1. Assuming the comboBox was populated in the form Load event, add the following logic
to button1:
MessageBox.Show
(Convert.ToString(comboBox1.SelectedIndex) + "\r\n" +
comboBox1.SelectedItem);
}
"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.
if (comboBox1.SelectedIndex == -1)
MsgBox.Show ("No choice was made")
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):
where:
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.
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.
MessageBox.Show(strMsg);
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:
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");
}
}
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.
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.
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.
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
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:
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.
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:
//Retrieve each filename from the array and place in the listbox:
foreach (FileInfo myfileinfo in myFiles)
{
//Loop through each file, processing only graphic files:
where:
• Directory and File manipulation commands are covered in detail in 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.
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.
"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.
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:
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:
:
myfileinfo.Name.EndsWith (".tif", true, null) ||
myfileinfo.Name.EndsWith (".png", true, null) )
{
listBox1.Items.Add(myfileinfo.Name)'
}
}
(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:
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.
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.
FileInfo[] myFiles =
myDirectory.GetFiles(listBox1.SelectedItem.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.
where:
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.
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.
• 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.
if (ifileFound >= 0)
listBox1.Items.RemoveAt(ifileFound);
}
where:
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:
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:
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.
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.
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:
This event fires when ever the checkBox is touched by an end-user. Try this now by
running the program.
if (checkBox1.CheckState == CheckState.Checked)
MessageBox.Show("It is checked, but I'll uncheck with button2");
else
MessageBox.Show("It is unchecked");
}
if (checkBox1.CheckState == CheckState.Checked)
if (checkBox1.Checked == true)
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.
ThreeState:
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).
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:
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):
if (checkBox1.CheckState == CheckState.Checked)
MessageBox.Show("It is checked");
else
MessageBox.Show("Ah, it still shows as Unchecked");
Testing ThreeState:
• 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.
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.
if (checkBox1.CheckState == CheckState.Checked)
icbxState = 1;
else
icbxState=0;
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:
There are an amazing number of other events tied to a checkBox, but only three are likely
useful in day-to-day programming:
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.
"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.
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.
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.
radioButton1
[Recommended Prefix: rbx, rb, rbtn]
Use if (rbxRadioButton1.Checked == true) boolean T/F
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.
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.
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";
:
if (rbxRed.Checked == true)
myFavoriteColor = "Red";
else if (rbxGreen.Checked == true)
myFavoriteColor = "Green";
:
: (etc) with other colors
:
else
myFavoriteColor = ""; //None selected
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 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.
3. Click Green.
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.
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
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:
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.
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
1. From the Toolbox flyout menu, drop a Progress Bar on the form.
Size the bar to any width and thickness desired.
Label1 will hold the "starting value" (zero) and label2 will be the current numeric value.
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.
5. button1 will trigger changes in the ProgressBar's value by growing 10% with each click.
lblfrontCounter.Visible = true;
lblbackCounter.Visible = true;
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.
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.
lblfrontCounter.Visible = true;
lblbackCounter.Visible = true;
lblbackCounter.Text = Convert.ToString(iCurrentValue);
ProgressBar1.Value = iCurrentValue;
}
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.
• It is good design to hide the progressBar (Design View, set Visible = false). Expose
only when an event warrants.
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.
progressBar1.Value = 5;
A110_LoginToServer();
progressBar1.Value = 18;
A120_WriteNewFiles();
progressBar1.Value = 42;
A200_PrintReport();
etc.
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)
monthCalendar1.MaxSelectionCount = 1
monthCalendar1_DateChanged
monthCalendar1_DateSelected
The datePicker control takes less real estate but most users will be unfamiliar with how to
actually use it.
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.
.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.
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.
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.
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.
where:
• 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.
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.
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)
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:
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();
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.
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
button1_Click(null, null);
// (Or substitute a simple MessageBox.Show command here)
}
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( );"
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:
6. For the experience, delete the "Options" menu. Highlight "Options"; other-mouse-click
and choose delete.
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.
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.
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.
BorderStyle = FixedSingle
Enabled = False
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.
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".
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.
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.
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:
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.
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:
: 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();
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.
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.
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.
where:
• 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:
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;
For example, this code can be used to launch another application from a graphic_Click
event. See Chapter 18 for more details.
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();
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.
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
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.
• 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.
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.
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).
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.
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 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.
To remove a field from the tab-list, set "Tab Index" to zero, and set "Tab Stop" to false.
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.
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.
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.
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#?:
The language is capable and mature. Even if this is your first programming language,
you will be pleased at its versatility and ease.
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.
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:
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.
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:
(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?
Overview:
myForm2.Show();
//myForm2.ShowDialog(); //to launch modal
where:
where:
• Near the top of the Child Form, build a Private and Public Property
• The Close Event require two routines
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:
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:
myCustomDialog.Public_FormRef = this;
myCustomDialog.ShowDialog();
See the chapter for details on how to query the results. See also, a related form,
NS815InputForm.cs.
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):
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:
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.
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 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
• 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.
• 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.
Once the new form is created, note the new tab along the top row of the Designer
(Form2.cs [Design]).
//this.Hide();
}
where:
• When Form2 was added to the project, it became a new class within the project:
"public partial class Form2"
• 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.'
4. Using the editor's top row of tabs, click "Form2.cs [Design]" and add these new
components (illustrated below):
for btnCloseForm2:
Form2 Logic: Close thyself
this.Close();
for btnHideForm2
Form2 Logic: Hide thyself
this.Hide();
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"
• 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.
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.
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.
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)
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:
this.Close();
Form1.Form1Anchor.Show(); //Restore the pointer
}
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.
Form1.Form1Anchor.Show();
}
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.
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.
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.
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.
This example assumes you have removed the previous logic from earlier in this chapter or
start a new project two forms, Form1 and Form2.
myForm2.Show();
//myForm2.ShowDialog(); //to launch modal
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:
Discussion:
The called-form (e.g. the child-form) needs a modification so it can accept the address of
the parent-form.
Starting with VS 2017, Microsoft displays a compiler Note, saying the "Object
initialization can be simplified, changing. Note the lightbulb icon on left margin:
to
Clicking "Object initialization can be simplified with change the code to the new design.
I am hard-pressed to see the difference.
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.
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.
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.
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).
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.)
• 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".
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.
Notice the variable's initial capitalization. Starting in Visual Studio 2017, the
capitalization difference is enforced with a compiler warning message:
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();
}
Program 11.3: Multiple Forms using FormRef; Form Closing event, completed
where:
Testing:
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 Forms:
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.
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:
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.
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:
2. In Form2 (the called program), add this logic to the Form_Load event:
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?
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.
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.
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."
In Solution Explorer,
3. Open the new class in Code View and create a quick-and-dirty global variable using
"internal static":
where:
• "internal static" creates a "quick and dirty" variable. "internal" is a slightly better
choice than "public" – limiting the variable to only this program.
ProgramGlobal.strMyCompanyName = textBox1.Text;
• 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.
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:
Launch the program. In Form1, type any value in textBox1 (e.g. "Acme, Inc."). Click
button "OpenForm2".
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.
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:
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:
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 "").
Again, all values must be initialized with something; you cannot pass a declared
variable that has not had an assignment.
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:
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:
• 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.
: etc.
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.
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:
public CL860_MyClass()
{
//This is this class's constructor, manually typed like
//any other method.
//Variables might be passed through the signature line.
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.
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.
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.
Create get-set properties (Form Properties) just after the class definition, again, as
described in previous sections:
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
With the basic form's open and close events completed, continue with these steps:
PnlCompanyName
PnlCompanyCity
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.
public Form2()
{
//The constructor lives here
:
where:
• 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:
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.
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:
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.
private_strCompanyName = PnlCompanyName.Text
}
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.
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;
//Diagnostics:
//Display CompanyName, as typed in Form2
MessageBox.Show
("Company Name: " + myForm2.Public_strCompanyName);
}
where:
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.
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:
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.
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.
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.
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:
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.
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:
Testing:
Launch the program. Position Form1 near the upper-left of the screen. Click button
OpenForm2.
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:
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:
switch (myAnswer)
{
case (DialogResult.Yes):
MessageBox.Show ("you clicked Yes");
break;
case (DialogResult.No):
MessageBox.Show ("you clicked no");
break;
}
}
where:
• 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
Icon Fluff:
DialogResult myAnswer;
myAnswer = MessageBox.Show ("Proceed with the program",
"Authorization to continue?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Stop);
The next sections show how to build reusable custom dialog boxes with many features
above and beyond a simple MessageBox.
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 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.
This is a summary on how to call the finished form; these are not the build steps.
D. In the same event, continue with this syntax to call the form:
myCustomDialog.Public_FormRef = this;
myCustomDialog.ShowDialog();
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.
• 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.
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.
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.
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.
4. Add these objects to the Form. See the illustration, below, for placement:
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.
• 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,
FrmDialog: Standard Setup using FormRef for Returning to the main form
using NS800_Util;
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:
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.
• 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
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):
:
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();
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.
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:
Public_FormRef.Show();
}
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:
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;
"FrmDialog myCustomDialog;"
This makes it visible to all methods and it can be used multiple times in the program:
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.
myCustomDialog.Public_FormRef = this;
myCustomDialog.ShowDialog(); //Must be a modal call
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.
Click on (Button3).
The custom dialog should appear. Buttons "Cancel, Process, and Rework" are not
functional yet.
Click "X" to close the Form.
Results:
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.
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:
Public_FormRef.Show();
this.Close();
}
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:
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:
myCustomDialog.Public_FormRef = this;
myCustomDialog.ShowDialog();
where:
retrieves the public value via the "get", and displays it in the parent form, or it could
be tested with logic such as this:
To test:
if (myCustomDialog.Public_IbuttonPushed == 1)
{
//They pressed "Cancel"; note, button numbers are base-1
}
switch (myCustomDialog.Public_IbuttonPushed)
{
case 1:
//They clicked Cancel;
break;
case 2:
//Clicked Process
break;
case 3:
//Clicked Rework
break;
}
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.
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.
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.
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.
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".
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.
Write a simple "BtnClose" function. This is not tied to an event but will be called by the
button routines.
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.:
:
:
namespace NS815_InputBoxDialog
{
public partial class frmInputBox : Form
{
CL800_Util util;
} //End of constructor
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.
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:
namespace ExampleProgram
{
public partial class Form1 : Form
{
CL800_Util util;
FrmDialg myCustomDialog; //From previous example
FrmInputBox myCustomInputBox;
public Form1()
{
InitializeComponent();
util = new CL800_Util;
}
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);
}
Results: FrmProcess reports back what was typed. If Cancel is clicked, null is returned.
• There are no restrictions on the length of the text field or other audits.
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.
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).
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());
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.
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.
Testing:
This completes the InputBox routines and this library can be easily inserted into any of
your projects.
A program is not restricted to integers and strings. For another example, a call to the
NS815_Input Dialog could return an array.
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:
Within the main, calling program, declare an array to receive the results and call the
NS815_Input form – or any other form:
getStuffFromOtherForm.Public_FormRef = this;
getStuffFromOtherForm.ShowDialog();
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.
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.
frmInputBoxVersion1.designer.cs
frmInputBoxVersion1.resx
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.
G. In the other logic in the program (usually a button_Click event), instantiate and use the
form as before:
myNewInputBox.Public_FormRef = this;
myNewInputBox.ShowDialog();
strreturnedValue = myNewInputBox.Public_strInputBox;
This completes the discussion of having multiple inputBoxes in the same program.
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();
//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:
}
}
FrmDialog - End
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
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.
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.
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.
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
Possible Solution:
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.
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.
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
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'.
example code:
private string myFunction()
{
if (util.IsBlank(mystring))
return mystring;
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.
c) If the method is in error, consider declaring the method as "public static...." or better
yet, "internal static" as in
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();"
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.
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].
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.
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);
Possible Solution:
Convert it to a string using one of these two techniques:
... (dataGridView1.SelectedRows[0].Cells[0].Value.ToString());
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)
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];
Possible Solution:
Assuming a single-dimension array (a linear array),
aArrayName.GetUpperBound(0);
Presumably you used aArrayName[x,x], when the array only had one dimension,
aArrayName[x].
Summary:
Typed as btnClose()
Should be typed as an Event: btnClose_Click(null, null);
If still an error, look in the output Window. (See top-menu, View, Output)
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.
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:
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.
Symptoms:
When attempting to launch SQL Server Management Studio
Possible Solution:
Are the services (Start, Run, Services.msc) "SQL Server" started?
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();
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 ()
}
if (A100_SomeMethod_ThatReturns_Bool() )
{
//corrected with ()
}
if (A100_SomeMethod_ThatReturns_Bool() == true)
{
//optional
}
Solution:
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.
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:
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;
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>
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].
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)
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:
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")
and then later, in a different method, initialize with a fixed size, as in:
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 + "'");
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);
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.
Likely Solution:
Declare (and possibly initialize) the variable before using:
string myString = "";
if (myString = "House")
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.
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:
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;
Solution:
The call, typically on btnFormName_Click, instantiates a new form, as in:
frmA031CategoryMaint catMaint = new frmA031CategoryMaint("");
public frmA031CategoryMaint()
{
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....
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".
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.
Solution:
In the foreach clause, did you use "DataGridViewRows" (and not just "DataGridView")?
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'
When using a Stored Procedure and attempting a SAVE or INSERT (ADD) operation.
Missing a connection clause with the SqlCommand. For example:
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.
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.
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
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.
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:
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 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 ("?
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
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.
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":
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:
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.
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.
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.
Solution:
You forgot to use a dot-method with the command.
For example: MessageBox.Show (...)
where the .Show was missing
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:
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.
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.
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).
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;
Solution:
Generally it means something is mis-spelled.
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):
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:
string [] aMyArray;
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();
Solution:
Use the "+" symbol to concatenate strings. You used to be a Visual Basic programmer,
weren't you?
Possible solution:
In a complex if-statement or while-loop clause, would an extra set of parenthesis help?
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
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);
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:
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.
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.
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:
Note: The error will only clear after run-time; it will not clear during the editing session
(VS2010).
Solution:
If this is in an if-statement, did you remember to use double-equals (==)?
Solution:
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
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.
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.
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.
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:
switch (strfoundString.Length)
{
:
}
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.
Solution:
Remove the parenthesis from the .Now. This is not Excel.
= DateTime.Now; //Not DateTime.Now()
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.
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:
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
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".
Solution:
You forgot to use a method: MessageBox.Show (
e.g., you forgot the ".Show"
The best overload method match for '<form(parameter)>' has some invalid arguments
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:
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)
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"
Possible Solution:
With if-statements, use a double-equal signs (not single) when comparing values; as in:
if (String.Compare (strReadLine, null) == 0)
Solution:
See "System.Configuration.ConfigurationSettings.AppSettings' is obsolete: "
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.
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
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.
The type or namespace name 'boolean' could not be found (are you missing a using directive or an
assembly reference?)
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;
Solution:
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.
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
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".
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)
Likely Solution:
A Convert.To phrase is missing a dot-property
It should read
Convert.ToInt32(textBox2.Text)
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
Possible Solution:
In your declarations, usually at the top of your routine, a variable, such as
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.
Recommendations:
Consider using this type of syntax:
int myInteger;
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'.
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.
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.
5. Once built, use Windows File Explorer to open the project's "bin\Release" folder
(for example: C:\data\Proj\VS\FileManipulation\bin\Release)
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.
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
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:
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.
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
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.
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
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.
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.
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:
For example, from Windows 8's Control Panel, Programs and Features:
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).
From the web site, download and follow the instructions. Once downloaded and
installed, close and restart Visual Studio.
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
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).
9. Finally, select top-menu "Build", "Build Solution". Note that this is not part of the
Wizard steps. This completes the MSI build.
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:
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.
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.
2. Or Enable the Software Inventory by filling out the fields in the "Software
Identification Tag" section.
Go to this site:
Magnicomp Software Tag Maker (free)
http://www.magnicomp.com/cgi-bin/mcswtagmaker.cgi
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"
Recompiling:
There are other features, such as automatic updates when version numbers change.
This is beyond the scope of this chapter.
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
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.
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.
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.
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.
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.
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:
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;
:
: 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.
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:
int iloopCounter = 1;
textBox1.Visible = true;
textBox1.Focus();
}
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:
Author Comments:
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:
Statements for opening and reading ASCII files are displayed here. Program 12.5 shows
the complete code in context of a program.
try
{
//Open 'try'
myInputFile = new StreamReader(strInputFileName); //Open the file
try
{
//The process 'try'
while (strReadLine != null)
{
// <loop details>
strReadLine = myInputFile.ReadLine(); //bottom of loop
}
}
using System.IO;
try //file-open-try
{
//Open the file
StreamReader myTest = new StreamReader(strInputFileName);
try //detail-try
{
while (strReadLine != null)
{
iRecordCount++;
try
{
StreamReader myInputFile;
myInputFile = new StreamReader(strInputFileName); //Open file
:
//Declare StreamWriter higher-up if you need to write from
//several different routines...
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 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".
For the test program, build a new Project, following the steps at the beginning of
Chapter 8.
• 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
If you do not have the CL800_Util libraries, some functions, such as "IsBlank" will
have to be written by hand.
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.
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.
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 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:
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.
A110_ReadTextFile();
}
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 "\".
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".
• The "StreamReader" command does not "read" the file; this is left for another
statement.
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.
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.
• 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:
• 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."
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.
• 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.
• Remove the + "\r\n" from line 15 and test again. Explain the results.
• 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).
• 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.
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.
while (!myTextFile.EndOfStream)
{
//(Move to the top of the loop: Read the 1st and next lines...
strReadLine = myTextFile.ReadLine();
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...
(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.
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.
try
{
StreamReader myInputFile;
myInputFile = new StreamReader(strInputFileName); //Open file
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 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.
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:
where:
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)
{
As you will see later, specific error conditions can be targeted and trapped by using
variations of the e.Message parameter.
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.
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.
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
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:
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.
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).
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":
try
{
StreamReader test = new StreamReader (strInputFileName);
//Stuff goes here
test.Close();
test.Dispose();
}
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.
The Exception class provides other information that you may find useful. Notice that
some of these are methods( ) and others are properties.
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.
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:
comments:
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 (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.
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":
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.
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.:
try //file-open-try
{
//Open the file
StreamReader myTest = new StreamReader(strInputFileName);
try //detail-try
{
while (strReadLine != null)
{
iRecordCount++;
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.
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:
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.
Manually type:
"private void A120_WriteFile()", along with its opening and closing braces.
Fill out the routine with the following statements:
try
{
StreamWriter myOutFile = new StreamWriter(stroutputFileName);
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:
where:
• 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.
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.
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.
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.
a. Declare the new StreamWriter class higher-up in the program, usually at the class-
level. Declare the name, but do not instantiate:
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
}
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.
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:
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.
try
{
StreamWriter myOutFile =
new StreamWriter(stroutputFileName, true); //<-Change
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.
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.
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:
A120_WriteFile();
BtnWrite.Enabled = false;
}
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.
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:
try
{
StreamReader sr1 = new StreamReader (strFileName1);
StreamReader sr2 = new StreamReader (strFileName2);
StreamWriter sw = new StreamWriter (strOutputFile.txt);
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.
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:
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.
Hint: Consider adding a break-point (red-ball) and step through the program using "F11".
try //file-open-try
{
//Open the file
StreamReader myInputFile = new StreamReader(strInputFileName)
if (util.IsBlank(strReadLine))
continue;
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.
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.
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.
Select File, Open and open the same file (you may need to change the file-type from
".xls" to "all files")
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.
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.
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
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.
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.
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
See SQL: An Error has occurred while establishing a connection to the server....
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'.
example code:
private string myFunction()
{
if (util.IsBlank(mystring))
return mystring;
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.
c) If the method is in error, consider declaring the method as "public static...." or better
yet, "internal static" as in
Possible Solution:
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.
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].
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.
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);
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);
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)
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];
Possible Solution:
Assuming a single-dimension array (a linear array),
aArrayName.GetUpperBound(0);
Presumably you used aArrayName[x,x], when the array only had one dimension,
aArrayName[x].
Summary:
Typed as btnClose()
Should be typed as an Event: btnClose_Click(null, null);
If still an error, look in the output Window. (See top-menu, View, Output)
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.
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:
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.
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.
Symptoms:
When attempting to launch SQL Server Management Studio
Possible Solution:
Are the services (Start, Run, Services.msc) "SQL Server" started?
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();
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 ()
}
if (A100_SomeMethod_ThatReturns_Bool() )
if (A100_SomeMethod_ThatReturns_Bool() == true)
{
//optional
}
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.
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:
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:
:
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;
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>
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].
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)
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:
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]);
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")
and then later, in a different method, initialize with a fixed size, as in:
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.
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 + "'");
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:
DateTime dtfileDate;
dtfileDate = (DateTime)A700_ReturnFileCreateDate(textBox1.Text);
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.
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.
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.
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:
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;
Solution:
The call, typically on btnFormName_Click, instantiates a new form, as in:
frmA031CategoryMaint catMaint = new frmA031CategoryMaint("");
catMaint.InstanceRef = this;
catMaint.ShowDialog();
public frmA031CategoryMaint()
{
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....
CS1061
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".
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.
Solution:
In the foreach clause, did you use "DataGridViewRows" (and not just "DataGridView")?
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'
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);
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.
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
Symptoms:
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.
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:
Symptoms:
Possible Solutions:
Confirm the SQL Service is running (Start, Run, Services.msc; look for MSSQL/SQL
Server).
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 ("?
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
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.
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":
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:
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.
Likely solution:
In a statement, such as:
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.
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.
Solution:
You forgot to use a dot-method with the command.
For example: MessageBox.Show (...)
where the .Show was missing
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:
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.
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.
Solution:
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;
Solution:
Generally it means something is mis-spelled.
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):
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:
string [] aMyArray;
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.
Solution:
Use the "+" symbol to concatenate strings. You used to be a Visual Basic programmer,
weren't you?
Possible solution:
In a complex if-statement or while-loop clause, would an extra set of parenthesis help?
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
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);
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.
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.
Warning: Possible unintended reference comparison; to get a value comparison, cast the left hand side
to type string
Solution:
Do one of the following by casting explicitly or implicitly:
Note: The error will only clear after run-time; it will not clear during the editing session
(VS2010).
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.
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.
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
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.
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.
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.
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)
{
:
}
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.
Solution:
Remove the parenthesis from the .Now. This is not Excel.
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.
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:
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
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".
The best overload method match for '<form(parameter)>' has some invalid arguments
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:
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)
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"
Possible Solution:
With if-statements, use a double-equal signs (not single) when comparing values; as in:
if (String.Compare (strReadLine, null) == 0)
Solution:
See "System.Configuration.ConfigurationSettings.AppSettings' is obsolete: "
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.
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
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:
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?)
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;
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.
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:
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".
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 + "'";
Likely Solution:
A Convert.To phrase is missing a dot-property
It should read
Convert.ToInt32(textBox2.Text)
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
Possible Solution:
In your declarations, usually at the top of your routine, a variable, such as
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.
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
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'.
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.
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.
5. Once built, use Windows File Explorer to open the project's "bin\Release" folder
(for example: C:\data\Proj\VS\FileManipulation\bin\Release)
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.
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
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:
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.
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
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.
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):
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.
The icon needs to be attached in two locations: One for the file system and a
second for the running program.
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.
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:
For example, from Windows 8's Control Panel, Programs and Features:
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.
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
9. Finally, select top-menu "Build", "Build Solution". Note that this is not part of the
Wizard steps. This completes the MSI build.
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:
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.
2. Or Enable the Software Inventory by filling out the fields in the "Software
Identification Tag" section.
Go to this site:
Magnicomp Software Tag Maker (free)
http://www.magnicomp.com/cgi-bin/mcswtagmaker.cgi
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"
Recompiling:
There are other features, such as automatic updates when version numbers change.
This is beyond the scope of this chapter.
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
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:
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:
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
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
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:
astrFieldNames = myStrInputLine.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.
.Split Summary
Follow these steps when using .Split. This example assumes a comma-delimited
input-line:
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."
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.
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.
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:
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:
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:
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];
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:
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
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.
string strinputLine =
"Fuel Expenses:,1000.00,1130.00,1200.00,45.00";
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.
• 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"):
string [] astrinputLineFields;
string strresults = "\r\n";
astrinputLineFields = strinputLine.Split(',');
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.
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:
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.
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:
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 [] astrinputLineFields;
string strresults = "\r\n";
astrinputLineFields = strinputLine.Split(',');
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.
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"
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:
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.
• 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 =
{ '(', ')', ',', ' ', '-', '.', 'x', '#' };
• 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.:
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:
• 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.)
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.
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.
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.
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:
Once written and saved into the CL800_Utility library, I use this routine, instead of a
.Split, in all but the simplest programs.
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.
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.
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.
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.
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.
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.
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.
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.
string strtempFieldString;
string strfoundHeader;
int ifrontSideNDX;
int ibackSideNDX;
Single fLineTotal; //A floating point number
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).
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:
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.
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.
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.
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.
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.
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:
// Mid-string logic:
strtempFieldString = strInputLine.Substring
(ifrontSideNDX + 1,
ibackSideNDX - ifrontSideNDX -1)
.Trim();
The results, "strtempFieldString" contains the parsed value and you can display this in a
MessageBox or use the debugger, which is described next.
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.
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:
• 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.
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.
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:
To aid with diagnostics, you could add a MessageBox statement after each found
strtempFieldString:
MessageBox.Show(strtempFieldString);
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.
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:
try
{
flineTotal =
flineTotal + Convert.ToSingle(strtempFieldString);
}
catch
{
// not a valid numeric field; skip for now
}
"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.
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.
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:
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.
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.
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.
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:
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".
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:
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):
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";
In the completed code above, notice the line at the end of the loop, in "Last-field
Processing" (line 47):
After the loop, in the Last-field processing, this line is poised to run:
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;.
"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.
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.
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".
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.
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:
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.
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:
3. After typing the A115 statement, hover the mouse over the statement and click the
lightbulb icon. Allow it to generate the A115 module:
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.
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:
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:
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 ( \" ):
• 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.
• 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):
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):
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.
• 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.
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.
Testing:
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.
string strReadLine =
"1/2/2008,Sale,\"Sweater, Blue\",1,14.95,Test Test";
string[] astrinputFields;
astrinputFields = A115_ParseCSVLine(strReadLine);
where:
Then, on the top-menu, click "Debug", Windows, "Immediate". This opens the
Immediate pane.
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.
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.
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.)
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.
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:
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.
where:
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.
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.
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
Before A115, you could have parsed the readline string with this command:
astrinputFields = strReadLine.Split(',');
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.
After the (A115 -.Split) would you have considered processing the returned array with a
nested loop, along the lines of:
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..."
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:
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:
At line 54, modify the statement so the results are appended to the newly-declared
strtempString: change the statement from
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.
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:
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:
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.
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:
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.
The CL800 utility routines IsBlank, IsFilled, IsNumeric will be particularly useful; be
sure they are linked in (see Chapter 8, Libraries).
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
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:
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.
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.
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.
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:
Program 13.7; Simple CSV ASCII File Read and Total; Part II preliminary
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.
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.
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?
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.)
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:
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:
Moving A115:
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).
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:
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.
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.
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>
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:
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();
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();
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]);
textBox1.Text +=
Convert.ToString(irecordCount) + ": " +
astrinputFields[2] + " " +
"Extended: " + Convert.ToString(flineExtension) +
"\r\n";
}
else
{
textBox1.Text += "Error at line: " +
Convert.ToString(irecordCount) + "\r\n";
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.
• 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.
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.
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:
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:
C:\>
dir C:\Windows\System32\*.* >C:\Data\testfile.txt
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.
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)
Hint: The <DIR> is always in the same position, starting at position 25, for a length
of 5.
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.
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.
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.
This is an example INI file that will be used by this chapter. The file is created in
Notepad.
[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:
The next chapter covers "app.config" and XML files, which are similar to INI files,
serving the same purpose.
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 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.
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:
3. In Form1_Load (typical)
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);
}
e.g.
strLocalProgramVersion = "";
strLocalServerName = "";
e.g.
private string strprivProgramVersion = "";
public string strpubProgramVersion
{
get { return strprivProgramVersion; }
set { strprivProgramVersion = value; }
}
e.g.
strprivProgramVersion = strLocalProgramVersion;
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.
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.
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.
[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.
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.
• 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 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.
• 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.
1. Using Notepad, create the test INI file, illustrated above, saving the file in a known
location, such as "C:\data\ExampleProgram.ini".
BtnClose Event
private void BtnClose_Click (object sender, EventArgs e)
{
this.Close();
Application.Exit();
}
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):
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.
Once set, stretch the field across the width of the panel.
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.
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.
//Working variables:
string strReadLine;
string strINIFileName = @"C:\Data\ExampleProgram.ini";
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:
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.
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.
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.
int ilineCounter = 0;
strINIFileName = A028_DiscoverINIFileName();
if (util.IsBlank(strINIFileName))
{
return true;
}
try
{
//Open the Preference INI File:
StreamReader mainINIFile = new StreamReader (strINIFileName);
mainINIFile.Close();
mainINIFile.Dispose();
}
catch (Exception e) //File Open Error logic here
{
//INI file can't be loaded or found
//Otherwise
return false; //No errors found
} //end A015
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();
}
return tstrReadLine;
}
where:
• 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".
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.
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.
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.
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.
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:
iloopCounter and
StreamReader mainINIFile
both are declared in A015 but would normally fall out of scope when
A019_ReadNextDetail is called (without the "ref").
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.
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:
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.
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:
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.
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.
try
{
//Open the preference/INIfile:
StreamReader mainINIFile = new StreamReader(strINIFileName);
//Housekeeping
mainINIFile.Close();
mainINIFile.Dispose();
}
catch (Exception e)
{
:
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.
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
}
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.
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).
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
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.
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
{
where:
• 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.
• 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.
:
MessageBox.Show (tstrReadLine);
return tstrReadLine;
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.
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:
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.
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;
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":
default:
pnlMsg.Text =
"Prefs Error: Invalid or unrecognized INI Line '" +
strKeyWord + " = " + strKeyValue + "'";
return true;
//break; //Not needed because of "Return"
}
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 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:
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.
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.
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.
Prepare your Test.INI file by following these steps. Use Program 14.1, along with all the
changes so far in this chapter:
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.
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.
This is the base path. Tunnel two folders deeper, into ...\bin\release. This is where the
INI-file needs to be placed.
Using Windows Explorer, copy the INI file, then tunnel to the bin\debug folder and
paste.
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.
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".
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.
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();
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:
where:
• 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
• 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:
...\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.
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.
5. Rename
Disguise the ...Bin\Debug\ExampleProgram.ini file by renaming to
"ExampleProgram.ini.bak".
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:
Note this is a different directory than before and the INI file could be named with any
name. For example:
where "ini=<path>" is a contrived design. The example program will parse the file's
location using the invented "ini=" clause.
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:
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.
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:
bool boolfileFound;
string strappData;
string strfullPathFileName;
:
//(Remainder of the File Found/File.Exist code is below here
Diagnostic Testing:
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.
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"
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:
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:
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:
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.
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.
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
A028_DiscoverINIFileName, end
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:
Consider what would happen if no parameters were passed, or if an INI= parameter was
not passed through the command-line. For example:
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:
where:
• aargs[0] is always the program name, aargs[1] would be the first real parameter; the
second item in the array.
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."
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.
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
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:
return false; //If you fall through here; all must be well
}
where:
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:
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.
try
{
StreamWriter mainINIFile = new StreamWriter(strINIFileName);
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";
Testing:
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.
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.
using System;
using System.Windows.Forms;
using System.IO;
using NS800_Util;
namespace ExampleProgram
{
public partial class Form1 : Form
{
CL800_Util util;
string strReadLine;
string strINIFileName = "ExampleProgram.ini"; //Default if none
public Form1()
{
InitializeComponent();
util = new CL800_Util();
}
strINIFileName = A028_DiscoverINIFileName();
if (util.IsBlank(strINIFileName))
{
return true;
}
try
{
//Open the Preference file
StreamReader mainINIFile = new StreamReader(strINIFileName);
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;
}
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
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
return false; //If you fall through here; all must be well
}
//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;
switch (strKeyWord)
{
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;
}
ilineCounter++;
tstrReadLine = mainINIFile.ReadLine();
if (util.IsFilled(tstrReadLine))
{
//(only trim if not null; worried about EOF processing)
tstrReadLine = tstrReadLine.Trim();
bool boolfileFound;
string strappData;
string strfullPathFileName;
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;
}
}
}
if (boolfileFound == true)
strfullPathFileName = strappData + "\\" + strINIFileName;
else
//File not found in expected location
mainINIFile.WriteLine("");
mainINIFile.WriteLine("[General]");
mainINIFile.WriteLine("ProgramVersion = 2.01");
mainINIFile.WriteLine("ServerName = omni");
mainINIFile.WriteLine("ShareName = vol1\\KB");
mainINIFile.Close();
mainINIFile.Dispose();
}
}
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.
Following Chapter 8 "Creating an External Program Class Library from Scratch", do the
following.
using NS800_Util;
using System.IO;
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.
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();
}
• 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:
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):
• 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.
From the code below, your program will only need to change in these three
areas and the changes are usually minor.
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.
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.
CL860_BasicINIRead.cs, completed
using System;
using NS800_Util;
using System.IO;
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;
//See also B000, B017 and B029 for other variable steps
// ****************************************************************
public CL860_BasicINIRead()
{
//Constructor
//InitializeComponent; - cannot be used; not a form-based program
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);
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
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;
}
string strKeyWord;
string strKeyValue;
//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;
switch (strKeyWord)
{
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;
}
ilineCounter++;
tstrReadLine = mainINIFile.ReadLine();
if (util.IsFilled(tstrReadLine))
{
//(only trim if not null; worried about EOF processing)
tstrReadLine = tstrReadLine.Trim();
}
return tstrReadLine;
}
bool boolfileFound;
string strappData;
string strfullPathFileName;
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;
}
}
}
if (boolfileFound == true)
strfullPathFileName = strappData + "\\" + strINIFileName;
else
//File not found in expected location
//return simple filename; let upstream code fail horribly
strfullPathFileName = strINIFileName;
}
mainINIFile.WriteLine("");
mainINIFile.WriteLine("[General]");
mainINIFile.WriteLine("ProgramVersion = 2.01");
mainINIFile.WriteLine("ServerName = omni");
mainINIFile.WriteLine("ShareName = vol1\\KB");
mainINIFile.Close();
mainINIFile.Dispose();
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.
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:
Select "CL860_BasicINIRead.cs"
Choose "Add" (not Add as Link); this makes a copy of the Class
"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.
CL860_BasicINIRead readINI;
readINI = new CL860_BasicINIRead();
readINI.B000_ReadINI();
// ****************************************************
// Retrieve values with logic similar to this:
// ****************************************************
//Diagnostics or other steps showing the retrieved values
//Change as needed for your program's needs:
where:
• 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
• 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.
From the code below, your program will only need to change in these three
areas and the changes are usually minor.
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.
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:
<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:
This chapter shows how to process these types of files, as well as the special app.config
file.
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.
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
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.
app.config files require setup before they can be used. For this section, begin with a new
Visual Studio project:
5. The xml file appears in Solution Explorer and opens in an editing window along the top
row of tabs.
All of your settings live within the <configuration> </configuration> section. Insert a
blank line between the two markers, giving yourself room to work.
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.
</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.
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:
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"
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.
This sample code displays each of the entries in the app.config collection:
string strkeyValue =
ConfigurationManager.AppSettings[strkey];
MessageBox.Show (strkey + ": " + strkeyValue);
}
:
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:
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:
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.
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.
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:
<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:
<mynames>
</namerecord>
</namerecord>
</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>
ID="0107"
Category="A"
<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".... >.
Continue editing the XMLFile1 test data file, filling in these detail records.
<mynames>
<car_record>
<BadCar>Pinto</BadCar>
</car_record>
</mynames>
• Opening and closing Tags are case-sensitive. This will cause problems.
<FName>John</fnAME>
• 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.
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.
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).
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.
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:
A100_ProcessFile(ref xtr);
xtr.Close();
xtr.Dispose();
}
catch
{
MessageBox.Show("Some horrible error opening xml file");
}
}
• 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".
• 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.
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.
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...
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:
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
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)
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.
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.
• xtr.ReadString(); reads until the closing-tag for the current detail element is
found. For example, </FName>
using System;
using System.Windows.Forms;
using System.Configuration; //Required for app.config
using System.Xml; //Required for xml
namespace ExampleProgramXML
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
A100_ProcessFile(ref xtr);
xtr.Close();
xtr.Dispose();
}
catch
{
MessageBox.Show("Error opening XML file");
}
}
//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
}
}
}
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;
xtr.Read();
}
}
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.
Topics:
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.
try
{
RegistryKey myRegKey = Microsoft.Win32.Registry.
CurrentUser.OpenSubKey (strSubKey, true);
try
{
//Update an existing value from previous examples:
myRegKey.SetValue ("ApplicationName", "A Neat Program");
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");
}
}
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.
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
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
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.
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.
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.
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;
Before doing the actual read, there are four things you should always do when setting up
this routine:
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.
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".
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.
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.
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();
where:
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.
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:
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:
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);
}
}
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):
In the try-catch, consider these error messages, which can help you with debugging:
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".
Of course, a better solution would be to create the missing Registry key and not tell the
user of the problem.
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:
...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.
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" is similar to the ".Split" method seen in earlier chapters, where the
results are stored in an array.
Steps:
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
where:
• Line 12 (code example below), declares an open-ended string array and the names
are shoveled into the list with the GetValueNames statement:
• 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:
Expand the code, starting at line 12, with the actual loop:
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.
"ApplicationName = My Program"
"ApplicationSwitch = 1"
Convert.ToSingle(myRegKey.GetValue (strCurrentValue))
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:
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;
}
}
}
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.
astrsystemBIOSVersion =
(string[])RegKey1.GetValue("SystemBIOSVersion");
strmyCPUSpeed = RegKey2.GetValue("~MHz").ToString();
RegKey1.Close();
RegKey2.Close();
comments:
• "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.
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".
To make changes to a Registry value, the open SubKey uses a "writeable" overload.
Append a comma-true (,true) to the Open command:
where:
• 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:
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 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:
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:
• 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
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:
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.
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
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).
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.
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).
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:
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.
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
Below is sample code attached to a button1 event. Requires this using statement:
using Microsoft.Win32;
string strprinterGroupKey =
"Software\\Microsoft\\Windows NT\\CurrentVersion\\Print\\Printers";
string strprinterDetailSubKey;
RegistryKey myPrinterGroupRegKey =
Microsoft.Win32.Registry.LocalMachine.OpenSubKey
(strprinterGroupKey);
MessageBox.Show
("Printer Name: " + printerKeyName + " " +
"Port: " + myPrinterDetailKey.GetValue("Port").ToString());
myPrinterDetailKey.Close();
}
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:
All examples were developed with Microsoft Office 2013. Older and newer versions of
office behave similarly.
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.
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.
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.
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;
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.
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();
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.
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:
int excelRowCount = 1;
int blankRowCount = 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);
Also, be careful to select the Workbook(s) property – and not the similarly-spelled
WorkbookOpen method.
The basic syntax for the open command starts out with this statement:
Excel.WorkBook newWorkbookName =
ExcelObj.Workbooks.Open(<15 parameters here>);
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.
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:
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:
//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);
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.
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.
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.
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.
:
:<The Excel 15-parm open statement went here>
System.Array afoundValues =
(System.Array)foundRange.Cells.Value2;
//: More....
where:
• Literally the address "A2" is assembled as a string concatenation and the value "2"
increments with the loop.
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).
For now, the logic was stubbed with a call to A120, with returned results like this:
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:
} //end of while-loop
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):
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.
where:
• The module's "A120" prefix is invented and only serves to help organize the
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.
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
where:
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.
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
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.
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:
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.
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):
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.
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.
// 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.
• There should be logic to make sure a valid-date-number was detected in the array,
afoundCells[0].
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.
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"
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.
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.
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 &ersand, not pluses, to
concatenate.
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.
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.
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.
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.
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).
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).
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).
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.
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.
There are two solutions to the problem and both are acceptable.
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
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.
this.qryCityStateZipTableAdapter.Fill(this.cityDataSet.qryCityStateZip);
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).
... 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"\";
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:
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:
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).
AutoCompleteSource = ListItems
AutoCompleteMode = SuggestAppend
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.
• 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.
A. Modify Program 17.1 (Read an Excel Sheet and display found-records in textBox1) and
make the following changes:
From the sample data, the first record should look like this:
1/1/2008 SALE PANTS 11.95
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].
D. Modify Exercise A with an audit that makes sure a valid number (date) was passed from
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.
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:
System.Diagnostics.Process procNotepad;
procNotepad.StartInfo.WindowStyle =
System.Diagnostics.ProcessWindowStyle.Maximized;
procNotepad.StartInfo.FileName = "notepad.exe";
procNotepad.StartInfo.Arguments = "test.txt"; //current directory
procNotepad.Start();
}
button1.Enabled = true;
MessageBox.Show("Notepad was closed");
}
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();
For DOS Programs, with console-output, see the IPConfig example later in this chapter.
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;
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());
}
}
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.
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:
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.
2. Highlight the "process1" place-keeper. In the Properties window, rename the object
from process1 to "launchNotepad"
3. In the launchNotepad (a.k.a. Process1) Properties window, scroll down to the +StartInfo
section. Click the +plus to open the details.
Notepad.exe is on the DOS Path and the executable will be found by the operating
system. Alternately, specify a fully-qualified path.
launchNotepad.Start();
}
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:
Press F5 and run the modified program; click Button1. Results: Notepad opens full
screen to the pre-typed document.
Results: A second copy of Notepad launches (two are running). The next section has
details to prevent this.
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
Return to design view and once again highlight the "launchNotepad" process object.
Then, make these three changes:
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:
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.
button1.Enabled = false;
launchNotepad.Start();
}
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.
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();
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.
System.Diagnostics.Process procNotepad;
procNotepad.StartInfo.WindowStyle =
System.Diagnostics.ProcessWindowStyle.Maximized;
procNotepad.StartInfo.FileName = "notepad.exe";
procNotepad.StartInfo.Arguments = "test.txt"; //current directory
procNotepad.Start();
}
button1.Enabled = true;
MessageBox.Show("Notepad was closed");
}
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.
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).
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):
System.Diagnostics.Process procNotepad;
procNotepad = new System.Diagnostics.Process();
procNotepad.StartInfo.WindowStyle =
System.Diagnostics.ProcessWindowStyle.Maximized;
procNotepad.StartInfo.FileName = "notepad.exe";
procNotepad.StartInfo.Arguments = ""; //No document name was passed
procNotepad.Start();
comments:
• Notice the WaitForExit clause does not require the EnableRaisingEvents and button1
was not disabled; there is no need, except for cosmetic reasons.
• 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:
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.
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);
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.
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:
Note: if you Start, Run, "ipconfig" (not using 'cmd'), the program runs and immediately
closes before you have a chance to read the results.
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:
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();
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);
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:
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:
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.
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.
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.
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
• 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.
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:
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).
Results: "<your process> is already running" and the second copy of the application
does not fully open.
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.
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.
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.
[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;
bool boolmutexCreated;
string strExeName;
string strLocation = Assembly.GetExecutingAssembly().Location;
if (boolmutexCreated == false)
{
//Already running...
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.)
myMutex =
new Mutex(true, "Global\\"+strExeName, out mutexCreatedSW);
Other Notes:
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.
B. Have the program in Exercise A display any message once Excel is closed. e.g. "Excel
has closed."
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:
namespace Printing
{
public partial class Form1 : Form
{
//Declare class-level PrintDocument:
PrintDocument printStuff = new PrintDocument();
PrintPreviewDialog viewStuff = new PrintPreviewDialog();
Font printFont;
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();
//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);
}
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:
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.
where:
• The actual layout work happens in another method - not here (See Layout1)
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.
Think of the two button events (layout1 and layoutMore) as Print rendering routines or
as print-layout commands:
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.
string textToPrint;
textToPrint = textBox1.Text + "\r\n";
textToPrint += "line 2" + "\r\n";
textToPrint += "line 3";
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.
string textToPrint;
textToPrint = "yaba-daba-doo";
The initial test will print correctly but a problem will be uncovered when a second print
is sent. Do the following:
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.
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.
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:
7. In the "layoutMore" method, change the location from (0,0) to (0,70), where 70
represents 70 pixels:
Testing Results: "yaba-daba-doo" prints below the original text. Only btnPrint2
executes the .Print method.
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).
The previous examples showed how text can be positioned by specifying a pixel
position, expressed in a traditional algebraic coordinate system (x,y).
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:
e.Graphics.DrawString (textToPrint,
printFont, Brushes.Black,
19.4F, 19.4F);
where:
• 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'.
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.
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.
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.
• Calling other methods from within a (layout) routine, can also help orgainize the
printing routines.
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.
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.
namespace Printing
{
public partial class Form1 : Form
{
//Declare a class-level variable "printStuff":
PrintDocument printStuff = new PrintDocument();
public Form1 ()
{
InitializeComponent();
}
Thus, two points determine a line (19.4, 44) and (194.4, 44):
The line is horizontal because both y-axis are the same.
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:
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:
Drawing a Line
:
Pen myPen = new Pen (Color.Black, 0.2F);
e.Graphics.DrawLine(myPen, 19.4F, 44F
194.4F, 44F);
where:
• 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.
Keep in mind from the left-edge of the paper, the end-point is 175mm + 6mm.
• 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.
Testing:
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.
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.
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:
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:
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).
:
//Previous code (printing text, lines and boxes)
//lives here...
e.Graphics.PageUnit = GraphicsUnit.Millimeter;
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.
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.
In btnPrint1_Click, add lines 9, 10, and 11. (See the previous section for the original
program):
• 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:
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".
Typically a Print Preview button, checkbox, or other such mechanism triggers the
preview. Continuing with the program used in previous sections, do the following:
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:
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:
• 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"
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.
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:
if (printPreviewSW == true)
{
//Show the preview but do not print:
viewStuff.Document = printStuff;
viewStuff.ShowDialog();
}
else
{
//Print normally
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.
:
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();
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.
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:
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)
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.
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).
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
2. Modify the pre-existing A700_Print1 signature line, adding the new parameter:
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:
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:
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.
• Finally, if the user clicked PageSetup's "Cancel", exit the entire A700 module with
no other action.
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.
• 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
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.
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:
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).
where:
• 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.
Other events (Begin and EndPrint) will also go here as the examples progress.
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:
Pay particular attention to the routine's signature line. On a whim, I used "ppeArgs"
instead of "e." ("PrintPageEventArgs").
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:
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.
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);
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:
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.
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:
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:
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:
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.
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.
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.
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.
This was never a problem in previous chapters because the loop was only called one time
The Solution:
:
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.
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'.
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:
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.
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.
End: printFileLayout
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 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:
3. Click the red-square on the toolbar to stop debugging (or press Shift-F5), returning to the
editor.
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
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.
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.
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?
pageCount = 0;
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.
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);
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
pageCount = 0;
and
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:
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
btnPrintFile_Click, completed
private void btnPrintFile_Click (object sender, EventArgs e)
{
//Setup the main ASCII File Print, now much smaller:
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.
Press F5 to run the program and then click on the Print File button.
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.
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.
• "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:
• DateTime.Now
• DateTime Parsing from Strings
• DateTimeParsing from Numeric Values
• DateTime .ToString, .Hour, etc.
• DateTime Picture Clauses {0:dd/MM/yyyy}
Font Summaries
Changing a Font Color: two methods displayed:
Changing Fonts:
textBox1.Font = new Font("Ariel", FontStyle.Regular);
"{0:$#,#0.00; ($#,#0.00);-0-}"
$1,223.15
($1,223.15)
-0- Variable pictures for positive,
negative and zero values
strNewPhoneNumber = formatting.PhoneNumberFormat
(strOriginalPhoneNumber,
strFormatStyle,
strDefaultAreaCode,
boolRequireAreaCode,
boolReturnBlankOnError);
ProperNameFormat
strNewText = formatting.ProperNamesFormat
(strOriginalText, "NAME")
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.
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:
See also, Chapter 10, Forms. The next section begins more proper formatting
statements.
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:
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:
* 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.
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).
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.
• {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.
• The 'tic-marks help illustrate the field width for these examples and are not required.
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:
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()
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):
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:
For example, this code takes the number 3.1451 and displays it as "3.15", noting the
automatic rounding:
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}".
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.
where:
• "{0,15:f3}" displays the first numeric-parameter {0} at 15 characters wide with three
decimals. Note the mixture of commas and colons.
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:
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)
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.
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:
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.
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:
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.
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.
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.43
($0.43) Using 0.43F. Note leading zero with
poundsigns for other leading digits
(#,#0.00)
"{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.
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.
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.
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.
Results:
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.
DateTime dtValue;
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.
• 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.
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:
where three numeric parameters, passed within the parenthesis. If the three values are
strings, convert each part to an Int32 prior.
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.
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:
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)
Methods:
dtValue.ToLongDateString() String: "Tuesday, March 24, 1981"
dtValue.ToLongTimeString() String: "7:23:09 AM"
dtValue.ToShortDateString() String: "3/24/1981"
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:
textBox1.Text =
String.Format ("{0: <picture clause here>}", dtValue);
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).
These examples use "3/24/1981 7:23:09 AM" for most of their dates and times. All
picture components are case-sensitive.
Year
Time
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:
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 entire string is enclosed in a single set of quotes. The free-form text "My date"
is part of that same string.
• 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.
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:
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.
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.
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"
The class is nearly ready to accept code but it will need to use the cl800_Utility library.
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.
8. Declare the utility library with this statement: "cl800_Util util;", which is placed
underneath the cl710 class name:
namespace ns710_Formatting
{
public class cl710_Formatting
{
cl800_Util util; //Declare here
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:
public cl710_Formatting()
{
//This is Formatting Class's Constructor
util = new cl800_Util();
}
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.
Consider these U.S. phone numbers, some of which are poorly formatted or poorly
typed. You can probably imagine other combinations:
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.
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:
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.
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.
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:
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:
• Five parameters are passed into the PhoneNumberFormat method and all are
required, although not all need to be populated.
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.
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:
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:
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").
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.
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:
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.
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.
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.
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:
namespace WindowsApplication1
{
public partial class Form1 : Form
{
cl800_Util util;
cl710_Formatting formatting;
public Form1 ()
{
InitializeComponent();
util = new cl800_Util();
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()
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.
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:
strFormatStyleFlag = strFormatStyleFlag.ToUpper();
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.
• Uppercase the passed FormatStyleFlag (AM1, AM2, EU) for easier testing. This is
an invented code; any naming scheme could be used.
7
I am on a mission to make the European phone-number format the new US standard.
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:
The signature lays the groundwork for the rest of the routines.
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
strFormatStyleFlag = strFormatStyleFlag.ToUpper();
//Early exits
if (util.IsBlank (strnewFormattedNumber))
return "";
comments:
• 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.
It is never too early for testing. Return to (Form1) and add these statements, illustrated
below, to button1_Click.
As a reminder, this gives the Class Library an aliased name "formatting. (dot)", similar
to "util.".
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.
where:
• A working variable, strtempvalue holds the results for the duration of the button-
click.
• 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.
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
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).
//For testing:
return "Pending formatting: " + strnewFormattedNumber;
}
where:
• 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
• 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:
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").
int idelimFront;
where:
• The function is "void", meaning it does not return a value to the calling routine.
Instead, values are returned via "ref".
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.
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":
//x-Extension Extract
phoneNumber_StripExtension
(ref strnewFormattedNumber, ref strextensionHold);
//For testing:
return "Pending Formatting: " + strnewFormattedNumber;
}
Function: phoneNumber_StripNormalPunctuation:
if (util.IsBlank(strPassedPhoneNumber))
return "";
return strstrippedNumber;
}
phoneNumber_StripNormalPunctuation, end
comments:
• 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).
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
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:
phoneNumber_StripLongDistancePrefix
(ref strnewFormattedNumber,
strDefaultAreaCode,
ref strlongdistancePrefixHold);
//For testing:
return "Pending Formatting: " + strnewFormattedNumber;
}
where:
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.
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 (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 = "";
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:
strtempvalue = formatting.PhoneNumberFormat
(textBox1.Text,
"AM",
"208",
true,
true);
}
where:
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.
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:
//For testing:
return "Pending Formatting: " + strnewFormattedNumber;
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:
This is the completed module, that was shown above in pieces. The last several
paragraphs, marked in bold deal with the current text.
string strnewFormattedNumber;
string strextensionHold = "";
string strlongDistancePrefixHold = "";
string strinternationalPrefix = "011";
strFormatStyleFlag = strFormatStyleFlag.ToUpper();
//Early-Exits:
if (util.IsBlank (strnewFormattedNumber))
return "";
//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);
//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();
}
where:
• 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.
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.
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.
string strpunctuation;
string strcurrentAreaCode;
string strprefix;
string strsuffix;
//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);
}
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
//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;
phoneNumber_Punctuate, end
where:
• This is not ready for testing. An additional module (below) is still needed.
• 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.
• 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.
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.
phoneNumber_AreaCodePunctuate, completed
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;
}
phoneNumber_AreaCodePunctuate, end
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.
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);
}
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.
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.
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:
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
8
"TLA" Three Letter Acronym.
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;
:
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".
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:
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:
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:
textBox2.Text =
formatting.ProperNamesFormat (textBox1.Text, "ADDRESS");
}
where:
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:
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:
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.
This is the completed main section for "ProperNamesFormat" and this is the "public
string" routine, visible to Form1. Type this code now:
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
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
ProperNamesFormat, end
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:
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.
string tSubString;
bool boolModified = false;
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";
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:
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":
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;
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":
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;
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":
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";
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";
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);
cl751_UCaseWord, end
UCase calls two minor subroutines and the code for these is listed below with internal
comments.
int ihpos;
string strtemp;
string strfront;
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.
Calling Notes:
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.
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:
C. Extra Credit
Using Excel or Notepad, create a tabbed-delimited table with a variety of name and
address information. Save the file.
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.
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
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.
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.
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
See SQL: An Error has occurred while establishing a connection to the server....
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'.
example code:
private string myFunction()
{
if (util.IsBlank(mystring))
return mystring;
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.
c) If the method is in error, consider declaring the method as "public static...." or better
yet, "internal static" as in
Possible Solution:
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.
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].
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.
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);
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);
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)
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];
Possible Solution:
Assuming a single-dimension array (a linear array),
aArrayName.GetUpperBound(0);
Presumably you used aArrayName[x,x], when the array only had one dimension,
aArrayName[x].
Summary:
Typed as btnClose()
Should be typed as an Event: btnClose_Click(null, null);
If still an error, look in the output Window. (See top-menu, View, Output)
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.
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:
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.
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.
Symptoms:
When attempting to launch SQL Server Management Studio
Possible Solution:
Are the services (Start, Run, Services.msc) "SQL Server" started?
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();
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 ()
}
if (A100_SomeMethod_ThatReturns_Bool() )
if (A100_SomeMethod_ThatReturns_Bool() == true)
{
//optional
}
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.
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:
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:
:
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;
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>
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].
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)
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:
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]);
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")
and then later, in a different method, initialize with a fixed size, as in:
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.
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 + "'");
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:
DateTime dtfileDate;
dtfileDate = (DateTime)A700_ReturnFileCreateDate(textBox1.Text);
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.
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.
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.
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:
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;
Solution:
The call, typically on btnFormName_Click, instantiates a new form, as in:
frmA031CategoryMaint catMaint = new frmA031CategoryMaint("");
catMaint.InstanceRef = this;
catMaint.ShowDialog();
public frmA031CategoryMaint()
{
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....
CS1061
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".
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.
Solution:
In the foreach clause, did you use "DataGridViewRows" (and not just "DataGridView")?
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'
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);
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.
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
Symptoms:
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.
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:
Symptoms:
Possible Solutions:
Confirm the SQL Service is running (Start, Run, Services.msc; look for MSSQL/SQL
Server).
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 ("?
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
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.
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":
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:
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.
Likely solution:
In a statement, such as:
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.
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.
Solution:
You forgot to use a dot-method with the command.
For example: MessageBox.Show (...)
where the .Show was missing
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:
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.
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.
Solution:
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;
Solution:
Generally it means something is mis-spelled.
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):
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:
string [] aMyArray;
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.
Solution:
Use the "+" symbol to concatenate strings. You used to be a Visual Basic programmer,
weren't you?
Possible solution:
In a complex if-statement or while-loop clause, would an extra set of parenthesis help?
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
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);
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.
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.
Warning: Possible unintended reference comparison; to get a value comparison, cast the left hand side
to type string
Solution:
Do one of the following by casting explicitly or implicitly:
Note: The error will only clear after run-time; it will not clear during the editing session
(VS2010).
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.
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.
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
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.
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.
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.
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)
{
:
}
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.
Solution:
Remove the parenthesis from the .Now. This is not Excel.
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.
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:
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
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".
The best overload method match for '<form(parameter)>' has some invalid arguments
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:
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)
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"
Possible Solution:
With if-statements, use a double-equal signs (not single) when comparing values; as in:
if (String.Compare (strReadLine, null) == 0)
Solution:
See "System.Configuration.ConfigurationSettings.AppSettings' is obsolete: "
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.
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
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:
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?)
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;
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.
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:
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".
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 + "'";
Likely Solution:
A Convert.To phrase is missing a dot-property
It should read
Convert.ToInt32(textBox2.Text)
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
Possible Solution:
In your declarations, usually at the top of your routine, a variable, such as
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.
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
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'.
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.
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.
5. Once built, use Windows File Explorer to open the project's "bin\Release" folder
(for example: C:\data\Proj\VS\FileManipulation\bin\Release)
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.
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
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:
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.
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
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.
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):
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.
The icon needs to be attached in two locations: One for the file system and a
second for the running program.
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.
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:
For example, from Windows 8's Control Panel, Programs and Features:
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.
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
9. Finally, select top-menu "Build", "Build Solution". Note that this is not part of the
Wizard steps. This completes the MSI build.
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:
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.
2. Or Enable the Software Inventory by filling out the fields in the "Software
Identification Tag" section.
Go to this site:
Magnicomp Software Tag Maker (free)
http://www.magnicomp.com/cgi-bin/mcswtagmaker.cgi
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"
Recompiling:
There are other features, such as automatic updates when version numbers change.
This is beyond the scope of this chapter.