Click here to Skip to main content
Click here to Skip to main content

Attributes-based Validation in a WPF MVVM Application

By , 28 Jul 2010
 

Introduction

In this article, I'm sharing a simple MVVM application with validation using an attribute-based approach. Basically, it means that you'll be able to describe validation rules using this syntax:

[Required(ErrorMessage = "Field 'FirstName' is required.")]
public string FirstName
{
    get
    {
        return this.firstName;
    }
    set
    {
        this.firstName = value;
        this.OnPropertyChanged("FirstName");
    }
}

Here is a picture of the demo application running:

The UI requirements for the demo application are:

  • Fill various information in 3 different tabs and provide error meaningful messages and feedback to the user when a field is incorrect:

  • Give real time feedback about the completeness of the filling process using 3 approaches:
    • When a tab is fully completed, it goes from red to green:

    • The progress is shown to the user using a progress bar:

    • When everything is filled, the Save button is activated:

Background

WPF provides several techniques in order to validate what the user enters in an application.

ValidationRules

From the MSDN article on ValidationRules:

When you use the WPF data binding model, you can associate ValidationRules with your binding object. To create custom rules, make a subclass of this class and implement the Validate method. The binding engine checks each ValidationRule that is associated with a binding every time it transfers an input value, which is the binding target property value, to the binding source property.

IDataErrorInfo

From the original blog post of Marianor about using attributes with IDataErrorInfo interface:

WPF provides another validation infrastructure for binding scenarios through IDataErrorInfo interface. Basically, you have to implement the Item[columnName] property putting the validation logic for each property requiring validation. From XAML, you need to set ValidatesOnDataErrors to true and decide when you want the binding invoke the validation logic (through UpdateSourceTrigger).

Combining IDataErrorInfo and attributes

In this article, we use a technique which combines validation attributes (an explanation of each validation attribute is out of the scope of this article) and the IDataErrorInfo interface.

Overall Design

Here is the class diagram of the application’s classes:

There is nothing really new here. The MainWindow’s Content is set to the MainFormView view. The associated ViewModel, MainFormViewModel manages a set of FormViewModel which is the base ViewModel class for each tab in the View.

In order to specify to the WPF engine how to render a FormViewModelBase, all we need to do is to create a DataTemplate and gives the correct TargetType. For example, in order to associate the ProfileFormViewModel with the ProfileFormView, we create this DataTemplate:

<DataTemplate DataType="{x:Type ViewModel:ProfileFormViewModel}">
<View:ProfileFormView />
</DataTemplate>

Then the DataBinding does everything to populate the content of the TabControl using this very simple XAML:

<TabControl ItemsSource="{Binding Forms}" />

This is very similar to an approach I blogged a couple of months ago (see here for more details). The new stuff resides in the ValidationViewModelBase class which is the new base class I’m introducing for ViewModel classes which need to support validation. That’s the goal of the next section of this article.

Attribute-based Validation and the IDataError Interface

The solution I’m using here to combine attributes and IDataError interface is based on the work of Marianor (full article here). I tweaked a little bit of his code in order to have a generic solution (and that’s the goal of the ValidationViewModelBase class). The basic idea is to be able to implement the IDataErrorInfo interface (and its 2 properties) in a generic way using attributes in System.ComponentModel. The class uses extensively LINQ in order to perform the validation.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;

using WpfFormsWithValidationDemo.Toolkit.Behavior;

