Wednesday, August 18, 2004

Perhaps for my own benefit, I post the following.  Writing down (or typing) something usually helps me remember things, so hopefully this will serve as a reminder going forward.  I do this once in a blue moon, and everytime I have to go hunt it down and then for some oddball reason, I promptly forget it, promising myself I won't.

Every once in a while, Internet Explorer (6.0 in my case) seems to forget to display controls (such as buttons and dropdowns) using the current Windows theme - that is, with the nice sleek edges, rounded corners, and mouseover behavior.  I haven't nailed down what it is that is causing this behavior; I don't have any CSS styles that would otherwise alter the border-style of the elements or anything.  However, there is a META tag that we can use to force the issue.

By simply adding the following tag to the <head> section of the page you can ensure that themes will be (or will NOT be) supported on the page:

<meta http-equiv=”MSThemeCompatible" content="yes">

Wednesday, August 18, 2004 5:02:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Saturday, August 14, 2004

Talk about frustrating!  Over the past several weeks I've been working on a web application that exhibited very interesting behaviors.

After compiling it, I would then browse to my virtual directory and the very first time it ran I would get errors reported.  Then I would refresh the page and it would work - for a bit.  Then I would get the same errors reported.  I'd refresh a second time and from that point on it ran flawlessly.  Interestingly, these problems did not exist on a coworker's machines.  However, if I ran the application from Visual Studio .NET, it ran fine from the get go.

Needless to say, I started to dive it and try to figure it out.  I robustified my error logging mechanisms in my application so that more descriptive messages would be written to the event log.  In short order, this led me to see that when from IE my ASP.NET Session and Application were recycling.  This is why I was getting the errors...I was loading objects up into Session at the start and then, all of the sudden, the values were gone.  I found it interesting that when run from VS.NET, the session objects would remain even after the Session recycled!

I did a little digging and found a couple of articles (article1, article2) on support.microsoft.com that offered some hope.  Well, after lots (and lots) of experimentation, I found that the articles offered the correct solution.  My virus scanner was affecting the session.  After I performed a rebuild (and a subsequent deploy), the OS marked those files as modified which recycled the process (ok, I was expecting that one).  Then, the anti-virus software would scan the files, affecting them again, again recycling the process.  The solution offered by the MS articles (and others) indicated to turn off AV scans on my \WINDOWS\Microsoft.NET\Framework\v1.1.4322\Temporary ASP.NET Files folder.  That didn't seem to fix the problem.  However, I turned off scanning on my \Dev folder and all was hunky dory!

Sometimes, what seem to be the simplest of issues...are!  Once you know what the solution is.  For the longest time I thought it was a threading issue, but my robustified error logging eliminated that possibility :)

Saturday, August 14, 2004 11:52:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, August 11, 2004

Here's a little technique that I created for reading values out of an application configuration file; be it an app.exe.config file or a web.config file, the strategy works the same.  While I am very appreciative of the ease that as .NET developers we have of getting up off the ground and reading values from an application configuration file (along the lines of what we did in unmanaged code with GetPrivateProfileString() for .ini files) I am frequently burdened by having to deal with the standard

<appSettings>
   <add key=“blah“ value=“blah, blah, blah“ />
</appSettings>

There's really nothing inherently wrong with this.  It's fast.  It's easy.  It's convenient.  It's easily readable.  It's easily retrievable:

string val = ConfigurationSettings.AppSettings[”blah”];

For the vast majority of applications this works out just fine.  That said, I'll point out that I'm kinda particular when it comes to my software - anyone who works with me would undoubtedly concur.  I'm a perfectionist when it comes to writing my code - sometimes to a fault.

The <add key=... /> tag just isn't all that descriptive and it makes your code cluttered with calls to ConfigurationSettings, breaking down the OO model to some extent.  If you want the OO feel, then this just doesn't cut it.  Sure, you can create a wrapper class that as it's properties are called, delegates the call on to the ConfigurationSettings object, but that, too, is clumsy as well as laborious to create.

