Thursday, April 22, 2010

Using SPList as Datasource for jQuery UI Autocomplete

A client recently wanted an auto complete field populated with a SharePoint list. Luckily, the jQuery UI library recently added an auto complete widget to their offering. So with the widget and SharePoint's ability to expose SPList data as xml we have a robust solution at our fingertips.

First, lets look at SharePoint's underutililized feature of exposing lists as xml through the url. That's right, use the right url and your list comes back as xml.  Here's the format

"#WEBURL#/_vti_bin/owssvr.dll?Cmd=Display&List={#LISTGUID#}&Query=Title%20Name&XMLDATA=TRUE"

Replace #WEBURL# with url of your site.
Replace #LISTGUID# with your list's guid.
The "Query=Title%20Name" limits the fields coming back to "Title" and "Name". These are the internal field names, one more reason not to use spaces in your field names.

These are really only the basics of what you can do with this techinque, the rest can be found here. URL Protocol

Next, you will need jQuery version 1.4.2 and jQuery UI version 1.8.0. You can either download these from their sites or use the Google cdn. Take a look here about adding the jQuery library and code into SharePoint if you need to. Use the code below and you have an input box with auto complete feed by a list. Pretty cool.

  <script type="text/javascript">     
           (function(){
                 var xmlSource = "#WEBURL#/_vti_bin/owssvr.dll?Cmd=Display&
                                  List={#LISTGUID}&Query=Title%20Name&XMLDATA=TRUE";

                 $.ajax({
                    url: xmlSource,
                    dataType: "xml",
                    success: function(xmlResponse) {
                        var data = $("z\\:row", xmlResponse).map(function() {
                            return {
                                title: $(this).attr("ows_Title"),
                                name: $(this).attr("ows_Name")
                            };
                        }).get();
                        $("#auto").autocomplete({
                            source: data,
                            minLength: 1,
                            select: function(event, ui) {
                                var title = ui.item.title;
                                var name = ui.item.name;
                           //TO DO what to do with the values that have been selected
                            }
                        });
                    }
                })//end .ajax

            }) //end function
  </script>

<input id="auto" type="text" />


This solution could be wrapped in a custom field control. If you were to do this you may be able to do away with lookup fields and provide auto complete everywhere in your site.

Wednesday, March 17, 2010

Too Many Users Remoted into Machine

There's a really easy way around the too many users remoted into the server. Just remember that you may be bumping someone else from the machine. An added bonus most of the time imo.

C:\>mstsc -v:MachinenameOrIP /F -admin

Wednesday, March 3, 2010

SharePoint TreeView Site Navigation

This is a really simple and powerful solution for site navigation within a site collection. It consists of a TreeView control, a PortalSiteMapProvider and a SiteMapDataSource. It sounds like there's a lot going on and there is, but SharePoint is taking care of most of the work for us.


The TreeView control gives a familiar look most users will quickly recognize. It can be easily styled  with css or skins.




Here is the editor of the webpart. The TreeView section has been added for a few customizations. The Site Map Provider is provided by SharePoint. Additional providers can be found in the web.config file. Number of levels to Show sets the number of levels to expand. Only Display Subsites if checked will only display the current site and it's subsites.

The code below creates and customizes the sitemapprovider.
        //setup the sitemapprovider
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            SiteMapProvider siteMapProvider = SiteMap.Providers[_siteMapProvider];
            if (siteMapProvider == null)
            { return; }

            InitPortalSiteMapProvider(siteMapProvider);
        }

        //set some defaults for the customized data provider
        //this is intended to only show sites and not pages
        private void InitPortalSiteMapProvider(SiteMapProvider siteMapProvider)
        {
            if (siteMapProvider is PortalSiteMapProvider)
            {
                _provider = siteMapProvider as PortalSiteMapProvider;
                _provider.DynamicChildLimit = 0;
                _provider.EncodeOutput = true;
                _provider.IncludePages = PortalSiteMapProvider.IncludeOption.Never;
                _provider.IncludeSubSites = PortalSiteMapProvider.IncludeOption.Always;
                _provider.IncludeHeadings = false;
                _provider.IncludeAuthoredLinks = false;
            }
        }

