Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

Friday, August 12, 2011

HttpPostedFile.FileName Beware…

Browser issues – you’ve gotta love them… 

Here’s the scenario…

We have an application that allows users to upload spreadsheets.  We parse the data in the spreadsheets to create new spreadsheets, save input to a data store, and a bunch of other useful functions.  Once the application was deployed, we started to get bug/issue reports on how the uploads were failing.  Typical user feedback: ‘…the Web site isn’t working;’ ‘…the uploading my database feature is broken;’ ‘…why can’t the Web site do it like Facebook?’  You get the idea…
We narrowed it down to an IE issue.  Most of the users were using IE8, while the developers were using Chrome, Firefox, and IE9.  Of course, the file upload and resulting file parsing was working fine for the developers and testers.  Why aren’t the users using Chrome, Firefox, IE9, Safari, etc…?  Well, because they are users Open-mouthed smile  Seriously though, the client’s organization is using IE8, so that’s what we should have used when developing and testing…

Continuing on…We couldn’t replicate the issue using IE9 in IE8 or IE7 mode; however, the bug/issue reports kept piling in…

Luckily, someone had an old VM with XP and IE8 installed.  So, we did a quick test using IE8, and sure enough, the ugly bug revealed itself.  Here’s a screenshot of what the YSOD could possibly look like (the paths and other revealing info is modified here, so the need for ‘possibly’).


Beyond the browser issue frustration, we couldn’t walk the code with VS2010, because the XP/IE8 VM did not have VS2010 installed.

What it all narrowed down to was how browsers offer up the file name (via HttpPostedFile.FileName property). All browsers (Chrome, FF, IE9, ...) but IE8- (IE8, 7, 6, ...) report ONLY the file name, while IE8- reports the fully qualified path on the client’s machine. The Fully Qualified Path (FQP) is the key here. Also, in IE9 in IE8 mode, the HttpPostedFile.FileName reports what IE9 would report and not IE8 (nice!).

The following is a snippet of the existing code (attachmentPath is the directory where the file will be saved to on the Server):

var fileName = Request.Files[i].FileName;
var filepath = Path.Combine(attachmentPath, fileName);

Do you see the issue? Well, when testing in all browsers but IE8-, this will work fine. However, in IE8-, the aforementioned snippet will result with a filepath of something like the following:

c:\directoryToStoreUploadedFiles\C:\Documents and Settings\xyz123\Desktop\ie8_testing.txt

while we are expecting the following:

c:\directoryToStoreUploadedFiles\ie8_testing.txt

So, make sure to parse out the file name correctly using the System.IO.Path.GetFileName() method. This method will return ONLY to file name (removing the directory path if it pre-pended).

The revised code snippet will work for all browsers:

var postedFile = Request.Files[i];
var fileName = Path.GetFileName(postedFile.FileName);
var filepath = Path.Combine(attachmentPath, fileName);

Now, we will always get what we are expecting.

If we would have looked at the MSDN documentation for the HttpPostedFile class (who does that??) - HttpPostedFile.FileName – we would have found the following documentation for the FileName property:

HttpPostedFile.FileName Property

Gets the fully qualified name of the file on the client.

Of course it does!?!? Not when the user is using Chrome, Firefox, IE9, or probably any other browser.

Simple solution, yet very difficult to track down. Something you may want to file away in your toolbox...

Thanks for reading…

Monday, February 7, 2011

MvcContrib Menu

Demo | Source

This page is a placeholder for now; however, I intent to create a post that describes the use of my own version of the MvcContrib Menu Helper.

For now, you can see a working demo by using the demo link.  Once I have the source code cleaned up a bit, I'll post the source code.

Below is a quick snippet of the code used to generate the main menu in the demo. Notice the fluent style and the use of Razor Templates. BTW, sorry for the poor color-code - Razor doesn't format well using SyntaxHighligher.

