Using the SPGridView and SPMenuField objects go a long way to encapsulate the functionality and look & feel of SharePoint into your custom controls, pages and web parts. This post describes a specific desired functionality, the obstacles encountered, and the way to get these controls to do what you want. For a detailed description of the basics of getting the SPGridView and SPMenuField working, check out these twoposts by POWLO, I highly recommend them.
Goal: Create an SPGridView with an SPMenuField that allows grouping, with one of the menu items posting back to the page on which the SPGridView is instantiated.
That sounds like a lot so let’s take it step by step and work through each piece, gradually adding the next layer of complexity. To flesh out the scenario imagine you are a large organization, with several hundred sub-units and occasionally these sub-units become inactive and you want to archive them. Call our sub-units stores. Also imagine that a record of these stores is kept in a table with the following schema:
CREATE TABLE [dbo].[blog_post_1](
[ID] [int] IDENTITY(1,1) NOT NULL,
[StoreName] [nvarchar](50) NOT NULL,
[StateName] [nvarchar](25) NOT NULL,
[IsArchived] [bit] NOT NULL)
The first thing we want to do is just display all of our stores in a list, with the store name link to an editor page and a menu field also linking to the same editor page. Do not enable grouping yet, we get to that later.
<asp:SqlDataSource ID="sqlData" runat="Server" ConnectionString="Server=127.0.0.1;Database=dev;Uid=;Pwd=;"
SelectCommand="SELECT * FROM blog_post_1" SelectCommandType="Text" />
<SharePointWebControls:MenuTemplate ID="spmStoreMenu" runat="server">
<SharePointWebControls:MenuItemTemplate Text="Edit Store" ID="miEditStore" runat="server"
Description="Manage this store" ImageUrl="/_layouts/images/actionseditpage.gif"
Sequence="1" ClientOnClickNavigateUrl="StoreManager.aspx?ID=%ID%" />
</SharePointWebControls:MenuTemplate>
<table width="300">
<tr>
<td>
<SharePointWebControls:SPGridView ID="spgvGrid" runat="server" DataKeyNames="ID"
DataSourceID="sqlData" AllowGrouping="false" AutoGenerateColumns="false" GroupField="StateName"
AllowGroupCollapse="true" DisplayGroupFieldName="false">
<Columns>
<SharePointWebControls:SPMenuField NavigateUrlFields="ID" NavigateUrlFormat="StoreManager.aspx?ID={0}"
TextFields="StoreName" MenuTemplateId="spmStoreMenu" HeaderText="Store" TokenNameAndValueFields="ID=ID" />
<SharePointWebControls:SPBoundField DataField="IsArchived" HeaderText="Status" />
</Columns>
</SharePointWebControls:SPGridView>
</td>
</tr>
</table>
Done. We now have a spiffy looking grid and our field links to an edit page so each store can be edited and we have a menu item. Now let’s get the menu setup with a new item performing a post back to the instantiating page to set the store to archived. The page needs to implement System.Web.UI.IPostBackEventHandler and you will need to come up with a scheme for your post back event args, but that would be a topic for another post.
Add the following to your basic page:
<asp:SqlDataSource ID="sqlData" runat="Server" ConnectionString="Server=127.0.0.1;Database=dev;Uid=;Pwd=;"
SelectCommand="SELECT * FROM blog_post_1" SelectCommandType="Text" />
<SharePointWebControls:MenuTemplate ID="spmStoreMenu" runat="server">
<SharePointWebControls:MenuItemTemplate Text="Edit Store" ID="miEditStore" runat="server"
Description="Manage this store" ImageUrl="/_layouts/images/actionseditpage.gif"
Sequence="1" ClientOnClickNavigateUrl="StoreManager.aspx?ID=%ID%" />
<SharePointWebControls:MenuItemTemplate Text="Archive" ID="miArchiveStore" runat="server"
Description="Mark this store as archived." ImageUrl="/_layouts/images/plicon.png"
Sequence="2" ClientOnClickUsingPostBackEvent="__page,ARCHIVE::%ID%" />
</SharePointWebControls:MenuTemplate>
<table width="300">
<tr>
<td>
<SharePointWebControls:SPGridView ID="spgvGrid" runat="server" DataKeyNames="ID"
DataSourceID="sqlData" AllowGrouping="true" AutoGenerateColumns="false" GroupField="StateName"
AllowGroupCollapse="true" DisplayGroupFieldName="false">
<Columns>
<SharePointWebControls:SPMenuField NavigateUrlFields="ID" NavigateUrlFormat="StoreManager.aspx?ID={0}"
TextFields="StoreName" MenuTemplateId="spmStoreMenu" HeaderText="Store" TokenNameAndValueFields="ID=ID" />
<SharePointWebControls:SPBoundField DataField="IsArchived" HeaderText="Status" />
</Columns>
</SharePointWebControls:SPGridView>
</td>
</tr>
</table>
A couple of interesting things are happening in the code we just added. The key piece here is the ClientOnClickUsingPostBackEvent attribute and the “__page,_ACTION_;%ID%” value. If you look at the ClientOnClickUsingPostBackEvent method in reflector you can see that it interprets this as being the containing page, and directs the post back there. You can actually send the post back event through any control that implements IPostBackEventHandler, which SPGridView does, opening up the possiblities for some elgant solutions. Anything after the “,” is passed as the eventArgument string to the RaisePostBackEvent. Also note that the TokenNameAndValueFields attribute of the menu field determines the substitution vars (ex: %ID%) available in the menu items.
If you got everything wired up correctly then your IPostBackEventHandler.RaisePostBackEvent method should be getting fired whenever you click on any of the “Archive” menu items and you should be getting as part of the eventArgument the id of each store. Beauty. Now we just have to enable grouping in the SPGridView and test the postback and we are all set.
However after we try a post back with grouping enabled we get an nice, friendly error:
Flying Monkey Buckets, now it renders correctly, but fails (and how) on post back. The only thing we changed from the working version was that we enabled grouping. So what gives? If you look at the error and spend some more time in reflector you will find that the args.Row.DataItem is null when the page tries to load the state, specifically in the GridView.OnRowCreated method. This is not an easy error to get at from the page level, so it turns out the easiest thing to do is create our own SPGridView class and fix it there; and it only takes a few lines of code – so if you ever feel like buying me a beer…. You will need to expose an event, override the LoadControlState method in your custom grid class, and add an event handler to you page.
namespace UserControls { public class CustomGrid : SPGridView { protected override void LoadControlState(object savedState) { base.LoadControlState(savedState); if (this.DataSource == null) { this.InvokeRequiresDataSource(); } } public event EventHandler RequiresDataSource; protected void InvokeRequiresDataSource() { EventHandler handler = this.RequiresDataSource; if (handler != null) { handler(this, new EventArgs()); } } } }
And the entire contents of the test page we ended up with:
<%@ Page Language="C#" Debug="true" MasterPageFile="~masterurl/default.master" Title="Post 1 Example"
meta:progid="SharePoint.WebPartPage.Document" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>
<%@ Assembly Name="System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Web.UI" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Register Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,PublicKeyToken=71e9bce111e9429c"
TagPrefix="SharePointWebControls" Namespace="Microsoft.SharePoint.WebControls" %>
<%@ Implements Interface="System.Web.UI.IPostBackEventHandler" %>
<%@ Register Assembly="CustomGrid, Version=1.0.0.0, Culture=neutral, PublicKeyToken=76ce4e6c43aad137" Namespace="UserControls" TagPrefix="uc" %>
<script runat="server">
void IPostBackEventHandler.RaisePostBackEvent(string eventArgument)
{
//parse the event argument and mark the store as archived.
spgvGrid.DataBind(); //need to rebind the grid.
}
protected void spgvGrid_RequiresDataSource(object sender, EventArgs e)
{
spgvGrid.DataBind();
}
</script>
<asp:Content ID="Content5" ContentPlaceHolderID="PlaceHolderMain" runat="server">
<asp:SqlDataSource ID="sqlData" runat="Server" ConnectionString="Server=127.0.0.1;Database=dev;Uid=;Pwd=;"
SelectCommand="SELECT * FROM blog_post_1" SelectCommandType="Text" />
<SharePointWebControls:MenuTemplate ID="spmStoreMenu" runat="server">
<SharePointWebControls:MenuItemTemplate Text="Edit Store" ID="miEditStore" runat="server"
Description="Manage this store" ImageUrl="/_layouts/images/actionseditpage.gif"
Sequence="1" ClientOnClickNavigateUrl="StoreManager.aspx?ID=%ID%" />
<SharePointWebControls:MenuItemTemplate Text="Archive" ID="miArchiveStore" runat="server"
Description="Mark this store as archived." ImageUrl="/_layouts/images/plicon.png"
Sequence="2" ClientOnClickUsingPostBackEvent="__page,ARCHIVE::%ID%" />
</SharePointWebControls:MenuTemplate>
<table width="300">
<tr>
<td>
<uc:CustomGrid ID="spgvGrid" runat="server" DataKeyNames="ID" DataSourceID="sqlData"
AllowGrouping="true" AutoGenerateColumns="false" GroupField="StateName" AllowGroupCollapse="true"
DisplayGroupFieldName="false" OnRequiresDataSource="spgvGrid_RequiresDataSource">
<Columns>
<SharePointWebControls:SPMenuField NavigateUrlFields="ID" NavigateUrlFormat="StoreManager.aspx?ID={0}"
TextFields="StoreName" MenuTemplateId="spmStoreMenu" HeaderText="Store" TokenNameAndValueFields="ID=ID" />
<SharePointWebControls:SPBoundField DataField="IsArchived" HeaderText="Status" />
</Columns>
</uc:CustomGrid>
</td>
</tr>
</table>
</asp:Content>
Now give it a shot and your page should postback without problem. You will need to do the same trick if you are creating a callback menu item, but that is another post.