This is where configuration sections enter the picture.  The .NET framework has carefully (and conveniently) provided a set of objects and interfaces that we can use to control the loading of configuration values.  Now, rather than the <add key=”blah” value=”abc” /> tag, we can use proper XML and say <blah>abc</blah>.  All we have to do is just a bit of wireup and we'll call it good.  In order to properly use custom configuration sections we have to make changes to the .config file as well as our application.

In order to create a custom section within the .config file, we must create what is known as a Configuration Section Handler.  A class that implements the IConfigurationSectionHandler interface will suffice.  This interface has a single method called Create() which is called by the .NET Framework when it's time to load the custom section.  It's up to us to parse the Xml contained within our custom node...but there's a cooler, fancier way than simply parsing the Xml as we'll see here shortly.

First, let's declare our custom configuration section and associate our IConfigurationSectionHandler to it:

<configuration>
   <configSections>
      <sectionGroup name="devstone">
         <section name="user" type="MyApp.MySectionHandler, MyApp" />
      </sectionGroup>
   </configSections>
</configuration>

The Xml code above declares that within the .config file there will be a group named devstone and within that group will be found a section (XmlNode) called user.  When loaded, the .NET Framework will automatically create an instance of the MyApp.MySectionHandler class found in the MyApp assembly.  The MySectionHandler class implements the IConfigurationSectionHandler interface.

Ok, this is pretty standard stuff, but let's get down to the code that actually reads the section and show some cool techniques.  The section handler code is all found within the IConfigurationSectionHandler.Create() method.  Reading the Xml from the file will be easy.  So easy, in fact, that we won't do it at all - directly.  In fact, we'll rely on the .NET Framework to do it for us.  The trick is to create an object that serializes out to the same Xml structure found in our configuration section.  But, how will the Framework know which object to create in order to deserialize our Xml section properly?  We'll embed the type name in the Xml node.

By doing this we can create a generic section handler that will simply read this type name from the node and deserialize the Xml to the designated object.  Here's the code for the new IConfigurationSectionHandler class:

public sealed class MySectionHandler : IConfigurationSectionHandler {

   object IConfigurationSectionHandler.Create(object parent, object configContext, XmlNode section) {
      // inspired by code by Marc Focas
      //
http://www.thecodeproject.com/csharp/ConfigSectionHandler.asp
     
      XPathNavigator nav = section.CreateNavigator();
      string objType = (string)nav.Evaluate("string(@type)");
      XmlSerializer ser = new XmlSerializer(Type.GetType(objType));
      return ser.Deserialize(new XmlNodeReader(section));
   }
  
}

That's all there is to it.  Now let's check out the custom section and its associated class.

<configuration>
   <configSections>...</configSections>
  
   <devstone>
      <user type="MyApp.UserSettings, MyApp">
         <name>Aaron</name>
         <title>Supreme Overlord</title>
      <user>
   </devstone>
</configuration>

The class that drives this is the Employee class found within the MyApp application:

[XmlRoot("user")]
public sealed class UserSettings {

   private string _name;
   private string _title;
  
   public Employee() {
      /* a public, parameterless constructor is required for serialization to work properly */
   }
  
   [XmlElement("name")]
   public string Name {
      get { return _name; }
      set { _name = value; }
   }
  
   [XmlElement("title")]
   public string Title {
      get { return _title; }
      set { _title = value; }
   }
  
}

Note the usage of the XmlRootAttribute and the XmlElementAttribute.  They are not strictly necessary, but it's important in this instance to use them because of the case.  Were I not to put the attributes on, the Xml would need to match case- and spelling-wise the properties and the class name.  I prefer camelCased Xml so I explicitly put the attributes on while maintaining the PascalCasing for my code.

Well, this is pretty slick!  Now comes the trick of loading the configuration section at runtime.  In order to achieve this, we can use the ConfigurationSettings.GetConfig() method.  But where do we perform the call?  And more importantly, where do we store the deserialized object?  Do we make it a global object?  Do we recreate it anytime we need access to a property?

I believe this is an excellent example of when a singleton object should be used.  At any given time we only have one set of settings for the application.  To achieve this, we'll modify the UserSettings class that we just created a bit:

[XmlRoot("user")]
public sealed class UserSettings {
   private static UserSettings _user = null;
  
