Tuesday, December 28, 2010

SharePoint People Picker Limit Selection

By default, the people picker will let a user select anyone visible to the picker. This is not always an ideal situation. Luckily, there are a couple of ways to easily limit what the user can select from.

First, if you need to limit selection to site collection users, you can use an stsadm command

stsadm -o setproperty –url http://sitecollectionURL
       –pn peoplepicker-onlysearchwithinsitecollection –pv yes


There are a couple of other options to limit selection that involve setup in active directory. They can be found here; Keep it Simple!
Peoplepicker: Stsadm properties


Second, customize the people editor control with the SharePointGroup property. I've found this solution very helpful in customizing input screens as it gives really fine grained control over an individual field. The field below will only allow selection from "SomeGroup".

<SharePoint:PeopleEditor ID="plpEdit" runat="server" SharePointGroup="SomeGroup" />

Sunday, December 26, 2010

SharePoint People Picker Multiple Domains

The people picker is an important part of a SharePoint farm. Making sure that the correct users are available for selection is key. Issues almost always show up in farms where multiple domains need to be available for selection. The fix is a couple of stsadm commands, these commands work in SharePoint 2007 and 2010. 

By default, the application pool identity is used to search active directory. If the account does not have the correct permissions, you will need to encrypt the password for the account that will be used to search that domain. This account needs to be noted for password changes!

Set the encryption key (run on each WFE)
stsadm -o setapppassword -password  *********
Set the domains that should be searched  (run on one WFE per web application)
stsadm -o setproperty -pn peoplepicker-searchadforests 
       -pv domain:domain1;domain:domain2,domain2\account,password 
       -url http://webapp
A more detailed discussion can be found here:
http://blogs.msdn.com/b/joelo/archive/2007/03/08/cross-forest-multi-forest-configuration-additional-info.aspx

Visual Studio 2010 Code Snippets

Code snippets are a great way to speed up development time. In this post I'll go through a simple way to create your own code snippets. I'll be creating a snippet to log to the 14\LOGS files.

Open a Visual Studio project and add a new xml file. Make sure to save it with a .snippet extension.

Right click in the new xml file,  select Insert Snippet and double click on snippet. The xml below will be inserted for you.



Fill in the values you would like for these tag. The most important is the shortcut - its what will start intellisence for you.
    <Title>SharePoint Logging</Title>
    <Author>Me</Author>
    <Shortcut>SPLog</Shortcut>
    <Description></Description>

Choose what type of snippet you want either SurroundsWith or Expansion, I'm using expansion here.
    <SnippetTypes>
      <SnippetType>SurroundsWith</SnippetType>
      <SnippetType>Expansion</SnippetType>
    </SnippetTypes>

Now enter what you would like to replace in instances of the snippet. Here the component name to be logged changes. If there are more replacements needed, add more Literal tags.
    <Declarations>
      <Literal>
        <ID>Component</ID>
        <Default>CustomComponent</Default>
      </Literal>
    </Declarations>

Change the code language to CSharp.
    <Code Language="CSharp">

Enter the code to be inserted in CDATA[ YOUR CODE GOES HERE  ]
  <![CDATA[
  SPDiagnosticsService.Local.WriteTrace(0,
                    new SPDiagnosticsCategory("$Component$", TraceSeverity.Unexpected, EventSeverity.Error), TraceSeverity.Unexpected, ex.Message, ex.StackTrace);
  ]]>


Save the snippet to the default location "C:\Users\Administrator\Documents\Visual Studio 2010\Code Snippets\Visual C#\My Code Snippets" and your ready to try your new snippet.

Completed snippet file
<CodeSnippet Format="1.0.0" xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <Header>
    <Title>SharePoint Logging</Title>
    <Author>Me</Author>
    <Shortcut>SPLog</Shortcut>
    <Description></Description>
    <SnippetTypes>

      <SnippetType>Expansion</SnippetType>
    </SnippetTypes>
  </Header>
  <Snippet>
    <Declarations>
      <Literal>
        <ID>Component</ID>
        <Default>CustomComponent</Default>
      </Literal>
    </Declarations>
    <Code Language="CSharp">
      <![CDATA[
  SPDiagnosticsService.Local.WriteTrace(0,
      new SPDiagnosticsCategory("$Component$", TraceSeverity.Unexpected, EventSeverity.Error), TraceSeverity.Unexpected, ex.Message, ex.StackTrace);
  ]]>
    </Code>
  </Snippet>
</CodeSnippet>

Logging in Sharepoint 2010

Logging in SharePoint 2010 is a pretty straight forward affair. The code block below will write to 14\LOGS 