namespace WpfFormsWithValidationDemo.Toolkit.ViewModel
{
    /// <summary>
    /// A base class for ViewModel classes which supports validation 
    /// using IDataErrorInfo interface. Properties must defines
    /// validation rules by using validation attributes defined in 
    /// System.ComponentModel.DataAnnotations.
    /// </summary>
    public class ValidationViewModelBase : ViewModelBase, 
		IDataErrorInfo, IValidationExceptionHandler
    {
        private readonly Dictionary<string, 
		Func<ValidationViewModelBase, object>> propertyGetters;
        private readonly Dictionary<string, ValidationAttribute[]> validators;

        /// <summary>
        /// Gets the error message for the property with the given name.
        /// </summary>
        /// <param name="propertyName">Name of the property</param>
        public string this[string propertyName]
        {
            get
            {
                if (this.propertyGetters.ContainsKey(propertyName))
                {
                    var propertyValue = this.propertyGetters[propertyName](this);
                    var errorMessages = this.validators[propertyName]
                        .Where(v => !v.IsValid(propertyValue))
                        .Select(v => v.ErrorMessage).ToArray();

                    return string.Join(Environment.NewLine, errorMessages);
                }

                return string.Empty;
            }
        }

        /// <summary>
        /// Gets an error message indicating what is wrong with this object.
        /// </summary>
        public string Error
        {
            get
            {
                var errors = from validator in this.validators
                             from attribute in validator.Value
                             where !attribute.IsValid(this.propertyGetters
				[validator.Key](this))
                             select attribute.ErrorMessage;

                return string.Join(Environment.NewLine, errors.ToArray());
            }
        }

        /// <summary>
        /// Gets the number of properties which have a 
        /// validation attribute and are currently valid
        /// </summary>
        public int ValidPropertiesCount
        {
            get
            {
                var query = from validator in this.validators
                            where validator.Value.All(attribute => 
			  attribute.IsValid(this.propertyGetters[validator.Key](this)))
                            select validator;

                var count = query.Count() - this.validationExceptionCount;
                return count;
            }
        }

        /// <summary>
        /// Gets the number of properties which have a validation attribute
        /// </summary>
        public int TotalPropertiesWithValidationCount
        {
            get
            {
                return this.validators.Count();
            }
        }

        public ValidationViewModelBase()
        {
            this.validators = this.GetType()
                .GetProperties()
                .Where(p => this.GetValidations(p).Length != 0)
                .ToDictionary(p => p.Name, p => this.GetValidations(p));

            this.propertyGetters = this.GetType()
                .GetProperties()
                .Where(p => this.GetValidations(p).Length != 0)
                .ToDictionary(p => p.Name, p => this.GetValueGetter(p));
        }

        private ValidationAttribute[] GetValidations(PropertyInfo property)
        {
            return (ValidationAttribute[])property.GetCustomAttributes
			(typeof(ValidationAttribute), true);
        }

        private Func<ValidationViewModelBase, object> 
			GetValueGetter(PropertyInfo property)
        {
            return new Func<ValidationViewModelBase, object>
			(viewmodel => property.GetValue(viewmodel, null));
        }

        private int validationExceptionCount;

        public void ValidationExceptionsChanged(int count)
        {
            this.validationExceptionCount = count;
            this.OnPropertyChanged("ValidPropertiesCount");
        }
    }
}

Please note I’m also exposing the number of valid properties and the total number of properties with validation (this is used in order to compute the value of the progress bar). From the developer point of view, using this class in an existing ViewModel is very straightforward: Inherit from the new ValidationViewModelBase class instead of your traditional ViewModelBase class and then add validation attributes on the properties which requires validation.

Available Attributes (from the System.ComponentModel.DataAnnotations namespace)

Name Description
RequiredAttribute Specifies that a data field value is required
RangeAttribute Specifies the numeric range constraints for the value of a data field
StringLengthAttribute Specifies the minimum and maximum length of characters that are allowed in a data field
RegularExpressionAttribute Specifies that a data field value must match the specified regular expression
CustomValidationAttribute Specifies a custom validation method to call at run time (you must implement the IsValid() method)

Dealing with Validation Exceptions

As I was working with this new approach based on attributes, I faced a problem: how to deal with validation exception. A validation exception happens when the user input is incorrect, for example if a TextBox has its Text property to an int property, then an input like "abc" (which cannot be converted of course to an int) will raise an exception.

The approach I'm proposing is based on a behavior. A complete description of what behaviors are is out of the scope of this article. For a nice description, you can check out this article.

The behavior I'm proposing must be attached to the parent UI elements which contain the input controls that can raise exception. These are a couple of lines in the XAML:

 <Grid>
  <i:Interaction.Behaviors>
    <Behavior:ValidationExceptionBehavior />
  </i:Interaction.Behaviors>

  <!-- rest of the code... -->

