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.
Wednesday, May 12, 2010
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.
It looks like this all put together
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 < 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>
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">
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: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>
<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:variable name="CountUniqueField"
select="count(preceding-sibling::*[@*['Sorted1']=$UniqueField])" />
<xsl:if test="$CountUniqueField < 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>
<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 < 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.
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.
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" />
(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.
Labels:
owssvr.dll jQuery autocomplete
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
C:\>mstsc -v:MachinenameOrIP /F -admin
Labels:
Remote Desktop Connection
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.
Here's the CreateChildControls method where everything is put together.
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;
}
}
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);
}
{
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);
}
Labels:
C#,
MOSS,
Navigation,
PortalSiteMapProvider,
SharePoint 2007,
SiteMapDataSource,
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
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.
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
Labels:
PowerShell,
SharePoint 2010
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".
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.
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.
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(','));
}
}
{
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;
}
{
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; }
}
{
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; }
}
Labels:
Multi Filter Sort,
ObjectDataSource,
SpGridView,
WebPart
Subscribe to:
Posts (Atom)




