I Prefer Jim Developer James Schubert shares his code and his thoughts.

18Jan/12Off

dotfiles backup using GitHub

I was recently looking for a solution to backup my configuration files (bash, vim, etc) using GitHub. After some looking around, I've compiled a pretty nice project for myself.

github:jimschubert/dotfiles

First, this script checks dependencies. My dependencies are git, ruby, vim, tree, rake, gem, bundle, and trash. You could check out the code and add any number of dependencies here. Rubygems and bundler are required because the script later installs all gems listed in Gemfile.

Next, the script copies ~/.bashrc to ~/.bashrc.local. This allows you to keep your current bash configuration as a 'local-only' config that doesn't get copied or committed to github.

The script, as I copied most of bootstrap.sh and the rakefile from @gf3, expects the repository to be cloned to ~/.dotfiles. From there, it calls rake.

Rake looks at every file in ~/.dotfiles and copies the corresponding file relatively from ~/ to, essentially, ~/dotfiles-backup/`date`. I recommend first running the backup to make sure your files are properly backed up.

rake backup

The script then calls 'bundle install' to install all gems. It then copies all files from ~/.dotfiles to replace those relative files that were previously backed up from ~/.

The post-install displays a message to remind you to edit .gitconfig and .hgrc.

Because I've done some copying and compiling, these are relative close to the three projects in the README for right now.

Here is an excerpt from the README:

Bash

$ tree ~/.bash
/home/jim/.bash
├── aliases
├── completions
├── completion_scripts
│   └── git_completion
├── config
├── functions
├── paths
└── prompt

The above files are loaded by .bashrc. The files are pretty self-explanatory, other than prompt which colorizes the bash prompt with tweaks for git.

Cool Aliases

  • cd : pushd
  • bd : popd
  • cd.. | .. : back one directory
  • cd... | ... : back two directories
  • ^ up to five directories
  • rm : trash
  • undopush
  • ip
  • GET | HEAD | POST | PUT | DELETE | TRACE | OPTIONS

Config

  • sets editor to vim
  • sets English/UTF-8
  • sets manpager
  • sets commands to ignore in history
  • sets noclobber (e.g. prevents cat > IMPORTANT_FILE mistakes )
  • sets nocaseglob (e.g. ls ~/.B* will list contents of ~/.bash)

Functions

The two functions, md and c may not seem like much, but they simplify some commands. For example:

$ md projects; git clone git@github.com:jimschubert/dotfiles.git && cd dotfiles

In the above line, md will create the projects directory and cd into it.

c stands for 'code' and works like this:

jim at computer in ~
$ pwd
/home/jim
jim at computer in ~
$ c dotfiles
~/projects/dotfiles ~
jim at computer in ~/projects/dotfiles on master
$

You can change it to whatever shortcut and issue reload, which is also an alias from this setup.

Screenshot

Notice the color scheme and github branch notifications created by ~/.bash/prompt.

flattr this!

16Aug/11Off

Posting complex models to ASP.NET MVC

Nick Riggs wrote a nice little jQuery plugin called postify that I have found extremely handy on many occasions. The code builds posted data into a format which is more understandable to the default ModelBinder in ASP.NET MVC.

Check out his post for details.

Here is Nick Riggs code for $.postify:

// postify.js
// Converts an object to an ASP.NET MVC  model-binding-friendly format
// Author: Nick Riggs
// http://www.nickriggs.com

$.postify = function(value) {
    var result = {};

    var buildResult = function(object, prefix) {
        for (var key in object) {

            var postKey = isFinite(key)
                ? (prefix != "" ? prefix : "") + "[" + key + "]"
                : (prefix != "" ? prefix + "." : "") + key;

            switch (typeof (object[key])) {
                case "number": case "string": case "boolean":
                    result[postKey] = object[key];
                    break;

                case "object":
                    if (object[key].toUTCString)
                        result[postKey] = object[key].toUTCString().replace("UTC", "GMT");
                    else {
                        buildResult(object[key], postKey != "" ? postKey : key);
                    }
            }
        }
    };

    buildResult(value, "");

    return result;
};