When this behavior is loaded, it adds an handler for the ValidationError.ErrorEvent RoutedEvent so that it is notified each time a validation error occurs. When this happens, the behavior calls a method on the ViewModel (through a simple interface in order to limit coupling) so that the ViewModel can track the number of validation errors. Here is the code of the behavior:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace WpfFormsWithValidationDemo.Toolkit.Behavior
{
    /// <summary>
    /// A simple behavior that can transfer the number of 
    /// validation error with exceptions
    /// to a ViewModel which supports the INotifyValidationException interface
    /// </summary>
    public class ValidationExceptionBehavior : Behavior<FrameworkElement>
    {
        private int validationExceptionCount;

        protected override void OnAttached()
        {
            this.AssociatedObject.AddHandler(Validation.ErrorEvent, 
		new EventHandler<ValidationErrorEventArgs>(this.OnValidationError));
        }

        private void OnValidationError(object sender, ValidationErrorEventArgs e)
        {
            // we want to count only the validation error with an exception
            // other error are handled by using the attribute on the properties
            if (e.Error.Exception == null)
            {
                return;
            }

            if (e.Action == ValidationErrorEventAction.Added)
            {
                this.validationExceptionCount++;
            }
            else
            {
                this.validationExceptionCount--;
            }

            if (this.AssociatedObject.DataContext is IValidationExceptionHandler)
            {
                // transfer the information back to the viewmodel
                var viewModel = (IValidationExceptionHandler)
				this.AssociatedObject.DataContext;
                viewModel.ValidationExceptionsChanged(this.validationExceptionCount);
            }
        }
    }
}

Progress Reporting

One of my requirements was “When a tab is fully completed, it goes from red: to green: ”. In order to realize this particular feature, I added an “IsValid” property to the FormViewModelBase class (which is the base class for all ViewModels in the TabControl). This property is updated whenever a PropertyChanged occurs by looking if the Error property (of the IDataErrorInfo interface) is empty:

protected override void PropertyChangedCompleted(string propertyName)
{
    // test prevent infinite loop while settings IsValid
    // (which causes an PropertyChanged to be raised)
    if (propertyName != "IsValid")
    {
        // update the isValid status
        if (string.IsNullOrEmpty(this.Error) &&
        	this.ValidPropertiesCount == this.TotalPropertiesWithValidationCount)
        {
            this.IsValid = true;
        }
        else
        {
            this.IsValid = false;
        }
    }
}

Then a simple trigger in the XAML is enough to have the visual effect I described:

<Style TargetType="{x:Type TabItem}">
  <Setter Property="HeaderTemplate">
    <Setter.Value>
      <DataTemplate>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="{Binding FormName}" VerticalAlignment="Center" Margin="2" />
          <Image x:Name="image"
                 Height="16"
                 Width="16"
                 Margin="3"
                 Source="../Images/Ok16.png"/>
        </StackPanel>
        <DataTemplate.Triggers>
          <DataTrigger Binding="{Binding IsValid}" Value="False">
            <Setter TargetName="image" 
		Property="Source" Value="../Images/Warning16.png" />
          </DataTrigger>
        </DataTemplate.Triggers>
      </DataTemplate>
    </Setter.Value>
  </Setter>
</Style>

In order to have the progress bar working, I’m computing the overall progress in the parent ViewModel (the one which owns the various FormViewModelBase ViewModels):

/// <summary>
/// Gets a value indicating the overall progress (from 0 to 100) 
/// of filling the various properties of the forms.
/// </summary>
public double Progress
{
    get
    {
        double progress = 0.0;
        var formWithValidation = this.Forms.Where(f => 
		f.TotalPropertiesWithValidationCount != 0);
        var propertiesWithValidation = this.Forms.Sum(f => 
		f.TotalPropertiesWithValidationCount);

        foreach (var form in formWithValidation)
        {
            progress += (form.ValidPropertiesCount * 100.0) / propertiesWithValidation;
        }

        return progress;
    }
}

Points of Interest

The goal of this technique as I said in the introduction is to have a replacement for the traditional ValidationRules approach which complicates the XAML a lot. Using LINQ and Attributes is a nice way to implement this new possibility. While approaching the end of the writing of this article, I noticed that a similar solution is available in a famous MVVM frameworks made by Mark Smith (the MVVM Helpers).

Acknowledgment

