Monday, 17 December 2012

Fun With PowerShell and SharePoint 2010

I recently had a project to create a repository for staff records, requiring a new site that required hundreds of SharePoint groups and individualy permissioned wiki pages with custom content and provisioned web parts. The idea of doing this manually seemed crazy and, as it was a one-off task, I decided to tackle it in PowerShell.
This post is broken into two parts, one for each script. The first deals with the creation and population of the SharePoint groups. And the second creates and customises the permissions and content for the wiki pages. I also provision a ListViewWebPart with custom view, and add a ContentEditorWebPart with content.

Creating and Populating SharePoint Groups

Once I had the group and member names (and verified them), this task was actually surprisingly simple. I simply created two arrays as shown below.
You can provide one or more users per group using a semi-colon as the delimiter.
Next we add the users to the site. This is important. I originally tried provisioning them directly to each group as it was created but this had the curious side effect of adding the users as group owners and not adding them to the group itself.
We then create an indexer for the users array and run a foreach loop against the groups array.
And that's it. Update the web and dispose.

Create Wiki pages with Custom Content

This script took quite a bit more work. SP2010's wiki pages have a few interesteing quirks that separate them from their Publishing or Web Part Page cousins. Firstly, they have no visible web part zone. They're treated more like documents and the content field accepts HTML as content.
Let's start by loading the SharePoint assembly and getting our required objects.

Customise List Permissions

I then needed to break permissions inheritance on the list and remove the site visitors role assignment to prevent access to the pages.
If you've ever worked with SharePoint permissions in code you know just how convoluted they can be. Is use the awesome PowerGui to interrogate the site objects and then drill down to find the required values. It's agreat learning tool and removes hours of guesswork and frustration.

Create the Pages

Now I was ready to create my pages. I used the same groups array I used earlier but you could just as easily create a new array and an indexer to pair it up against your group names. In my case, the the group names already represented suitable page names.
Note the use of the rootfolder path and SPTemplateFileType. I also used the new page's Item property to get a reference to my pages for later.

Create Item-Level Permissions

I then needed to break inheritance for each page and modify it's permissions by adding the required access group, while also reducing the Owners and Members groups access to Read in order to prevent tampering.
I pass $false to the first param because I don't want to copy role assignments. This was also important from a looping perspective to avoid having all the new groups added to subsequent pages. And finally remove the current user (me) by ID just for clarity.

Add Web Parts to Wiki Pages

As stated earlier, this is slightly different to adding web parts to other page types. Wiki pages have a hidden web part zone called WPZ which efeectively lives inside the pages content field. In addition, you're required to add the HTML container markup for the webparts to this field!
This had me chasing my tail for hours because I couldn't work out why the web part maintenance view for my pages showed the web parts but no content was being rendered to my page.
As with adding any web part, we first need to get the LimitedWebPartManager for the page.
Add a ListViewWebPart with Custom View
Once of the tricker requirements was providing a custom view for each page instance that was filtered by the page/group name so users would only see items relevant to them.
While adding a listview web part is quite straightforward, trying to specify or modify a custom view is far less so. As soon as you add a LVWP to a page, a hidden view instance is created on the source list. I found precious few references online as to how to modify this list instance.
The approach I took was to customise the original view's Query property and update it each time. This was fine for my purposes because I then simply deleted the view from the list after implementation.
Also note the format of the GUID for wiki page webparts. For easons unknown this is different the standard GUID format we've come to expect for anything SharePoint. The replace method below shows how to convert it to the correct format.
But of course nothing ever goes as planned. It turned out that the views needed tweaking AFTER page creation. I could have deleted all the pages and started again but I was curious to see if there wasn't a way to somehow get a reference to this elusive hidden view and update it directly.
I again used PowerGui to drill down into the webparts properties to grab what I needed. After getting a reference to the web part I wnated by its Title property, I simply grabbed it's ViewGuid and passed this to my list to get the hidden instance before updating the Query and the view.
Add CEWP with Content
One final requirements was to hide the unrequired Recently Updated list from the Wiki page's QuickLaunch. I'd initially hoped I could embed the required CSS to the page's content field but SharePoint prepends the style with a new class that prevented this.
A little investigation revealed that this web part has a Content property of type XmlElement. So you first need to define a root element and then add your markup to its InnerText node.

Add Content to Wiki Pages