flattr this!

14Aug/11Off

Running additional Google Chrome profiles in Linux

Google Chrome supports running multiple profiles.

In newer versions of Google Chrome, you'll be able to enable profiles by navigating to chrome://flags and enabling Multiple Profiles. Restart the browser, and you'll have a switcher and multiple profiles which can be activated in different windows. This allows you to sync to different gmail accounts. For instance, if you have a personal account and a work or school account, each with different bookmarks... you can run two or three different profiles and have access to all of your data.

If you're using an older version of Chrome or Chromium and you don't have the Multiple Profiles option, you can use the switch

--user-data-dir

when opening the browser and the profile for your session will be pulled from the given folder instead of the Default folder. The folder specified should be created automatically. To be safe, I like to create the folder before providing it as an argument.

As explained here, the default directory is located at:

Google Chrome: ~/.config/google-chrome/Default
Chromium: ~/.config/chromium/Default

Open a terminal and create a new directory for your second profile. For instance:

mkdir ~/.config/google-chrome/work

Then, open Google Chrome with the switch:

google-chrome --user-data-dir='~/.config/google-chrome/work'  &

If this is a profile you'll be using often, you can also create a menu entry under Applications -> Internet ('Chrome|Work', maybe?).

flattr this!

13Aug/11Off

System.Data.OracleClient and Windows 7… love at first sight!

</sarcasm>

I spent nearly two days trying to resolve this issue. My new desktop at work is running Windows 7 (64-bit) and some of our applications are still using System.Data.OracleClient as an adapter instead of Oracle's own ODP.NET. Microsoft's driver interops with Oracle's own client installed on the developer machine (oci.dll). We need to continue running our web applications under 32-bit IIS, which is where the problem lies.

FYI... According to Microsoft:

The types in System.Data.OracleClient are deprecated. The types are supported in version 4 of the .NET Framework but will be removed in a future release. Microsoft recommends that you use a third-party Oracle provider.