@Html.MvcContrib().Menu().Items(menu => {
    menu.Action<HomeController>(c => c.Index(), "no text displayed", Url.Content("~/Content/ico/house.png"))
        .ItemAttributes(@class => "solo").DisplayText(false); 
           
    menu.Link("About", null, Url.Content("~/Content/ico/application_side_boxes.png")).Items(sub => {
        sub.Content(
            @:@Html.Partial("_MvcContribLogo")    
        );
    }).ListAttributes(style => "width: 450px;", @class => "sf-shadow-off");
    
    menu.Link("Secure", null, Url.Content("~/Content/ico/lock_open.png")).Items(sub => {
        sub.Secure<HomeController>(c => c.Index(), null, Url.Content("~/Content/ico/application_view_tile.png"));
        sub.Secure<HomeController>(c => c.About(), null, Url.Content("~/Content/ico/info2.png"));
        sub.Secure<HomeController>(c => c.SecurePageOne(), null, Url.Content("~/Content/ico/shield.png"));
        sub.Secure<HomeController>(c => c.SecurePageTwo(), null, Url.Content("~/Content/ico/shield_go.png"));
    });
    
    menu.Link("Insecure", null, Url.Content("~/Content/ico/lock.png")).Items(sub => {
        sub.Action<HomeController>(c => c.Index(), null, Url.Content("~/Content/ico/application_view_tile.png"));
        sub.Action<HomeController>(c => c.About(), null, Url.Content("~/Content/ico/info2.png"));
        sub.Action<HomeController>(c => c.SecurePageOne(), null, Url.Content("~/Content/ico/shield.png"));
        sub.Action<HomeController>(c => c.SecurePageTwo(), null, Url.Content("~/Content/ico/shield_go.png"));
    });
    
    menu.Action<MenuController>(c => c.Index(), "Menu Examples", Url.Content("~/Content/ico/house_go.png"));
})

Demo | Source

Saturday, January 8, 2011

Themed & Ajaxified MVCContrib Grid - with page size dropdown

Demo | Source

MVCContrib Grid.

Okay, that's a little biased; however, within a couple of hours of toying around with the MVCContrib Grid, I was sold.  I won't go into all the functionality provided by the grid - you can read about it here, read a more detailed writeup on Jeremy Skinner's blog here, and there's a nice writeup on the use of the grid here. What I really like about the MVCContrib Grid is the fluent code design and GridModels.  These two items are reviewed  on the MVCContrib Grid page and Jeremy's posts.

I'm kind of a demanding guy that is rarely satisfied.  And while the MVCContrib Grid is sweet, I still wanted more.  What are my demands?  I use 'demands' lightly, but many people I know would disagree... I digress.  Here they are:
  • Themeable via jQuery UI Themes
  • AJAX enabled
  • Page Size enabled,
  • Easy to use, and
  • Easy to apply to an existing MVCContrib Grid implementation.
The AJAX purests out there will disagree with my implementation of the AJAX functionality.  I call my implementation 'poor man's AJAX.'  I make the request and pass back markup.  Why?  Referring back to my list of 'demands'... the last two bullets are very important.  I don't want fellow developers to have to learn something new - Microsoft .NET related technologies are moving at a rapid pace - there's already enough to learn - I'm writing this post on a Saturday - go away... ;-)  I want the developers to make maybe a handful of simple changes to existing code and bam - it's done.

My implementation includes three parts:
  1. C# Class Library.  A class that overrides the HtmlTableGridRenderer (HtmlTableGridThemedRenderer), a class that defines a new Pager called PagerThemed, and a couple new HTML Helper extensions.
  2. A jQuery Plugin - mvccontribGrid.  This plugin handles the AJAX requests and some grid stylization. This plugin works like any jQuery plugin.  You pass a selector and a few options and the grid's good-to-go.
  3. CSS.  Simple CSS classes that provide the grid-specific styles.
In order to use the grid, you will also need jQuery and a jQuery Theme.

A little word on design.  I originally put all the CSS classes and selectors markup in the C# code; however, those markup features started to weigh down the AJAX response, so I moved CSS classes and selector additions to the plugin.  The HtmlTableGridThemedRenderer has a method called LighenUp.  This method sets an internal field that will either reduce (or not) the aforementioned CSS classes and selectors on the server-side.  The default it true - meaning the CSS classes and selectors will be rendered via the jQuery plugin - client-side.