using Microsoft.SharePoint.Administration;

                SPDiagnosticsService.Local.WriteTrace(0,
                    new SPDiagnosticsCategory("CustomComponent", TraceSeverity.Unexpected, EventSeverity.Error), TraceSeverity.Unexpected, ex.Message, ex.StackTrace);

If you want to create your own custom logging component, the following two blogs will assist you.

http://blog.mastykarz.nl/logging-uls-sharepoint-2010/
http://www.sharepointproconnections.com/article/sharepoint-development/SharePoint-Logging-What-s-New-in-SharePoint-2010.aspx

Sunday, October 3, 2010

ASP.NET Align Labels with Text

The simple task of aligning labels and text can become not so easy when you move away from using tables. In this example I'm wrapping the lables and textboxes in div and using a bit of CSS to produce the result below. The important bits are highlighted.


div{
      clear:left
      margin: 5px 0 0; padding: 1px 3px;
      width: 400px;
}
.input{
      display:block; float: left 
      margin: 0 0 5px; padding: 3px 5px;
      text-align:right;width: 130px;
}




<div>
      <asp:Label AssociatedControlID="txtOne" CssClass="input" ID="label1" runat="server"  
      Text="Text One:"></asp:Label>
      <asp:TextBox ID="txtOne" runat="server"></asp:TextBox>
</div>
<div>
     <asp:Label AssociatedControlID="txtTwo"    CssClass="input" ID="label2" runat="server"
     Text="Text Two:"></asp:Label>
     <asp:TextBox ID="txtTwo" runat="server"></asp:TextBox>
</div>
<div>
     <asp:Label AssociatedControlID="ddlOne" CssClass="input" ID="label3" runat="server" 
      Text="DropDown:"></asp:Label>
     <asp:DropDownList ID="ddlOne" runat="server">
           <asp:ListItem>Choice1</asp:ListItem>
           <asp:ListItem>Choice2</asp:ListItem>
     </asp:DropDownList>
</div>
<div>
    <asp:Label AssociatedControlID="txtThree" CssClass="input" ID="label4" runat="server"  
    Text="Text Three:"></asp:Label>
    <asp:TextBox ID="txtThree" runat="server" Rows="5" TextMode="MultiLine"></asp:TextBox>
</div>
<div>
    <asp:Label AssociatedControlID="txtFour" CssClass="input" ID="label5" runat="server"
    Text="Text Four:"></asp:Label>
    <asp:TextBox ID="txtFour" runat="server" Rows="5" TextMode="MultiLine"></asp:TextBox>
</div>

Saturday, July 31, 2010

Enable IntelliSense for SharePoint Client Object Model in Visual Studio 2010

Microsoft released some guidance on enabling intellisense for the SharePoint client object model and was disappointed, it only gave partial intellisense. After a bit of investigating, I found that there are a number of debug.js files in the layouts folder. Adding the SP.Core.debug.js file gave me the result I was looking for. 

<script type="text/javascript" src="/_layouts/MicrosoftAjax.js" ></script>
<script type="text/javascript" src="/_layouts/SP.debug.js" />
<script type="text/javascript" src="/_layouts/SP.Core.debug.js" />


Sunday, July 25, 2010

Table of Contents Web Part

The table of contents web part isn't showing all the sites. Go to site settings, navigation and increase the number of sites to display.

Thursday, July 22, 2010

Windows 2008R2 boot VHD

I was in need of a faster SharePoint 2010 development environment and saw some posts on booting from a vhd and decided to give it a try. I'm running Windows 7 ultimate on my laptop and will install Windows 2008 R2 on the vhd. The first thing I tried was converting my virtualbox hard drive. Big waste of time, the conversion from vdi to vhd just didn't work. So, it was time to create a new vhd and install everything all over again. In this post I'll go thorough the steps to create a new vhd and install Windows 2008R2 on to it.

Change the bios settings on your machine so the dvd will boot before the hard drive.

Boot your machine from the install dvd. Oh, you don't have an install dvd, you have an iso file don't you? So you need to create an install dvd from the iso file. It's not so bad. Use a utility like ImgBurn to unpack the image file to disk, you'll see all the setup files when your done. 

Boot your machine from the install dvd. Select your language and click next.


Go to the command prompt  - enter shift+F10
x:\sources > diskpart
diskpart > create vdisk file=c:\win2008r2.vhd maximum=40000 type=expandable
diskpart > select vdisk file=c:\win2008r2.vhd
diskpart > attach vdisk
diskpart > exit
x:\sources > exit

Install the operating system - you will be able to select your new vhd later.

