Skip to content

Month: March 2011

Talk Technet Episode 13 Now Available

The Talk Technet episode that I did last week on SharePoint is now available as an MP3. You can download it here. It was the first time I’ve done something like it, and definitely had fun with it. I think that we covered a fair bit of ground, and you may find it interesting, if you’re bent that way.

Leave a Comment

Building an Automatic XML Sitemap Generator for your SharePoint Site

Although SharePoint 2010 provides a top notch environment for building corporate web sites, one of the things that it does not do is to generate an XML sitemap file automatically. This is unfortunate, as this type of file is used by the major search engines to help discover content on your site. Luckily, the development tools for SharePoint make this process relatively straightforward. Below, I’ll walk through the process of creating a branded event receiver to rebuild the site map whenever a page is approved.

In order to follow along, you’ll need a copy of Visual Studio 2010 installed on a machine that also has SharePoint Server installed (SharePoint Foundation won’t cut it for this one – we’re using the publishing features). You’ll also need the Visual Studio Tools for SharePoint installed.

If you don’t want to walk through the whole creation process, and just want a site map builder, you can download the solution file from codeplex here. Just note that your web application will need an internet zone for this to work properly.

1. Create an Event Receiver Project

Open Visual Studio and create a new project. Select the SharePoint node, and the Event Receiver project template. Give the Solution and the Project a name, then click OK.

image

The project name will be the name of the SharePoint solution. It can be changed later, but it’s much easier to get it right ahead of time. The next prompt will ask for the debugging site, and whether this is a farm, or a sandbox solution. The debugging site will need to have the publishing features enabled (this can be done later, and the debugging site can be changed through the Project Properties). Select a farm solution and click Next.

The next screen will ask what type of event receiver that you want to build. The available options are a function of what is available in your debugging site (chosen previously). For example, the Pages library will not be an option for the event source if the Publishing infrastructure has not been enabled. For our purposes, we want this to un whenever a page in the Pages library has been updated. Therefore we select the type to be List Item Events, the source to be a Pages library, and the event to be “An item was updated”.

image

Click Finish when done. The system will create a feature and an event receiver for you.

2. Make Any Branding and Name Changes

This is not absolutely necessary, but the first thing that I like to do is to change my assembly name and my root namespace to distinguish the work done by my organization from any other things installed. To do this, you open the project properties page, click the Application tab, and change them there.

image

The Assembly Name controls the file name of the DLL that is generated, and the root namespace controls where your classes are found in the .Net Framework. Unfortunately changing the root namespace does not update the assembly references in the project, and if you attempt to debug the project at this point, you will receive this oh so helpful error:

“Error occurred in deployment step ‘Activate Features’: Operation is not valid due to the current state of the object.”

What you need to do is to update all references to the old namespace in the project. Specifically, the Elements.xml file in the event receiver folder needs the correct starting namespace. Open the file for editing and replace the old assembly name with your new one.

image

Save the file, and close it if you wish,but we will be coming back to it.

Next, we want to name our feature. The feature will have an internal name that is used when it is referred to programmatically (through powershell, sysadm, etc) and a display name (title), that will be used in the UI. First we’ll modify the internal name. The easiest way to do this is to open the Features folder, and rename the Feature1 node. We’ll call our feature xmlSiteMapBuilder.

The tools are smart about renaming everything in the features folder. Next, double click on the feature node (in our case, xmlSiteMapBuilder). This opens the feature designer. All that we need to do here is to change the title, the description and the scope. The first two are cosmetic (but important!). However, we want our event receiver to run on all pages in the site collection, so we need to change its scope from Web to Site.

image

At this point, it’s a good idea to run the project to make sure that everything is OK. One you’ve done so, and the browser window opens, go to Site Actions-Site Settings, and select Site Collection Features. You should see your feature there, in an activated state, with your title and description.

image

Next, we want to change the name of our event receiver to something other than “EventReceiver1”. click on the EventReciver1 folder and rename it, in our case to PageChangedEventReceiver. Then, rename your EventReceiver1 class in a similar fashion. You will be prompted to update all references to the class when you do this, so select yes. Unfortunately, the updates don’t completely affect our pesky Elements.xml file, and we need to perform these manually. Open this file and change all references to the old name to use the new one.

