Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]

.pdf
Скачиваний:
104
Добавлен:
16.08.2013
Размер:
29.8 Mб
Скачать

488 C H A P T E R 1 3 F I L E S A N D S T R E A M S

<hr>

<%# GetVersionInfoString(DataBinder.Eval(Container.DataItem, "FullName")) %> </ItemTemplate>

</asp:DataList>

The data binding expressions are fairly straightforward. The only one that needs any expression is the GetVersionInfoString() method. This method is coded inside the page class. It creates a new FileVersionInfo object for the file and uses that to extract the version information and product name.

protected string GetVersionInfoString(object path)

{

FileVersionInfo info = FileVersionInfo.GetVersionInfo((string)path); return info.FileName + " " + info.FileVersion + "<br>" + info.ProductName + " " + info.ProductVersion;

}

Of course, most developers have FTP tools and other utilities that make it easier to manage files on a web server. However, this page provides an excellent example of how to use the .NET file and directory management classes. With a little more work, you could transform it into a fullfeatured administrative tool for a web application.

Reading and Writing Files with Streams

The .NET Framework uses a stream model in several areas of the framework. Streams are abstractions that allow you to treat different data sources in a similar way—as a stream of ordered bytes. All

.NET stream classes derive from the base System.IO.Stream class. Streams represent data in a memory buffer, data that’s being retrieved over a network connection, and data that’s being retrieved from or written to a file.

Here’s how you create a new file and write an array of bytes to it through a FileStream:

FileStream fileStream = null; try

{

fileStream = new FileStream(fileName, FileMode.Create); fileStream.Write(bytes);

}

finally

{

if (fileStream != null) fileStream.Close();

}

In this example, the FileMode.Create value is specified in the FileStream constructor to indicate that you want to create a new file. You can use any of the FileMode values described in Table 13-10.

Table 13-10. Values of the FileMode Enumeration

Value

Description

Append

Opens the file if it exists and seeks to the end of the file, or creates a new file.

Create

Specifies that the operating system should create a new file. If the file already

 

exists, it will be overwritten.

CreateNew

Specifies that the operating system should create a new file. If the file already

 

exists, an IOException is thrown.

C H A P T E R 1 3 F I L E S A N D S T R E A M S

489

Value

Description

Open

Specifies that the operating system should open an existing file.

OpenOrCreate

Specifies that the operating system should open a file if it exists; otherwise, a

 

new file should be created.

Truncate

Specifies that the operating system should open an existing file. Once opened,

 

the file will be truncated so that its size is 0 bytes.

 

 

And here’s how you can open a FileStream and read its contents into a byte array:

FileStream fileStream = null; try

{

fileStream = new FileStream(fileName, FileMode.Open); byte[] dataArray = new byte[fileStream.Length];

for(int i = 0; i < fileStream.Length; i++)

{

dataArray[i] = fileStream.ReadByte();

}

}

finally

{

if (fileStream != null) fileStream.Close();

}

On their own, streams aren’t that useful. That’s because they work entirely in terms of single bytes and byte arrays. .NET includes a more useful higher-level model of writer and reader objects that fill the gaps. These objects wrap stream objects and allow you to write more complex data, including common data types such as integers, strings, and dates. You’ll see readers and writers at work in the following sections.

Tip Whenever you open a file through a FileStream, remember to call the FileStream.Close() method when you’re finished. This releases the handle on the file and makes it possible for someone else to access the file. In addition, because the FileStream class is disposable, you can use it with the using statement, which ensures that the FileStream is closed as soon as the block ends.

Text Files

You can write to a file and read from a file using the StreamWriter and StreamReader classes in the System.IO namespace. When creating these classes, you simply pass the underlying stream as a constructor argument. For example, here’s the code you need to create a StreamWriter using an existing FileStream:

FileStream fileStream = new FileStream(@"c:\myfile.txt", FileMode.Create); StreamWriter w = new StreamWriter(fileStream);

You can also use one of the static methods included in the File and FileInfo classes, such as CreateText() or OpenText(). Here’s an example that uses this technique to get a StreamWriter:

StreamWriter w = File.CreateText(@"c:\myfile.txt");

This code is equivalent to the earlier example.

490 C H A P T E R 1 3 F I L E S A N D S T R E A M S

TEXT ENCODING

You can represent a string in binary form using more than one way, depending on the encoding you use. The most common encodings include the following:

ASCII: Encodes each character in a string using 7 bits. ASCII-encoded data can’t contain extended Unicode characters. When using ASCII encoding in .NET, the bits will be padded, and the resulting byte array will have 1 byte for each character.

Full Unicode (or UTF-16): Represents each character in a string using 16 bits. The resulting byte array will have 2 bytes for each character.