The final step was to add some instructional text to the pages. For this we can simply pass a raw HTML string to the page's WikiField. Th eonly problem I had here was with the quirky syntax used by PowerShell's here-string. The beauty of here-strings is that you don't need to escape them. Just be sure to use the $($myparam) format for any PS vars or they will be interpretted literally.
You can also see the markup required for the web part containers and the less than obvious way to pass in the required GUIDs.
We can finally close our loop and update the page, before updating and disposing of the web. And we're done!
What's that you say? You found a typo in the page content? Oh dear... :{
Never fear. Just as we update the WikiField's value, we can also grab it. Just get a reference to it, then re-provide your new content string and set it again. You can also append new content in the same manner, while leaving the existing content alone.

Wrapping Up

When you put it all together you can see that it all makes perfect sense. Well, mostly. But there were many little catches and caveats along the way that made this exercise take far longer than it should have. A distinct lack of documentation being the usual issue.
That being said, including development time, I knocked out 147 custom pages in less then 3 hours. Esitmate how long it would have taken to do all this through the UI. ;)

Friday, 16 November 2012

SharePoint Page View Counter

In my last post [SharePoint Hit Counter] we looked at a solution to add an ad hoc hit counter to selected lists. While this solution could be tweaked to work with any kind of list, I wanted to look at a more global solution that could track unique visits and store them in a more centralised location.
This solution uses a web control which is referenced in the site's masterpage. When the masterpage is hit, the URL, logged in user and today's date are written to a centralised list. The control itself just renders the number of visits to the current page. The code behind perofrms a check to see if our Sats list exists and creates it if it doesn't.
If your site uses two master pages - one for Site and one for System pages - then you can apply it to one or both. In may case, I only referenced the control in my custom master. The reason for this is that I only wanted to target Publishing Pages. For starters, the URLs cleaner and I already had a separate solution in place for these. Feel free to modify the solution to suit your needs.

The Masterpage

If you already have a master page feature then you can just make the required changes and add the control where you like.
This requires two additions to be made. A tagprefix at the top of your page which registers our tagprefix name and associates it to our assembly code. I used the same namespace as my previous list stats solution for consistency.
And then a reference to your control where you would like to see it appear. I chose to place mine to the right of the breadcrumbs but it would go just as well in the footer.

The List

In this case I chose to add the list creation code into my control class. There are many (possibly better) ways to do this, such as via feature receiver or list template. The benefit to this method is that if the list is ever re-named or deleted (by accident or design), it will be recreated automatically on the next page view.

The Web Control

The web control itself is quite simpe and has no properties or controltemplate for redering. Add the appropriate references and using statements to your project, and a brief description of what it does.
Then can then begin defining our control class. The first thing we need to do in this case is to make sure our list exists and, if not, create it. The following is a useful means of getting a list by name, which allows you to pass in the SPListCOllection of your choice.
We then override the OnLoad method and begin by getting our current context items. Then open our elevated priviliges block, open a new SPWeb object and grab the values we want to capture for the Stats list.
Then we check to see if our list exists and create it otherwise. I also decided to create a custom view and make it the default. I chose to group by Title (URL) and display Page name, Date of view and the User.
And if it does exist, we query it and update the list only if the current user has not hit the page today.
Now all that's left is to override the control's render method and display the view count on the page. The query simply gets all items from the Stats list that match the current URL.


The final result is simple but effective. as in my previous post, you could take this a step further by creating a stats viewer web part. But in my case the custom list view which I added a Total for to the Date field, provided everything I needed.
As usual, all feedback on how you might improve this solution is welcomed.

Thursday, 15 November 2012

SharePoint Hit Counter

If you've ever enabled and viewed the Usage Analysis reports for SharePoint, you'll already know that they're...well, lacking. I was recently requested to provide hit counts for annoucements on a coporate Intranet and rose to the challenge, of which their were many. :)
Now while there are many ways to skin this cat, my requirements were to provide an ad hoc solution that can could be enabled at the list level by anyone with the Manage List permission level.
I have described a different approach [SharePoint Page View Counter] for Publishing Pages that uses a dedicated list, gathers more information, and comprises a custom webcontrol that is then referenced in the site's custom master page.
The following describes a MOSS 2007 solution comprising of two web parts - a Hit Counter and a Hit Viewer - as well as a site column definition, and is deployed as a site level feature. This could be easily modified for 2010.
The end result will look something like this.
Fig.1 - Hit Viewer web part
Fig.2 - Hit Counter web part
I use WSPBuilder for convenience when creating MOSS solutions, so some of these steps will vary.
Start by adding a web part feature to your project called CounterWP. Then add a second web part called HitViewerWP.
Open up your elements.xml file and add the following field definition for the site column.
There are several advantages to deploying the site column. Firstly, it removes any guess work or room for error on behalf of the content author who is adding it to the target list. Secondly, we're able to hide the field from any of the list form views; something you can't really do via the UI.

