Adding Breadcrumb Navigation to SharePoint 2010 Application Pages

The application pages in my SharePoint 2010 FBA Pack were not properly displaying the breadcrumb navigation. They didn’t include ‘Site Settings’ in the navigation – they went straight from Home to the application page.

FBA User Management Title Before Changes

I used the following blog entry to add the breadcrumb navigation:

http://weblogs.asp.net/jan/archive/2008/04/16/adding-breadcrumb-navigation-to-sharepoint-application-pages-the-easy-way.aspx

Unfortunately after making the changes, I still didn’t see a single change on the page.  I found a few other people on the forums wondering how to do breadcrumbs in SharePoint 2010, but nobody with a solution.  I decided to dig into the out of the box Site Settings application pages and see how they did it, as their breadcrumb navigation was displaying flawlessly, with the Site Settings as part of the navigation:

Site Columns Application Page Title

Looking at the Site Columns application page, mngfield.aspx, I realized the problems.  First, the SharePoint 2010 master page v4.master uses SPSiteMapProvider and SPContentMapProvider for the breadcrumb site map providers. These don’t build the breadcrumb from the layouts.sitemap file that define the breadcrumb in 2007.  The mngfield.aspx application page overrides v4.master and uses SPXmlContentMapProvider for the site map provider, which does read from the layouts.sitemap file. The second thing the out of the box application page does differently is override the PlaceHolderPageTitleInTitleArea content and hard codes the breadcrumb navigation. What I had mistaken for the bread crumb navigation was actually the title area.  The breadcrumb navigation in SharePoint 2010 is accessed with the ‘Navigate Up’ folder icon.

SharePoint 2010 Breadcrumb Control

SharePoint 2010 Title Area

So here are the steps required to get breadcrumb navigation working in SharePoint 2010:

SPFarm.Local.Services.GetValue<SPWebService>().
                    ApplyApplicationContentToLocalServer();
  • Add the following PlaceHolderTitleBreadcrumb section to your application page:
<asp:Content contentplaceholderid="PlaceHolderTitleBreadcrumb" runat="server">
  <SharePoint:UIVersionedContent UIVersion="3" runat="server"><ContentTemplate>
	<asp:SiteMapPath
		SiteMapProvider="SPXmlContentMapProvider"
		id="ContentMap"
		SkipLinkText=""
		NodeStyle-CssClass="ms-sitemapdirectional"
		RootNodeStyle-CssClass="s4-die"
		PathSeparator="&#160;&gt; "
		PathSeparatorStyle-CssClass = "s4-bcsep"
		runat="server" />
  </ContentTemplate></SharePoint:UIVersionedContent>
  <SharePoint:UIVersionedContent UIVersion="4" runat="server"><ContentTemplate>
	<SharePoint:ListSiteMapPath
		runat="server"
		SiteMapProviders="SPSiteMapProvider,SPXmlContentMapProvider"
		RenderCurrentNodeAsLink="false"
		PathSeparator=""
		CssClass="s4-breadcrumb"
		NodeStyle-CssClass="s4-breadcrumbNode"
		CurrentNodeStyle-CssClass="s4-breadcrumbCurrentNode"
		RootNodeStyle-CssClass="s4-breadcrumbRootNode"
		HideInteriorRootNodes="true"
		SkipLinkText="" />
  </ContentTemplate></SharePoint:UIVersionedContent>
</asp:Content>
  • Replace your PlaceHolderPageTitleInTitleArea section with the following:
<asp:Content ContentPlaceHolderId="PlaceHolderPageTitleInTitleArea" runat="server">
	<a href="settings.aspx"><SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,settings_pagetitle%>" EncodeMethod="HtmlEncode"/></a>&#32;<SharePoint:ClusteredDirectionalSeparatorArrow runat="server" />
	My Application Page Title
</asp:Content>

After those changes my application pages display as they should, with the proper breadcrumb navigation:

FBA User Management Title Area After

FBA User Management Breadcrumbs After

Bulk Delete SharePoint Site Users with PowerShell

Below is a PowerShell script for deleting a filtered list of users from a SharePoint site.  Simply copy the script to a .ps1 file, adjust the $SITEURL to the url of the site and adjust the $USERNAMEFILTER to a lowercase string that is contained in all of the usernames you would like to delete.