The only part of my implementation that 'smells' a bit it the requirement to have a View and a PartialView that serve almost the same function.  Why does it smell?  It violates DRY.  However, I couldn't figure out another way to make it work without redesigning the grid control - that was not an option - I've got client work to attend to folks... :-)

Mentioning DRY, rather than cut and paste from the client (the MVC app) implementation and put it in this post, you can download the whole source code (with demo code) here.  The demo code is easy to follow.  Plus, I don't expend anyone to even read this post unless they have some developer knowledge and can easily figure out the simple logic.  Note that the source code includes a bunch of GAC assemblies copied as local references.  Since I designed the code using and for Razor (MVC 3), the GAC assemblies copied as local references was required for me to push the demo to my host - original development was MVC 3 RC2.  You can simply remove the local assemblies and replace with you GAC assemblies.

Oh yeah, almost forgot - take a look at the demo here and play around with it.  Notice that the demo include the jQuery themeroller, so you can place around with the various themes available from jQuery UI.

Thanks for reading...

References:

Demo | Source

Wednesday, December 29, 2010

ASP.NET MVC 3 Custom Validation

Data annotations Rock!  Using data annotations on your model classes make data validation (client and server) trivial.  Just add the data annotation to you model class, wire up an edit/create view to your controller via the 'Add View' wizard, and you have client- and server-side validation with very little effort.

A little disclaimer here - the implementation below it not MVC 3 specific; however, I developed the solution using MVC 3, tested using MVC 3, and it works using MVC 3.  I'm pretty sure it will work as-is using MVC 2.

What if you need validation that is not provided via data annotations out-of-box?  You create your own.  That's what this post is all about.

Creating your own validators that support client- and server-side validation is essentially a four-step process:
  1. Create a custom attribute that extends the ValidationAttribute, or better yet, you extend one of the existing data annotations.
  2. Create a custom validator that extends the DataAnnotationsModelValidator, where T is the type of custom attribute you created in step one.
  3. Create a client-side script to handle the client-side validation.
  4. Register the attribute/validator classes in your app's bootstrapper or Global.asax.
Side note: It seems that every time I have a good idea for a post, Phil Haack has beat me to the punch... and this pattern is also true in this scenario :-) For a similar article on the same subject, please see Phil's ASP.NET MVC 2 Custom Validation post.