   public static UserSettings Current {
      get {
         if ( null == _user ) {

            lock ( typeof(UserSettings) ) {
               if ( null == _user ) _user = ConfigurationSettings.GetConfig("devstone/user") as UserSettings;
            }
         }
         return _user;
      }
   }
  
   //...the rest of the code still goes here
}

Now, the section will be loaded the first time the UserSettings class is accessed.  The call to GetConfig("devstone/user") instructs the .NET Framework to load the devstone section group at the user section.  It will, in turn, and by virtue of the <configSections /> Xml block, load our MySectionHandler class which inspects the Xml, thereby deserializing our UserSettings class.  Now in order to retrieve the user name in the .config file we make the following call in our code:

string name = UserSettings.Current.Name;

That's a lot more attractive and much more object-oriented than:

string name = ConfigurationSettings.AppSettings["Name"];

A very powerful usage of this model is that of creating components.  It's a powerful paradigm to be able to drop your own company-specific sections in a .config file and have the classes necessary to read them.  This way, you won't have to go mucking up someone else's .config file and potentially clashing with their already extant names.  It's provides a much more professional feel to your application.

Enjoy!

Wednesday, August 11, 2004 4:29:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Friday, August 06, 2004

I ran into something today that had me stumped for a little bit.  I'm developing a website that streams images down to the client.  These images originate from a SQL Server database and are context sensitive as well as user-definable.  I don't yet have my image caching implemented, but that doesn't really play into this discussion.

I have set up an HttpHandler mapped to my custom extension (.dsix) which gets intercepted by aspnet_isapi to be processed (can't wait for Whidbey in which we'll get handlers out of the box for the .asix extension - active server image files :-)  My HttpHandler resolves some HttpContext sensitive details about the request and then proceeds to acquire the image bits from the database.

Now before anyone jumps down my throat, these images are very small (6k-8k) and yes, I'm storing them in memory on the server.  I'm not concerned about this as a matter of fact because 1) my site has very few users (5-15) and 2) there is only 1 image per user in memory.  Should the site get to be more demanding I'll refactor and reevaluate the storage.

Ok, now to the issue...

I created a SqlDataReader object to read my values from the database and could successfully load the image from the database with the following code:

if ( !dr.IsDBNull(USERSTATE_PORTRAIT) ) {
  
long bufferLength = dr.GetBytes(USERSTATE_PORTRAIT, 0, null
, 0, 0);
  
byte[] buffer = new byte
[bufferLength];
   dr.GetBytes(USERSTATE_PORTRAIT, 0, buffer, 0, (int)bufferLength);

  
using ( MemoryStream ms = new MemoryStream(buffer) ) {
      _portrait = 
Bitmap.FromStream(ms) as Bitmap;
  
}
}

This information was later cached.  When it came time to render the image, I used the following code, where res is a reference to the HttpResponse:

user.Portrait.Save(res.OutputStream, user.Portrait.RawFormat);

I had stored a PNG file in the database.  Upon making the call to Image.Save, I would receive a System.Runtime.InteropServices.ExternalException.  The message was 'A generic error occurred in GDI+'.  Well thanks!  At least is wasn't a specific error!

I did a little digging and found an interesting article in Microsoft support about how Bitmaps need access to their originating stream, file, etc.  It turns out that GDI+ may arbitrarily choose to discard the memory occupied by a bitmap, only to rehydrate it when needed later.  For this reason, the source stream or file gets locked.  Aha! I wish I had understood that behavior several years ago when I really needed it.  Well, as it turns out, I don't still have the MemoryStream from which the image was created just hanging around when it comes time to render the image.  So what I have to do is make a copy of the image, and dispose of the original:

...
using ( MemoryStream ms = new MemoryStream(buffer) ) {
   // see Q814675 - PRB: Bitmap and Image Constructor Dependencies for details
  
Bitmap orig = Bitmap.FromStream(ms) as Bitmap;
   _portrait = getPortraitImage(orig);
   orig.Dispose();
}
...