The script is based on a combination of the scripts from:

http://blogs.msdn.com/b/vijgang/archive/2009/04/26/powershell-script-to-remove-all-users-from-a-sharepoint-group.aspx

and

http://nikspatel.wordpress.com/2010/08/10/delete-orphaned-ad-users-from-the-site-collection/

################################################################################################################

[System.Reflection.Assembly]::Load("Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c")
[System.Reflection.Assembly]::Load("Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c")
[System.Reflection.Assembly]::Load("Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c")
[System.Reflection.Assembly]::Load("System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")

###########################
# "Enter the site URL here"
$SITEURL = "http://demo2010a:2114"

# "Enter the username filter (lowercase) here"
$USERNAMEFILTER = "member"

###########################

$site = new-object Microsoft.SharePoint.SPSite ( $SITEURL )
$web = $site.OpenWeb()
"Web is : " + $web.Title

$usersToDelete = @()

# Iterate through the site's users and add usernames to
# an array of users to delete if the name contains
# contains the username filter.
foreach ($user in $web.SiteUsers)
{
	if ($user.LoginName.ToLower().Contains( $USERNAMEFILTER ))
	{
		$usersToDelete += $user.LoginName
	}
}

# Delete each user selected from the SiteUsers array.
# The SiteUsers array can't be iterated through directly
# as it gets changed as users are removed from it.
foreach ($user in $usersToDelete)
{
	"Removing user : " + $user
	$web.SiteUsers.Remove($user);
}

$web.Update();

################################################################################################################

Bulk Delete ASP.NET Membership Users

Below is a SQL script that can be used to delete a list of users from an ASP.NET membership database.  It retrieves a list of users into a temporary table and then deletes the users from all the relevant tables in the database.

SELECT UserID, UserName
into #temp
FROM aspnet_Users
WHERE UserName in ('MEMBER10001','MEMBER10002','MEMBER10003','MEMBER10004','MEMBER10005')

-- Adjust the WHERE Clause to filter the users
-- for example WHERE UserName LIKE 'MEMBER%'
-- would delete all users whose username started with 'MEMBER'