Here's the CreateChildControls method where everything is put together.
        protected override void CreateChildControls()
        {
            Controls.Clear();
            //create the datasource
            _datasource = new SiteMapDataSource();
            //associate the datasource with the customized provider
            _datasource.Provider = _provider;
            //if true only show self and subsites
            _datasource.StartFromCurrentNode = startAtCurrentWeb;

            treeView = new TreeView();
            treeView.ExpandDepth = levels;
            //set the datasource of the treeview and bind it
            treeView.DataSource = _datasource;

            treeView.DataBind();
      
            Controls.Add(treeView);
        }

Monday, February 22, 2010

PowerShell ISE Server 2008 R2

By default PowerShell ISE is not available in Server 2008 R2. It's a feature that you will need to add. Go to Server Manager -> Features -> Add Features

PowerShell uses the "profile" concept somewhat similar to Unix. Profiles can be really useful in setting up your PowerShell session defaults. There are a number of built in profiles I was expecting to be available and was surprised when I had to create one.
   
How to use Profiles in Windows PowerShell ISE
How to Create Profiles in Windows PowerShell ISE

Create a new Profile
if (!(test-path $profile.CurrentUserAllHosts)) 
{new-item -type file -path $profile.CurrentUserAllHosts -force}

Add the SharePoint Snapin and run all my commands without annoyance. Execute the psEdit command and add the Add-PSSnapin and Set-Executionpolicy commands in the tabbed window at the top. You'll have to save the changes.
psEdit $profile.CurrentUserAllHosts
Add-PSSnapin Microsoft.SharePoint.Powershell
Set-ExecutionPolicy Bypass

Tuesday, February 16, 2010

SPGridView WebPart with Multiple Filter and Sort Columns

The SPGridView is one of the most useful SharePoint controls. For viewing data it's tough to beat. It provides a very nice interface for sorting, filtering, paging and grouping. It gives custom data sources the look and feel of SharePoint list views, providing a consistent look and feel across your site

Unfortunately, it can be quite a handful and good examples of using the features in combination are hard to come by. This example doesn't combine all of the features, notably grouping has not been included. Wouldn't want to spoil all of your fun.

This webpart contains an ObjectDataSource that feeds an SPGridView. The sort and filter properties are maintained in viewstate. The ObjectDataSource does most of the work with a little bit of help with sorting. For the sorting to work, the SortParameterName needs to be set to the value of the persisted sort property. This will then be passed into the SelectMethod of the ObjectDataSource where we will need to implement a custom sort. This solution has been built with WSPBuilder and Visual Studio 2008.

A couple of screen shots of the finished webpart, looks familiar ?
 