This post is very code centric; therefore, if the concepts are unfamiliar or a brushing up is necessary, please take a look at Phil's post.  Also, Brad Wilson has a few different blog posts/series on Data Annotations and ModelMetadata.  Brad and Phil's knowledge of the ins and outs of anything ASP.NET MVC (and arguably C#) scares me...

The custom attribute and validator that I'm going to create is an Email validator.  There are plenty examples on the net, including here, here, etc...  What these email validator solutions don't provide is client-side validation.  I will show you how.  So, lets start with step one:

1. Create a Custom Attribute
Below are two solutions.  They both result in the same functionality.  The first solution is a full implementation that extends the ValidationAttribute class, while the second solution extends the RegularExpressionAttribute.

EmailAttribute Extending Validation Attribute
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Text.RegularExpressions;

namespace Validation
{
    /// <summary>
    /// Email Attribute class used for email validation. Used similar to the System.ComponentModel.DataAnnotations.RegularExpressionAttribute.
    /// </summary>
    public class EmailAttribute : ValidationAttribute
    {
        #region Properties

        /// <summary>
        /// Gets or sets the Regular expression.
        /// </summary>
        /// <value>The regex.</value>
        private Regex Regex { get; set; }

        /// <summary>
        /// Gets the pattern used for email validation.
        /// </summary>
        /// <value>The pattern used for email validation.</value>
        /// <remarks>
        /// Regular Expression Source - Comparing E-mail Address Validating Regular Expressions
        /// <see cref="http://fightingforalostcause.net/misc/2006/compare-email-regex.php"/>
        /// </remarks>
        public string Pattern
        {
            get
            {
                return
                    @"^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-zA-Z0-9]{1}[a-zA-Z0-9\-]{0,62}[a-zA-Z0-9]{1})|[a-zA-Z])\.)+[a-zA-Z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$";
            }
        }

        #endregion Properties

        #region Ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="EmailAttribute"/> class.
        /// </summary>
        public EmailAttribute()
        {
            this.Regex = new Regex(this.Pattern);
        }

        #endregion Ctors

        /// <summary>
        /// Determines whether the specified value of the object is valid.
        /// </summary>
        /// <param name="value">The value of the object to validate.</param>
        /// <returns>
        /// true if the specified value is valid; otherwise, false.
        /// </returns>
        public override bool IsValid(object value)
        {
            // convert the value to a string
            var stringValue = Convert.ToString(value, CultureInfo.CurrentCulture);

            // automatically pass if value is null or empty. RequiredAttribute should be used to assert an empty value.
            if (string.IsNullOrWhiteSpace(stringValue)) return true;

            var m = Regex.Match(stringValue);

            // looking for an exact match, not just a search hit.
            return (m.Success && (m.Index == 0) && (m.Length == stringValue.Length));
        }
    }
}

EmailAttribute Extending RegularExpressionAttribute
using System.ComponentModel.DataAnnotations;

namespace Validation
{
    /// <summary>
    /// Email Attribute class used for email validation. Used similar to the System.ComponentModel.DataAnnotations.RegularExpressionAttribute.
    /// </summary>
    public class EmailAttribute : RegularExpressionAttribute
    {
        #region Ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="EmailAttribute"/> class.
        /// </summary>
        public EmailAttribute()
            : base(
                @"^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-zA-Z0-9]{1}[a-zA-Z0-9\-]{0,62}[a-zA-Z0-9]{1})|[a-zA-Z])\.)+[a-zA-Z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$"
                )
        {
        }

        #endregion Ctors
    }
}
ddd
Again, whatever implementation you choose is up to you.  Both implementations have the same resulting functionality - Email validation via data annotations.

2. Create a custom validator that extends the DataAnnotationsModelValidator
This class - the validator class - is the class that provides metadata to enable client-side validation.  This class essentially transfers settings, in our case the ErrorMessage and Pattern properties of the EmailAttribute class, from the attribute decorations on your model object to the client-side consumer.  The client-side consumer is what provides the client-side validation.

using System.Collections.Generic;
using System.Web.Mvc;
using Chinook.Framework.Validation;

namespace Chinook.Web.Core.Validation
{
    /// <summary>
    /// Provides a model validator for the EmailAttribute annotation.
    /// </summary>
    public class EmailValidator : DataAnnotationsModelValidator<EmailAttribute>
    {
        #region Fields

        private readonly string _errorMessage;
        private readonly string _pattern;

        #endregion Fields

        #region Ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="EmailValidator"/> class.
        /// </summary>
        /// <param name="metadata">The metadata.</param>
        /// <param name="context">The context.</param>
        /// <param name="attribute">The attribute.</param>
        public EmailValidator(ModelMetadata metadata, ControllerContext context, EmailAttribute attribute)
            : base(metadata, context, attribute)
        {
            this._errorMessage = attribute.ErrorMessage;
            this._pattern = attribute.Pattern;
        }

        #endregion Ctors

        #region Methods

        /// <summary>
        /// Retrieves a collection of client validation rules.
        /// </summary>
        /// <returns>A collection of client validation rules.</returns>
        public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
        {
            var rule = new ModelClientValidationRegexRule(this._errorMessage, this._pattern);
            return new[] {rule};
        }

        #endregion Methods
    }
}
So now that we have the attribute and validator classes created, we need to...

3. Create a client-side script to handle the client-side validation
I feel like I'm cheating on you here.  Out scenario - email validation - and it's implementation do not require us to write client-side script.  I know, I know... if you feel robbed, you can create your own client-side script, and register is with the jQuery Validate plugin.  I'll try and create a future post will another validator that requires writing client-side script and jQuery Validate registration, but for now we going to enjoy the luxuries of extending and using existing functionality.  But, HOW do we get away with using existing client-side script?

Take a look at the following code snippet from step two:
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
    var rule = new ModelClientValidationRegexRule(this._errorMessage, this._pattern);
    return new[] {rule};
}