I would like to profusely thank people who helped me to review this article: my co-worker Charlotte and Sacha Barber (CodeProject and Microsoft MVP).

History

  • 28th July 2010: Original version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Jeremy Alles
Software Developer (Junior)
France France
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5membermasdket10 May '13 - 21:16 
Really nice
GeneralExcellent Postmemberyannik steiger16 Jan '13 - 17:41 
Thanks for your excellent work. Thats what I was looking for! Best regards
GeneralRe: Excellent PostmemberJeremy Alles17 Jan '13 - 19:46 
Glad you like the post.
 
Cheers,
Jeremy
Generalmy vote of 5memberbahman aminipour6 Nov '12 - 6:16 
Excellent, thank you very much.
GeneralRe: my vote of 5memberJeremy Alles6 Nov '12 - 20:00 
Glad you liked it Smile | :)
QuestionErrorMessage issuememberGoran _18 Sep '12 - 1:11 
Hi Jerremy, thanks for this great code (5 from me). One thing I do not understand. If I remove ErrorMessage from Attribute declaration, validation errors would not occur at all. Why is this happening? I am using a localization that doesn't use resource files (for some specific requirements), so I use Custom attributes for validation, where I localize messages.
So, when I remove ErrorMessage, validation stops. Do you know why?
AnswerRe: ErrorMessage issuememberJeremy Alles18 Sep '12 - 2:16 
Hi Goran,
 
Not sure but I guess the string this[string propertyName] property returns an empty string if the ErrorMessage property is not set. In this case, the IDataErrorInfo might not works... Maybe you could try returns " " instead...
GeneralRe: ErrorMessage issuememberGoran _18 Sep '12 - 11:02 
I jjust needed to go with GetFormattedMessage instead of ErrorMessage, and it started working. THe last problem I face is that this does not work with reference properties that contain null as initial value. An example would be that I have a ComboBox bound to some list, and SelectedValue to the foreign key property for this reference. UI never gets infored about validation errors, because this[string PropertyName] never gets fired for reference property (probably because its null). Do you know any workaround for this problem?
GeneralRe: ErrorMessage issuememberGoran _19 Sep '12 - 13:11 
If anyone else faces this problem, the trick is to bind to SelectedItem property, instead of SelectedValue.
QuestionLinked to your Articlemembernazzyg11 Sep '12 - 10:49 
Thought those reading this article might find these other ones useful. http://blog.dotnetdude.net/2012/09/great-mvvm-wpf-soa-and-ef4-articles.html[^]
Question1) Dynamic controls? 2) Localization?memberClaire Streb14 Aug '12 - 15:35 
Hi,
 
I really, really want to use this, but I'm having difficulty with my dynamic controls and localization.
 
1) Dynamic controls are added programmatically (at runtime), so we don't know what they are at compile time. So we cannot, for example, have:
 
[Required(ErrorMessage = "The 'Age' field is required")]
[Range(1, 130, ErrorMessage = "The 'Age' field must be between 0 and 150 (inclusive)")]
public int Age
...
 
We have to add our attributes using C# instead of attribute decorators. I found that I can create a custom MetaDataProvider that inherits from DataAnnotationsModelValidatorProvider and can do this:
 
ModelValidatorProviders.Providers.Add(new DynamicValidatorProvider());
 
and then add the attributes like this (the example is for a different dynamic string control):
 
List<Attribute> newAttributes = new List<Attribute>(attributes); 
newAttributes.Add(new StringLengthAttribute(maxLength) { MinimumLength = 2, ErrorMessage = "Field: {0} must have a length between 2 and {1}" });.
 
but it looks like I'll have to reference System.Web.Mvc.ModelValidatorProviders or System.Web.ModelBinding.ModelValidatorProviders in my WPF solution, which doesn't seem right to me, and I don't even know if it will work. What do you (or anyone else who knows) suggest?
 
2) The ErrorMessage values in the attribute decorators are in English. How would I localize them?
 
Thanks in advance.
Claire Streb
http://clairenstreb.brinkster.net

AnswerRe: 1) Dynamic controls? 2) Localization?memberJeremy Alles24 Aug '12 - 23:46 
I'm not sure to understand what you mean by "dynamic control" ? Can you detail what you're trying to do ?
 