Here are the filter and sort properties being saved to and retrieved from viewstate. The format of the strings is important. It seems that filters work best with this format ((col1 = 'val1') AND (col2 = 'val2')). The sort works best in the format of "col1 DESC, col2 ASC".
        string FilterExpression
        {
            get
            {
                if (ViewState["FilterExpression"] == null)
                { ViewState["FilterExpression"] = ""; }

                return (string)ViewState["FilterExpression"];
            }
            set
            {
                string thisFilterExpression = "(" + value.ToString() + ")";
                List<string> fullFilterExpression = new List<string>();
             
                if (ViewState["FilterExpression"] != null)
                {
                    string[] fullFilterExp = ViewState["FilterExpression"].ToString().Split(_ssep, StringSplitOptions.RemoveEmptyEntries);
                    fullFilterExpression.AddRange(fullFilterExp);
                 
                    //add the filter if not present
                    int index = fullFilterExpression.FindIndex(s => s.Contains(thisFilterExpression));
                    if (index == -1)
                    { fullFilterExpression.Add(thisFilterExpression); }
                }
                else
                {
                    fullFilterExpression.Add(thisFilterExpression);
                }
                //loop through the list<T> and serialize to string
                string filterExp = string.Empty;
                fullFilterExpression.ForEach(s => filterExp += s + " AND ");
                filterExp = filterExp.Remove(filterExp.LastIndexOf(" AND "));
                if (!filterExp.EndsWith("))") && filterExp.Contains("AND"))
                { filterExp = "(" + filterExp + ")"; }
                ViewState["FilterExpression"] = filterExp;
            }
        }


        string SortExpression
        {
            get
            {
                if (ViewState["SortExpression"] == null)
                { ViewState["SortExpression"] = ""; }

                return (string)ViewState["SortExpression"];
            }
            set
            {
                string[] thisSE = value.ToString().Split(' ');
                string thisSortExpression = thisSE[0];
                List<string> fullSortExpression = new List<string>();

                if (ViewState["SortExpression"] != null)
                {
                    string[] fullSortExp = ViewState["SortExpression"].ToString().Split(_sep);
                    fullSortExpression.AddRange(fullSortExp);

                    //does the sort expression already exist?
                    int index = fullSortExpression.FindIndex(s => s.Contains(thisSortExpression));
                    if (index >= 0)
                    {
                        string s = string.Empty;
                        if (value.ToString().Contains("DESC"))
                        { s = value.ToString(); }
                        else
                        {
                            s = fullSortExpression[index];
                            if (s.Contains("ASC"))
                            { s = s.Replace("ASC", "DESC"); }
                            else
                            { s = s.Replace("DESC", "ASC"); }
                        }
                        //reset the sort direction
                        fullSortExpression[index] = s;
                    }
                    else
                    {
                        if (value.ToString().Contains("DESC"))
                        { fullSortExpression.Add(value.ToString()); }
                        else
                        { fullSortExpression.Add(thisSortExpression + " ASC"); }
                    }
                }
                else
                {
                    if (value.ToString().Contains("DESC"))
                    { fullSortExpression.Add(value.ToString()); }
                    else
                    { fullSortExpression.Add(thisSortExpression + " ASC"); }
                }
                //loop through the list<T> and serialize to string
                string sortExp = string.Empty;
                fullSortExpression.ForEach(s => sortExp += s);
                sortExp = sortExp.Replace(" ASC", " ASC,");
                sortExp = sortExp.Replace(" DESC", " DESC,");
                ViewState["SortExpression"] = sortExp.Remove(sortExp.LastIndexOf(','));
            }
        }

This is the CreateChildControls where the ObjectDataSource and SPGridView controls are instantiated and properties are set.

        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            try
            {
                //build the datasource
                gridDS = new ObjectDataSource();
                gridDS.ID = "gridDS";
                gridDS.SelectMethod = "SelectData";
                gridDS.TypeName = this.GetType().AssemblyQualifiedName;
                gridDS.EnableViewState = false;
              
                //pass the SortExpression to the select method
                gridDS.SortParameterName = "SortExpression";

                //this resets the dropdown options for other columns after a filter is selected
                gridDS.FilterExpression = FilterExpression;
                //add the data source
                Controls.Add(gridDS);

                //build the gridview
                gridView = new SPGridView();
                gridView.AutoGenerateColumns = false;
                gridView.EnableViewState = false;
                gridView.ID = "gridView";
              
                //sorting
                gridView.AllowSorting = true;
              
                //filtering
                gridView.AllowFiltering = true;
                gridView.FilterDataFields = ",FirstName,LastName,Department,Country,Salary";
                gridView.FilteredDataSourcePropertyName = "FilterExpression";
                gridView.FilteredDataSourcePropertyFormat = "{1} = '{0}'";
              
                //set header icons for sorting an filtering
                gridView.RowDataBound += new GridViewRowEventHandler(gridView_RowDataBound);
              
                //sorting
                gridView.Sorting += new GridViewSortEventHandler(gridView_Sorting);
              
                //paging
                gridView.AllowPaging = true;
                gridView.PageSize = 5;
              
                //create gridView columns
                BuildColumns();

                //set the id and add the control
                gridView.DataSourceID = gridDS.ID;
                Controls.Add(gridView);
     
                SPGridViewPager pager = new SPGridViewPager();
                pager.GridViewId = gridView.ID;
                Controls.Add(pager);

            }
            catch (Exception ex)
            {
                //To Do Log it
            }
        }