HitCounter Web Part

Now let's create a class file for the Counter web part. I'm inheriting from System.Web...WebPart class just to reduce the using footprint. First thing I want to do is create my global booleans for an ID key/pair value in the current URL and the presence of the custom Counter field.
The rest of the code is commented pretty heavily but I'll break it up anyway. As we only want the code to run on DispForm pages with an valid ID present, I'm going to check for the ID first in the OnInit event. No point continuing otherwise.
If the ID is there we can then grab the current context in the OnLoad event. Because we're elevating privileges, we want to get our contexts before trying any update methods.
We don't want the counter field to increment when the page is in Edit mode, or the field doesn't exist, so we'll check for both.
Now we can create our elevated privileges block and open a new SPSite instance to re-grab our needed objects. I do an additional (redundant) check for the Counter field because that's how I roll. You could/should just as easily use a try/cach block.
Then check for a null value on Counter and set it to zero. This is important, because if we add the field to a list with potentially hundreds of existing items, we don't want to have to set a value for all of them. New items get the default value of zero, as defined in the field definition.
Notice that I check the field value's object for null. Also important. Checking for counterStr as a null or empty string does not yield the correct result. Don't ask me why.
We then increment the current value by 1 to count the current hit.
Now we're ready to perform our updates. As we're wanting to update the database from a GET request, we need to set allowunsafeupdates to true on the SPWeb.
We're also going to do an additional check for Content Approval on the list object and approve the item at the same time, otherwise we'll leave the updated item in a Pending state.
Here's the rest of our OnLoad code.
Now let's render the results to screen to provide feedback for the user. This is optional but I provide it here for completeness. I used the RenderContents method for future proofing.
And that's it for our CounterWP. Build your project and let's move on to our HitViewerWP.

HitViewer Web Part

Again, I inherit from System.Web.UI and define a couple of globals for error checking and to get/set the user-selected query for the report viewer.
I chose to check and set a few web part properties during initialising. Again, this is optional.
Now add the CreateChildControls method which will call our render code. As I'm rendering multiple controls this method is preferable to the RenderContents method in this scenario.
Our LoadLists function firstly gets the list collection for the current SPWeb and adds the user controls for the view selector dropdown list.
I then chose to add a script block with a little jQuery goodness to collapse the lists on page load, and an onclick event to toggle the items display. If you don't already have a jQuery reference in your masterpage, you will need to add a reference to the library here as well.
").AppendLine(); ]]>
Then begin rendering the container and grab only those lists whose field collection contains our Counter field. No point querying lists that don't and causing performance issues.
Now let's try to grab the list titles and define the queries for the view dropdown list. I provided two options. The default query includes a filter that grabs only those items modified in the last 30 days. The second doesn't discriminate. It will grab all items.
In both cases I've restricted the queries to the first ten items matching the criteria. I also specify only the ViewFields required to render my items. This is again for performance.
I use SPQuery over other methods (such as SPSiteDataQuery and CrossListQueryInfo) because I already have my filtered list collection, and because it offers me more control.
Now we can define the render method for our list items, catch any exceptions, and add our literal control to the page.
Finish off by adding our exception handling function and we're done!

In Closing...

Because we're updating the list item, any list views that order by modified date will be affected. There are pros and cons to this. The pro, is that your list will now be effectively sorting by popularity (most recently viewed items.
If this isn't what you want, or you have the need to keep some items 'sticky', then you have a number of options. Sort the view by Created date, create a new Yes/No column to keep important items at the top...the choice is yours. In my case I did both.
As a final touch you could use a little jQuery to check for a 'yes' value in your 'sticky' field and append a ! to the item's title to make it clear why some items are at the top.
Enjoy! And as usual, I look forward to any feedback.