About the localization, I think an approach could be to set the ID of a resource in the ErrorMessage property of the attribute. Then insead of using the ErrorMessage direcly, just fetch the resource string before.
GeneralRe: 1) Dynamic controls? 2) Localization?memberClaire Streb25 Aug '12 - 7:38 
Hello Jeremy,
 
That's a good idea to use the ID for localization. It will definitely work.
 
Dynamic controls are added at runtime. We have a generic structure containing string data, and one of the attributes is data type. So for the Age example, the data type would be int. For this, we would dynamically (programatically) add a textbox in the code-behind, and one of the validations for the control would be to ensure only integers are entered. For another example, say our data type is DateTime, so we would programatically add a datepicker control. Therefore, we cannot use the attribute decorators like [Required...] and [Range...] for these dynamic controls.
 
I hope this is a better explanation and that you have another brilliant idea!
 
Thanks so much,
Claire
Claire Streb
http://clairenstreb.brinkster.net

QuestionCombobox validationmemberMember 450897825 Jun '12 - 2:46 
I am new to WPF MVVM. I am able to display edits for text boxes.However I am not able to display required filed messge for my combobox, by using this approach. If possible,please provide me reference.
AnswerRe: Combobox validationmemberJeremy Alles24 Aug '12 - 23:42 
I think it has to deal with the template of the combobox itself. Did you took a look at it ?
GeneralMy vote of 5memberMember 450897822 Jun '12 - 2:11 
Very nice..
GeneralMy vote of 5memberFlorian.Witteler25 Apr '12 - 7:47 
Take my five for this excellent article. It provides exactly these tiny bits of information, I was looking for.
SuggestionGreat solutionmemberrliviu17 Apr '12 - 14:28 
I was looking for some time a way to validate my model using validation attributes, same as in asp.net mvc.
Still the code needs some optimizations because the validations are called over and over again and can make the application feel unresponsive. For this issue I've used a dictionary that stores the validation messages, which are generated on initialization and in OnPropertyChanged() the messages are updated.
    public class ValidationViewModelBase : ViewModelBase, IDataErrorInfo, IValidationExceptionHandler
    {
        (...)
 
        private Dictionary<string, string[]> m_propertyErrorMessages;
        
        public string this[string propertyName]
        {
            get
            {
                if (m_propertyErrorMessages.ContainsKey(propertyName))
                    return string.Join(Environment.NewLine, m_propertyErrorMessages[propertyName]);
                return string.Empty;
            }
        }
        
        public string Error
        {
            get
            {
                return string.Join(Environment.NewLine,
                                   m_propertyErrorMessages.Where(err => err.Value.Length > 0)
                                                          .Select(err => string.Join(Environment.NewLine,
                                                                                   err.Value
                                                                                   )
                                                                )
                                                        .ToArray()
                                  );
            }
        }
      
        public int ValidPropertiesCount
        {
            get
            {
                int validProperties = m_propertyErrorMessages.Where(p => p.Value == null || p.Value.Length == 0).Count() - this.validationExceptionCount;
                return (validProperties < 0 ? 0 : validProperties);
            }
        }
 
        public int TotalPropertiesWithValidationCount
        {
            get
            {
                return this.m_propertyValidators.Count();
            }
        }
 
        public ValidationViewModelBase()
        {
            {...}
 
            this.m_propertyErrorMessages = this.m_propertyValidators.ToDictionary(v => v.Key, v => PropertyValidationErrors(v.Key));
        }
        private string[] PropertyValidationErrors(string i_propertyName)
        {
            if (m_propertyValidators.ContainsKey(i_propertyName))
            {
                return m_propertyValidators[i_propertyName]
                            .Where(v => !v.IsValid(m_propertyGetters[i_propertyName](this)))
                            .Select(va => va.ErrorMessage)
                            .ToArray();
            }
            return null;
        }
(...)
}
 

QuestionExcellent solution!memberRobert Brower1 Feb '12 - 7:05 
This is an excellent solution!
Robert

GeneralCompleted with hierarchical VM and voted 5memberJac5 Oct '11 - 0:54 
Great article. So helpful that I've based all my View Models on it.
 