Here is the SelectMethod of the ObjectDataSource. Note the signature, a DataTable is being returned, necessary for filtering. The SortParameterName, SortExpression, is passed to enable custom sorting. The custom sort is necessary due to a bug? with the sort ascending / descending menu options.

        public DataTable SelectData(string SortExpression)
        {
            DataTable dataSource = new DataTable();

            dataSource.Columns.Add("ID");
            dataSource.Columns.Add("LastName");
            dataSource.Columns.Add("FirstName");
            dataSource.Columns.Add("Department");
            dataSource.Columns.Add("Country");
            dataSource.Columns.Add("Salary", typeof(double));

            dataSource.Rows.Add(1, "Smith", "Laura", "Sales", "IreLand", 150000);
            dataSource.Rows.Add(2, "Jones", "Ed", "Marketing", "IreLand", 75000);
            dataSource.Rows.Add(3, "Jefferson", "Bill", "Security", "Britian", 87000);
            dataSource.Rows.Add(4, "Washington", "George", "PMO", "France", 110000);
            dataSource.Rows.Add(5, "Bush", "Laura", "Accounting", "USA", 44000);
            dataSource.Rows.Add(6, "Clinton", "Hillory", "Human Resources", "USA", 121000);
            dataSource.Rows.Add(7, "Ford", "Jack", "IT", "France", 150000);
            dataSource.Rows.Add(8, "Hailey", "Tom", "Networking", "Canada", 72000);
            dataSource.Rows.Add(9, "Raul", "Mike", "Accounting", "Canada", 97000);
            dataSource.Rows.Add(10, "Shyu", "Danny", "Sales", "Britian", 89000);
            dataSource.Rows.Add(11, "Hanny", "Susan", "Sales", "USA", 275000);

            //clean up the sort expression if needed - the sort descending
            //menu item causes the double in some cases
            if (SortExpression.ToLowerInvariant().EndsWith("desc desc"))
                SortExpression = SortExpression.Substring(0, SortExpression.Length - 5);

            //need to handle the actual sorting of the data
            if (!string.IsNullOrEmpty(SortExpression))
            {
                DataView view = new DataView(dataSource);
                view.Sort = SortExpression;
                DataTable newTable = view.ToTable();
                dataSource.Clear();
                dataSource = newTable;
            }

            return dataSource;
        }

The gridView_RowDataBound event is used to add the sort and filter images to the header.
        private void gridView_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            if (sender == null || e.Row.RowType != DataControlRowType.Header)
            { return; }

            SPGridView grid = sender as SPGridView;

            // Show icon on filtered and sorted columns
            for (int i = 0; i < grid.Columns.Count; i++)
            {
                DataControlField field = grid.Columns[i];

                if (FilterExpression.Contains(field.SortExpression) &&
                    !string.IsNullOrEmpty(FilterExpression))
                {
                    PlaceHolder panel = HeaderImages(field, "/_layouts/images/filter.gif");
                    e.Row.Cells[i].Controls[0].Controls.Add(panel);
                }
                else if(SortExpression.Contains(field.SortExpression))
                {
                    string url = sortImage(field);
                    PlaceHolder panel = HeaderImages(field, url);
                    e.Row.Cells[i].Controls[0].Controls.Add(panel);
                }        
            }
        }


        private string sortImage(DataControlField field)
        {
            string url = string.Empty;
            string[] fullSortExp = SortExpression.Split(_sep);
            List<string> fullSortExpression = new List<string>();
            fullSortExpression.AddRange(fullSortExp);

            //does the sort expression already exist?
            int index = fullSortExpression.FindIndex(s => s.Contains(field.SortExpression));
            if (index >= 0)
            {
                string s = fullSortExpression[index];
                if (s.Contains("ASC"))
                { url = "_layouts/images/sortup.gif"; }
                else
                { url = "_layouts/images/sortdown.gif"; }
            }
            return url;
        }


        private PlaceHolder HeaderImages(DataControlField field, string imageUrl)
        {
            Image filterIcon = new Image();
            filterIcon.ImageUrl = imageUrl;
            filterIcon.Style[HtmlTextWriterStyle.MarginLeft] = "2px";

            Literal headerText = new Literal();
            headerText.Text = field.HeaderText;

            PlaceHolder panel = new PlaceHolder();
            panel.Controls.Add(headerText);

            //add the sort icon if needed
            if (FilterExpression.Contains(field.SortExpression) &&
                SortExpression.Contains(field.SortExpression))
            {
                string url = sortImage(field);
                Image sortIcon = new Image();
                sortIcon.ImageUrl = url;
                sortIcon.Style[HtmlTextWriterStyle.MarginLeft] = "1px";
                panel.Controls.Add(sortIcon);
                //change the left margin to 1
                filterIcon.Style[HtmlTextWriterStyle.MarginLeft] = "1px";
            }

            panel.Controls.Add(filterIcon);
            return panel;
        }