image

Now we’re ready to write some code!

3. Add the Logic

You can add all of your code directly into your event receiver class. However in our case, we need to perform the same functions not only when the event fires, but also when the feature is activated. Therefore, we add a new class to the project, simply called Builder. In addition, we will need to access the Microsoft.SharePoint.Publishing namespace, so we need to add a reference to it to our project.

Without going through it line by line, our code will walk through our site collection, find all of the pages, check to see if they have been published and then build a site map entry for each one, using the URL prefix for the Internet zone. The complete code is available on the Codeplex site mentioned above, but the content of the Builder class is below.

Imports System.Text

Imports Microsoft.SharePoint.Publishing

Imports System.IO

Imports Microsoft.SharePoint.Administration

 

Public Class BuilderMain

    Private _siteURL As String

    Private _SiteID As Guid

 

    Dim textWriter As StringBuilder = Nothing

    Dim dateFormatString As String = "yyyy'-'MM'-'dd"

   

    Public Sub New(ByVal siteID As Guid)

        _SiteID = siteID

    End Sub

    Public Sub New(ByVal Url As String)

        Dim st As New SPSite(Url)

 

        _SiteID = st.ID

    End Sub

 

    Public Sub Run()

        Try

            textWriter = New StringBuilder(String.Empty)

         

            Dim site As SPSite = New SPSite(_SiteID, SPUrlZone.Internet)

            With site

                Dim web As SPWeb = site.RootWeb

                With web

                    textWriter.AppendLine("<?xml version=""1.0"" encoding=""UTF-8""?>")

                    textWriter.AppendLine("<urlset xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:schemaLocation=""http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"" xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">")

                    LoadTreeViewForSubWebs(web) ' kick it off with the root web As 

                    textWriter.AppendLine("</urlset>")

                    Dim stream As MemoryStream = New MemoryStream(Encoding.UTF8.GetBytes(textWriter.ToString()))

                    web.Files.Add("sitemap.xml", stream, True)

                    stream.Close()

                End With

            End With

 

        Catch ex As Exception

            errorHandler(ex, "BuildSitemap")

        End Try

    End Sub

 

    Private Sub LoadTreeViewForSubWebs(ByVal currentWeb As SPWeb)

        Dim pPageCol As PublishingPageCollection = Nothing

        Dim tagLabel As String = String.Empty

        If PublishingWeb.IsPublishingWeb(currentWeb) Then

            Dim pWeb As PublishingWeb = PublishingWeb.GetPublishingWeb(currentWeb)

            pPageCol = pWeb.GetPublishingPages()

        End If

        'create xml link to site

        'to remove the link to the site (without a page), remove the next line

        writeSitemapNode(currentWeb.Url + "/", currentWeb.LastItemModifiedDate.ToString(dateFormatString))

 

        'create xml links to site pages

        If Not pPageCol Is Nothing Then

            LoadTreeViewForSubWebPages(pPageCol)

            pPageCol = Nothing

        End If

        For Each web As SPWeb In currentWeb.Webs

            Dim _file As SPFile = web.GetFile(web.Url.ToString() + "nopost.xml")

            If Not _file.Exists Then

                LoadTreeViewForSubWebs(web)

            End If

        Next

        currentWeb.Close()

 

    End Sub

 

    Private Sub LoadTreeViewForSubWebPages(ByVal currentPages As PublishingPageCollection)

        Dim page As PublishingPage

        For Each page In currentPages

            If page.Url.EndsWith("aspx") Then

                If page.ListItem.HasPublishedVersion Then

                    writeSitemapNode(page.PublishingWeb.Url + "/" + page.Url, page.LastModifiedDate.ToString(dateFormatString))

                End If

 

            End If

        Next

    End Sub

 

    Private Sub writeSitemapNode(ByVal pageLocation As String, ByVal lastModified As String)

        textWriter.AppendLine(vbTab + "<url>")

        'replace secured links with non-secured links

        textWriter.AppendLine(vbTab + vbTab + "<loc>" + pageLocation.Replace("https:", "http:") + "</loc>")

        textWriter.AppendLine(vbTab + vbTab + "<lastmod>" + lastModified + "</lastmod>")

        textWriter.AppendLine(vbTab + "</url>")

    End Sub

 

 

    Private Sub errorHandler(ByVal errorMessage As Exception, ByVal errorLocation As String)

 

        Try

            Dim _eventLog As System.Diagnostics.EventLog = New System.Diagnostics.EventLog("Timer Jobs")

            _eventLog.Source = "Sitemap Generator"

            _eventLog.WriteEntry("Error (" + errorLocation.ToString() + "): " + errorMessage.Message.ToString())

            _eventLog.Close()

            _eventLog = Nothing

        Catch

 

        End Try

    End Sub

End Class

 

Next, we need to call our builder from our event receiver. Our code will go into the ItemUpdated sub. The builder constructor takes either a URL or a Site ID as an argument, and since the item can be obtained through the properties object, our job is pretty straightforward. All we need to do is to check to see if the item has been approved.

Public Overrides Sub ItemUpdated(ByVal properties As SPItemEventProperties) 

   MyBase.ItemUpdated(properties)

   If properties.ListItem.ModerationInformation.Status = SPModerationStatusType.Approved Then

        Dim smb As New BuilderMain(properties.SiteId)

        smb.Run()    

    End If

End Sub

 

4. Add a Feature Receiver

Of course, we don’t want to wait until a page is edited, we want to build a site map as soon as the feature is activated. To do that, we need to add a feature event receiver. To do this, we simply right click on our feature node (in this case, xmlSiteMapBuilder) and select Add Event Receiver. The designer will open the new class, and the 4 event receivers will be commented out. Simply uncomment the FeatureActivated Sub, and add the required code.

Public Overrides Sub FeatureActivated(ByVal properties As SPFeatureReceiverProperties)

    Dim Parent As SPSite = CType(properties.Feature.Parent, SPSite)

    Dim smb As New BuilderMain(Parent.ID)

    smb.Run()

End Sub

 

We don’t need to clean anything up when the feature is deactivated, so this is the only code that we need to add. Go ahead and run the project, and you should find a brand new sitemap.xml file in the root of your site collection. You can use SharePoint Designer to see it, or just use the browser with a url of http://yoursitecollectionurl/sitemap.xml

That’s all there is to it. A little bit of code, and you’re well on your way to Search Engine Optimization.

4 Comments

Using Silverlight and the SharePoint Client Object Model With Anonymous Access

One of the nicer features that SharePoint 2010 brought with it was the Client Object Model. Previously, all custom code had to either run on the server, using the SharePoint API. As an alternative, you could call the SharePoint SOAP web services from a client application, but in many cases, they left a little to be desired. The client object model brings the richness of the SharePoint API to client side development using either the full .NET framework, Silverlight, or jQuery.

Why is client side development important? Well, simply put, if you want to write an application or an add on for SharePoint, and you want it to run in both on-premise installations and in Office 365, it’s pretty much your only option, because you can’t deploy custom code to SharePoint Online. (OK, you can run sandboxed code in the cloud, but that brings its own set of severe limitations).

There are some great tutorials out there about working with the SharePoint Client Side Object Model (CSOM), so I’m not going to go into that here, but Tobias Zimmergren has an excellent getting started post – Getting Started With The Client Object Model. His example outlines using it with with the .NET CLR version, which is almost identical to Silverlight. As always, it’s the almost part that gets you. But more about that later.

There are a number of extra considerations when using the CSOM with an anonymous site. Since the code is executing on the client side, obviously the data needs to be accessible anonymously. Before we even touch the code, there are a number of things to check on the site itself.

1. Ensure that anonymous access is enabled

This may seem obvious, but it’s worth stating. Anonymous access needs to be enabled at both the web application level, and at the site collection level. One less that clear thing is that the option for enabling it at the site collection level does not even appear until it has been enabled at the application level. Randy Drisgill has an excellent post on how to do this here. As a best practice, I maintain one zone (usually the default) that uses Windows authentication, and my external zone, which allows ONLY anonymous access.

2. Turn off the ViewFormPagesLockDown feature

SharePoint publishing sites are designed to serve up pages, and to ONLY serve up pages to anonymous users. Standard SharePoint list items and forms cannot by default be accessed by anonymous users. This behaviour is controlled by the ViewFormPagesLockdown feature. This feature is enabled by default for publishing sites.

If the Silverlight application will be accessing any SharePoint list data, we will need to turn this feature off. Keep in mind that anonymous users will then have access to content stored in the site if they know how to access them, so you may want to use distinct anonymous access levels on some of your lists/libraries.

To turn off this feature, open a command prompt on your SharePoint server, and enter the following (replacing urlofsite with your url):

stsadm -o deactivatefeature -url http://urlofsite -name ViewFormPagesLockDown

Once you perform this step, you may need to re-set the anonymous access level for the site collection, as it doesn’t always pick up right away.

3. Allow GetItems() calls

For some bizarre (in my opinion…) reason, calls to the GetItems function is disabled by default for anonymous users. Waldek Mastykarz has a post about this issue, and what to do about it, which boils down to the following 3 lines of Powershell.

   1: $wa = Get-SPWebApplication -Identity "http://sharepoint" 

   2: $wa.ClientCallableSettings.AnonymousRestrictedTypes.Remove([Microsoft.SharePoint.SPList], "GetItems") 

   3: $wa.Update()

This will remove this restriction. Replacing “Remove” with “Add” will add it back.

4. Run the Application in Context

In order to work with the CSOM you will need to instantiate an SPContext object. This is the starting object for working with SharePoint objects. You can do this one of two ways. If you’re running the Silverlight application directly from a client (likely in development mode) or out of browser, you use its New constructor with the URL of the site as an argument.

_Context = New ClientContext(_URL)

 

If, However, you’re running from a SharePoint page, or a web part, you can use the current context.

_Context = ClientContext.Current

In Tobias’s tutorial mentioned above, he indicates that you can control the authentication mode through the AuthenticationMode property of the SPContext object. Possible values are Anonymous, Default, and Forms. As luck would have it, this property is not available to the Silverlight implementation of the SPContext object.

When working as an authenticated user, both options are available to you, but anonymous access only works with the second option. This makes debugging with anonymous sites difficult, as you need to either use a web part project, or copy your compile XAP file to the server manually. However, it’s only one line of code to change, so its entirely feasible to almost all of the development on the internal zone, and remember top change the mechanism before deploying to the anonymous zone.

5. Ensure That All of Your Resources Are Available.

If you’ve ever worked with an external site you’re familiar with the need to check in and approve all public facing content. It’s no different here. You need to ensure that all of the resources used by your application are available to anonymous users. I normally just try to load them all manually in a browser.

Once all of these steps are completed, you should be good to go with your anonymous site.

I wanted to add one other pointer here, although it has nothing to do with anonymous access. When you’re working with the CSOM, all of your calls are made asynchronously. This means that you make a request, and the result calls back into another method of your code. If you’re data binding, and showing results on screen, you will want to show the result as soon as it comes back. The problem is that many calls happen on a background thread, and when the try to access a data bound object, you’ll get the following error:

UnauthorizedAccessException: Invalid cross-thread access in Silverlight application

The trick is to force the update to happen on the UI thread. Steve Willcock at StackOverflow answered a similar question that includes a handy little helper object for forcing execution on the UI thread.

You don’t have to do client side development with SharePoint, but there are many advantages to doing so. The coming release of Office 365 makes it that much more compelling.

2 Comments

Talk TechNet Episode 13: SharePoint

I’ll be the guest on the March 16 episode of Talk TechNet with Keith Combs. The topic will of course be SharePoint, and I hoping to get a chance to talk some Business Intelligence as it relates to SharePoint. The format is a talk show and the conversation will be guided by callers. Please feel free to join us, and hopefully we’ll all learn something!

You can click here to register, or go to http://technet.microsoft.com/en-ca/talk to see all of the upcoming (and past) Talk TechNet Episodes.

Leave a Comment

SharePoint Conference 2011 Registration is Now Open

It’s been some time since the last SharePoint Conference, which was held in Las Vegas in October 2009. The next one is being held in Anaheim, from October 3-6, and registration is now open. The past few SharePoint Conferences have sold out very quickly, so if you’re interested, I would suggest moving quickly.

Registration is online at http://www.mssharepointconference.com. Registration is $1199 per person, and Microsoft has reserved a block of rooms at 3 hotels adjacent to the convention center for between $155 and $165 per night.

I’m registered…. what are you waiting for?

Leave a Comment