However I've added a small thing that may have an interest for others.
 
I needed a hierarchical View Model so I added the followings to the ValidationViewModelBase:
//The parent VM of this VM
public ValidationViewModelBase ParentVM { get; protected set; }
 
//Override in a derived class that hold collections of derived ValidationViewModelBase  
protected virtual bool IsMyDataValid() { return true; }
The default implementation of IsMyDataValid always return "true", i.e. the model is always valid.
 
In your example application an implementation of IsMyDataValid can easily be done in the MainFormViewModel (based on SaveFormCanExecute function) :
protected override bool IsMyDataValid()
  {
      if (this.forms != null)
          return this.forms.All(f => f.IsValid);
      else
          return true;
  }

Then to have a child VM notify his parent if he is valid or not i have completed the PropertyChangedCompleted method like this:
     protected override void PropertyChangedCompleted(string propertyName)
        {
            // test prevent infinite loop while settings IsValid 
            // (which causes an PropertyChanged to be raised)
            if (propertyName != "IsValid")
            {
                // update the isValid status. Added & IsMyDataValid()
                if (string.IsNullOrEmpty(this.Error) & this.ValidPropertiesCount ==
                                        this.TotalPropertiesWithValidationCount & IsMyDataValid())
                {
                    this.IsValid = true;
                }
                else
                {
                    this.IsValid = false;
                }
                //Added these lignes
                if (this.ParentVM != null)
                    this.ParentVM.PropertyChangedCompleted(string.Empty);
            }
        }
This will correctly "upward cascade set" IsValid on the parent(s) VM Smile | :)
 
I hope that this will be useful to somebody else as your article was for me.
 
Thanks for your article.
Jacques
 
Life is beautifull... and that's all right Smile | :)

GeneralMy vote of 5memberPuchko Vasili14 Apr '11 - 11:26 
Very good sample!
QuestionPossible Extension?memberTom Thorp6 Nov '10 - 14:26 
First, Great Article!
 
Second, one problem that I've not seen a good solution for in the MVVM pattern has to do with where to put the validation attributes. My preference would be to put them in the model. But to use validation attributes in the VM, I would have to duplicate all the validation attributes. Would it be possible to adapt this code to copy all the attributes from the model instead of the VM? Then I have all the validation attributes in one place.
Generalcross field validationmemberMember 39408249 Sep '10 - 6:17 
I like this approach. It feels right to be able to have a clean defintion of the validation requirements outside the xaml. I wonder how you would extend your code to allow for cross-field validation. For example, if your Report View contained a start date and and end date you might want to check the relationship between these two fields.
GeneralRe: cross field validationmemberkiduk15 Jul '12 - 5:06 
Take a look at the IValidatableObject interface.
GeneralSimiliar to Presentmemberthejuan10 Aug '10 - 21:49 
http://adammills.wordpress.com/2010/07/21/mvvm-validation-and-type-checking/[^]
GeneralNicememberShaun Stewart4 Aug '10 - 5:55 
I've been looking for something like this. Have a 5 on me
Quis custodiet ipsos custodes

GeneralRe: NicememberJeremy Alles8 Aug '10 - 22:01 
Glad you appreciate it ! Thanks for the vote
GeneralMy vote of 5memberShaun Stewart4 Aug '10 - 5:54 
Very nice piece of work.
GeneralVery nice, have a 5mvpSacha Barber29 Jul '10 - 3:59 
Very nice, have a 5
Sacha Barber
  • Microsoft Visual C# MVP 2008-2010
  • Codeproject MVP 2008-2010
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue
 
My Blog : sachabarber.net

GeneralRe: Very nice, have a 5memberJeremy Alles8 Aug '10 - 22:01 
Thank you Sacha for your vote and for taking time to review this article !
GeneralMy vote of 4memberaman.tur28 Jul '10 - 23:21 
Excellent man I wanted to try to achieve something like this.
GeneralRe: My vote of 4memberJeremy Alles29 Jul '10 - 3:36 
Glad you like it Smile | :)
GeneralYour articlememberMember 157228 Jul '10 - 19:21 
Too good !

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130516.1 | Last Updated 28 Jul 2010
Article Copyright 2010 by Jeremy Alles
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid