Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
498 C H A P T E R 1 3 ■ F I L E S A N D S T R E A M S
The Logger class locks itself before accessing the log file, creating a critical section. This ensures that only one thread can execute the LogMessage() code at a time, removing the danger of file conflicts.
However, for this to work you must make sure every class is using the same instance of the Logger object. You have a number of options here—for example, you could respond to the HttpApplication.Start event in the global.asax file to create a global instance of the Logger class and store it in the Application collection. Alternatively, you could expose a single Logger instance through a static variable in the global.asax file, as shown here:
private log = new Logger(); public Logger Log
{
get { return Logger; }
}
Now any page that uses the Logger to call LogMessage() gets exclusive access:
// Update the file safely. Application.Log.LogMessage(myMessage);
Keep in mind that this approach is really just a crude way to compensate for the inherit limitations of a file-based system. It won’t allow you to manage more complex tasks, such as having individual users read and write pieces of text in the same file at the same time. Additionally, while a file is locked for one client, other requests will have to wait. This is guaranteed to slow down application performance and lead to an exception if the object isn’t released before the second client times out. Unless you invest considerable effort refining your threading code (for example, you can use classes in the System.Threading namespace to test if an object is available and take alternative action if it isn’t), this technique is suitable only for small-scale web applications. It’s for this reason that ASP.NET applications almost never use file-based logs—instead, they write to the Windows event log or a database.
Compression
.NET 2.0 adds built-in support for compressing data in any stream. This trick allows you to compress data that you write to any file. The support comes from two classes in the new System.IO.Compression namespace: GZipStream and DeflateStream. Both of these classes represent similarly efficient lossless compression algorithms.
To use compression, you need to wrap the real stream with one of the compression streams. For example, you could wrap a FileStream (for compressing data as it’s written to disk) or a MemoryStream (for compressing data in memory). Using a MemoryStream, you could compress data before storing it in a binary field in a database or sending it to a web service.
For example, imagine you want to compress data saved to a file. First, you create the FileStream:
FileStream fileStream = new FileStream(@"c:\myfile.txt", FileMode.Create);
Next, you create a GZipStream or DeflateStream, passing in the FileStream and a CompressionMode value that indicates whether you are compressing or decompressing data:
GZipStream compressStream = new GZipStream(fileStream, CompressionMode.Compress);
To write your actual data, you use the Write() method of the compression stream, not the FileStream. The compression stream compresses the data and then passes the compressed data to the underlying FileStream. If you want to use a higher-level writer, such as the StreamWriter or BinaryWriter, you supply the compression stream instead of the FileStream:
StreamWriter w = new StreamWriter(compressStream);
C H A P T E R 1 3 ■ F I L E S A N D S T R E A M S |
499 |
Now you can perform your writing through the writer object. When you’re finished, flush the GZipStream so that all the data ends up in the file:
w.Flush();
fileStream.Close();
Reading a file is just as straightforward. The difference is that you create a compression stream with the CompressionMode.Decompress option, as shown here:
FileStream fileStream = new FileStream(@"c:\myfile.bin", FileMode.Open);
GZipStream decompressStream = new GZipStream(fileStream,
CompressionMode.Decompress);
StreamReader r = new StreamReader(decompressStream);
■Note Although GZIP is a industry-standard compression algorithm (see http://www.gzip.org for information), that doesn’t mean you can use third-party tools to decompress the compressed files you create. The problem is that although the compression algorithm may be the same, the file format is not. Namely, the files you create won’t have header information that identifies the original compressed file.
Serialization
You can use one more technique to store data in a file—serialization. Serialization is a higher-level model that’s built on .NET streams. Essentially, serialization allows you to convert an entire live object into a series of bytes and write those bytes into a stream object such as the FileStream. You can then read those bytes back later to re-create the original object.
For serialization to work, your object must all meet the following criteria:
•The object must have a Serializable attribute preceding the class declaration.
•All the public and private variables of the class must be serializable.
•If the class derives from another class, all parent classes must also be serializable.
Here’s a serializable class that you could use to store log information:
[Serializable()] public class LogEntry
{
private string message; private DateTime date;
public string Message
{
get {return message;} set {message = value;}
}
public string DateTime
{
get {return date;} set {date = value;}
}
public LogEntry(string message)
{
500 C H A P T E R 1 3 ■ F I L E S A N D S T R E A M S
this.message = message; this.date = DateTime.Now;
}
}
■Tip In some cases, a class might contain data that shouldn’t be serialized. For example, you might have a large field you can recalculate or re-create easily, or you might have some sensitive data that could pose a security request. In these cases, you can add a NonSerialized attribute before the appropriate variable to indicate it shouldn’t be persisted. When you re-create the class, nonserialized variables will return to their default values.
You may remember serializable classes from earlier in this book. Classes need to be serializable in order to be stored in the view state for a page or put into an out-of-process session state store. In those cases, you let .NET serialize the object for you automatically. However, you can also manually serialize a serializable object and store it in a file or another data source of your choosing (such as a binary field in a database).
To convert a serializable object into a stream of bytes, you need to use a class that implements the IFormatter interface. The .NET Framework includes two such classes: BinaryFormatter, which serializes an object to a compact binary representation, and SoapFormatter, which uses the SOAP XML format and results in a longer text-based message. The BinaryFormatter class is found in the System.Runtime.Serialization.Formatters.Binary namespace, and SoapFormatter is found in the System.Runtime.Serialization.Formatters.Soap namespace. (To use SoapFormatter, you also need to add a reference to the assembly System.Runtime.Serialization.Formatters.Soap.dll.) Both methods serialize all the private and public data in a class, along with the assembly and type information needed to ensure that the object can be deserialized exactly.
To create a simple example, let’s consider what you need to do to rewrite the logging page shown earlier to use object serialization instead of writing data directly to the file. The first step is to change the Log() method so that it creates a LogEntry object and uses the BinaryFormatter to serialize it into the existing file, as follows:
private void Log(string message)
{
//Check for the file. FileMode mode;
if (ViewState["LogFile"] == null)
{
ViewState["LogFile"] = GetFileName(); mode = FileMode.Create;
}
else
{
mode = FileMode.Append;
}
//Write the message.
string fileName = (string)ViewState["LogFile"];
using (FileStream fs = new FileStream(fileName, mode))
{
// Create a LogEntry object.
LogEntry entry = new LogEntry(message);
// Create a formatter.
BinaryFormatter formatter = new BinaryFormatter();
C H A P T E R 1 3 ■ F I L E S A N D S T R E A M S |
501 |
// Serialize the object to a file. formatter.Serialize(fs, entry);
}
}
The last step is to change the code that fills the label with the complete log text. Instead of reading the raw data, it now deserializes each saved instance using the BinaryFormatter, as shown here:
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))
{
// Create a formatter.
BinaryFormatter formatter = new BinaryFormatter();
// Get all the serialized objects. while (fs.Position < fs.Length)
{
// Deserialize the object from the file.
LogEntry entry = (LogEntry)formatter.Deserialize(fs);
// Display its information.
lblInfo.Text += entry.Date.ToString() + "<br>"; lblInfo.Text += entry.Message + "<br>";
}
}
}
}
So, exactly what information is stored when an object is serialized? Both the BinaryFormatter and the SoapFormatter use a proprietary .NET serialization format that includes information about the class, the assembly that contains the class, and all the data stored in the class member variables. Although the binary format isn’t completely interpretable, if you display it as ordinary ASCII text, it looks something like this:
?ÿÿÿÿ? ?GApp_Web_a7ve1ebl, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null?? ?LogEntry??message?date????Page loaded for the first time. ????
The SoapFormatter produces more readily interpretable output, although it stores the same information (in a less compact form). The assembly information is compressed into a namespace string, and the data is enclosed in separate elements:
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Body>
<a1:LogEntry id="ref-1" xmlns:a1=
"http://schemas.microsoft.com/clr/assem/App_Web_m9gesigu%2C%20Version%3D0.0.0.0%2C %20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull ">
<message id="ref-3">Page loaded for the first time.</message>
502 C H A P T E R 1 3 ■ F I L E S A N D S T R E A M S
<date>2005-09-21T22:50:04.8677568-04:00</date> </a1:LogEntry>
</SOAP-ENV:Body> </SOAP-ENV:Envelope>
Clearly, this information is suitable just for .NET-only applications. However, it provides the most convenient, compact way to store the contents of an entire object.
Summary
In this chapter, you learned how to use the .NET classes for retrieving file system information. You also examined how to work with files and how to serialize objects. Along the way you learned how data binding can work with the file classes, how to plug security holes with the Path class, and how to deal with file contention in multiuser scenarios. You also considered data compression using GZIP.
P A R T 3
■ ■ ■
Building ASP.NET
Websites
506 C H A P T E R 1 4 ■ U S E R C O N T R O L S
User Control Basics
User control (.ascx) files are similar to ASP.NET web-form (.aspx) files. Like web forms, user controls are composed of a user interface portion with control tags (the .ascx file) and can use inline script or a .cs code-behind file. User controls can contain just about anything a web page can, including static HTML content and ASP.NET controls, and they also receive the same events as the Page object (like Load and PreRender) and expose the same set of intrinsic ASP.NET objects through properties (such as Application, Session, Request, and Response).
The key differences between user controls and web pages are as follows:
•User controls begin with a Control directive instead of a Page directive.
•User controls use the file extension .ascx instead of .aspx, and their code-behind files inherit from the System.Web.UI.UserControl class. In fact, the UserControl class and the Page class both inherit from the same TemplateControl class, which is why they share so many of the same methods and events.
•User controls can’t be requested directly by a client. (ASP.NET will give a generic “that file type is not served” error message to anyone who tries.) Instead, user controls are embedded inside other web pages.
Creating a Simple User Control
To create a user control in Visual Studio, select Website Add New Item, and choose the Web User Control template.
The following is the simplest possible user control—one that merely contains static HTML. This user control represents a header bar.
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="Header.ascx.cs" Inherits="Header" %>
<table width="100%" border="0" bgcolor="blue"> <tr>
<td><font face="Verdana,Arial" size="6" color="yellow"><b> User Control Test Page</b></font>
</td>
</tr>
<tr>
<td align="right"><font size="3" color="white"><b>
An Apress Creation © 2004</b></font> </td>
</tr>
</table>
You’ll notice that the Control directive identifies the code-behind class. However, the simple header control doesn’t require any custom code to work, so you can leave the class empty:
public partial class Header : System.Web.UI.UserControl {}
As with ASP.NET web forms, the user control is a partial class, because it’s merged with a separate portion generated by ASP.NET. That automatically generated portion has the member variables for all the controls you add at design time.
Now to test the control, you need to place it on a web form. First, you need to tell the ASP.NET page that you plan to use that user control with the Register directive, as shown here:
<%@ Register TagPrefix="apress" TagName="Header" Src="Header.ascx" %>
C H A P T E R 1 4 ■ U S E R C O N T R O L S |
507 |
This line identifies the source file that contains the user control using the Src attribute. It also defines a tag prefix and tag name that will be used to declare a new control on the page. In the same way that ASP.NET server controls have the <asp: ... > prefix to declare the controls (for example, <asp:TextBox>), you can use your own tag prefixes to help distinguish the controls you’ve created. This example uses a tag prefix of apress and a tag named Header.
The full tag is shown in this page:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="HeaderTest.aspx.cs" Inherits="HeaderTest" %>
<%@ Register TagPrefix="apress" TagName="Header" Src="Header.ascx" %> <html>
<head>
<title>HeaderHost</title>
</head>
<body>
<form id="Form1" method="post" runat="server">
<apress:Header id="Header1" runat="server"></apress:Header>
</form>
</body>
</html>
At a bare minimum, when you add a user control to your page, you should give it a unique ID and indicate that it runs on the server, like all ASP.NET controls. Figure 14-1 shows the sample page with the custom header.
Figure 14-1. Testing the header user control
In Visual Studio, you don’t need to code the Register directive by hand. Instead, once you’ve created your user control, simply select the .ascx in the Solution Explorer and drag it onto the drawing area of a web form. Visual Studio will automatically add the Register directive for you as well as an instance of the user control tag.
The header control is the simplest possible user control example, but it can already provide some realistic benefits. Think about what might happen if you had to manually copy the header’s HTML code into all your ASP.NET pages, and then you had to change the title, add a contact link, or something else. You would need to change and upload all the pages again. With a separate user control, you just update that one file. Best of all, you can use any combination of HTML, user controls, and server controls on an ASP.NET web form.