This is the sorting method of the SPGridView. The event handler is registered in CreateChildren and is where the sortexpression property is built. The filter needs to be reset here or it will be lost.
        void gridView_Sorting(object sender, GridViewSortEventArgs e)
        {
            string sDir = e.SortDirection.ToString();
            sDir = sDir == "Descending" ? " DESC" : "";

            SortExpression = e.SortExpression + sDir;
            e.SortExpression = SortExpression;

            //if the filter is not reset it will be cleared
            if (!string.IsNullOrEmpty(FilterExpression))
            { gridDS.FilterExpression = FilterExpression; }

        }
      

        void buildFilterView(string filterExp)
        {
            string lastExp = filterExp;
            if (lastExp.Contains("AND"))
            {
                if (lastExp.Length < lastExp.LastIndexOf("AND") + 4)
                { lastExp = lastExp.Substring(lastExp.LastIndexOf("AND") + 4); }
                else
                { lastExp = string.Empty; }
            }
          
            //update the filter
            if (!string.IsNullOrEmpty(lastExp))
            { FilterExpression = lastExp; }

            //reset object dataset filter
            if (!string.IsNullOrEmpty(FilterExpression))
            { gridDS.FilterExpression = FilterExpression; }
        }

Friday, February 5, 2010

Custom Site Collection Creation

Site collections are the fundamental building blocks of SharePoint applications. They provide security boundaries and the smallest built-in unit for backup and disaster recovery. Most of the time, the central administration interface is fine for creating site collections. Problems arise when you need to frequently create site collections or allow users to create site collections. More than likely you're going to need a custom solution.

Recently, a client requested just such a custom solution for creating site collections. Not wanting to start from scratch, I found a very good solution from Gary Lapointe here.  The solution consists of two main components an application page, for user input, and a web service to create the site collection. It's really very well done and customizable depending on your needs.      

Tuesday, January 26, 2010

SharePoint MaskedEdit Field Control

A control that I used over and over in the good old days of VB6 was the masked edit control. I recently came across a jQuery plugin for a masked edit control and wanted to implement it as a custom SharePoint field. Download the code here

What you will need. 
jQuery
jQuery Masked Edit Plugin
WSPBuilder

This is masked edit field as seen when adding it to a list. I've defined a small number of masks, you can add more! This interface is built with the next two code blocks. The first code block is the user control that contains the HTML and the server controls. The second code block is the code behind for the user control. 

<%@ Control Language="C#" Inherits="MaskedEditField.jQueryMaskedEditFieldEditor, MaskedEditField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ab6ae01ba130938e"    compilationMode="Always" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormControl" src="~/_controltemplates/InputFormControl.ascx" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>

<wssuc:InputFormControl runat="server" LabelText="Input Masks Explained">
    <Template_Control>
        <div>A mask is defined by a format made up of mask literals and mask definitions.
        Any character not in the definitions list below is considered a mask literal.
        Mask literals will be automatically entered for the user as they type and will
         not be able to be removed by the user.
        <ul>
            <li>a - Represents an alpha character (A-Z,a-z)</li>
            <li>9 - Represents a numeric character (0-9)</li>
            <li>* - Represents an alphanumeric character (A-Z,a-z,0-9)</li>
        </ul>  
        </div>
    </Template_Control>
</wssuc:InputFormControl>