Select the disk you just created, there will be more than one to choose from. Choose carefully. Ignore the warning that you can't use the disk.

Continue with the installation. 

When the installation is finished, remove the dvd from the drive and restart your machine. You will be able to choose between Windows 7 and Windows 2008R2.

Saturday, June 26, 2010

Migrating to SharePoint 2010

Recently I've been migrating farms from SharePoint 2007 to 2010 using the attach database method. In this post I'm going to outline the steps I've used to perform the upgrade. I'm assuming SharePoint 2010  has been set up and that the visual compatibility mode will be used. 

Clean Up Your Current Environment 
Remove unused site collections, features, etc. Run "stsadm -o preupgradecheck" to identify potential problems.

Find the Customizations
Your disaster recovery plan will really come in handy here. If you don't have a disaster recovery plan, this will be a good start for one. Go to central administration and note your settings. They will need to be used to configure your new farm. Find the custom code and files that have been deployed to your farm. If solutions have been used to deploy customizations, they can be used to deploy to the new farm. Make copies of the web.config files, many of the custom settings have been made by hand. Think safe controls, http modules, application settings, connection strings. Run a comparison between your 12 hive and an oob 12 hive to find any differences. Check IIS for any folders or applications that have been added.

Copy your Content Databases
The databases can be detached and copied or backups can be taken. Copy the .mdf files to the new SQL Server.

Install Customizations to the new 2010 Farm
Install your solutions and features, move files into the 14 hive, edit the web.config files... Create new Web Applications for the ones you’re moving. Delete the configuration databases that are created.  

Attach the Content Databases to the new SQL Server 


Test Content Database against the Web Applications
Run Powershell command:     Test-SPContentDatabase -Name "contentdbname" -WebApplication "http://webapplicationurl"  This will give you the guids of missing features and let you know if there are any errors that would stop the upgrade.  

Attach the Content Database to the farm
Run Powershell command:     Mount-SPContentDatabase -Name "contentdbname" -WebApplication "http://webapplicationurl"

Test the upgraded farm

Wednesday, May 12, 2010

Copy DLL from GAC

From time to time you will need to copy a dll from the GAC or assembly. The easiest way I've found to copy the file is to open a command prompt and enter the following command.

           SUBST M: C:\Windows\Assembly

This will create a new drive M: that you can use to copy the file. 

Monday, May 10, 2010

Content Query Web Part - Common Enhancements

The content query web part affectionately known as CQWP is one of the go to oob SharePoint components. It's really great at rollups on content types within a site collection. The CQWP is built with xslt so this post will focus on modifications to "ItemStyle.xsl" or your own custom item style file. Be careful of white spaces and line returns when formatting the XSLT. A couple of tools that will help a great deal with the CAML and fields are U2U CAML query Builder, SharePoint Manager and SharePoint designer.

Use additional fields

First, export the webpart and open it in notepad. Edit the CommonViewFields property. The fields are formatted like this "internal name","type"; After adding the fields, save the file and import it back into the site. Test and make sure the web part still works. If not check names, types, white space and new lines.
   <property name="CommonViewFields" type="string">Sorted1,Text;Status,Text;CDate,Date;
   </property>

Formatting Dates

Add the highlighted line to the xsl:stylesheet tag.
<xsl:stylesheet
  version="1.0"
  exclude-result-prefixes="x d xsl msxsl cmswrt"
  xmlns:x="http://www.w3.org/2001/XMLSchema"
  xmlns:d="http://schemas.microsoft.com/sharepoint/dsp"
  xmlns:cmswrt="http://schemas.microsoft.com/WebParts/v3/Publishing/runtime"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt"
  xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime">

You can then format the date like this.
<xsl:value-of select="ddwrt:FormatDate(string(@CDate) ,1033 ,1)"/>

Headers and Footers

There are a number of posts out there on this subject but this is what has worked for me and has been the least painful to develop. First create variables for the header and footer and then use them at the top and bottom of the template.
   <xsl:variable name="HEADER">
     <xsl:if test="count(preceding-sibling::*)=0">
          <![CDATA[<table border="0" cellspacing="0" width="100%">
                <tr>
                <th>Header1</th>
                <th>Header2</th>
                <th>Header3</th>                </tr>]]>
      </xsl:if>
   </xsl:variable>

   <xsl:variable name="FOOTER">
     <xsl:if test="count(following-sibling::*)=0">
         <![CDATA[ </table> ]]>
      </xsl:if>
   </xsl:variable>
       You can then use them like this.
<xsl:value-of select="$HEADER" disable-output-escaping="yes"/>
<xsl:value-of select="$FOOTER" disable-output-escaping="yes"/> 

