SharePoint Custom WebParts and list views

It looks as though I’m on a sharepoint-roll; this time I will explain how I put together a custom web part which essentially extends the existing functionality of a list view (SPListView) to execute a custom filter, which in my case required the list view to display only items made by the current user AND users in the same group as the current user (based on a group filter). So for example, let’s say you have a list of grades for a class that has group assignments. I want the list view to display only my own grades and my groupmates’ grades but ignore all others (not as a security thing, but as just a filter thing since I’d have read only access to all the grades in this example).

Starting Off

Add a new “Visual Web Part” to your solution. I think a plain old regular “Web Part” would work as well, but in the examples I had seen on the web it required a visual one, despite not making use of the “ascx” files. Open the feature in the project and add your web part to it – this will be required or else your web part will not show up in the web part gallery on SharePoint. Customize your web part’s name and description as needed then dive right into the override CreateChildControls method body. You will notice that by default there’s some code added for you to create a control based on the ASCX file in your project – remove that code, start with a blank method body. You can remove the privately-scoped variable referencing the ASCX file as well.

Custom Properties

You’ll want your web part to be customizable, so we need to add some properties to it. In my case, I wanted the list name, list view name, and a group prefix to filter on to be customizable. The following code will allow for this. Note that we have to actually define the privately scoped variables and update them, and have a default value or else the web part may cause problems when initially added to a page.

private string listName = "Docs";
[WebBrowsable(true),WebDescription("Set the list to get data from"),WebDisplayName("List Name"),Personalizable(PersonalizationScope.User)]
public string ListName
{
    get { return listName; }
    set { listName = value; }
}

private string listView = "";
[WebBrowsable(true), WebDescription("Set the name of the list view to use"), WebDisplayName("List View"), Personalizable(PersonalizationScope.User)]
public string ListView
{
    get { return listView; }
    set { listView = value; }
}

private string groupFilterPrefix = "";
[WebBrowsable(true), WebDescription("Filter based on this group name prefix"), WebDisplayName("Group Name Prefix"), Personalizable(PersonalizationScope.User)]
public string GroupFilterPrefix
{
    get { return groupFilterPrefix; }
    set { groupFilterPrefix = value; }
}

Now that we have our properties, we can start building the method body and make use of them.

Method Body

In summary you’ll want to do the following:

  1. Call CreateChildControls on the base
  2. Get the site and collection
  3. Create a blank new ListView control
  4. Set up the properties of the list view control as required
  5. Get the current user and all the users in the groups matching the ones of the current user (this is required because items only store the name of the user that created them, they do not store that user’s group – as a result, we will need to know all the relevant users we want to see data from, not just the groups they would be in).
  6. Update the existing selected view by querying it for only users we are interested in
  7. Display the results

Here’s the code I have for that:

private Microsoft.SharePoint.WebPartPages.ListViewWebPart lvSPListViewControl;
protected override void CreateChildControls()
{
	base.CreateChildControls();
	SPSite oSiteCollection = SPContext.Current.Site;
	SPWeb oWebSite = SPContext.Current.Web;
	lvSPListViewControl = new Microsoft.SharePoint.WebPartPages.ListViewWebPart();
	lvSPListViewControl.Visible = true;
	lvSPListViewControl.EnableViewState = true;
	SPList list = oWebSite.Lists[ListName];

	lvSPListViewControl.ListName = list.ID.ToString("B").ToUpperInvariant();
	lvSPListViewControl.WebId = list.ParentWeb.ID;

	lvSPListViewControl.ListId = (System.Guid)list.ID;

	SPView webPartView;

	if (ListView.CompareTo("") != 0)
	{
	webPartView = list.Views[ListView];
	}
	else
	{
	webPartView = list.DefaultView;
	}
	lvSPListViewControl.TitleUrl = webPartView.Url;

	lvSPListViewControl.HelpMode = WebPartHelpMode.Modeless;

	//get current user.
	SPUser currUser = oWebSite.CurrentUser;

	// Create a dictionary of users for which we want to see list items
	Dictionary<string,SPUser> usersToView = new Dictionary<string,SPUser>();

	// Always show the own user's items
	usersToView.Add(currUser.LoginName, currUser);

	//get all groups that the current user is a member
	if (currUser.Groups.Count > 0)
	{

	foreach (SPGroup g in currUser.Groups)
	{
		if (g.Name.Length >= groupFilterPrefix.Length && g.Name.Substring(0, groupFilterPrefix.Length) == groupFilterPrefix)
		{
		foreach(SPUser u in g.Users) {
			if(!usersToView.ContainsKey(u.LoginName)) {
			usersToView.Add(u.LoginName,u);
			}
		}
		}
	}

	SPUser[] userArray = new SPUser[usersToView.Values.Count];
	usersToView.Values.CopyTo(userArray, 0);

	//generate CAML query based on user groups.
	webPartView.Query = buildQuery(userArray);
	oWebSite.AllowUnsafeUpdates = true;
	webPartView.Update();
	oWebSite.AllowUnsafeUpdates = false;
	}

	lvSPListViewControl.ViewGuid = webPartView.ID.ToString("B").ToUpperInvariant();
	Controls.Add(lvSPListViewControl);
}
private string buildQuery(SPUser[] users)
{
	StringBuilder result = new StringBuilder();
	result.Append("");
	int totalUsers = users.Length;
	for(int i=0;i<users.Length;i++) {
	if (i < totalUsers - 1) result.Append("");
	result.AppendFormat("{0}", users[i].Name);
	}
	for (int i = 0; i < users.Length-1; i++)
	{
	result.Append("");
	}
	result.Append("");
	return result.ToString();
}

Note that for the CAML query we can’t use a logical operator on more than two operands. As a result, we have to chain them together as a binary tree. Here are CAML queries to find where the item was created by one, two, or three users:

One User

<Where><Eq><FieldRef Name="Author" /><Value Type="User">MyFirstUser</Value></Eq></Where>

Two Users

<Where><Or><Eq><FieldRef Name="Author" /><Value Type="User">MyFirstUser</Value></Eq><Eq><FieldRef Name="Author" /><Value Type="User">MySecondUser</Value></Eq></Or></Where>

Three Users

<Where><Or><Eq><FieldRef Name="Author" /><Value Type="User">MyFirstUser</Value></Eq><Or><Eq><FieldRef Name="Author" /><Value Type="User">MySecondUser</Value></Eq><Eq><FieldRef Name="Author" /><Value Type="User">MyThirdUser</Value></Eq></Or></Or></Where>

If you’re getting a generic SPException thrown when doing GetItems (or trying to check the ‘Count’ on the resulting SPListItemCollection) then it probably means your CAML query is invalid. You can check that from PowerShell by using Get-SPSite to get your site, finding the list in the resulting object’s “Lists” property – this will have all the views and items. Take a view you’re interested in, then update it’s query property and call list.GetItems(query). If you’re getting errors with the same CAML query as you’re using in your code then you know it is the query to blame.

Closing Thoughts

The generic SPException from the invalid CAML query was by far the most annoying part of figuring this out. The fact that there was no error message of any kind made it that much more confusing than it should have been.

0 Comments

Leave a Reply

Your email address will not be published.