<wssuc:InputFormControl runat="server" LabelText="Input Masks">
    <Template_Control>
        <table>
            <tr>
                <td><asp:RadioButton Text="Phone Number" GroupName="MaskedEdit" ID="rPhone" runat="server"></asp:RadioButton></td>
                <td><asp:TextBox Enabled="false" ID="txtPhone" runat="server">(999) 999-9999</asp:TextBox></td>
            </tr>
            <tr>
                <td><asp:RadioButton Text="SSN" GroupName="MaskedEdit" ID="rSSN" runat="server"></asp:RadioButton></td>
                <td><asp:TextBox Enabled="false" ID="txtSSN" runat="server">999-99-9999</asp:TextBox></td>
            </tr>
            <tr>
                <td><asp:RadioButton Text="Zip Code + 4" GroupName="MaskedEdit" ID="rZip4" runat="server"></asp:RadioButton></td>
                <td><asp:TextBox Enabled="false" ID="txtZip4" runat="server">99999-9999</asp:TextBox></td>
            </tr>
            <tr>
                <td><asp:RadioButton Text="Zip Code" GroupName="MaskedEdit" ID="rZip" runat="server"></asp:RadioButton></td>
                <td><asp:TextBox Enabled="false" ID="txtZip" runat="server">99999</asp:TextBox></td>
            </tr>
            <tr>
                <td><asp:RadioButton Text="Custom" GroupName="MaskedEdit" ID="rCustom" runat="server"></asp:RadioButton></td>
                <td><asp:TextBox Enabled="true" ID="txtCustom" runat="server"></asp:TextBox></td>
            </tr>
        </table>
    </Template_Control>
</wssuc:InputFormControl>



User Control Code Behind
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace MaskedEditField
{
    public class jQueryMaskedEditFieldEditor : UserControl, IFieldEditor
    {
        // Fields
        protected RadioButton rPhone;
        protected RadioButton rSSN;
        protected RadioButton rZip;
        protected RadioButton rZip4;
        protected RadioButton rCustom;
        protected TextBox txtPhone;
        protected TextBox txtSSN;
        protected TextBox txtZip;
        protected TextBox txtZip4;
        protected TextBox txtCustom;
        private jQueryMaskedEdit fldjQueryMaskedEdit;

        public void InitializeWithField(SPField field)
        {
            this.fldjQueryMaskedEdit = field as jQueryMaskedEdit;

            if (this.Page.IsPostBack)
            {
                return;
            }

            //when modifying a field check the correct radio button
            if (field != null)
            {
                string prop = fldjQueryMaskedEdit.MyCustomProperty;

                if (prop.Equals(txtPhone.Text))
                { rPhone.Checked = true; }
                else
                if (prop.Equals(txtSSN.Text))
                { rSSN.Checked = true; }
                else
                if (prop.Equals(txtZip.Text))
                { rZip.Checked = true; }
                else
                if (prop.Equals(txtZip4.Text))
                { rZip4.Checked = true; }
                else
                {
                    rCustom.Checked = true;
                    txtCustom.Text = prop;
                }
            }
        }

        //save the value for the mask from the ascx to the field property
        //this is the value the plugin will use to format the masked input
        public void OnSaveChange(SPField field, bool bNewField)
        {
            jQueryMaskedEdit jme = (jQueryMaskedEdit)field;

            jme.IsNew = bNewField;

            if (rPhone.Checked)
            { jme.MyCustomProperty = txtPhone.Text; }
            else
            if (rSSN.Checked)
            { jme.MyCustomProperty = txtSSN.Text; }
            else
            if (rZip.Checked)
            { jme.MyCustomProperty = txtZip.Text; }
            else
            if (rZip4.Checked)
            { jme.MyCustomProperty = txtZip4.Text; }
            else
            { jme.MyCustomProperty = txtCustom.Text.Trim(); }
        }


        // Properties
        public bool DisplayAsNewSection
        {
            get
            {
                return false;
            }
        }
    }
}

This is the field class. Here I' inheriting form the SPFieldText since it most closely matches what we're trying to produce. This class provides the plumbing for the field. It saves the settings from the user control above, saves the values that users type in, validates the input, provides default display rendering. Luckily, most of this functionality is built into the SPFieldText class or provided by the Visual Studio project.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Web.UI;
using System.Web.UI.WebControls;


namespace MaskedEditField
{
    public class jQueryMaskedEdit : SPFieldText
    {
        private static string[] CustomPropertyNames = new string[] { "MyCustomProperty" };

        public jQueryMaskedEdit(SPFieldCollection fields, string fieldName)
            : base(fields, fieldName)
        {
            InitProperties();
        }