UTF-7 Unicode: Uses 7 bits for ordinary ASCII characters and multiple 7-bit pairs for extended characters. This encoding is primarily for use with 7-bit protocols such as mail, and it isn’t regularly used.

UTF-8 Unicode: Uses 8 bits for ordinary ASCII characters and multiple 8-bit pairs for extended characters. The resulting byte array will have 1 byte for each character (provided there are no extended characters).

.NET provides a class for each type of encoding in the System.Text namespace. When using the StreamReader and StreamWriter, you can specify the encoding you want to use with a constructor argument, or you can simply use the default UTF-8 encoding.

Here’s an example that creates a StreamWriter that uses ASCII encoding:

FileStream fileStream = new FileStream(@"c:\myfile.txt", FileMode.Create); StreamWriter w = new StreamWriter(fileStream, System.Text.Encoding.ASCII);

Once you have the StreamWriter, you can use the Write() or WriteLine() method to add information to the file. Both of these methods are overloaded so that they can write many simple data types, including strings, integers, and other numbers. These values are essentially all converted into strings when they’re written to a file, and they must be converted back into the appropriate types manually when you read the file. To make this process easier, you should put each piece of information on a separate line by using WriteLine() instead of Write(), as shown here:

w.WriteLine("ASP.NET Text File Test");

//

Write

a

string.

w.WriteLine(1000);

//

Write

a

number.

When you finish with the file, you must make sure you close it. Otherwise, the changes may not be properly written to disk, and the file could be locked open. At any time, you can also call the Flush() method to make sure all data is written to disk, as the StreamWriter will perform some inmemory caching of your data to optimize performance (which is usually exactly the behavior you want).

// Tidy up. w.Flush(); w.Close();

When reading information, you use the Read() or ReadLine() method of the StreamReader. The Read() method reads a single character, or the number of characters you specify, and returns the data as a char or char array. The ReadLine() method returns a string with the content of an entire line. ReadLine() starts at the first line and advances the position to the end of the file, one line at a time.

Here’s a code snippet that opens and reads the file created in the previous example:

StreamReader r = File.OpenText(@"c:\myfile.txt"); string inputString;

inputString

=

r.ReadLine();

//

=

"ASP.NET Text File Test"

InputString

=

r.ReadLine();

//

=

"1000"

C H A P T E R 1 3 F I L E S A N D S T R E A M S

491

ReadLine() returns a null reference when there is no more data in the file. This means you can read all the data in a file using code like this:

//Read and display the lines from the file until the end

//of the file is reached.

string line; do

{

line = r.ReadLine(); if (line != null)

{

// (Process the line here.)

}

} while (line != null);

Tip You can also use the ReadToEnd() method to read the entire content of the file and return it as a single string. The File class also includes some shortcuts with static methods such as ReadAllText() and ReadAllBytes(), which are suitable for small files only.

Binary Files

You can also write to a binary file. Binary data uses space more efficiently but also creates files that aren’t readable. If you open a binary file in Notepad, you’ll see a lot of extended characters (politely known as gibberish).

To open a file for binary writing, you need to create a new BinaryWriter class. The class constructor accepts a stream, which you can create by hand or retrieve using the File.OpenWrite() method. Here’s the code to open the file c:\binaryfile.bin for binary writing:

BinaryWriter w = new BinaryWriter(File.OpenWrite(@"c:\binaryfile.bin"));

.NET concentrates on stream objects, rather than the source or destination for the data. This means you can write binary data to any type of stream, whether it represents a file or some other type of storage location, using the same code. In addition, writing to a binary file is almost the same as writing to a text file, as you can see here:

string str = "ASP.NET Binary File Test"; int integer = 1000;

w.Write(str);

w.Write(integer);

w.Flush();

w.Close();

Unfortunately, when you read data, you need to know the data type you want to retrieve. To retrieve a string, you use the ReadString() method. To retrieve an integer, you must use ReadInt32(), as follows:

BinaryReader r = new BinaryReader(File.OpenRead(@"c:\binaryfile.bin")); string str;

int integer;

str = r.ReadString(); integer = r.ReadInt32();

492 C H A P T E R 1 3 F I L E S A N D S T R E A M S

Note There’s no easy way to jump to a location in a text or binary file without reading through all the information in order. While you can use methods such as Seek() on the underlying stream, you need to specify an offset in bytes. This involves some fairly involved calculations to determine variable sizes. If you need to store a large amount of information and move through it quickly, you need a dedicated database, not a binary file.

Uploading Files

ASP.NET includes two controls that allow website users to upload files to the web server. Once the web server receives the posted file data, it’s up to your application to examine it, ignore it, or save it to a back-end database or a file on the web server.