Back to the comment about IIS. Apparently, IIS 7 in Windows 7 is 64-bit only. Yes, you can set an application pool to 32-bit. That would be perfectly fine, except that Oracle's client native methods don't like running through WoW64. So, you have to install both the 64-bit client and the 32-bit client. After all, 32-bit adapter code can't call a 64-bit client. If you fire up procmon, though, you'll see that calling the 32-bit Oracle client still queries 64-bit settings (and fails if they don't exist). WoW64!, indeed.

Continue reading to see my resolution...
Note: this forces Oracle 11.2.0.1 to use 32-bit only in a 64-bit environment.

flattr this!

23Jul/11Off

Linux tip: Alias ‘cd’ in bash shell

A while ago, I stumbled across an excellent article at O'Reilly with tips for your bash shell.

I wanted to expand on the "pushd/popd" section a little.

First, check your ~/.bashrc file for the following lines:

if [ -f ~/.bash_aliases ]; then
    . ~/.bash_aliases
fi

This checks that the ~/.bash_aliases file exists, and if it does, it loads it using the '.' builtin. If you aren't familiar with the source/. builtin, here is an excerpt from the documentation:

. (a period)
. filename [arguments]

Read and execute commands from the filename argument in the current shell context. If filename does not contain a slash, the PATH variable is used to find filename. When Bash is not in posix mode, the current directory is searched if filename is not found in $PATH. If any arguments are supplied, they become the positional parameters when filename is executed. Otherwise the positional parameters are unchanged. The return status is the exit status of the last command executed, or zero if no commands are executed. If filename is not found, or cannot be read, the return status is non-zero. This builtin is equivalent to source.

Now, create or edit ~/.bash_aliases with the following content:

alias cd="pushd"
alias bd="popd"
alias cd..="cd .."
alias cd...="cd ../.."
alias cd....="cd ../.."
alias cd.....="cd ../../.."
alias cd......="cd ../../../.."

This aliases cd to push the result onto the directory stack (pushd). Then, we introduce a bd command to pop the last directory off the stack (popd).

If you're like me, you get tired of typing cd ../../../.., which is still faster than typing cd /home/jim/projects/al [TAB]/src or something similar. So, the last lines alias quite a few levels of directory changes so you type cd.. followed by the number of directories you want to navigate back.

Here are some examples:

jim@schubert:~$ cd projects/newtabredirect/
~/projects/newtabredirect ~

jim@schubert:~/projects/newtabredirect$ cd ~/projects/select-actions/
~/projects/select-actions ~/projects/newtabredirect ~

jim@schubert:~/projects/select-actions$ cd ~/Documents/blogs/
~/Documents/blogs ~/projects/select-actions ~/projects/newtabredirect ~

jim@schubert:~/Documents/blogs$ bd
~/projects/select-actions ~/projects/newtabredirect ~

jim@schubert:~/projects/select-actions$ cd....
~ ~/projects/select-actions ~/projects/newtabredirect ~

jim@schubert:~$ pwd
/home/jim

flattr this!

Tagged as: , No Comments
7Feb/11Off

Linking to a Google Doc .doc as .pdf

I recently decided to update my resume, which forced me to revisit an issue I originally faced when posting my resume to my site: I am writing it as a doc file in Google Docs but I want to link to it as a PDF and have my updates visible immediately.

I know this is a selfish thing to want, and I settled for the quick and easy solution of copying the PDF to my host and pointing to the local file. After updating my resume today, I thought, "I'm sure someone has already encountered this." A quick search on Google took me to a Google knol article in which the other provides a link which will convert the Google Doc (provided by a document id) to a desired format. This link causes the PDF to download immediately, but when passed to the document viewer, it works as expected. Perfect!

Here is the article citation:

Google Docs Guide 2. Google Docs Help: Howto Link Straight To A Public Docs Pdf Or Other Supported File [Internet]. Version 2. Knol. 2008 Oct 8. Available from: http://knol.google.com/k/google-docs-guide-2/google-docs-help-howto-link-straight-to/2vcnhxffa8r42/20.

And the link template described in the article:


http://docs.google.com/MiscCommands?command=saveasdoc&exportformat=[FORMAT]&docID=[ID]

Check out my resume for an example.

flattr this!

27Nov/10Off

Syncing between Motorola Droid and Banshee or Rhythmbox in Ubuntu

I like to download those cheap albums on AmazonMP3 that are like "The 99 Darkest Classical Pieces" or whatever. The AmazonMP3 download, however, puts these mp3s in folders according to the artist instead of Various Artists or by album name. Because the Droid's music player reads music based on the folder structure and not on the id3 tags, this means I have about 30 or 40 different entries for the same album. It's annoying, to say the least.

I recently decided to use Rhythmbox to pull all of the music from the Droid and then copy it back over to see if it would automatically put the mp3s in the correct folders. To my surprise,

flattr this!

15Nov/10Off

Tip for Writers and Novelists: Use Subversion to track changes in your work!

As a developer, I take some things like version control for granted.

Recently, I suggested version control to my wife to make it easier for her to compare her current revisions with earlier revisions.  Currently, when she wants to make a new revision, she creates a new file with a different filename.  This doesn't give her to Word comparing or change tracking.  So, if she deleted a paragraph two, three, or twenty revisions ago, she could easily go back to a draft containing the paragraph and retrieve the text or compare against the current revision.  It would even be possible to check out the earlier revision!

On top of change tracking, a subversion repository offers a complete backup of documents.  This would be excellent for someone who is worried about losing their documents.  For instance, you can create a subversion repository on an external hard drive and you'll never lose anything earlier than your last check-in.  Granted, you have to adopt the version control process, but the benefits greatly outweigh the possible headaches of having no sort of version control or revision history.

For any writers who may stumble across my site, this post will walk you through setting up a local subversion repository for free.

Required Download: TortoiseSVN

My version of TortoiseSVN is 1.6.6.  Although your version may be slightly different than the screenshots, the overall steps should remain the same.  After installing TortoiseSVN, be sure to reboot your computer to make sure the svn caching mechanism is working. Then, continue reading...

flattr this!

10Nov/10Off

System76 Ubuntu NetBook and Tethering on Verizon Droid (Android)

One of the first things I wanted to do when I got my netbook was to setup tethering.  However, I don't want to pay $30 more a month just to view the same pages I'd otherwise be viewing on my phone (i.e. Facebook, Twitter, Google News/Gmail).  The answer to this is an application called EasyTether.  It's currently only $10 on the Android Market.  Click here to download EasyTether.

The setup guide in EasyTether will walk you through getting the application configured on your phone and connecting that to your computer.  However, some people may have Linux-based NetBooks and not have the technical knowledge to write a simple script to startup 'easytether' in a console.  I'd like to provide those steps.  Please read on if you're interested!

Note: You *MUST* have your sudo password to proceed.

flattr this!

26May/10Off

System.ComponentModel.DataAnnotations for ASP.NET Web Forms

Although I'm primarily an ASP.NET Web Forms developer, I regularly dabble in new and interesting technologies.  I've toyed with other Microsoft technologies such as ASP.NET MVC and Dynamic Data web sites.

ASP.NET MVC offers an interesting mechanism for validating view models called DataAnnotations.   Examples can be seen here and here.

Some of the more useful attributes for validation include:

  • RequiredAttribute
  • RegularExpressionAttribute
  • RangeAttribute
  • DataTypeAttribute

I recently decided to implement a data validation schema (for lack of a better term) for web forms similar to that of ASP.NET MVC. This would allow us to maintain validation of a model object via attributes at the class level, instead of dispersing these validation rules through the web form code or any of the business logic layers. However, there will be occasions where an object's validity depends upon some state or validity of another object. I've handled this by allowing the validation method to accept any action to be called after the object has been validated.

In order to make a domain object validatable, I'm going to implement the following interface:

IValidatable.cs

namespace Validatable
{
    interface IValidatable
    {
        System.Type EntityType { get; set; }
        System.Collections.Generic.List<object> Errors { get; set; }

        /// <summary>
        /// Determine whether the object is valid.  If invalid, errors are added to item.Errors
        /// </summary>
        /// <returns>true if valid, false if invalid</returns>
        bool IsValid();

        /// <summary>
        /// Validate this item against a supplied action
        /// </summary>
        /// <param name="action">The action to use for validation</param>
        void Validate(System.Action action);

        /// <summary>
        /// Validate this object against a Repository.
        /// <example>
        /// Item a = new Item();
        /// a.Validate(new ItemRepository());
        /// </example>
        /// Note: Repository must have a Validate(T item) method
        /// </summary>
        /// <param name="repository">The Repository to use for validation</param>
        void Validate(object repository);
    }
}

As you can see, this abstract class adds a property accessor for the entity's type, a list of validation errors, an IsValid method, and two Validate methods.

Validatable<T>

I'll take the code in chunks, since there is a lot more than I usually post.
First of all, the using directives required for this class are pretty sparse:

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

The class definition is:

   [Serializable]
    public abstract class Validatable<T> : IValidatable where T : class, new()
    { /* class */ }

If you're unfamiliar with generic constraints, the where T : class, new() above enforces the T passed as in to be a class and to have a parameterless constructor (new())

The properties can be auto-implemented properties, or they can implement any backing logic you prefer. The constructor as I have it is:

 protected Validatable()
 {
     Errors = new List<object>();
     EntityType = typeof(T);
 }

Now for the methods and a slight explanation of each:

IsValid()

        public virtual bool IsValid()
        {
            Errors = new List<object>();
            PropertyInfo[] props = this.GetType().GetProperties();

            foreach (PropertyInfo property in props)
            {
                foreach (ValidationAttribute va in
                    property.GetCustomAttributes(true).OfType<ValidationAttribute>())
                {
                    var value = property.GetValue(this, null);
                    if (!va.IsValid(value))
                    {
                        Errors.Add(va.ErrorMessage);
                    }
                }
            }

            return Errors.Count <= 0;
        }

As a side note, all DataAnnotation attributes inherit from ValidationAttribute. You can create your own Validation Attributes by inheriting from this class. This method clears the list of errors and repopulates it by looping over any properties with validation attributes of this object, validates that property's value against the attribute, and adds the ErrorMessage associated with it to the list of Errors.

Validate Methods

        public virtual void Validate(Action action)
        {
            if (IsValid())
            {
                action();
            }
        }

        public virtual void Validate(object repository)
        {
            MethodInfo method = repository.GetType().GetMethods()
                .Where(x => x.Name.Equals("Validate")).FirstOrDefault();
            if (method != null)
            {
                object[] parameters = new object[] { this };
                Validate(() => method.Invoke(repository, parameters));
            }
        }

These two methods can be changed to throw an exception if the object is invalid. I'll leave that up to you. The way these are set up is to allow you to validate the object and perform either some unknown action or call Validate(T item) against some other object, arbitrarily called a repository. In actuality, you could have a Validate method on some other object that takes an object of this type and essentially chain validations.

The simplicity of the Validate(Action action) method is what makes it beautiful. For instance, you can have an item (a) and cause it save after validation by doing something like:

a.Validate(() => new ItemDAL().Save(a));

As you can see, a Save method in your Item's Data Access Layer taking Item as a parameter can be called only when a.IsValid() is true. Again, there are a number of ways to tweak this and change it to your liking, but I'll leave that up to you.

DataAnnotationValidator

Here is the wonderful part about all of this. You can create a custom validator that validates in a similar way as the Validatable class. I'll post the code and quickly explain what it does. I got the idea from this from another blog and tweaked it a little.

DataAnnotationValidator.cs

// DataAnnotationValidator.cs
namespace Validatable
{
    using System;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Reflection;
    using System.Web.UI;
    using System.Web.UI.WebControls;

    [ToolboxData("<{0}:DataAnnotationValidator runat=\"server\" ControlToValidate=\"[Required]\" Display=\"Dynamic\" Text=\"*\" SourceTypeName=\"[FullyQualifiedTypeName]\" PropertyToValidate=\"[PropertyName]\" />")]
    public class DataAnnotationValidator : BaseValidator
    {
        /// <summary>
        /// THe Property that should be checked
        /// </summary>
        public string PropertyToValidate { get; set; }

        /// <summary>
        /// The object's type
        /// </summary>
        public string SourceTypeName { get; set; }

        protected override bool EvaluateIsValid()
        {
            Type source = GetValidatedType();
            PropertyInfo property = GetValidatedProperty(source);
            string value = GetControlValidationValue(ControlToValidate);

            foreach (ValidationAttribute va in property
                .GetCustomAttributes(typeof(ValidationAttribute), true)
                .OfType<ValidationAttribute>())
            {
                if (!va.IsValid(value))
                {
                    if (string.IsNullOrEmpty(ErrorMessage))
                    {
                        this.ErrorMessage = va.ErrorMessage;
                    }
                    return false;
                }
            }

            return true;
        }

        private Type GetValidatedType()
        {
            if (string.IsNullOrEmpty(SourceTypeName))
            {
                throw new InvalidOperationException("Null SourceTypeName can't be validated");
            }

            Type validatedType = Type.GetType(SourceTypeName);
            if (validatedType == null)
            {
                throw new InvalidOperationException(
                    string.Format("{0}:{1}", "Invalid SourceTypeName", SourceTypeName));
            }

            return validatedType;
        }

        private PropertyInfo GetValidatedProperty(Type source)
        {
            PropertyInfo property = source.GetProperty(PropertyToValidate,
              BindingFlags.Public | BindingFlags.Instance);

            if (property == null)
            {
                throw new InvalidOperationException(
                  string.Format("{0}:{1}", "Validated Property Does Not Exists", PropertyToValidate));
            }
            return property;
        }
    }
}

This creates a validator web control which has 'PropertyToValidate' and 'SourceTypeName'. When a Validator web control is added to a page, it must have ControlToValidate, PropertyToValidate, and SourceTypeName specified or an error will be thrown. The validation method verifies the type and the property, gets the value of the property and finally, much like the Validatable abstract class, it loops over all ValidationAttributes and validates the value against that attribute. You can use this with multiple attributes, so it doesn't hurt to add this validator to a property-- that just means you can add an attribute in the future and everything is already wired up! That's pretty cool.

Example

As an example, I'm going to create a single page to **input** a Customer object. The customer object is very simple:

 public class Customer : Validatable.Validatable<Customer>
    {
        [Required(ErrorMessage="UserId is Required")]
        public int UserId { get; set; }

        [DataType(DataType.PhoneNumber, ErrorMessage="Invalid Phone Number")]
        public string PhoneNumber { get; set; }

        [RegularExpression(@"(^\d{5}(-\d{4}){0,1}$)", ErrorMessage="Invalid Zip Code")]
        public string ZipCode { get; set; }
    }

And the Page in full is collapsed below:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Example._Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:Label ID="lblStatus" runat="server" />
    <asp:ValidationSummary ID="ValidationSummary1" runat="server" />
    <div>
        <asp:FormView ID="FormView1" runat="server" DataSourceID="srcRepository"
            Width="119px" AllowPaging="True">
            <EditItemTemplate>
                UserId:
                <asp:TextBox ID="UserIdTextBox" runat="server" Text='<%# Bind("UserId") %>' />
                <example:DataAnnotationValidator ID="DataAnnotationValidator1" runat="server" ControlToValidate="UserIdTextBox" Display="Dynamic"
                    PropertyToValidate="UserId" ErrorMessage="Invalid User Identification Number"
                    SourceTypeName="Example.Customer, Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
                    Text="*"/>
                <br />
                PhoneNumber:
                <asp:TextBox ID="PhoneNumberTextBox" runat="server"
                    Text='<%# Bind("PhoneNumber") %>' />
                <example:DataAnnotationValidator ID="DataAnnotationValidator2" runat="server" ControlToValidate="PhoneNumberTextBox" Display="Dynamic"
                    PropertyToValidate="PhoneNumber" OnInit="GetTypeName" SourceTypeName="Example.Customer, Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" Text="*"/>
                <br />
                ZipCode:
                <asp:TextBox ID="ZipCodeTextBox" runat="server" Text='<%# Bind("ZipCode") %>' />
                <example:DataAnnotationValidator ID="DataAnnotationValidator3" runat="server" ControlToValidate="ZipCodeTextBox" Display="Dynamic"
                    PropertyToValidate="ZipCode" SourceTypeName="Example.Customer, Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" Text="*"/>
                <br />
                EntityType:
                <asp:TextBox ID="EntityTypeTextBox" runat="server"
                    Text='<%# Eval("EntityType") %>' />
                <br />
                <asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True"
                    CommandName="Update" Text="Update" />
                &nbsp;<asp:LinkButton ID="UpdateCancelButton" runat="server"
                    CausesValidation="False" CommandName="Cancel" Text="Cancel" />
            </EditItemTemplate>
            <InsertItemTemplate>
                UserId:
                <asp:TextBox ID="UserIdTextBox" runat="server" Text='<%# Bind("UserId") %>' />
                <example:DataAnnotationValidator ID="DataAnnotationValidator1" runat="server" ControlToValidate="UserIdTextBox" Display="Dynamic"
                    PropertyToValidate="UserId"  OnInit="GetTypeName"  Text="*"/>
                <br />
                PhoneNumber:
                <asp:TextBox ID="PhoneNumberTextBox" runat="server"
                    Text='<%# Bind("PhoneNumber") %>' />
                <example:DataAnnotationValidator ID="DataAnnotationValidator2" runat="server" ControlToValidate="PhoneNumberTextBox" Display="Dynamic"
                    PropertyToValidate="PhoneNumber" OnInit="GetTypeName"  Text="*"/>
                <br />
                ZipCode:
                <asp:TextBox ID="ZipCodeTextBox" runat="server" Text='<%# Bind("ZipCode") %>' />
                <example:DataAnnotationValidator ID="DataAnnotationValidator3" runat="server" ControlToValidate="ZipCodeTextBox" Display="Dynamic"
                    PropertyToValidate="ZipCode" OnInit="GetTypeName"  Text="*"/>
                <br />
                EntityType:
                <asp:TextBox ID="EntityTypeTextBox" runat="server"
                    Text='<%# Eval("EntityType") %>' />
                <br />
                <asp:LinkButton ID="InsertButton" runat="server" CausesValidation="True"
                    CommandName="Insert" Text="Insert" />
                &nbsp;<asp:LinkButton ID="InsertCancelButton" runat="server"
                    CausesValidation="False" CommandName="Cancel" Text="Cancel" />
            </InsertItemTemplate>
            <ItemTemplate>
                UserId:
                <asp:Label ID="UserIdTextBox" runat="server" Text='<%# Bind("UserId") %>' />
                <br />
                PhoneNumber:
                <asp:Label ID="PhoneNumberTextBox" runat="server"
                    Text='<%# Bind("PhoneNumber") %>' />
                <br />
                ZipCode:
                <asp:Label ID="ZipCodeTextBox" runat="server" Text='<%# Bind("ZipCode") %>' />
                <br />
                EntityType:
                <asp:Label ID="EntityTypeLabel" runat="server"
                    Text='<%# Bind("EntityType") %>' />
                <br />
                <asp:LinkButton ID="EditButton" runat="server" CausesValidation="False"
                    CommandName="Edit" Text="Edit" />
                &nbsp;<asp:LinkButton ID="NewButton" runat="server" CausesValidation="False"
                    CommandName="New" Text="New" />
            </ItemTemplate>
        </asp:FormView>
        <asp:ObjectDataSource ID="srcRepository" runat="server"
            DataObjectTypeName="Example.Customer" InsertMethod="Save"
            oninserted="srcRepository_Saved" onupdated="srcRepository_Saved"
            SelectMethod="Get" TypeName="Example.CustomerRepository" UpdateMethod="Save"></asp:ObjectDataSource>
    </div>
    </form>
</body>
</html>

Of particular note here is in the EditTemplate, you'll see

                  UserId:
                <asp:TextBox ID="UserIdTextBox" runat="server" Text='<%# Bind("UserId") %>' />
                <example:DataAnnotationValidator ID="DataAnnotationValidator1" runat="server" ControlToValidate="UserIdTextBox" Display="Dynamic"
                    PropertyToValidate="UserId" ErrorMessage="Invalid User Identification Number"
                    SourceTypeName="Example.Customer, Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
                    Text="*"/>

in which the SourceTypeName must be the full Assembly Qualified Name. This is necessary if the Validatable and DataAnnotationValidator classes are in an external library. This is a rather tedious property to fill for each validator. Another way to get around this is to populate the SourceTypeName as seen in the InserItemTemplate, e.g.

                UserId:
                <asp:TextBox ID="UserIdTextBox" runat="server" Text='<%# Bind("UserId") %>' />
                <example:DataAnnotationValidator ID="DataAnnotationValidator1" runat="server" ControlToValidate="UserIdTextBox" Display="Dynamic"
                    PropertyToValidate="UserId"  OnInit="GetTypeName"  Text="*"/>

and the code in the code behind is very simple:

        protected void GetTypeName(object sender, EventArgs e)
        {
            DataAnnotationValidator validator = (DataAnnotationValidator)sender;
            validator.SourceTypeName = new Customer().EntityType.AssemblyQualifiedName;
        }

Also notice that the first validator (the one with the assembly qualified name) has an ErrorMessage property specified. Doing this allows you to override the error message returned from the validated object. We can do this if the model says "UserId" and you want the user to see this property referred to as "User Identification".

Conclusion

Sometimes dabbling in other technologies can open the door for new and simpler ways of doing things. I like this example because it allows you to build web applications quickly and easily, while making them maintainable in the future (new validations only have to be added to the model, not to every control where the model must be validated).

For further reference, this project is linked below. Please download and modify if necessary.
Validatable.zip

flattr this!