private Bitmap getPortraitImage(Bitmap original) {
   Bitmap ret = new Bitmap(original.Width, original.Height, PixelFormat.Format24bppRgb);
   // copy the original image into the new one
  
using ( Graphics g = Graphics.FromImage(ret) ) {
      g.DrawImage(original, 0, 0);
   }
   return ret;
}

Now, in reality, my actual code is a little different than what I've shown - I've trimmed this down a bit for simplicity.  What you'd probably want to do that I've not shown is check the color depth of the original image and create an image of the same type rather than defaulting to a 24bit image.  Now the HttpHandler might look something like this:

user.Portrait.Save(res.OutputStream, ImageFormat.Jpeg);

Works like a champ!

Friday, August 06, 2004 5:53:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Wednesday, August 04, 2004

Have you ever been working on a project and found yourself constantly copying and pasting code between files to maintain consistency (such as comment preambles, disclaimers, (even code snippets! - ok, I'd recommend against that one!) etc).  It can get quite tedious and error-prone, especially if what you're pasting has a bug, misprint, or something that needs to change universally.  If you've ever had occasion to, upon adding a new file to a VS project, go through and rearrange all of the code to fit your standards and add in your comments (and waste hours upon hours doing so), then creating a custom template is for you.  Using the proper jargon: creating a custom project item is for you.

Creating custom templates isn't an entirely trivial endeavor as you may know if you've tried it, but it's not overly complex.  It involves creating (or editing) a .vsz (Wizard) file, a .vsdir file, and customizing your template files.  This process involves navigating several directories and making tiny changes in each one.

The game changes a little if you're trying to add a WebForm to a web project, as I discovered while spending an hour churning over what seemed like it should have worked.  The reason for my frustration is that VS.NET will, while accepting my .aspx template code, generate its own .aspx.cs code.  In fact, it bases it's code on the C:\Program Files\Microsoft Visual Studio .NET 2003\VC#\DesignerTemplates\1033\NewWebFormCode.cs.  I didn't want to go in and edit that file (except for making a few standards-abiding changes) because that would affect every page on every website that I develop, and that's too far-reaching for my tastes.

Here's what I did.  Note that all directories that I reference are found in C:\Program Files\Microsoft Visual Studio .NET 2003\VC#\ as subdirectories.  Also, the example templates, er, project items that I created are for a website called DevTracker.

1)  Navigate to \CSharpProjectItems and create my wizard file called CSharpAddDevTrackerWebFormWiz.vsz.  The contents of this file:

VSWIZARD 7.0
Wizard=VsWizard.VsWizardEngine.7.1
Param="WIZARD_NAME = CSharpAddDevTrackerWebFormWiz"
Param="WIZARD_UI = FALSE"
Param="PROJECT_TYPE = CSPROJ"

The VsWizard.VsWizardEngine.7.1 is the ProgId (yes, back to COM) of the VS.NET-provided IDTWizard implementation - which saves me the trouble of having to create my own wizard - I'll just hijack the plane already in the runway as it gets me off the ground faster.  In fact, that's the recommended practice in this case.

2)  In the \CSharpProjectItems\WebProjectItems folder create a DevstoneWebProjectItems.vsdir file.  The contents of the .vsdir file:

..\CSharpAddDevTrackerWebFormWiz.vsz|{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}|DevTracker WebForm|1|A webform for the Devstone DevTracker Website|{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}|4534|0|DevTrackerWebForm.aspx

For a description on the format of the .vsdir files, check out this link.  Basically, each line in the file is pipe (|) delimited.  Suffice it to say, the first parameter references the .vsz file we created.  The second param (the GUID) identifies the C# extensibility library (also used later as an identifier for the dll so we can extract the same icon as used for WebForms).  The third parameter is the short description that appears along with the icon.  The fourth parameter is the ordinal position of the icon within the 'Add Project Item' dialog.  Then we have the long description, the GUID, the icon resource identifier, a zero ;-), and the default name to use for the to-be-added file.

3)  Navigate to the \CSharpProjectItems\WebProjectItems\UI folder and create a DevstoneUI.vsdir file.  I chose to do this rather than edit the extant ui.vsdir so I can place all of my custom project items therein and will be able to copy my directory tree over the C# folder and not run the risk of overwriting any files.  It's great that the VS.NET IDE automatically picks up all .vsdir files rather than looking for specific ones!  The contents of the .vsdir file:

..\..\CSharpAddDevTrackerWebFormWiz.vsz|{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}|DevTracker WebForm|1|A webform for the Devstone DevTracker Website|{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}|4534|0|DevTrackerWebForm.aspx

You'll probably see a lot of similarities to the previously created file in #2.

4)  Within the \VC#Wizards directory, create a subdirectory called CSharpAddDevTrackerWebFormWiz.  This name should match what you entered in the .vsz file in step #1 as the WIZARD_NAME.

5)  It is within this new folder that the script file and templates get created - well, in subfolders.  The easiest thing to do is to copy files that already exist (such as those from the CSharpAddWebFormWiz folder).  Then you should have \Scripts\1033 and \Templates\1033 folders with some files.

6)  Feel free to delete the Templates.inf file from \Templates\1033 - it's not needed.  In fact it will screw you up when overriding VS.NET's default behavior of auto-generating the code-behind files.

7)  Edit your WebForm1.aspx file as necessary (I renamed mine to DevTrackerWebForm1.aspx).

8)  Now it's time to create your code-behind file.  The best thing to do is to copy the NewWebFormCode.cs mentioned above and rename it (e.g. DevTrackerWebForm1.aspx.cs).  At this point you're not done.

9)  The \Scripts\1033\default.js file is the code that drives (sets up) the process of replacing [!output] tags in your source.  In order to override the code-behind behavior we have to edit this file.  That's because the default.js file provided for WebForms is written to handle just one file.  The underlying functionality behind all default.js files is actually found within the script \VC#Wizards\1033\common.js.  Fortunately for me :-D, I didn't have to scour this too much.  I found a great article by Chris Sells that identifies how to do what I've already talked about to some depth.  I took his default.js file and tweaked it to match my needs and I'm now ready to go.

All in all, it's not that bad, but it doesn't seem too well documented and can be a bit disconcerting to a newcomer to the template, dang!, project item arena.

Wednesday, August 04, 2004 7:01:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Monday, August 02, 2004

A few days ago, I posted a blog entry mentioning my foray into SCM systems.  The purpose of the investigation was two-fold: 1) to move away from VSS (Sourcesafe) and 2) to enable remote source code access.  I work with a client out in California routinely and using VSS is impractical in this situation.  It didn't make sense not being able to sync up our code every couple weeks when I went to Santa Barbara, and then when he travelled here to SLC not having a SCM...well, let's just say that it hampered our work.  Yes, we found ways to work around the system and we've been pretty effective, but it became too painful, plain and simple.

We considered using SOS (Source Offsite).  SOS runs as an agent over a VSS repository, TCP/IP enabling it.  This would be great - except that we'd still be using VSS which would have violated goal #1.  At my previous employment we used SOS in maintaining source code and sharing it with our remote client site.  It worked well, but VERY slow.  Now, if I'm not mistaken, the server was patched, rebuilt, or something and the perf was noticeably better.

Of the SCM systems we were investigating, the two that stood out the most were SourceGear's Vault and PerForce.

I've heard nothing but good about PerForce.  There seems to be a very strong community backing.  I downloaded it and started to play with it.  To be honest, I really liked it, though it was at first a bit confusing.  I attribute the confusion to my VSS background.  In PerForce the terminology is quite different from VSS (e.g. 'Open for Edit' vs 'Check Out', 'Submit' vs 'Check In'), but I found a terminology differences document and that helped substantially.  Once you look at it and think about it, though, it's really not that bad.  After installing a Visual Studio plug in, I was up and going.  I was very impressed with the speed and ease of use of PerForce (even though I was running it on a Virtual PC).

Vault, on the other hand, is a relative newcomer to the SCM world.  Written purely in C#, and backed to SQL Server, this was an intriguing offering.  After downloading the single-user trial (free BTW) and playing with it I fell in love with it.  Perhaps the biggest advantage to my getting up and running here was that it was built to look and feel like VSS.