This method 'retrieves' a collection of client validation rules.'  And since we are essentially using a RegularExpressionAttribute class (via extension), we can use the client validation rules used by the RegularExpressionAttribute's Model Validator class via the ModelClientValidationRegexRule passing our Error Message and Regular Expression pattern.

This bring us to our last step...

4. Register the attribute/validator classes in your app's bootstrapper or Global.asax
To make all this sweetness happen, we need to register the attribute and validator classes when the app domain kicks off its like cycle.  In your Global.asax's Application_Start method, register the attribue and validator classes using the following code snippet:
// register custom model validators
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(EmailAttribute), typeof(EmailValidator));
To use the attribute and validator classes use just created, you need to decorate you model/POCO classes with data annotations, including your EmailAttribute class.  The following shows how the EmailAttribute is used (along with other data annotations) to decorated a property in your model class:
[Display(Name = "Email")]
[Required(ErrorMessage = "Email is Required.")]
[Email(ErrorMessage = "Not a valid Email Address.")]
[StringLength(60, ErrorMessage = "Email must be under 60 characters.")] 
public string Email { get; set; }

Once you create strongly typed Edit and/or Create Views using your models, include the script libraries, and run the application, you will see the fruits of your labor and get client-side validation.  The screenshot below is an example.

Anyway, I hope you got a little something out of this post.

Thanks for reading...

Tuesday, August 17, 2010

C# Delegates

Nice post on Action, Predicate, and Func delegates: Cool Delegates

Quick Reference:
/*
* Action -    
*    o accepts zero or up to four parameters and returns void
*    o Action                         :: public delegate void DelegateName()
*    o Action<T>                      :: public delegate void DelegateName(T)
*    o Action<T1, T2>                 :: public delegate void DelegateName(T1, T2)
*    o Action<T1, T2, T3>             :: public delegate void DelegateName(T1, T2, T3)
*    o Action<T1, T2, T3, T4>         :: public delegate void DelegateName(T1, T2, T3, T4)
* 
* Predicate - 
*    o accepts a single parameter and returns a bool  
*    o Predicate<T>                   :: public delegate bool DelegateName(T)
*     
* Func -
*    o acepts zero or up to four parameters and returns any type (TResult)
*    o Func<TResult>                  :: public delegate <TResult> DelegateName()
*    o Func<T, TResult>               :: public delegate <TResult> DelegateName(T1)
*    o Func<T1, T2, TResult>          :: public delegate <TResult> DelegateName(T1, T2)
*    o Func<T1, T2, T3, TResult>      :: public delegate <TResult> DelegateName(T1, T2, T3)
*    o Func<T1, T2, T3, T4, TResult>  :: public delegate <TResult> DelegateName(T1, T2, T3, T4)
* 
*/

System.Environment - EnvironmentList Utility Class

I'm traditionally a Web developer; however, lately I've found myself doing a bunch of console work to help automate various tasks (including work flow, file and report merging, moving builds to various environments, etc...).  I've come to embrace, among many, the System.IO.Path, System.Environment, and System.Diagnostics.Process classes.  These classes help manipulate file and directory paths, provide information about means of manipulating the current environment, and access to starting and stopping local services.

I often find myself investigating these class and their members and forgetting exactly what the member does.  I'll locate a property or method that sounds like and is described (via comments) as something I would like to use; however, the comments are short and sweet and I must have a dozen questions pop in my mind.  Note to self - create a framework for interactive library documentation .  I digress... I'll typically create a quick console app an determine if the respective member is what I really need.  This takes time - every time!  So, I came up with a utility class...

The following is a informational utility class that builds a collection of key: value pair of the current environment.  Fundamentally, all that this class does is abstract and enumerate the various members of the System.Environment class, puts the results into a list of key: value pairs, and provides a property that exposes the results.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;

/// <summary>
/// Environment List Class.
/// </summary>
public partial class EnvironmentList
{
   #region Fields
   #endregion Fields

   #region Properties
   /// <summary>
   /// Gets the Environment Key: Value collection.
   /// </summary>
   /// <value>The items.</value>
   public List<KeyValuePair<string, string>> Items { get; private set; }
   #endregion Properties