The controls that allow file uploading are HtmlInputFile (an HTML server control) and FileUpload (an ASP.NET web control). Both represent the <input type="file"> HTML tag. The only real difference is that the FileUpload control takes care of automatically setting the encoding of the form to multipart/form data. If you use the HtmlInputFile control, it’s up to you to make this change using the enctype attribute of the <form> tag—if you don’t, the HtmlInputFile control won’t work.

Declaring the FileUpload control is easy. It doesn’t expose any new properties or events that you can use through the control tag.

<asp:FileUpload ID="Uploader" runat="server" />

The <input type="file"> tag doesn’t give you much choice as far as the user interface is concerned (it’s limited to a text box that contains a filename and a Browse button). When the user clicks Browse, the browser presents an Open dialog box and allows the user to choose a file. This behavior is hard-wired into the browser, and you can’t change it. Once the user selects a file, the filename is filled into the corresponding text box. However, the file isn’t uploaded yet—that happens later, when the page is posted back. At this point, all the data from all the input controls (including the file data) is sent to the server. For that reason, it’s common to add a Button control to post back

the page.

To get information about the posted file content, you can access the FileUpload.PostedFile object. You can save the content by calling the PostedFile.SaveAs() method, as demonstrated in the following example.

Here’s the event-handling code for the Button.Click event:

protected void cmdUpload_Click(object sender, EventArgs e)

{

// Check if a file was submitted.

if (Uploader.PostedFile.ContentLength != 0)

{

try

{

if (Uploader.PostedFile.ContentLength > 1064)

{

//This exceeds the size limit you want to allow.

//You should check the size to prevent a denial of

//service attack that attempts to fill up your

//web server's hard drive.

//You might also want to check the amount of

//remaining free space.

lblStatus.Text = "Too large. This file is not allowed";

}

else

C H A P T E R 1 3 F I L E S A N D S T R E A M S

493

{

//Retrieve the physical directory path for the Upload

//subdirectory.

string destDir = Server.MapPath("./Upload");

//Extract the filename part from the full path of the

//original file.

string fileName = Path.GetFileName(Uploader.PostedFile.FileName);

//Combine the destination directory with the filename. string destPath = Path.Combine(destDir, fileName);

//Save the file on the server. Uploader.PostedFile.SaveAs(destPath); lblStatus.Text = "Thanks for submitting your file";

}

}

catch (Exception err)

{

lblStatus.Text = err.Message;

}

}

}

In the example, if a file has been posted to the server and isn’t too large, the file is saved using the HttpPostedFile.SaveAs() method. To determine the physical path you want to use, the code combines the destination directory (Upload) with the name of the posted file using the static utility methods of the Path class.

Figure 13-3 shows the page after the file has been uploaded.

Figure 13-3. Uploading a file

You can also interact with the posted data through the stream model, rather than just saving it to disk. To get access to the data, you use the FileUpload.PostedFile.InputStream property. For example, you could use the following code to display the content of a posted file (assuming it’s text-based):

494 C H A P T E R 1 3 F I L E S A N D S T R E A M S

// Display the whole file content.

StreamReader r = new StreamReader(Uploader.PostedFile.InputStream); lblStatus.Text = r.ReadToEnd();

r.Close();

Note By default the maximum size of the uploaded file is 4 MB. If you try to upload a bigger file, you’ll get a runtime error. To change this restriction, modify the maxRequestLength attribute of the <httpRuntime> setting in the application’s web.config file. The size is specified in kilobytes, so <httpRuntime maxRequestLength="8192"/> sets the maximum file size to 8 MB.

Making Files Safe for Multiple Users

Although it’s fairly easy to create a unique filename, what happens in the situation where you really do need to access the same file to serve multiple different requests? Although this situation isn’t ideal (and often indicates that a database-based solution would work better), you can use certain techniques to defend yourself.

One approach is to open your files with sharing, which allows multiple processes to access the same file at the same time. To use this technique, you need to use the four-parameter FileStream constructor that allows you to select a FileMode. Here’s an example:

FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read,

FileShare.Read);

This statement allows multiple users to open the file for reading at the same time. However, no one will be able to update the file.

Tip Another technique that works well if multiple users need to access the same data, especially if this data is frequently used and not excessively large, is to load the data into the cache (as described in Chapter 11). That way, multiple users can simultaneously access the data without a hitch. Of course, this approach may not suit your needs if another process is responsible for creating or periodically updating the file, in which case you can’t be sure the data you’ve cached is up-to-date.

It is possible to have multiple users open the file in read-write mode by specifying a different FileAccess value (such as FileAccess.Write or FileAccess.ReadWrite). In this case, Windows will dynamically lock small portions of the file when you write to them (or you can use the FileStream.Lock() method to lock down a range of bytes in the file). If two users try to write to the same locked portion at once, an exception can occur. Because web applications have high concurrency demands, this technique is not recommended and is extremely difficult to implement properly. It also forces you to use low-level byte-offset calculations, where it is notoriously easy