Only Show the Most Recent Entry

This is common requirement when rolling up status reports. This technique requires that the list items be in the correct order. To ensure that the list items are in the correct order the web part itself needs to be modified. To do this export the webpart and open it in notepad. Edit the QueryOverride property with the correct CAML statements. Save the changes and import the web part. (Any valid CAML can be placed in the QueryOverride tag.)
   <property name="QueryOverride" type="string"><![CDATA[<OrderBy><FieldRef Name="Sorted1"
     Ascending="True"/><FieldRef Name="CDate" Ascending="False"/></OrderBy>]]></property>
Now that the data is in the correct order, only show the first record. First create a variable that contains the value of the unique field. Then use the variable to get a count of records before the current record that contains the unique value.
   <xsl:variable name="UniqueField" select="normalize-space(@Sorted1)" />
   <xsl:variable name="CountUniqueField"
        select="count(preceding-sibling::*[@*['Sorted1']=$UniqueField])" />     
Wrap your detail html with the following if statement.
<xsl:if test="$CountUniqueField &lt; 1">
     <!-- Your xslt & html here --->
</xsl:if>

Format as KPI/Stop Light/Green Yellow Red

Here the status field is compared to green/yellow/red and the src attribute of the <img> tag is set appropriately.
   <img>
      <xsl:attribute name="src">
         <xsl:if test="normalize-space(@status) = 'Green'">/images/green.gif</xsl:if>
         <xsl:if test="normalize-space(@status) = 'Yellow'">/images/yellow.gif</xsl:if>
         <xsl:if test="normalize-space(@status) = 'Red'">/images/red.gif</xsl:if>
      </xsl:attribute>
   </img>


It looks like this all put together

    <xsl:template name="CustomItemStyle" match="Row[@Style='CustomItemStyle']"
    mode="itemstyle">
       
        <xsl:variable name="SafeLinkUrl">
              <xsl:call-template name="OuterTemplate.GetSafeLink">
                   <xsl:with-param name="UrlColumnName" select="'LinkUrl'" />
              </xsl:call-template>
        </xsl:variable>
        <xsl:variable name="DisplayTitle">
              <xsl:call-template name="OuterTemplate.GetTitle">
                <xsl:with-param name="Title" select="normalize-space(@Title)" />
                <xsl:with-param name="UrlColumnName" select="'LinkUrl'" />
              </xsl:call-template>
        </xsl:variable>
        <xsl:variable name="LinkTarget">_blank</xsl:variable>
        <xsl:variable name="UniqueField" select="normalize-space(@Sorted1)" />
        <xsl:variable name="CountUniqueField"
               select="count(preceding-sibling::*[@*['Sorted1']=$UniqueField])" />
         
        <xsl:variable name="HEADER">
            <xsl:if test="count(preceding-sibling::*)=0">
                 <![CDATA[<table border="0" cellspacing="0" width="100%">
                     <tr>
                       <th>Link To Item</th>
                       <th>Status</th>
                       <th>Date</th>           
                    </tr>
                 ]]>
           </xsl:if>
       </xsl:variable>

       <xsl:variable name="FOOTER">
           <xsl:if test="count(following-sibling::*)=0">
                <![CDATA[ </table> ]]>
           </xsl:if>
       </xsl:variable>

       <xsl:value-of select="$HEADER" disable-output-escaping="yes"/>
  
       <xsl:if test="$CountUniqueField &lt; 1">
           <tr>
              <td class="ms-vb">
                <xsl:call-template name="OuterTemplate.CallPresenceStatusIconTemplate"/>
                  <a href="{$SafeLinkUrl}" target="{$LinkTarget}" title="{@LinkToolTip}"
                   style="font-size:12px;font-weight:bold;">
                       <xsl:value-of select="$DisplayTitle" />
                  </a>
              </td>
                       
              <td class="ms-vb">
                 <img>
                     <xsl:attribute name="src">
                         <xsl:if test="normalize-space(@status) = 'Green'">
                            /images/green.gif</xsl:if>
                         <xsl:if test="normalize-space(@status) = 'Yellow'">
                            /images/yellow.gif</xsl:if>
                         <xsl:if test="normalize-space(@status) = 'Red'">
                            /images/red.gif</xsl:if>
                     </xsl:attribute>
                  </img>
              </td>

              <td class="ms-vb">
                 <xsl:value-of select="ddwrt:FormatDate(string(@CDate) ,1033 ,1)"/>
              </td>

           </tr>
  
       </xsl:if>

       <xsl:value-of select="$FOOTER" disable-output-escaping="yes"/>

    </xsl:template>

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; }
        }