        public jQueryMaskedEdit(SPFieldCollection fields, string typeName, string displayName)
            : base(fields, typeName, displayName)
        {
            InitProperties();
        }

        #region Property storage and bug workarounds - do not edit

        /// <summary>
        /// Indicates that the field is being created rather than edited. This is necessary to
        /// work around some bugs in field creation.
        /// </summary>
        public bool IsNew
        {
            get { return _IsNew; }
            set { _IsNew = value; }
        }
        private bool _IsNew = false;

        /// <summary>
        /// Backing fields for custom properties. Using a dictionary to make it easier to abstract
        /// details of working around SharePoint bugs.
        /// </summary>
        private Dictionary<string, string> CustomProperties = new Dictionary<string, string>();

        /// <summary>
        /// Static store to transfer custom properties between instances. This is needed to allow
        /// correct saving of custom properties when a field is created - the custom property
        /// implementation is not used by any out of box SharePoint features so is really buggy.
        /// </summary>
        private static Dictionary<string, string> CustomPropertiesForNewFields = new Dictionary<string, string>();

        /// <summary>
        /// Initialise backing fields from base property store
        /// </summary>
        private void InitProperties()
        {
            foreach (string propertyName in CustomPropertyNames)
            {
                CustomProperties[propertyName] = base.GetCustomProperty(propertyName) + "";
            }
        }

        /// <summary>
        /// Take properties from either the backing fields or the static store and
        /// put them in the base property store
        /// </summary>
        private void SaveProperties()
        {
            foreach (string propertyName in CustomPropertyNames)
            {
                base.SetCustomProperty(propertyName, GetCustomProperty(propertyName));
            }
        }

        /// <summary>
        /// Get an identifier for the field being added/edited that will be unique even if
        /// another user is editing a property of the same name.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        private string GetCacheKey(string propertyName)
        {
            return SPContext.Current.GetHashCode() + "_" + (ParentList == null ? "SITE" : ParentList.ID.ToString()) + "_" + propertyName;
        }

        /// <summary>
        /// Replace the buggy base implementation of SetCustomProperty
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="propertyValue"></param>
        new public void SetCustomProperty(string propertyName, object propertyValue)
        {
            if (IsNew)
            {
                // field is being added - need to put property in cache
                CustomPropertiesForNewFields[GetCacheKey(propertyName)] = propertyValue + "";
            }

            CustomProperties[propertyName] = propertyValue + "";
        }

        /// <summary>
        /// Replace the buggy base implementation of GetCustomProperty
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="propertyValue"></param>
        new public object GetCustomProperty(string propertyName)
        {
            if (!IsNew && CustomPropertiesForNewFields.ContainsKey(GetCacheKey(propertyName)))
            {
                string s = CustomPropertiesForNewFields[GetCacheKey(propertyName)];
                CustomPropertiesForNewFields.Remove(GetCacheKey(propertyName));
                CustomProperties[propertyName] = s;
                return s;
            }
            else
            {
                return CustomProperties[propertyName];
            }
        }

        /// <summary>
        /// Called when a field is created. Without this, update is not called and custom properties
        /// are not saved.
        /// </summary>
        /// <param name="op"></param>
        public override void OnAdded(SPAddFieldOptions op)
        {
            base.OnAdded(op);
            Update();
        }

        #endregion


        public override BaseFieldControl FieldRenderingControl
        {
            get
            {
                BaseFieldControl fieldControl = new jQueryMaskedEditControl(this);
                fieldControl.FieldName = InternalName;
                return fieldControl;
            }
        }


        public override void Update()
        {
            SaveProperties();
            base.Update();
        }


        public override string GetValidatedString(object value)
        {
            if ((this.Required == true) && (value == null))
            { throw new SPFieldValidationException("This is a required field."); }

            return base.GetValidatedString(value);
        }


        public override object GetFieldValue(string value)
        {
            if (String.IsNullOrEmpty(value))
                return null;

            return value;
        }

        public string MyCustomProperty
        {
            get { return this.GetCustomProperty("MyCustomProperty") + ""; }
            set { this.SetCustomProperty("MyCustomProperty", value); }
        }
    }

}



This is the class that renders the control users interact with in the new and edit forms. It closely resembles a webpart. Here is where the JavaScript is written to the page.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;