to make small, aggravating errors.

So, what is the solution when multiple users need to update a file at once? One option is to create separate user-specific files for each request. Another option is to tie the file to some other object and use locking. The following sections explain these techniques.

Creating Unique Filenames

One solution for dealing with user-concurrency headaches with files is to avoid the conflict altogether by using different files for different users. For example, imagine you want to store a

C H A P T E R 1 3 F I L E S A N D S T R E A M S

495

user-specific log. To prevent the chance for an inadvertent conflict if two web pages try to use the same log, you can use the following two techniques:

Create a user-specific directory for each user.

Add some information to the filename, such as a timestamp, GUID (global unique identifier), or random number. This reduces the chance of duplicate filenames to a small possibility.

The following sample page demonstrates this technique. It defines a method for creating filenames that are statistically guaranteed to be unique. In this case, the filename incorporates a GUID.

Here’s the private method that generates a new unique filename:

private string GetFileName()

{

//Create a unique filename. string fileName = "user." + Guid.NewGuid().ToString();

//Put the file in the current web application path.

return Path.Combine(Request.PhysicalApplicationPath, fileName);

}

Note A GUID is a 128-bit integer. GUID values are tremendously useful in programming because they’re statistically unique. In other words, you can create GUID values continuously with little chance of ever creating a duplicate. For that reason, GUIDs are commonly used to uniquely identify queued tasks, user sessions, and other dynamic information. They also have the advantage over sequential numbers in that they can’t easily be guessed. The only disadvantage is that GUIDs are long and almost impossible to remember (for an ordinary human being). GUIDs are commonly represented in strings as a series of lowercase hexadecimal digits, like 382c74c3-721d- 4f34-80e5-57657b6cbc27.

Using the GetFileName() method, you can create a safer logging application that writes information about the user’s actions to a text file. In this example, all the logging is performed by calling a Log() method, which then checks for the filename and assigns a new one if the file hasn’t been created yet. The text message is then added to the file, along with the date and time information.

private void Log(string message)

{

// Check for the file. FileMode mode;

if (ViewState["LogFile"] == null)

{

//First, create a unique user-specific filename. ViewState["LogFile"] = GetFileName();

//The log file must be created.

mode = FileMode.Create;

}

else

{

// Add to the existing file. mode = FileMode.Append;

}

496C H A P T E R 1 3 F I L E S A N D S T R E A M S

//Write the message.

//A using block ensures the file is automatically closed,

//even in the case of error.

string fileName = (string)ViewState["LogFile"];

using (FileStream fs = new FileStream(fileName, mode))

{

StreamWriter w = new StreamWriter(fs); w.WriteLine(DateTime.Now); w.WriteLine(message);

w.Close();

}

}

For example, a log message is added every time the page is loaded, as shown here:

protected void Page_Load(object sender, System.EventArgs e)

{

if (!Page.IsPostBack)

{

Log("Page loaded for the first time.");

}

else

{

Log("Page posted back.");

}

}

The last ingredients are two button event handlers that allow you to delete the log file or show its contents, as follows:

protected void cmdRead_Click(object sender, System.EventArgs e)

{

if (ViewState["LogFile"] != null)

{

string fileName = (string)ViewState["LogFile"];

using (FileStream fs = new FileStream(fileName, FileMode.Open))

{

StreamReader r = new StreamReader(fs);

//Read line by line (allows you to add

//line breaks to the web page). string line;

do

{

line = r.ReadLine(); if (line != null)

{

lblInfo.Text += line + "<br>";

}

} while (line != null); r.Close();

}

}

}

protected void cmdDelete_Click(object sender, System.EventArgs e)

{

C H A P T E R 1 3 F I L E S A N D S T R E A M S

497

if (ViewState["LogFile"] != null)

{

File.Delete((string)ViewState["LogFile"]);

}

}

Figure 13-4 shows the web page displaying the log contents.

Figure 13-4. A safer way to write a user-specific log

Locking File Access Objects

Of course, in some cases you do need to update the same file in response to actions taken by multiple users. One approach is to use locking. The basic technique is to create a separate class that performs all the work of retrieving the data. Once you’ve defined this class, you can create a single global instance of it and add it to the Application collection. Now you can use the C# lock statement to ensure that only one thread can access this object at a time (and hence only one thread can attempt to open the file at once).

For example, imagine you create the following Logger class, which updates a file with log information when you call the LogMessage() method, as shown here:

public class Logger

{

public void LogMessage()

{

lock (this)

{

// (Open the file and update it.)

}

}

}