Input Files, Output Files, and String Functions Although much of the world's data is stored in databases, there are many times when a database is not needed. In those cases, you can store your data in a single file. File: a collection of data that is located in secondary memory (hard drive, flash drive, CD/DVD, the "cloud", etc.). Text File: a file that only contains ASCII characters (strings). The following control characters may be included: tab, carriage return, line feed. File operations: Read data from an existing file ("input") Write data to a new file ("output") Append data to an existing file ("output") C# tries to simplify file handling by using stream objects. Stream: an ordered collection of bytes (characters). An input file is referred to as an input stream. To read data from an input file, create a StreamReader object. An output file is referred to as an output stream. To write data to an output file, create a StreamWriter object. Create a new XNA project called Files. To use streams, add the following to the top of your program: using System.IO; 2/9/2016 Document1 Page 1 of 7 Writing to an output file An output file is created using a StreamWriter object. To declare an output file: StreamWriter outFile; StreamWriter is the data type. It tells C# that we are creating an object that will be used to write data to an output file. Put this in the declarations area of your program. The identifier outFile is the name the programmer has chosen for this StreamWriter. There is nothing special about it; it is a name that the programmer makes up. It follows the rules for creating identifiers in C# and should describe the purpose of the file. To open an output file: This statement prepares the file for writing (called "opening" the file): outFile = new StreamWriter(path); The path is a string variable that identifies the file path and name. Example outFile = new StreamWriter("E:/Stuff/HighScore.txt"); Note that I used slashes (/) instead of backslashes (\) which is what is normally used in Windows paths. In C#, it is legal to use either slashes or backslashes, but if you use backslashes, you must remember to use two backslashes everywhere where you would normally use one backslash. So the previous statement could also be written like this: outFile = new StreamWriter("E:\\Stuff\\HighScore.txt"); There is another trick that allows the programmer to only put single backslashes, and that is to put the "at" symbol (@) before the string, like this: outFile = new StreamWriter(@"E:\Stuff\HighScore.txt"); Any of these three methods will work. However, it just seems cleaner to use regular slashes. If you only supply a file name and do not put any path information (which disk drive the file is on, which folder(s) it is in), the file will automatically go in the bin/x86/debug folder of your application. This is called a relative path and is the preferred method in this class. A relative path is called so because its path information is relative to the location of the program that is running. The advantage of using relative path names is that you can move your program folder anywhere and all of the file names will still work! So when you hand in a program that uses relative path names, I will be able to run the program on my computer without any modifications. This is an example of a relative file name (no drive letter or backslash at the beginning). outFile = new StreamWriter("Output.txt"); The file Output.txt will be written to the bin/x86/debug folder. 2/9/2016 Document1 Page 2 of 7 Writing data using a StreamWriter The Write and WriteLine methods are used to write data to an output file. They work exactly the same way they work when using the Console object to write to the output window. Writing to a file To write text to the output file without moving to a new line afterwards: outFile.Write("String to write"); Subsequent output will appear on the same line as the "String to write". To write a line of text to the output file and move to a new line afterwards: outFile.WriteLine("String to write"); And if you have already written a line to the output file and just want to move to a new line: outFile.WriteLine(); And, when you are done, close the file: outFile.Close(); Closing a file causes any data left in a buffer to be written to the file and frees up any system resources that the file is using. Warning: If you do not close the output file, the last chunk of data in the buffer may not get sent to the file! Some of your data will be missing! 2/9/2016 Document1 Page 3 of 7 Example Here is a procedure that will write the numbers 1 to 1,000 to a file. It requires that the path be sent to it as a parameter. Add this procedure to your game program and execute the procedure when the user presses the "A" button on the game controller. Here is code to call the Save procedure. It will save data in the default directory because no path information is provided. First, declare the file at the top of the program as a module-level variable: StreamWriter outFile; GamePadState gamePad, prevGamePad; Then add code to see if it's time to write the data to the file in the Update procedure: gamePad = GamePad.GetState(PlayerIndex.One); if (gamePad.Buttons.A == ButtonState.Pressed && prevGamePad.Buttons.A == ButtonState.Released) Save("junk.txt"); prevGamePad = gamePad; Then add the Save procedure: public void Save(String path) { StreamWriter outFile = new StreamWriter(path); outFile.WriteLine("Data"); for (int i = 0; i < 1000; i++) { outFile.WriteLine(i.ToString()); } outFile.Close(); } Look at the output file when done. Then delete the output file and run the program again, but comment out the outFile.Close() command. Take a look at the output file again. It will be incomplete because you didn't close it! 2/9/2016 Document1 Page 4 of 7 Reading from an input file An input file can be read by using a StreamReader object. To read from a file, we need a file to read from. Create a sample data file in Notepad: George John Thomas James Andrew 95 85 75 65 55 Save the file in the bin/x86/debug folder of your project. Call it HighScores.txt. To declare an input file: Declare it like this: StreamReader inFile; StreamReader is the data type. It tells C# that we are creating an object that will be used to read data to from an input file. The identifier inFile is the name the programmer has chosen for this StreamReader. It is a name that the programmer makes up. It follows the rules for creating identifiers in C# and should describe the purpose of the file. To open an input file: This statement prepares the file for reading (called "opening" the file): inFile = new StreamReader(path); The path is a string and must be a valid path name on the system that the program is running on. NOTE: There is nothing special about the name inFile. It is just the identifier that we chose for our StreamReader object. We could have called it anything as long as we followed C#'s rules for identifiers. NOTE: If you want to use the file in more than one procedure, you need to make it a modulelevel variable (declare it at the top of the program). Example inFile = new StreamReader("E:/Stuff/HighScores.txt"); 2/9/2016 Document1 Page 5 of 7 Reading data using a StreamReader The Read and ReadLine methods are used to read data from an output file. They work exactly the same way they work when using the Console object to read from the keyboard. In this class, we will almost always use the ReadLine method. Reading from a file To read a line from the input file: line = inFile.ReadLine(); where line must be a string variable. With text files, all of the data is string data. Example We are going to read the data from our data file into a pair of arrays. We will need the following module-level variables. Add them to the declaration area of your program. const int maxPlayers = 10; // array size int playerCount = 0; // players read so far string [] player; // player names int [] score; // player scores The arrays need to have memory allocated in the Initialize procedure. NOTE that the following code does NOT work because we are re-declaring the variables! The module-level variables never get initialized! If we initialize them when we declare them, the declarations will look like this: string [] player = new string [maxPlayers]; int [] score = new int [maxPlayers]; If you put the code to allocate memory for the array in Initialize, it will look like this: player = new string [maxPlayers]; score = new int [maxPlayers]; We also need a signal to read the file. We will use a press of the "B" button. Add this code to your Update method: if (gamePad.Buttons.B == ButtonState.Pressed && prevGamePad.Buttons.B == ButtonState.Released) Read("HighScores.txt"); 2/9/2016 Document1 Page 6 of 7 NOTE: The following code assumes that there are at least maxPlayers lines in the output file. Here is the code to read the file. Add this method to your program. public void Read(string path) { string line; inFile = new StreamReader(path); for (int i = 0; i < maxPlayers; i++) { line = inFile.ReadLine(); //Get the line player[playerCount] = line.Substring(0, 10); score[playerCount] = Convert.ToInt32(line.Substring(10)); Console.WriteLine(player[playerCount] + " " + score[playerCount]); playerCount++; } Console.WriteLine("Players: " + playerCount); } Reading a file, Version 2 Another way to read the file might be to read one line every time the player clicks on the button. There's no way to prevent the player from clicking on the button too many times. So we need a way of knowing when we run out of data. Let's re-write our program to work like this. public void Read() { string line; line = inFile.ReadLine(); //Get the line player[playerCount] = line.Substring(0, 10); score[playerCount] = Convert.ToInt32(line.Substring(10)); Console.WriteLine(player[playerCount] + " " + [playerCount]); playerCount++; } public void OpenFile(String path) { inFile = new StreamReader(path); playerCount = 0; } To test for end of file The StreamReader class has a built-in method called Peek which returns an integer. The value of the integer will always be non-negative as long as more data is available in the file. But it will return the value -1 when there is no more data available. So we can solve our problem by testing for end of file before we read a new line. if (inFile.Peek() >= 0) { // read from the file } 2/9/2016 Document1 Page 7 of 7