   #region Methods
   /// <summary>
   /// Builds the Environment dictionary/list.
   /// </summary>
   private void BuildDictionary()
   {
      this.Items.Add(new KeyValuePair<string, string>(ReflectionUtility.GetPropertyName(() => Environment.SystemDirectory), Environment.SystemDirectory));
      this.Items.Add(new KeyValuePair<string, string>(ReflectionUtility.GetPropertyName(() => Environment.CurrentDirectory), Environment.CurrentDirectory));
      this.Items.Add(new KeyValuePair<string, string>(ReflectionUtility.GetPropertyName(() => Environment.MachineName), Environment.MachineName));
      this.Items.Add(new KeyValuePair<string, string>(ReflectionUtility.GetPropertyName(() => Environment.OSVersion), Environment.OSVersion.ToString()));
      this.Items.Add(new KeyValuePair<string, string>(ReflectionUtility.GetPropertyName(() => Environment.Version), Environment.Version.ToString()));
      this.Items.Add(new KeyValuePair<string, string>(ReflectionUtility.GetPropertyName(() => Environment.UserName), Environment.UserName));
      this.Items.Add(new KeyValuePair<string, string>(ReflectionUtility.GetPropertyName(() => Environment.UserDomainName), Environment.UserDomainName));
      this.Items.Add(new KeyValuePair<string, string>(ReflectionUtility.GetPropertyName(() => Environment.ProcessorCount), Environment.ProcessorCount.ToString()));

      foreach (string name in Enum.GetNames(typeof(Environment.SpecialFolder)))
         this.Items.Add(new KeyValuePair<string, string>(name, Environment.GetFolderPath((Environment.SpecialFolder)Enum.Parse(typeof(Environment.SpecialFolder), name))));

      this.Items.Add(new KeyValuePair<string, string>("Logical Drives", string.Join("; ", Environment.GetLogicalDrives())));

      ParseEnvironmentVariables(EnvironmentVariableTarget.Machine);
      ParseEnvironmentVariables(EnvironmentVariableTarget.Process);
      ParseEnvironmentVariables(EnvironmentVariableTarget.User);
   }

   /// <summary>
   /// Parses the environment variables in the context of @target.
   /// </summary>
   /// <param name="target">The target.</param>
   private void ParseEnvironmentVariables(EnvironmentVariableTarget target)
   {
      foreach (DictionaryEntry item in Environment.GetEnvironmentVariables(target))
      {
         this.Items.Add(new KeyValuePair<string, string>(item.Key.ToString(), item.Value.ToString()));
      }
   }
   #endregion Methods

   #region Event Handlers
   #endregion Event Handlers

   #region Ctors
   /// <summary>
   /// Initializes a new instance of the <see cref="EnvironmentList"/> class.
   /// </summary>
   public EnvironmentList()
   {
      this.Items = new List<KeyValuePair<string, string>>();
      this.BuildDictionary();
   }
   #endregion Ctors
}

/// <summary>
/// Reflection Utility Class
/// <see cref="http://handcraftsman.wordpress.com/2008/11/11/how-to-get-c-property-names-without-magic-strings/"/>
/// </summary>
public static class ReflectionUtility
{
   /// <summary>
   /// Gets the name of the property.
   /// </summary>
   /// <typeparam name="T"></typeparam>
   /// <param name="expression">The expression.</param>
   /// <returns></returns>
   public static string GetPropertyName<T>(Expression<Func<T>> expression)
   {
      MemberExpression body = (MemberExpression)expression.Body;
      return body.Member.Name;
   }
}
To use the class, instantiate the EnvironmentList object and enumerate the Items property.

Console:
EnvironmentList el = new EnvironmentList();
Array.ForEach(el.Items.ToArray(), i => Console.Out.WriteLine("{0}: {1}{2}", i.Key, i.Value, Environment.NewLine));
Web:

Add a GridView to the markup and bind the grid on page load:
<asp:GridView runat="server" id="gv"/>
EnvironmentList el = new EnvironmentList();
this.gv.DataSource = el.Items;
this.gv.DataBind();
Nothing Earth shattering here, but a nice utility if you need to view the properties of the System.Environment class for the current environment.

Thanks for reading...