DELETE FROM dbo.aspnet_Membership WHERE UserId IN (Select UserId from #temp)

DELETE FROM dbo.aspnet_UsersInRoles WHERE UserId IN (Select UserId from #temp)

DELETE FROM dbo.aspnet_Profile WHERE UserId IN (Select UserId from #temp)

DELETE FROM dbo.aspnet_PersonalizationPerUser WHERE UserId IN (Select UserId from #temp)

DELETE FROM dbo.aspnet_Users WHERE UserId IN (Select UserId from #temp)

Mixed Anonymous and Secure Content with SharePoint 2010

In this tutorial i’m going to step you through how to set up a SharePoint 2010 publishing portal with both anonymous and secure content.

1. Create a new Web Application in Central Admin. Make sure that you select ‘Yes’ for Allow Anonymous.  All other values can remain at the defaults. For this example I’m going to use FBA Claims based authentication.

2. Using central admin, create a site collection on the web application you just created.  In this example I’m creating a publishing site.

3. Visit the site you just created. You will be asked to authenticate.  This is because even though ‘Allow Anonymous’ was turned on at the web application level, it still has to be specified at the site level. You will be shown the default home page.

4. First we’ll create a new Home page that will be the page anonymous users will see when they first access the site.  From Site Actions, choose New Page and call it “Home”. When the new page appears, just type in a quick welcome message and save it.

5. With a publishing site the content will need to be both published and approved before it will be visible by anonymous users. To publish the page, click ‘Submit’ on the ‘Publish’ tab and follow the wizard.  Once the submission is complete, approve the page by clicking ‘Approve’ on the ‘Publish’ tab and follow the wizard.  Note that to approve the page, you will first have to add your user to the ‘Approvers’ group under ‘People and Groups’ in ‘Site Settings’. The page is now published, and will be visible to anonymous users once we enable anonymous access.

Note that any resources that have been added to the page, such as images from the Images library, will also have to be published and approved before they can be viewed by anonymous users.

6. Under ‘Site Settings’, ‘Welcome Page’ set the welcome page to be the new ‘Home’ page we just created.

Now when we got to our site root page, we’ll be redirected to the ‘Home’ page we just published. If you sign out and visit Pages/Home.aspx, you’ll notice that you’ll be prompted to enter your credentials, so we still need to enable anonymous access to the page.

7. Sign back in to the site.  Go to ‘Site Settings’, ‘Site Permissions’. Click on ‘Anonymous Access’. From the ‘Anonymous users can access: ‘ dialog, choose ‘Lists and Libraries’ and click OK. Alternatively you can click ‘Entire Web Site’, which will make everything available anonymously, however I prefer to define exactly which resources have anonymous access.  By choosing ‘Lists and Libraries’ your entire web site is still secured.  You have to directly configure each List/Document Library to enable anonymous access. If you sign out and visit Pages/Home.aspx, you will notice that you will still be prompted to authenticate to view the page.

8. Now anonymous access needs to be enabled on the ‘Pages’ library. Go to ‘View all site content’, ‘Pages’. Click ‘Library Permissions’ on the ‘Library’ tab. Click ‘Stop Inheriting Permissions’. Click ‘Anonymous Access’. Select ‘View Items’ in the ‘Anonymous Access’ dialog and click OK. Now all of the content in the ‘Pages’ library will be accessible anonymously.  Note that you can ‘Manage Permissions’ and ‘Stop Inheriting Permissions’ for individual items (including folders) in the library, if you want to prevent them from having anonymous access. Unfortunately you cannot enable anonymous access on individual items, only the whole library.

Now if you sign out of the site and visit Pages/Home.aspx, you’ll be able to view it and won’t be prompted to authenticate.

9. There’s another problem you might notice.  Home.aspx is our default page, and allows anonymous access, however if we visit the site root we’re still prompted to authenticate.  This is because we chose the more secure option of allowing anonymous access on ‘Lists and Libraries’ instead of the ‘Entire Web Site’. PowerShell needs to be used to allow anonymous access to the site while keeping the ‘Lists and Libraries’ settings. Run the following PowerShell commands (based on instructions from http://stackoverflow.com/questions/1338809/anonymous-access-to-a-sharepoint-site-root) – substitute your own SPWeb address:


[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
$web = Get-SPWeb http://demo2010a:42076
$web.AnonymousState = [Microsoft.SharePoint.SPWeb+WebAnonymousState]::Enabled
$web.AnonymousPermMask64 = "Open, ViewPages"
$web.Update()

Note that if you look at ‘Anonymous Access’ under ‘Site Permissions’, it will say ‘Entire Web Site’ – however you will still need to explicitly specify which lists and libraries have anonymous access, just as if it was set to ‘Lists and Libraries’.

Now if you sign out and visit the root of the site, you should be redirected to Pages/Home.aspx and not asked to authenticate. We finally have controlled anonymous access!

10. The other part of this tutorial is to show you how to have secure content mixed in with your anonymous content.  Now that you can control which content is available anonymously, adding secure content is very straightforward.  I had mentioned earlier that you can secure content within a library with anonymous access by clicking ‘Stop Inheriting Permissions’.  The problem with this is that by default a document will have anonymous access, and you will have to specify exactly which documents are secure.  For that reason it’s better to keep secure pages in their own library with anonymous access turned off.  For this tutorial we’re going to create a child site called ‘User’.  It’s ‘Pages’ library will not allow anonymous access, so all of it’s content will be secured.

Select ‘Site Actions’,’New Site’ and create a new ‘Publishing Site with Workflow’ called ‘User’. Click Create.

11. Edit the default page and add some custom content. Save, Publish and Approve the page.

As the new site’s Pages library is secured by default, nothing additional has to be done to secure it.  Notice that if you sign out and visit the site root, you can still access Pages/Home.aspx (although you won’t see a link to the User site). If you attempt to access ‘User/Pages/default.aspx’, you will be asked to authenticate.

Congratulations! You now have a SharePoint 2010 site with both mixed and secure content.

If you’d like to learn how to access your mixed content over both http and https see: Mixed Http and Https Content with SharePoint 2010

Simplify Your Caching By Using a Generic Method

I needed to add caching to my data access routines to improve the performance of commonly accessed data.  This is pretty straightforward:

  • Check to see if the cache entry already exists
  • If the cache entry does exist, return the cached value
  • If the cached entry doesn’t exist, fetch the data and add it to the cache.

The problem was, I had a lot of data access routines that I wanted to add caching to.  Adding the 4 or 5 lines of code needed to do this to each routine would be tedious, and all of the repetition would be messy.  I knew I had to centralize my caching functionality in one place.  Since my data access routines returned many different types, I decided to use a generic method to perform the caching.  Since the data validation routines essentially had to be wrapped by this function (so that it’s only run if the value doesn’t exist in the cache), I call it using a statement lambda.

Here’s what I came up with:
//I use ASP.Net's cache object.  I get a reference to it with System.Web.HttpRuntime.Cache
//as I won't always be using it in the context of ASP.Net
private static System.Web.Caching.Cache DataCache = System.Web.HttpRuntime.Cache;

public static T CacheFetch<T>(string key, Func<T> fetcher) where T:class
{
	//Don't bother with the caching if the key is null or blank
	if ((key ?? "").Length <= 0)
	{
		return fetcher();
	}

	T result = DataCache[key] as T;

	if (result == null)
	{
		result = fetcher();

		if (result == null)
		{
			DataCache.Remove(key);
		}
		else
		{
			DataCache[key] = result;
		}
	}

	return result;
}

And here’s some examples of how I used it:

Original data access routine:
public static IEnumerable<Trustee> GetTrustees(int infoReturnID)
{
	InfoReturnEntities context = new InfoReturnEntities(new Uri(Common.InfoReturnDataServiceURL));

	IEnumerable<Trustee> trustees = from trustee in context.Trustees
									where trustee.InfoReturnID == infoReturnID
									select trustee;

	return trustees;
}
Updated data access routine:
public static IEnumerable<Trustee> GetTrustees(int infoReturnID)
{
	return Common.CacheFetch<IEnumerable<Trustee>>("Trustees_" + infoReturnID.ToString(), () =>
	{
		InfoReturnEntities context = new InfoReturnEntities(new Uri(Common.InfoReturnDataServiceURL));

		IEnumerable<Trustee> trustees = from trustee in context.Trustees
										where trustee.InfoReturnID == infoReturnID
										select trustee;

		return trustees;
	});
}
And another example:
public static IEnumerable<RevenueRange> GetRevenueRanges()
{
	return Common.CacheFetch<IEnumerable<RevenueRange>>("RevenueRanges", () => {
		InfoReturnEntities context = new InfoReturnEntities(new Uri(Common.InfoReturnDataServiceURL));

		IEnumerable<RevenueRange> revenueRanges = from e in context.RevenueRanges
												  where e.RevenueRangeCD != "?"
												  orderby e.LowerLimit
												  select e;

		return revenueRanges;
	});
}

My method was inspired by this.

Performance Profiling a SharePoint 2010 Project using EQATEC Profiler

I wanted to profile a SharePoint 2010 project i’d been working on, in order to find some of the slower methods so that they could be improved.  I decided to give EQATEC Profiler a try since it had some good recommendations on StackOverflow and was FREE.

EQATEC works by modifying and rebuilding your assemblies.  Since I use Visual Studio 2010’s built in package and deployment features, it needs to be run after the build, but before the project is packaged and deployed to the SharePoint server.

Here’s how I got it working:

  1. Start EQATEC Profiler. Under ‘Application to profile’ select your project’s bin\debug folder. Check-mark all of the assembly’s you would like to profile.
  2. Click ‘Build’.  This will rebuild the project with added profiling bits, as well as add ‘app.eqconfig’ and ‘EQATEC.Profiler.RuntimeFullNet.dll’ to your output folder.
  3.  The EQATEC assembly needs to be added to your project’s package for it to function properly.
    • Open your project in Visual Studio 2010.
    • Open your project’s package and click on the ‘Advanced’ tab.
    • Click ‘Add’ -> ‘Add Existing Assembly…’. Browse and select ‘EQATEC.Profiler.RuntimeFullNet.dll’ from your bin\debug folder.
    • You can leave the default location and selection of ‘Global Assembly Cache’.
    • Click OK.
    • Save the package.



  4. Open the project properties and select the ‘Build Events’ tab.  Add the following command to the Post-Build Commands:
    "C:\Program Files (x86)\EQATEC\EQATECProfiler\EQATECProfilerCmd" -build app.eqconfig

    This will cause your assemblies to be recompiled with EQATEC once the standard build is done.

  5. Deploy your project to SharePoint.
  6. Exercise your project in SharePoint.
  7. In EQATEC, click on the Run tab.  You should see messages that your profiled application has started.
  8. Once SharePoint has “warmed up” and your project is performing normally, click the ‘Reset Counters’ button in EQATEC.  This will reset the profiling so that you can begin collecting data.
  9. Exercise your project in SharePoint. Make sure you exercise all code you’d like profiled.
  10. Back in EQATEC, click the ‘Take Snapshot’ button.  This will create a report of your profiling session.
  11. Under ‘View snapshot reports’ double-click on the report you’d like to view.  This will open the view tab, where you can explore the results of your profiling session and find the slower methods in your project.
  12. Don’t forget when you’re done profiling to remove the EQATEC assembly from your package as well as remove the post-build step.  Leaving them in will decrease your project’s performance.

Search Refinement Panel Web Part “Show More” Link Not Working

I recently discovered that the “Show More” link on the Search Refinement Panel web part had stopped working, seemingly out of the blue.  With a bit of debugging I discovered that the functions defined in the onclick event:


onclick="SearchEnsureSOD();ToggleRefMoreLessFilters(this, true);"

were returning “Object Expected”.  For some reason the functions were no longer defined.

I eventually figured out that the problem was that I had removed the Search Box web part from the page and replaced it with my own custom search box.  Apparently the Search Box web part is required on the same page as the Refinement Panel web part, as it injects JavaScript into the page that is used by the Refinement Panel.  I fixed the problem by adding the Search Box web part back onto the page and hiding it.

Deploying Localized Satellite Assemblies with SharePoint 2010

Localizing web parts in a SharePoint 2010 project is pretty straight forward. Add resource files to your project, set the Deployment Type to AppGlobalResource, and then access them with GetGlobalResourceObject or with the <%$Resources> tag. Unfortunately this only works where you have the HttpContext – so great for web parts, but useless for Timer jobs and event receivers.

In order to access the localized resources when there is no HttpContext you’ll need to access the embedded resource files.  If the resource file build action is “Embedded Resource”, Visual Studio will generate a satellite assembly for each locale that you have a resource file for. If you check your build output folder, you should see something like this:

MyProject.dll Main assembly
fr-FR\MyProject.resources.dll Satellite Assembly
en-US\MyProject.resources.dll Satellite Assembly

Great! So now that we have the resources embedded in our satellite assemblies, we can just access them with Resources.MyResource.Key or ResourceManager.GetString, right? That’s what I thought, but whenever I went to retrieve the resource i’d only ever get the default value, i’d never get the localized value.  After snooping through the WSP file I realized that although the satellite assemblies are built automatically, they are not automatically packaged in the WSP.

In order to access the localized resources, the satellite assemblies need to be manually added to the package:

  1. Open the Package in the SharePoint solution
  2. Select the Advanced Tab
  3. Click Add and select Add Existing Assembly… (Add Existing Assembly from Project Output… does not work)
  4. For the Source Path find and select the satellite assembly you’d like to add
  5. Leave the Deployment Target set to Global Assembly Cache
  6. The location will be automatically set to the name of your satellite assembly.  The locale folder needs to be appended to the location name. For example the fr-FR satellite assembly would read fr-FR\MyProject.resources.dll.
    Adding Satellite Assembly
  7. Click OK (Safe Controls and Satellite Resources do not need to be added)
    Satellite Assembly Added
  8. Repeat for each Satellite Assembly in your project

After deploying the modified package, you should now be able to access your localized resources.

Gotchas

One problem with this solution is that the path to the satellite assemblies is hard coded. So when you switch from a Debug to Release configuration (or vice-versa), you now have to modify each of the satellite assembly entries to point to the new location. Microsoft suggests that if you will be changing configurations often, you should add a post-build step that copies all of the satellite assemblies to a specified folder and then references that copy of the assemblies in the package.

More Info

http://msdn.microsoft.com/en-us/library/gg615453.aspx