We decided to go with Vault as our SCM system for the following reasons.  PerForce offers many of the same capabilities, so this list isn't a set of “Vault has these and PerForce doesn't“ features.

  • Familiarity (no learning curve)
  • TCP/IP capabilities
  • Transactional check-ins
  • Simplicity to install
  • The backing database automatically incorporates itself into my database backup maintenance plan
  • Price ($)
  • The ability to turn off Vault being my primary SCM (I didn't see this anywhere in PerForce).  That way, I can still fall back and use VSS for my own and legacy projects - very important!
Monday, August 02, 2004 4:03:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Wednesday, July 28, 2004

No doubt if you dabbled around in .NET you've come across an attribute in the System namespace called CLSCompliantAttribute (CLSCompliant for short). If not, try running FxCop against your code once in a while - you'll see it real quick. The CLS in this context doesn't mean 'CLear Screen'. Instead it is short for Common Language Specification.

What is the Common Language Specification you ask? Well, in brief, the CLS helps ensure interoperability between languages by defining basic language features and rules; it is a subset of the Common Type System (CTS). Code that is CLS-compliant is guaranteed to be accessible from another CLS-supporting programming language. This level of support and interoperability is fantastic.

If, when writing code you conform to the CLS rules, you forget to add the CLSCompliantAttribute to your assembly, your assembly is NOT compliant. However, if you take the time and effort to add the attribute to your assembly you enter a world where you must now ensure compliance in all types, or at least explicitly identify those that aren't. The same holds true for a type. If you mark a type (i.e. class, struct, interfaces, etc) as CLS-compliant, you must mark all non-CLS-compliant members. Alternatively, you could fix up your code to be CLS-compliant; there is usually a CLS-compliant mechanism that provides the same capabilities.  Types that derive from non-compliant types are inherently not compliant and should therefore also be marked as such.

All you need to do to apply the attribute to your assembly is the following:

using System;
[assembly: CLSCompliant(true)]

As a best practice, I highly encourage you to add this attribute. It will definitely facilitate reuse of your components, and causes the C# compiler to check for compliance in all types.

But what are some of the rules associated with CLS compliance? What constitutes a non-compliant entity? Let's check it out.

   1. Naming conventions: protected or public identifiers must begin with a letter. For example, you can't begin an identifier with an underscore.

   2. Uniqueness of names: in order for a type to be CLS-compliant, it cannot contain members that differ in name solely by case. For example, the following is invalid:

class MyClass {
   protected int value;
   public int Value;
}

   Some languages (such as C# support this), but it's not CLS-compliant, so an alternative might be the following (ok, I know, it's a lame example):

class MyClass {
   protected int val;
   public int Value;
}

   3. Data types: variables of the unsigned varieties (e.g. uint, ulong, etc) are not CLS-compliant. If you expose an unsigned integer from a protected or public member, your type will not be compliant. Instead, consider using the signed variety.

I don't pretend to be exhaustive with this short set of rules, but these are the ones with which I believe most developers will run into trouble. For a more comprehensive view, check this out.

Perhaps it's important to mention that these rules (for the most part) apply only to protected and public members; these are the members that other CLS-compliant langauges will be using to interact with the objects.  You can do what you want in private fields, variables, methods, types, etc.

Wednesday, July 28, 2004 4:53:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Monday, July 26, 2004

Over the years I've been a user of Microsoft's SourceSafe.  Mostly this is because the tool was there and was free (well, it was part of the Visual Studio family of tools).  While for the most part this experiece has been a good one, I have had my fair share of mishaps and issues (mostly regarding web applications).  I'm not going to take the time to delve into these issues as they are incredibly frustrating and I'd spend the time venting, which is not what this blog is about.  I am wanting to get away from VSS for a variety of reasons and am looking into other solutions.

I'm in the process of evaluating other SCM packages and was wondering what your experiences have been with each.  I'm looking for constructive, decision-making feedback.  I'm in the process of evaluating Sourcegear's Vault, and was wondering if anyone has had any experiences (good or bad) with it.

1) VSS
2) Source Depot
3) Perforce
4) CVS
5) Vault
6) Other...

Monday, July 26, 2004 9:51:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [4]  |  Trackback