namespace MaskedEditField
{
    public class jQueryMaskedEditControl : BaseFieldControl
    {
        private jQueryMaskedEdit field;
        private TextBox txtBox;
        private string txtVal;
      
        // Used for the linked script file
        private const string MaskedInputJS = "jquery.maskedinput-1.2.2.min.js";
        private const string MaskedInputScriptKey = "jQueryMaskedInputKey";
        private const string IncludeScriptFormat =
        @"<script type=""{0}"" src=""{1}""></script>";
      

        public jQueryMaskedEditControl(jQueryMaskedEdit parentField)
        {
            this.field = parentField;
            this.txtBox = new TextBox();
        }

        //get/set value for custom field
        public override object Value
        {
            get
            {
                return txtBox.Text;
            }
            set
            {
                txtBox.Text = string.Empty;
                string txtVal = value as String;
                if (txtVal != null)
                {
                    txtBox.Text = txtVal;
                }
            }
        }


        protected override void CreateChildControls()
        {
            if (this.Field == null
                || this.ControlMode == SPControlMode.Display
                || this.ControlMode == SPControlMode.Invalid)
                return;
          
            base.CreateChildControls();

            txtBox = new TextBox();
            this.Controls.Add(txtBox);
        }

        //Update field value with user input & check field validation
        public override void UpdateFieldValueInItem()
        {
            this.EnsureChildControls();
            try
            {
                this.Value = this.txtBox.Text;
                this.ItemFieldValue = this.Value;
            }

            catch (Exception ex)
            {
                this.IsValid = false;
                this.ErrorMessage = "* " + ex.Message;
            }
        }


        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            if (this.ControlMode == SPControlMode.Edit ||
                this.ControlMode == SPControlMode.New)
            { RegisterCommonScript(); }
        }


        //Function which will register the linked file script and the embedded script
        protected void RegisterCommonScript()
        {
            string location = null;
         
            //include the mask plugin on the page
            if (!Page.ClientScript.IsClientScriptBlockRegistered(MaskedInputScriptKey))
            {
                location = @"/_layouts/";
                string includeScript =
                String.Format(IncludeScriptFormat, "text/javascript", location + MaskedInputJS);
                Page.ClientScript.RegisterClientScriptBlock(typeof(jQueryMaskedEditControl), MaskedInputScriptKey, includeScript);
            }

            if (!Page.ClientScript.IsClientScriptBlockRegistered(txtBox.ClientID))
            {
                //this is where the mask plugin is hooked into our field, its all happening on the clients machine
                string function =
                     "<script type=\"text/javascript\">jQuery(function($){$(\"input[id*=" + txtBox.ClientID +
                     "]\").mask(\"" + field.MyCustomProperty + "\"); });</script>";
                Page.ClientScript.RegisterClientScriptBlock(typeof(jQueryMaskedEditControl), txtBox.ClientID, function);
            }
        }

    }
}


XML used to define the field.
<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
  <FieldType>
    <Field Name="TypeName">jQueryMaskedEdit</Field>
    <Field Name="ParentType">Text</Field>
    <Field Name="TypeDisplayName">jQueryMaskedEdit</Field>
    <Field Name="TypeShortDescription">jQuery Masked Edit</Field>
    <Field Name="UserCreatable">TRUE</Field>
    <Field Name="Sortable">TRUE</Field>
    <Field Name="AllowBaseTypeRendering">TRUE</Field>
    <Field Name="Filterable">TRUE</Field>
    <Field Name="FieldTypeClass">MaskedEditField.jQueryMaskedEdit, MaskedEditField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ab6ae01ba130938e</Field>
    <Field Name="FieldEditorUserControl">/_controltemplates/jQueryMaskedEditFieldEditor.ascx</Field>
    <PropertySchema>
      <Fields>
        <Field Hidden="TRUE" Name="MyCustomProperty"
        DisplayName="My Custom Property"
        Type="Text">
        </Field>
      </Fields>
      <Fields></Fields>
    </PropertySchema>
    <RenderPattern Name="DisplayPattern">
        <Column />
    </RenderPattern>
  </FieldType>
</FieldTypes>


Display View





Edit View