Yammer SharePoint App Installation Error

YammerError

Yammer and Microsoft released a new SharePoint app available in the Office Store here: Yammer App details

This is a great first step towards better Yammer and SharePoint integration Smile

However on installing it on some sites I encountered an error:

image

If this was an on-prem installation I’m sure the ULS would be giving some huge clues as to what is up, but I was lucky enough to be using Office365 (I love the fact bugs are Microsoft’s problem to diagnose Smile). So the only way forward was to raise Service Request. So after some problem investigation with the Support guy we got to the answer.

The key reason this App fails to install is the supported locales from Microsoft. As you can see from the screenshot below, only US English is a supported locale.

image

Ok so nothing massively unusual there? Nope in SP2010 this is the only English available, but with SP2013 Microsoft finally worked out the UK uses ‘proper’ English with all it’s quirky spellings for things like ‘colour’. So with this in mind all of our tenant site collections are set to UK English as the locale, as you can see in the screenshot below.

image

So when you add the app you think everything would be fine…. oh how wrong you’d be… So the add new app pops the ‘trust’ dialog. My first comment here is that it also includes the language options, so not really only about ‘trust’ is it.

image

Second bad user experience here is that the languages selection is hidden by default. So being a typical user I didn’t read the information and clicked ‘trust it’. And that’s when the install error happens.

So what should you be doing?

image

So the killer ‘feature’ is that the ‘Trust it’ dialog is picking up the current sites default locale (in this case English UK) and installing the app with that locale. Now if you remember the Yammer App only supports English US locale, so you need to select this locale from the dropdown.

image

So now the app is installing with its supported English US locale into our English UK sites. So our users get the language they want in most of SharePoint and Yammer works in it’s supported language.

image

image

I think this is pretty bad that it defaults to install in a locale it doesn’t support and provides no feedback to the user. So this experience is littered with badly designed UX and errors which would be very easy to avoid, and thus wasting the time of both the user and MS Support. Sad smile Lets hope someone fixes this in some later releases.

Further fixes and information can be found here: http://blogs.msdn.com/b/ragnarh/archive/2013/07/02/yammer-app-for-sharepoint-amp-office-365-tips-amp-tricks.aspx

I hope this saves someone the hassle of raising a ticket for something so simple.

Speaking at the London SharePoint User Group

LondonSUGUK

The London SharePoint user group kicks off 2012 with Steve Smith and I presenting.

Session 1

 

SharePoint Administration – I always wondered what that was for.

By Steve Smith – Combined Knowledge

In this session Steve is going to help everyone understand some of the quirks and options that we see in SharePoint and what they do/break and how we then go about building the Infrastructure for it. An ideal session for SharePoint Admins and Developers alike. Plus he will throw in some Power user stuff to give everyone something to take away.

Session 2

 

Extending the SharePoint 2010 activity system

By Wes Hackett – Content and Code

Amongst the most anticipated new features of SharePoint 2010 were the social activity feed features which bring colleague activity as a feed to an individual. Natively the activity feed displays user profile changes, tagging and notes activity. Microsoft provides an API to extend the activity feed system with your own content. With this extensibility API it is possible to extend this to include custom activities. In this session we’ll explore the native system and the elements needed to extend it.

When and where?

March 1st

Start Time: 6PM
Finish: 9PM

After the event in a local watering hole.

Unfortunately there may not be food or beverage available for the meeting so please bring any drinks with you just in case.

Location:
Cavendish Conference Centre
22 Duchess Mews
London W1G 9DT

http://www.cavendishconferencevenues.co.uk/west-end-conference-venues/location/

Sign up… http://suguk.org/forums/thread/28133.aspx

See you there….Open-mouthed smile

SharePoint Saturday 2011

SPSUK

Saturday 12th November saw the 2011 SharePoint Saturday at Nottingham’s Conference Centre. Another great event organised with so many quality speakers and companies in attendance.

I presented a session on Social Intranet and the slides can be seen below. I hope everyone found the session useful Smile I certainly enjoyed presenting to such an interactive audience.

Just goes to thank everyone involved in the organisation and planning for the event, it was awesome again SmileSmile

Ratings and SharePoint Search better together

SearchResultsWithRatings.png

SharePoint 2010 introduces a new feature to allow a user to rate content within the sites. Depicted by five stars the user can rate content and these ratings are collated to provide the average rating for the item. Displaying content based on ratings can assist users determining the quality of content easily. Ratings will also help content authors understand which content is considered to be higher quality by the readers.

The native rating user interface is a collection of five stars. After a user selects the desired rating it is submitted and averaged with the other ratings for the content. The ratings are processed by a timer job process so there is some small delay.

By default ratings can be configured on lists and libraries and also added to page layouts. They also get fed into the Activity Feed system as rating activities. These add some great user focused features, but in my opinion there is a missing piece.

Search….

So I got thinking having seen some articles about additional search managed properties. Seeing as the average rating and rating count are both columns added to the list/library I decided to investigate whether it was possible to index and therefore present the rating within the search results.

So here the journey of rating discovery begins…

Adding ratings to lists or libraries

 

Before we can investigate getting ratings into the search experience we need to get rating up and running on some content. So lets go ahead and create a document library as our demo content. Upload numerous documents into the library (at least five) so that we have some content to rate.

So that’s the content ready for rating so what next?

  • Browse to the library.
  • From the ‘Library’ ribbon click the ‘Library settings’ button.
  • Under the general heading click the ‘Rating settings’ link.
  • Under the ‘Allow items in this list to be rated’ click yes and ‘Ok’.

This adds two columns to the library. The ‘Rating (0-5)’ and ‘Number of Ratings’. By default the Rating column is added to the view. You can also add the number column to the view if you want (more of that idea later). It should also be noted that these columns will be added to any bound content types as well.

That’s prepared the content for rating so now go ahead and click some ratings. For my demo I logged in as several users and rated the documents with ratings to demonstrate several of the number of stars.

DocLibRating

 

Timer job

 

As mentioned earlier the ratings are calculated via a timer job called {user profile SA name} – Social Rating Synchronization Job. This job aggregates the ratings. To speed up the development you can manually execute the timer job to cause the rating aggregation.

RatingJob

 

Search Managed Properties

 

So with the native rating functionality function configured and ready for indexing it’s time to swing over to the farm Search Service Application to perform the steps needed to index our ratings.

As with any element within SharePoint content for it to get indexed it needs to be a managed property. By default most common content fields are already configured. Ratings however are not setup in this way. So the first step is to create these managed properties for the ‘Rating (0-5)’ and ‘Number of Ratings’ columns.

 

AverageRating Managed Property

 

To create the average rating property map the ‘ows_AverageRating(Decimal)’ as shown in the screenshot below.

AverageRatingProp

 

RatingCountProp

Once the properties are configured a index of the content source is required. Browse to the ‘content sources’ and start a crawl. While this is whizzing along in the background the modifications to the search results web part can be made.

 

Search Results web part

 

The standard Search Results web part provides the view of the content found matching the query term.

DefaultSearchResults

As you can see there are no ratings information displayed. So we’re going to modify the results web part to include the rating and rating count below the content description.

 

Adding the columns to fetched data

 

To be able to show the AverageRating and RatingCount results they need to be added to the ‘Fetched Properties"’ xml within the web part settings (the circled element in the screen shot below)

AddingColumns

Modify the xml to add the new columns to it. The example below lists the new columns last, you can copy this or append your existing list.

<Columns>
<Column Name="WorkId"/>
<Column Name="Rank"/>
<Column Name="Title"/>
<Column Name="Author"/>
<Column Name="Size"/>
<Column Name="Path"/>
<Column Name="Description"/>
<Column Name="Write"/>
<Column Name="SiteName"/>
<Column Name="CollapsingStatus"/>
<Column Name="HitHighlightedSummary"/>
<Column Name="HitHighlightedProperties"/>
<Column Name="ContentClass"/>
<Column Name="IsDocument"/>
<Column Name="PictureThumbnailURL"/>
<Column Name="PopularSocialTags"/>
<Column Name="PictureWidth"/>
<Column Name="PictureHeight"/>
<Column Name="DatePictureTaken"/>
<Column Name="ServerRedirectedURL"/>
<Column Name="AverageRating"/>
<Column Name="RatingCount"/>
</Columns>

Apply the changes to the web part. Now the data is coming back within the search result set.

 

Modifying the XSLT

 

Next step is to get these properties displaying.

XSLEdit

I’m not that skilled in front end coding so this will demo the concept and I’m sure those more creative design peeps will add their own flare to the visuals Winking smile

So I’m choosing to inject the rating and number of raters just after the title and description. Therefore locate the ‘<div class="srch-Metadata2">’ div to inject the new code. Below is a snippet from the section and includes the calls to the new templates.

<div class="srch-Metadata2">

<xsl:call-template name="stars">
<xsl:with-param name="starCount" select="averagerating"/>
</xsl:call-template>
<xsl:call-template name="ratingcount">
<xsl:with-param name="ratingCount" select="ratingcount"/>
</xsl:call-template>
<br/>

<xsl:call-template name="DisplayAuthors">
<xsl:with-param name="author" select="author" />
</xsl:call-template>…..

As you can see I’ve introduced two new templates, one for each property.

Before we dive into the templates there is something important to share. To improve performance most visual images displayed in css sprite format. This means that css class positions the images to display the required section from a large map of images. The rating control is no different and uses the sprite image found ‘/_layouts/Images/Ratings.png’

 

Ratings

 

So the template to render the stars needs to make use of the same native css classes.

The following is the star rating template. It contains the logic to read the rating value and generate the relevant css positioned rating image.

<!– The Stars displaying –>
<xsl:template name="stars">
<xsl:param name="starCount"/>

<span class="ms-currentRating">
<!– Set the correct css sprite for the number of stars –>
<xsl:choose>
<xsl:when test="$starCount &gt;= 4.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_5" alt="Current average rating is 5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 4.5 and $starCount &lt; 5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 4.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_4_5" alt="Current average rating is 4.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 4 and $starCount &lt; 4.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 4 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_4" alt="Current average rating is 4 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 3.5 and $starCount &lt; 4" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 3.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_3_5" alt="Current average rating is 3.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 3 and $starCount &lt; 3.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 3 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_3" alt="Current average rating is 3 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 2.5 and $starCount &lt; 3" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 2.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_2_5" alt="Current average rating is 2.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 2 and $starCount &lt; 2.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 2 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_2" alt="Current average rating is 2 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 1.5 and $starCount &lt; 2" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 1.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_1_5" alt="Current average rating is 1.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 1 and $starCount &lt; 1.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 1 star.</xsl:text>
</xsl:attribute>
<img class="ms-rating_1" alt="Current average rating is 1 star." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 0.5 and $starCount &lt; 1" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 0.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_0_5" alt="Current average rating is 0.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="title">
<xsl:text>Current average rating is 0 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_0" alt="Current average rating is 0 stars." src="/_layouts/Images/Ratings.png" />
</xsl:otherwise>
</xsl:choose>
</span>

</xsl:template>

 

As well as the rating we’re adding the number or people who have rated. This gives the consuming user a decent idea of the level of interest the item has had. The following is the rating count template.

<!– The Rating Count displaying –>
<xsl:template name="ratingcount">
<xsl:param name="ratingCount"/>

<xsl:choose>
<xsl:when test="$ratingCount = 1" >
<xsl:text>Rated by 1 person.</xsl:text>
</xsl:when>
<xsl:when test="$ratingCount &gt;= 1" >
<xsl:text>Rated by </xsl:text>
<xsl:value-of select="$ratingCount" />
<xsl:text> people.</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>Not rated.</xsl:text>
</xsl:otherwise>
</xsl:choose>

</xsl:template>

 

With all these changes applied to the web part settings, save and publish the page and you should find the results now look something like the following screenshot.

SearchResultsWithRatings

Ok so that is pretty cool, users can now see content and peoples ratings of them to assist in finding the right things. It’s not quite the whole story on all the cool things to display, refiners are the other.

 

Refining by Ratings

 

With the ratings being displayed in the results it got me thinking that having a refiner for the rating value and number of people who had rated.

So the native refinement web part allows the customisation of the refiners via its settings. One important point to note is that the web part settings will have no effect unless the following check box is unchecked.

RefinerCheckBox

The Rating refiner is below.

<Category Title="Rating"
          Description="The average rating for the item." 
          Type="Microsoft.Office.Server.Search.WebControls.ManagedPropertyFilterGenerator"
          MetadataThreshold="5"
          NumberOfFiltersToDisplay="4"
          MaxNumberOfFilters="0"
          SortBy="Frequency"
          SortDirection="Ascending"
          SortByForMoreFilters="Name"
          SortDirectionForMoreFilters="Ascending"
          ShowMoreLink="True"
          MappedProperty="AverageRating"
          MoreLinkText="show more"
          LessLinkText="show fewer"/>

The Number of Ratings refiner is below.

<Category Title="Number of ratings"
          Description="The number of ratings from people for the item."
          Type="Microsoft.Office.Server.Search.WebControls.ManagedPropertyFilterGenerator"
          MetadataThreshold="5"
          NumberOfFiltersToDisplay="4"
          MaxNumberOfFilters="0"
          SortBy="Frequency"
          SortDirection="Ascending"
          SortByForMoreFilters="Name"
          SortDirectionForMoreFilters="Ascending"
          ShowMoreLink="True"
          MappedProperty="RatingCount"
          MoreLinkText="show more"
          LessLinkText="show fewer"/>

Add these to the XML property of the web part.

RefinerXml

Save the page and you end up with the refiners for ratings and number of ratings available.

RefinersAndSearch

Going the extra step with the refiners

So the initial refiners display any found values. A nice proof of concept would be to add some filter groups to group rating values together into Bronze, Silver, Gold instead of 0-5. That is possible with the refiner property grouping using ranges.

Wrap-up

Hopefully this no-code solution adds some extra sweetness to the use of ratings and search in equal measure.

A status by any other name would still smell as sweet

Status
While investigating status updates from within the Activity Feed it was necessary to understand the native social status update logic. The aim was to allow commenting a-la Facebook against any of the activity feed events in the feed. This article presents the information learnt about the native status update logic.
 
An example Facebook comment:
 
Status1
 
 
Fundamentally this could be achieved by adding a new ‘Social Comment’ against that item, I suspect there will be some challenges with this approach like the fact it would generate ‘Note Board’ activity events by default. Before diving into that I thought it would be a sound idea to understand more about where the data comes from for each activity type. This article covers the Profile Status.
 
In my previous post about the Activity Templates you can find the template specific for the ‘Status update’
Keyname:
"ActivityFeed_Status_Message_SV_Template"
Template:
{Publisher} says "{Value}".
 
This is used to display the Status update to your colleagues.
 
So first up I wanted to understand what happens when you type a new status.
 
Status2
 
 
The control which is used is the ‘Microsoft.SharePoint.Portal.WebControls.StatusNotesControl’. This control renders the required input box and references for the client side script to make this pretty seamless for the end user. Under the bonnet it sets a property called ‘SPS-StatusNotes’ against the current user’s profile.
 
Status3
 
 
Looking at the profile property several take-away information nuggets are worth noting.
  • The default max length for a status update is 512 characters
  • The privacy setting is ‘Everyone’
  • The ‘show in the profile properties section of the users profile page’ is unchecked. (If checked the value would have appeared below ‘Development’ in the screenshot above)
  • The property is not configured to be indexed by Search. This could be useful to enable if you wanted to show this value within the results page or build a component to return these results using search.
Once committed the ‘People’ search incremental crawl captures this change marker and the next time the Activity Feed timer job executes it will generate the status update event.

Activity Feed Item Templates

ActivityFeed

The SharePoint Activity Feed rendering is controlled by templates. These templates are used within the ActivityTemplate class http://msdn.microsoft.com/en-us/library/microsoft.office.server.activityfeed.activitytemplate.aspx which is responsible for controlling the rendering.

The OOB templates are held within a resources file located:

C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\Resources\osrvcore.resx

This contains the following templates:

Keyname: “ActivityFeed_ProfilePropertyChange_SV_Template”
Template: {Publisher} updated profile.&lt;br/&gt;{Name}: {Value}

Keyname: “ActivityFeed_ProfilePropertyChange_MV_Template”
Template: {Publisher} updated profile.&lt;br/&gt;{List}

Keyname: “ActivityFeed_Birthday_Reminder_SV_Template”
Template: {Publisher} is celebrating a birthday on {Name}.

Keyname: “ActivityFeed_Birthday_Today_SV_Template”
Template: {Publisher} is celebrating a birthday today.&lt;br/&gt;Wish {Publisher} a happy birthday!

Keyname: “ActivityFeed_WorkplaceAnniversary_Reminder_SV_Template”
Template: {Publisher} is celebrating a {Value} year workplace anniversary on {Name}.

Keyname: “ActivityFeed_WorkplaceAnniversary_Today_SV_Template”
Template: {Publisher} is celebrating a {Value} year workplace anniversary today.&lt;br/&gt;Wish {Publisher} a happy anniversary!

Keyname: “ActivityFeed_ColleagueAddition_SV_Template”
Template: {Publisher} added a new colleague.&lt;br/&gt;{Link}

Keyname: “ActivityFeed_ColleagueAddition_MV_Template”
Template: {Publisher} added {Size} new colleagues.&lt;br/&gt;{List}

Keyname: “ActivityFeed_TitleChange_SV_Template”
Template: {Publisher} has a new job title.&lt;br/&gt;{Value}

Keyname: “ActivityFeed_ManagerChange_SV_Template”
Template: {Publisher} has a new manager. &lt;br/&gt; {Link}

Keyname: “ActivityFeed_BlogUpdate_SV_Template”
Template: {Publisher} published a new blog post.&lt;br/&gt;{Link}

Keyname: “ActivityFeed_DLMembershipChange_SV_Template”
Template: {Publisher} has a new membership. &lt;br/&gt; {Link}

Keyname: “ActivityFeed_DLMembershipChange_MV_Template”
Template: {Publisher} has {Size} new memberships. &lt;br/&gt; {List}

Keyname: “ActivityFeed_SocialTaggingByColleague_SV_Template”
Template: {Publisher} tagged {Link} with {Link2}.

Keyname: “ActivityFeed_SocialTaggingByColleague_MV_Template”
Template: {Publisher} tagged {Link}.&lt;br/&gt;{List}

Keyname: “ActivityFeed_NoteboardPosts_SV_Template”
Template: {Publisher} posted a note on {Link}.&lt;br/&gt;{Value}

Keyname: “ActivityFeed_SocialTaggingByAnyone_SV_Template”
Template: {Publisher} tagged {Link} with your interest.&lt;br/&gt;{Link2}

Keyname: “ActivityFeed_SocialRatings_SV_Template”
Template: {Publisher} rated {Link} as {Value} of {Name}.

Keyname: “ActivityFeed_SharingInterest_SV_Template”
Template: {Publisher} shares an interest with you. &lt;br/&gt; {Value}

Keyname: “ActivityFeed_SharingInterest_MV_Template”
Template: {Publisher} shares {Size} interests with you. &lt;br/&gt; {List}

This contains the following template names:

Keyname: “ActivityFeed_ChangeMarker_SV_Template”
Template: Previous Gatherer Run

Keyname: “ActivityFeed_Status_Message_SV_Template”
Template: {Publisher} says “{Value}”.

Keyname: “ActivityFeed_ProfilePropertyChange_Type_Display”
Template: Profile update

Keyname: “ActivityFeed_Birthday_Reminder_Type_Display”
Template: Upcoming birthday

Keyname: “ActivityFeed_Birthday_Today_Type_Display”
Template: Birthday

Keyname: “ActivityFeed_WorkplaceAnniversary_Reminder_Type_Display”
Template: Upcoming workplace anniversary

Keyname: “ActivityFeed_WorkplaceAnniversary_Today_Type_Display”
Template: Workplace anniversary

Keyname: “ActivityFeed_ColleagueAddition_Type_Display”
Template: New colleague

Keyname: “ActivityFeed_TitleChange_Type_Display”
Template: Job title change

Keyname: “ActivityFeed_ManagerChange_Type_Display”
Template: Manager change

Keyname: “ActivityFeed_BlogUpdate_Type_Display”
Template: New blog post

Keyname: “ActivityFeed_DLMembershipChange_Type_Display”
Template: New membership

Keyname: “ActivityFeed_SocialTaggingByColleague_Type_Display”
Template: Tagging by my colleague

Keyname: “ActivityFeed_NoteboardPosts_Type_Display”
Template: Note Board post

Keyname: “ActivityFeed_SocialTaggingByAnyone_Type_Display”
Template: Tagging with my interests

Keyname: “ActivityFeed_SharingInterest_Type_Display”
Template: Sharing Interests

Keyname: “ActivityFeed_ChangeMarker_Type_Display”
Template: Gatherer Change Marker

Keyname: “ActivityFeed_SocialRatings_Type_Display”
Template: Rating

Keyname: “ActivityFeed_Status_Message_Type_Display”
Template: Status Message

The curly brackets are then replaced by the UI web part rendering code.

Hopefully this sheds a little bit more light on the internals of the activity feed internals.

SP2010 MySites – Part Six: MySite Centric

MyProfile

This is the sixth article in a series which explores the SharePoint 2010 MySite features. This article will discuss the approach taken to use the MySite as the centre of the intranet.

Series contents:

  1. Overview, which details the MySite functionality provided by SP2010
  2. Anatomy, we delve inside the MySite and dissect its inner secrets
  3. Customisation for Branding, how to change the look and feel
  4. Customisation of My Content page, how to change the page layout and contents
  5. Customisation of a MySite Blog, how to alter the Blog
  6. The MySite centric intranet, putting the MySite at the centre of the universe – This Article

The approach

 

To understand how to approach using the MySite as the homepage we need to examine the distance and control of information for a user. This will help to explain the compelling reason for using the technical MySite approach as the homepage for the intranet.

The following diagram all follow the same theme, you as the user is the top left. As the axis moves further to the right the information is consumed/shared with more people. As the axis move down from ‘Me’ that user has less control of the authoring and creation of the information.

First is the MySite. Here the user has full unlimited control to the information and functionality available. The concept is similar to that of iGoogle where a use can add the widgets to the page they want. This section is not really consumed by other people and is really about that individuals context.

SocialIntranetOveriew1

Second is the Profile. This again is closely controlled by the individual user but starts to have outside influence from such things as HR or Active Directory entries. This information begins to be used and consumed by other people. This in concept is similar to a LinkedIn or Facebook profile.

SocialIntranetOveriew2

Third is Team Sites. This is where an individual use begins to collaborate or consume information shared with more people. Examples might be Wikipedia, YouTube and forums.

SocialIntranetOveriew3

Forth is the Community Sites. These sites move away from the traditional organisation team/unit and more towards natural work team sites. Again these are collaborative consume and contributed to by many.

SocialIntranetOveriew4

Fifth are the Corporate Sites. These sites are what can termed the ‘traditional’ intranet. The sites are authored by only a small number of users and consumed by many. This is normally where Marketing and Internal Communication sites are located.

SocialIntranetOveriew5

Sixth is External access. While not technically a content section the ability to access the platform from any location is important to the user.

SocialIntranetOveriew6

Finally are the Search, communicator and desktop sharing capabilities which should underpin the entire platform.

SocialIntranetOveriew7

Conclusion

 

So that wraps up the MySite series. Hopefully this has shown how the MySite is constructed and how it can be customised in a supportable manner. And finally how to view its positioning in the information architecture design for an intranet platform.

Speaking at SharePoint Saturday UK

SharePointSaturdayBanner.jpg

SharePointSaturdayBanner

I’m pleased to announce my SPS2011 session called ‘Putting you at the centre of the social intranet’.

With users experiencing contextual information like never before with use of Facebook, Twitter and LinkedIn how can we utilise these concepts for our intranet. The session will introduce the social intranet, provide some examples in SharePoint 2010 and then cover the challenges and benefits.

My session is part of the ‘Social’ track, see the full agenda for all the other top sessions planned.

If you haven’t yet signed up to attend the SharePoint Saturday 2011 visit the registration site – SharePoint Saturday 2011

Look forward to seeing you there Smile

Extending the activity feed with enterprise content

ActivityFeed

Introduction

 

Amongst the most anticipated new features of SharePoint 2010 were the social activity feed features which bring colleague activity as a feed to an individual. Natively the activity feed displays user profile changes, tagging and notes activity. Microsoft provides an API to extend the activity feed system with your own content. With this extensibility API it is possible to extend this to include enterprise content activity, such as activity around documents. Bringing ECM data into the activity feed gives a full 360 degree picture of activity within the SharePoint system. This article describes key areas of the logic and elements required to achieve this.

What do we get out of the box?

 

Before diving into how to extend the activity feed it’s important to understand what we already have. The activity system collects two main areas of data. Firstly user profile changes such as job title, colleague changes, shared interest and anniversary reminders. Secondly are the social feedback events such as tags, notes, ratings and personal blog posts.

Both types are generated by a scheduled timer job which queries for the changes and generates new activity events for an individual. The process then broadcasts this activity to the individual’s colleagues.

There are two web part displays of an individual’s activity feed. The consolidated feed is everything I track. This type is used on My Newsfeed page. The published feed is my activities. This type is used on My Profile page.
 
The data is also available as ATOM:

Key concepts

 

There are a number of key extensibility concepts to highlight when working with the Activity system. The activity application is the container which contains the activity types. It is the parent container of the extensions being built. The activity type is the ‘what’ definition for example ‘New Image Added’. The activity template defines the formatting on an activity event. The template is a tokenized string used during the rendering process. It also has a flag indicating whether it displays multi-value or single activity events. The activity event is the instance of the activity type displayed using the activity template.

Building the extension

 

There are three elements to the extension, the activity application, the templates and types used in the gatherer and finally the user interface. Throughout this article I will assume you are familiar with basic SharePoint coding and focus on the specific API calls related to the extensions. All the code samples provided will in places utilize the Patterns and Practices logging which again is excluded for brevity.

Enterprise information covers a large amount of data and data types so a sensible approach is to build a gathering service for each required ECM data type then register these all within the same activity application to provide the collective link. This provides a good level of scheduling granularity for the platform admin team.

The activity application

 

The first step is to create a Farm scoped feature to register the custom activity application. Let’s call our ActivityApplication ‘ECMActivity’. Listing 1 contains the code required to register a new ActivityApplication into the platform.

public ActivityApplication EnsureActivityApplication()
{
ActivityApplication app=null;
if (SPServiceContext.Current != null)
{
ActivityManager activityManager = new ActivityManager();
//Check to see that the current user has permissions to access the social data
if (activityManager.PrepareToAllowSchemaChanges())
{
app = activityManager.ActivityApplications[“ECMActivity”];
if (app == null)
{
app = activityManager.ActivityApplications.Create(“ECMActivity”);
app.Commit();
app.Refresh(true);
//log to ULS and event log
logger.LogToOperations(ActivityFeedConstants.ActivityApplication + " created.", TraceAreaCategory);
}
return app;
}
else
{
try
{
//The process user doesn’t have access to profiles service as manage social data role
return activityManager.ActivityApplications[“ECMActivity”];
}
catch (ActivityFeedPermissionDeniedException ex)
{
logger.LogToOperations(ex, ErrorAreaCategory);
}
}
}
return app;
}

Listing 1: Register the custom ActivityApplication

At the time of writing there is a missing API implementation for the removal of ActivityApplication. The API throws a ‘NotImplementedException’.
 

These methods are encapsulated into an ‘ECMActivityFeedApplication’ object and have the feature receiver call the relevant methods during activate and deactivate. Once this feature is active there is now a custom container in the system for all the individual ECM activity feed types to be registered within.

Gathering activities

 

What do we mean by gathering? Well in the case of the Activity system gathering is the process actioned to query for changes and generate new activity events for both published and consolidated feeds. A recommendation would be to use encapsulation to design the gathering mechanism so that ECM data types are self-contained allowing easier management.

There are a couple of mechanisms that could be used for the gatherer. For ECM data, event receivers on the item added and updated events become a possible gathering point. After investigation these proved too expensive from a performance perspective and also required some workarounds for the elevation required for certain API calls.
The other obvious option is a timer job. The out of the box gathering is done through timer jobs which indicates that this is a better approach. There are several advantages, performance wise as it executes away from the WFE servers and the limitations of the web process, timer jobs also allow more granular execution and scheduling. Therefore the ECM extensions are designed around the timer job approach.
 

A web application scoped feature registers the ActivityType into the custom activity application via methods on the Gatherer object and registers the timer job.

Gathering timer job

 
You need to implement a web application scoped timer job and deployment feature to register the job on the application servers. This timer job is responsible for executing the gatherer for the required ECM data type. As the timer job gathering execution logic is relatively intensive it would be advised to only execute on the application servers.
 

Timer jobs are only the scheduling mechanism, so the actual job calls down onto a custom ‘Gatherer’ object which provides an execute method. During investigations it became apparent that certain ECM activity occurs more frequently than others. This helped to determine an approach where each ECM data type is its own timer job and gatherer pair. This allows the admin to change the scheduling frequency as required for distinct types of activity.

The Gatherer

 

The data gathering and event generation are handled by the custom ‘Gatherer’. The out of the box contains two gatherers, ‘ActivityFeedGathererProfile’ which creates the profile based events and ‘ActivityFeedGathererSocial’ which creates the ratings, comments and tag events.

Following this model each ECM data type you wish to collect as activity should be implemented as a custom Gatherer. Let’s examine what the gatherer contains for Image ECM data as an example.
 
As mentioned earlier the gatherer contains the register and unregister logic for the activity type. The registration logic first adds the ActivityType and commits this change to the system. The next step is to register both single and multi-value templates. A single value template is used when the activity event instance contains only a single image for example. For multiple image events the multi-value template is used. The activity feed system determines whether the event is single or multi-value based on whether the ActivityEvent ‘LinksList’ property contains objects. Listing 2 shows the logic to register the ActivityType and the ActivityTemplates for the Image example. Note that you only define one Type while you have single and multi-value templates associated with it.
protected override void CreateActivityTypes()
{
if (SPServiceContext.Current != null)
{
//Instantiate a new activity manager… remember the account will need to be admin on
//User profile service app or you’ll need to elevate
ActivityManager activityManager = new ActivityManager();
ActivityApplication app = new ActivityFeedECMApplication().EnsureActivityApplication();
if (app != null)
{
ActivityType createType = app.ActivityTypes["ActivityFeed_Image_ItemAdded"];
if (createType == null)
{
createType = app.ActivityTypes.Create("ActivityFeed_Image_ItemAdded");
createType.ActivityTypeNameLocStringResourceFile = "ActivityFeedECM";
createType.ActivityTypeNameLocStringName = "ActivityFeed_Image_ItemAdded_Type_Display";
createType.IsPublished = true;
createType.IsConsolidated = true;
createType.Commit();
createType.Refresh(true);
}
//Create the associated Single Value template
ActivityTemplate createTemplate = createType.ActivityTemplates[ActivityTemplatesCollection.CreateKey(false)];
if (createTemplate == null)
{
createTemplate = createType.ActivityTemplates.Create(false);
createTemplate.TitleFormatLocStringResourceFile = "ActivityFeedECM";
createTemplate.TitleFormatLocStringName = "ActivityFeed_Image_ItemAdded_SV_Template";
createTemplate.IsMultivalued = false;
createTemplate.Commit();
createTemplate.Refresh(true);
}
ActivityTemplate createMVTemplate = createType.ActivityTemplates[ActivityTemplatesCollection.CreateKey(true)];
if (createMVTemplate == null)
{
createMVTemplate = createType.ActivityTemplates.Create(true);
createMVTemplate.TitleFormatLocStringResourceFile = "ActivityFeedECM";
createMVTemplate.TitleFormatLocStringName = "ActivityFeed_Image_ItemAdded_MV_Template";
createMVTemplate.IsMultivalued = true;
createMVTemplate.Commit();
createMVTemplate.Refresh(true);
}
}
}
}
Listing 2: Register ActivityType and its templates
 

Removal is almost the reverse of registration, first remove the associated ActivityTemplates then finally remove the ActivityType. Again it is important to note the API has not yet been implemented.
 
The templates live in a resource file deployed to ‘14/Resources’. Each template is matched to a tokenized resource string. Listing 3 shows the template used for a single value ActivityEvent rendering, as you can see the curly braces define the tokens. These tokens are then replaced by the rendering logic of the web parts. Listing 4 shows the multi-value template, the main difference being the inclusion of the {List} token which will define the multiple images during rendering.
{Publisher} has uploaded a new image {Link} into the {Link2} library.
Listing 3: ActivityFeed_Image_ItemAdded_SV_Template template
{Publisher} uploaded {Size} new images into the {Link} library. <br/> {List}
Listing 4: ActivityFeed_Image_ItemAdded_MV_Template template
 
That covers the registration of the ActivityType and its templates. Next let’s look at the generation of the events when the timer job executes.
 
The first step during execution is to retrieve the required data. One technique that can be used is to iterate the site collections and perform a specific query using a SPSiteDataQuery with the relevant CAML query. For small scale SharePoint systems this is likely to meet the performance requirements for the gatherer execution and server load. For enterprise level systems it would be recommended to utilize the change log to retrieve the required data.
 
The security permissions for the User Profile Service Application should be configured to give the timer service account ‘Full control’ so that is can correctly access certain API methods for the ActivityManager. Listing 5 demonstrates how to create an ActivityManager using elevated UserProfileManager and context. You will require the ActivityManager in many places throughout the logic.
try
{
//This is the creation of an elevated activity manager
//If you don’t do this nothing works!!!
SPServiceContext context2 = SPServiceContext.GetContext(aSite);
userProfileManager = new UserProfileManager(context2);
string adminUserName = Environment.UserDomainName + "\\" + Environment.UserName;
if (!userProfileManager.UserExists(adminUserName))
{
Logger.TraceToDeveloper("Profile does not exist for:  " + adminUserName + " so calling create.", TraceAreaCategory);
userProfileManager.CreateUserProfile(adminUserName);
}
UserProfile p = userProfileManager.GetUserProfile(adminUserName);
activityManager = new ActivityManager(p, context2);
}
catch (UserProfileApplicationNotAvailableException ex)
{
//This is an example of logging to the operations version
Logger.LogToOperations(ex, 0, EventSeverity.Error, ErrorAreaCategory);
//You might not need to throw… up to your needs
throw;
}
Listing 5: Creating a ActivityManager with elevated security
 
So at this point we’ve retrieved the data, created an ActivityManager. The next step is to begin processing the data into ActivityEvents. In order to determine whether data should be single or multi-value you need to create an algorithm to process the data.
 
In order to facilitate the process you could use a ProcessingContainer. This holds various information for example the SPListItem, Owner and some calculated values which indicate if it’s a multi-value and has items with unique permissions. Figure 1 shows the ProcessingContainer class diagram.
 
DIWUG_Figure1_WesHackett
Figure 1: Class diagram for the ProcessingContainer
Now you may obviously have your own requirements which determine single versus multi-value, the logic detailed in Listing 6 may provide a proven starting point. The basic logic is that the data is sorted by location, then date created and finally by the created by user. This basically means that events have to be in the same location, by the same user and within a defined timespan (5 minutes in the listing) to qualify to become multi-value events.
 
As you can see the use of the processing container is to collect the information such as owner and to hold the SPListItem(s). You might be wondering why the SPListItems are being collected and not just relying on the data returned by the query. To facilitate the security trimming logic and being able to call the SPListItem method ‘DoesUserHavePermissions’ storing the SPListItem means you only have to retrieve the item once during the processing. Again for performance you will need to assess whether this approach would meet your requirements. Other approaches might involve using search based trimmers. You would also then need to extend the container with required data fields to hold the required attributes for the ActivityEvent information.
Logger.TraceToDeveloper("Sorting data", TraceAreaCategory);
TimeSpan fiveMinuteTimeSpan = new TimeSpan(0, 5, 0);
//Select it
var itemData = from g in dtAllItems.AsEnumerable()
select g;
var sortedItemData = itemData.OrderBy(item => (item["EncodedAbsUrl"] + (item["FileRef"].ToString().Substring(0, item["FileRef"].ToString().LastIndexOf("/") + 1)).Substring(item["FileRef"].ToString().IndexOf("#") + 1))).ThenByDescending(x => x["Author"]).ThenBy(x => x["Created"]);
string previousUrl = String.Empty;
string previousOwner = String.Empty;
DateTime previousDateTime = DateTime.Now;
bool firstCycle = true;
//Instantiate the processing containers collection
List<ProcessingContainer> activityContainers = new List<ProcessingContainer>();
//Create the first container before entering the
ProcessingContainer currentContainer = null;
//Iterate the sorted collection to generate the processing container
foreach (var sortedItem in sortedItemData)
{
string url = sortedItem["EncodedAbsUrl"] + (sortedItem["FileRef"].ToString().Substring(0, sortedItem["FileRef"].ToString().LastIndexOf("/") + 1)).Substring(sortedItem["FileRef"].ToString().IndexOf("#") + 1);
string owner = sortedItem["Author"].ToString().Substring(sortedItem["Author"].ToString().IndexOf("#") + 1);
DateTime dateTime = Convert.ToDateTime(sortedItem["Created"].ToString());
string cleanFileRef = sortedItem["FileRef"].ToString().Substring(sortedItem["FileRef"].ToString().IndexOf("#") + 1);
Logger.TraceToDeveloper("Sorting data for location " + url, TraceAreaCategory);
Logger.TraceToDeveloper("Sorting data for the file " + cleanFileRef, TraceAreaCategory);
TimeSpan timeDifference = dateTime – previousDateTime;
//Only check if we’ve been through before
if (!firstCycle)
{
Logger.TraceToDeveloper("Not First data cycle", TraceAreaCategory);
//Check to see if this item should become a multivalue addition to the current processing container
if ((url == previousUrl) && (owner == previousOwner) && (timeDifference.Ticks < fiveMinuteTimeSpan.Ticks))
{
Logger.TraceToDeveloper("The event matched a previous item", TraceAreaCategory);
//If no container create a new one
if (currentContainer == null)
{
currentContainer = new ProcessingContainer();
currentContainer.Url = url;
currentContainer.Owner = owner;
}
//Get the list item
using (SPSite siteCollection = new SPSite(url))
{
using (SPWeb myWeb = siteCollection.OpenWeb())
{
SPListItem currentItem = myWeb.GetListItem(cleanFileRef);
if (currentItem != null)
{
currentContainer.ListItems.Add(currentItem);
}
}
}
//Set the previous values ready for the next run through
previousOwner = owner;
previousUrl = url;
previousDateTime = dateTime;
}
else
{
Logger.TraceToDeveloper("The event didn’t matched a previous item", TraceAreaCategory);
//We failed to match current so commit the processing container as completed
if (currentContainer != null)
{
activityContainers.Add(currentContainer);
currentContainer = null;
}
//Start a new cycle
if (currentContainer == null)
{
currentContainer = new ProcessingContainer();
currentContainer.Url = url;
currentContainer.Owner = owner;
}
using (SPSite siteCollection = new SPSite(url))
{
using (SPWeb myWeb = siteCollection.OpenWeb())
{
SPListItem currentItem = myWeb.GetListItem(cleanFileRef);
if (currentItem != null)
{
currentContainer.ListItems.Add(currentItem);
}
}
}
previousOwner = owner;
previousUrl = url;
previousDateTime = dateTime;
}
}
else
{
//The item didnt match the previous so will begin a new container
if (currentContainer == null)
{
currentContainer = new ProcessingContainer();
currentContainer.Url = url;
currentContainer.Owner = owner;
}
using (SPSite siteCollection = new SPSite(url))
{
using (SPWeb myWeb = siteCollection.OpenWeb())
{
SPListItem currentItem = myWeb.GetListItem(cleanFileRef);
if (currentItem != null)
{
currentContainer.ListItems.Add(currentItem);
}
}
}
previousOwner = owner;
previousUrl = url;
previousDateTime = dateTime;
firstCycle = false;
}
}
//We failed to match current so commit the processing container as completed
if (currentContainer != null)
{
Logger.TraceToDeveloper("Commit the final item", TraceAreaCategory);
activityContainers.Add(currentContainer);
currentContainer = null;
}
Listing 6: example sorting and processing of returned data
 
Once all the processing is complete the next stage is to generate the ActivityEvent objects. While iterating the ProcessingContainer collection the logic decides whether a single or multi-value event should be generated. Added complexity here is the logic for dealing with multi-value containers which have items containing unique permissions. It would be recommended that if an item has unique permissions that it becomes a single value. This ensures that once multicasting to colleagues occurs trimming logic for colleagues is easier.
 
If the container contains a single SPListItem or comes from a unique permissioned SPListItem then we need to create a single value ActivityEvent. Listing 7 details the logic to create the ActivityEvent. Most of the code is related to setting various attributes of the event, one special line I’d like to highlight is the ‘imageLink.Value = currentItem["EncodedAbsThumbnailUrl"].ToString();’ line. This might not seem that important but this is where we give ourselves the ability to create more exciting Facebook/LinkedIn style rendering. By using the thumbnail url and assigning it to the ‘Value’ attribute of the link. We can then use this to generate a visual thumbnail in the web part logic. The logic also stores the link to the list containing the item.
protected ActivityEvent CreateSingleValueImageActivityEvent(ActivityManager activityManager, Entity owner, Entity publisher, SPListItem currentItem)
{
using (SPMonitoredScope scope = new SPMonitoredScope("CreateSingleValueImageActivityEvent"))
{
ActivityType createType = activityManager.ActivityApplications["ECMActivity"].ActivityTypes["ActivityFeed_Image_ItemAdded"];
if (createType != null)
{
//Generate a new activity event
//Again if you haven’t elevated the activity manager then this WILL throw access denied!!!
ActivityEvent activityEvent = ActivityEvent.CreateActivityEvent(activityManager, createType.ActivityTypeId, owner, publisher);
if (activityEvent != null)
{
activityEvent.Name = createType.ActivityTypeName;
activityEvent.ItemPrivacy = (int)Privacy.Public;
activityEvent.Owner = owner;
activityEvent.Publisher = publisher;
activityEvent.Value = ActivityFeedServiceConstants.ActivityType.Image.ImageContentTypeValue;
if (currentItem[SPBuiltInFieldId.Last_x0020_Modified] != null)
{
DateTime lastModified = Convert.ToDateTime(currentItem[SPBuiltInFieldId.Last_x0020_Modified].ToString());
activityEvent.DateValue = lastModified;
}
#region Image
Link imageLink = new Link();
//Set the title to the display name
if (!String.IsNullOrEmpty(currentItem.Title))
{
imageLink.Name = currentItem.Title;
}
else
{
imageLink.Name = currentItem.DisplayName;
}
//Set the href of the image
imageLink.Href = (currentItem.ParentList.ParentWeb.Site.MakeFullUrl(currentItem.ParentList.DefaultDisplayFormUrl) + "?ID=" + currentItem.ID.ToString());
//Set the thumbnail url
imageLink.Value = currentItem["EncodedAbsThumbnailUrl"].ToString();
//Set the link 2
activityEvent.Link = imageLink;
#endregion
#region Image Library Link
//Create the Link to the image library root link
Link imageLibraryLink = new Link();
//Set the title to the list title
imageLibraryLink.Name = currentItem.ParentList.Title;
//Set the href for the list default view
imageLibraryLink.Href = SPHttpUtility.UrlPathEncode(currentItem.Web.Site.MakeFullUrl(currentItem.ParentList.DefaultViewUrl),true);
//Set the link to this
activityEvent.Link2 = imageLibraryLink;
#endregion
activityEvent.Commit();
return activityEvent;
}
return null;
}
return null;
}
}
Listing 7: Creating a single value Image ActivityEvent
 
If the ProcessingContainer is a multi-value then we pass it to the method dealing with the generation of multi-valued ActivityEvents. Listing 8 shows the important difference with this logic. The individual SPListItems are processed into ‘Link’ objects which are then added to the ‘LinksList’ collection of the ActivityEvent. This is the way you inform the Activity system that this event is multi-value. You will also see the list link goes into a different property. These are subtle differences to note as they become important during the web part rendering and template resource file values.
protected ActivityEvent CreateMultiValueImageActivityEvent(ActivityManager activityManager, Entity owner, Entity publisher, List<SPListItem> items)
{
using (SPMonitoredScope scope = new SPMonitoredScope("CreateMultiValueImageActivityEvent"))
{
ActivityType createType = activityManager.ActivityApplications["ECMActivity"].ActivityTypes["ActivityFeed_Image_ItemAdded"];
if (createType != null)
{
//Generate a new activity event
//Again if you haven’t elevated the activity manager then this WILL throw access denied!!!
ActivityEvent activityEvent = ActivityEvent.CreateActivityEvent(activityManager, createType.ActivityTypeId, owner, publisher);
if (activityEvent != null)
{
activityEvent.Name = createType.ActivityTypeName;
activityEvent.ItemPrivacy = (int)Privacy.Public;
activityEvent.Owner = owner;
activityEvent.Publisher = publisher;
activityEvent.Value = ActivityFeedServiceConstants.ActivityType.Image.ImageContentTypeValue;
if (items[0][SPBuiltInFieldId.Last_x0020_Modified] != null)
{
DateTime lastModified = Convert.ToDateTime(items[0][SPBuiltInFieldId.Last_x0020_Modified].ToString());
activityEvent.DateValue = lastModified;
}
#region Image Library Link
//Create the Link to the image library root link
Link imageLibraryLink = new Link();
//Set the title to the list title
imageLibraryLink.Name = items[0].ParentList.Title;
//Set the href for the list default view
imageLibraryLink.Href = items[0].Web.Site.MakeFullUrl(items[0].ParentList.DefaultViewUrl);
//Set the link to this
activityEvent.Link = imageLibraryLink;
#endregion
#region Images
foreach (SPListItem currentImage in items)
{
Link imageLink = new Link();
//Set the title to the display name
if (!String.IsNullOrEmpty(currentImage.Title))
{
imageLink.Name = currentImage.Title;
}
else
{
imageLink.Name = currentImage.DisplayName;
}
//Set the href of the image
imageLink.Href = (currentImage.ParentList.ParentWeb.Site.MakeFullUrl(currentImage.ParentList.DefaultDisplayFormUrl) + "?ID=" + currentImage.ID.ToString());
//Set the thumbnail url
imageLink.Value = currentImage["EncodedAbsThumbnailUrl"].ToString();
//Set the link 2
activityEvent.LinksList.Add(imageLink);
}
#endregion
activityEvent.Commit();
return activityEvent;
}
return null;
}
return null;
}
}
Listing 8: Creating a multi value Image ActivityEvent
 
Once the ProcessingContainer collection has been converted to a collection of ActivityEvents the next step is to broadcast them to colleagues. The basic process of broadcasting to colleagues of the event owners is shown in Listing 9.
 
As you see the main methods are provided by the ActivityFeedGatherer object. The logic first gets the colleagues of the event owner. Then it calls the ‘MulticastActivityEvents’ method to generate the ActivityEvents which appear in colleagues ‘Newsfeed’ page.
 
In the Activity system the ActivityEvents generated for the owner of the event appear in the Profile page and are known as Published activity events, those which are multicast are known as Consolidated activity events and these appear on the ‘Newsfeed’ page.
 
While Listing 9 shows the basics of multicasting, further logic would need to be injected here which processed security trimming for each colleague. For example if one of the owners colleagues doesn’t have permission to the underlying ECM item then the logic would need to remove their identifier from the MinimalPerson collection used to multicast to. This code has been excluded from Listing 9 to focus on the essential logic needed but would be injected before the call to ‘ActivityFeedGatherer.MulticastActivityEvents’.
public void MulticastPublishedEvents(List<ActivityEvent> activityEvents, ActivityManager activityManager)
{
if (activityEvents.Count == 0)
{
return;
}
List<long> publishers = new List<long>();
foreach (ActivityEvent activityEvent in activityEvents)
{
if (!publishers.Contains(activityEvent.Owner.Id))
{
publishers.Add(activityEvent.Owner.Id);
}
}
Dictionary<long, MinimalPerson> owners;
Dictionary<long, List<MinimalPerson>> colleaguesOfOwners;
ActivityFeedGatherer.GetUsersColleaguesAndRights(activityManager, publishers, out owners, out colleaguesOfOwners);
Dictionary<long, List<ActivityEvent>> eventsPerOwner;
ActivityFeedGatherer.MulticastActivityEvents(activityManager, activityEvents, colleaguesOfOwners, out eventsPerOwner);
List<ActivityEvent> eventsToMulticast;
ActivityFeedGatherer.CollectActivityEventsToConsolidate(eventsPerOwner, out eventsToMulticast);
WriteEvents(eventsToMulticast);
}
Listing 9: Multicasting the events
 
The final step is to batch write the new ActivityEvents to the Activity system. This is shown in Listing 10 which shows the call to batch commit the events using the ‘ActivityFeedGatherer.BatchWriteActivityEvents’ method.
protected virtual void WriteEvents(List<ActivityEvent> events, ActivityManager activityManager)
{
int startIndex = 0;
while (startIndex + activityManager.MaxEventsPerBatch < events.Count)
{
ActivityFeedGatherer.BatchWriteActivityEvents(events, startIndex, activityManager.MaxEventsPerBatch);
startIndex += activityManager.MaxEventsPerBatch;
}
ActivityFeedGatherer.BatchWriteActivityEvents(events, startIndex, events.Count – startIndex);
}
Listing 10: Writing the events out to the Activity system
 

That concludes the logic for the gathering mechanism. At this point you have the data generation functionality available. The out of the box Activity system will now show new custom ECM events in the ‘Newsfeed’ and profile pages. The new ECM events will also come through the ATOM feeds.

Improving the web parts

 

The out of the box web part renders the ActivityEvent collection using a tokenization mechanism. For each event it derives which type and retrieves the mask from the relevant resource file. Passing the event and mask through to the html generator to render the individual ActivityEvent in the UI. There isn’t an easy way to replace the rendering of out the box ActivityTypes without some custom coding.

The aim is to provide a UI experience that includes a more engaging visual. The first focus is the method required to get the right data for the ‘Newsfeed’ page. The consolidated event web part inherits from a common base web part created to handle the majority of the rendering logic. The consolidated web part is only really responsible for call to the data API as shown in Listing 11. The code is part of the ‘OnPreRender’ in the derived class override. You can see that it uses the overload which takes a date and time and number of items to display to retrieve the required data.
if (this.MinEventTime <= ActivityManager.MinEventTime)
{
base.activityEvents = base.ActivityManager.GetActivitiesForMe(ActivityManager.MinEventTime, this.MaxItemsToDisplay);
}
else
{
base.activityEvents = base.ActivityManager.GetActivitiesForMe(this.MinEventTime, this.MaxItemsToDisplay);
}
base.OnPreRender(e);
Listing 11: Get the consolidated activities from the Activity system
 
During the rendering logic the web part html is constructed. In the out of the box web part each ActivityEvent is passed to the ActivityTemplateVariable object which registers token replacement logic. This class contains the logic which would replace for example the ‘{Publisher}’ token as seen in Listing 4. In order to extend and even replace the token replacement you have to implement your own version of the ActivityTemplateVariable class. The suggested approach is to create your own version and call the ActivityTemplateVariable if you don’t require any special rendering.
 
So as of yet we haven’t seen anything in the Listing 3 and 4 tokens not handled by the ActivityTemplateVariable out of the box logic. In order to create something more visually close to the rich Facebook/LinkedIn UI we need to introduce the concept of the SUI version. The SUI (special user interface) allows us to keep our ECM extension functional through the out of the box Activity system, but also use the same data to render improved UI elements.
So the first step is to add the SUI version of the template mask to the resource file as shown in Listing 12. This mask use the same name but with a suffix of ‘_SUI’. The Listing 3 version compatible with the out of box rendering differs as the SUI version in Listing 12 which removes the ‘{Link}’ token and adds a new custom token ‘{ImageThumbnail}’ token after a line break.
{Publisher} has uploaded a new image into the {Link2} library. <br/> {ImageThumbnail}
Listing 12: Image added single value SUI template mask
 
During rendering when the ActivityTemplate resource name is retrieved the logic in Listing 13 is invoked to check for a SUI version before falling back to the normal template. This technique can also be used to replace the template mask for out of the box templates. The listing shows the ‘osrvcore’ substitution to replace the out of box masks.
protected string GetActivityTypeDisplayFormat(ActivityTemplate template, bool useAlternativeFormat)
{
if (useAlternativeFormat)
{
string format = String.Empty;
if (template.TitleFormatLocStringResourceFile.Contains("OsrvCore"))
{
string resourceFileName = template.TitleFormatLocStringResourceFile.Replace("OsrvCore", "OsrvCore_SUI");
format = GetResourceString(resourceFileName, template.TitleFormatLocStringName + "_SUI", (uint)CultureInfo.CurrentUICulture.LCID);
}
else
{
format = GetResourceString(template.TitleFormatLocStringResourceFile, template.TitleFormatLocStringName + "_SUI", (uint)CultureInfo.CurrentUICulture.LCID);
}
if (!String.IsNullOrEmpty(format))
{
return format;
}
}
return GetResourceString(template.TitleFormatLocStringResourceFile, template.TitleFormatLocStringName, (uint)CultureInfo.CurrentUICulture.LCID);
}
Listing 13: Template mask SUI selection logic
 
Once you have this mask the custom token will be recognized by your ExtendedActivityTemplateVariable logic. Listing 14 shows how you might deal with the ‘{ImageThumbnail}’ token.
public static string ImageThumbnailToString(string tag, ActivityTemplateVariable atv, ContentType ct, CultureInfo ci)
{
StringBuilder builder = new StringBuilder(string.Empty);
switch (ct)
{
case ContentType.PlainText:
return atv.Link.LinkToString(ct, ci);
case ContentType.Html:
if (string.IsNullOrEmpty(atv.Link.Href))
{
builder.Append(SPHttpUtility.HtmlEncodeAllowSimpleTextFormatting(atv.Link.Title));
break;
}
builder.Append("<a href=\"");
builder.Append(atv.Link.Href);
builder.Append("\" title=\"" + atv.Link.Name + "\">");
if (!string.IsNullOrEmpty(atv.Link.Value))
{
builder.Append("<img src=’" + atv.Link.Value + "’ alt=’" + atv.Link.Name + "’ style=’max-width:96px;max-height:96px’ />");
}
else if (!string.IsNullOrEmpty(atv.Link.Name))
{
builder.Append(SPHttpUtility.HtmlEncode(atv.Link.Name));
}
builder.Append("</a>");
break;
default:
return null;
}
return builder.ToString();
}
Listing 14: Logic to create the image thumbnail
Using this technique you can see how a single value image looks in the out of the box in Figure 2 and how it looks in the custom SUI web part in Figure 3.
 
DIWUG_Figure2_WesHackett
Figure 2: Single value image in the out of box web part
 
DIWUG_Figure3_WesHackett

Figure 3: Single value image in the custom SUI web part

Conclusion

 
In this article we have looked at how to extend the SharePoint 2010 Activity system with enterprise content activity. Hopefully this has given you an understanding of the elements and an example approach to implement this extension. Other elements to consider are the security trimming approaches, subscription mechanism so that you don’t have to be colleagues to see activity and embellishing the UI even further with jQuery/Ajax style implementations.

SP2010 MySites – Part Five: Blog

MyProfile

This is the fifth article in a series which explores the SharePoint 2010 MySite features. This article will examines the default Blog creation and highlight changes required to customise this default.

Series contents:

  1. Overview, which details the MySite functionality provided by SP2010
  2. Anatomy, we delve inside the MySite and dissect its inner secrets
  3. Customisation for Branding, how to change the look and feel
  4. Customisation of My Content page, how to change the page layout and contents
  5. Customisation of a MySite Blog, how to alter the Blog – This Article
  6. The MySite centric intranet, putting the MySite at the centre of the universe

The challenge

 

By default the OOB MySite contains a web part which gives the user a quick prompt to create a personal blog on both the default.aspx and the persons profile page.

image

When clicked this will create a sub site based on the Blog site definition. It provisions a new blog site with some default data. So which approach do you take if you wanted to add additional functionality or change that default data for the blog site? The obvious answer would be to create the required features and staple these to the Blog site definition.

So assume we have several stapled features which replace the default content and make some additional tweaks to provision a client specific ‘enhanced’ blog. In theory when the user clicks the link to ‘Create Blog’ these features will get activated during site creation and all will be sorted.

You have to consider that stapled features can’t act upon anything being provisioned in the definition modules. If you examine the Blog site definition you’ll see that the lists and data are provisioned this way. So you need to take another approach to modify the data within provisioned lists. One approach would be to use the List provisioned or Site provisioned event handlers and put the logic there.

Well this works without error but the end result is not what you’d quite expect.

Unexpected results

 

So nothing unusual there, the blog created with no errors, works as it should and is still based on the OOB Blog site definition. But the custom data is not there and blog posts were not showing in the NewsFeed. Fearing something was fundamentally broken the investigation began. The following is the details about what’s going on in depths when the user clicks ‘Create Blog’

The fundamental problem was that when the user clicks this specific web part link it goes through some MySite specific logic to attach three event receivers to the Post list.

The ‘Microsoft.Office.Server.UserProfiles.BlogListEventReceiver’ needs to have the item added, updated and deleted events registered against the blog site Post list. This handler is responsible for generating the ActivityFeed change marker which drives the activity gathering event generation.

The OOB page contains the Microsoft.SharePoint.Portal.WebControls.BlogView web part. This web part checks for the existence of a property bag property of ‘urn:schemas.microsoft.com:sharepoint:portal:profile:blogwebid’ if this is not null or empty it renders the link for creating a new blog site. The generated url is something like ‘/_layouts/MySite.aspx?Blog=1’. As you can see this is pointing at the MySite aspx page but interesting has a query string value. This is the important part. Normally this page will simply check the existing user has a MySite and create one if not. With the query string value of ‘Blog=1’ it’s also instructed to generate a blog.

The logic basically checks the ‘urn:schemas.microsoft.com:sharepoint:portal:profile:blogwebid’ to see if a blog exists (will navigate to it if it did). If no value is set the the code will activate the ‘MySiteBlogs’ feature. This feature generates the site then sets the property. Once the site exists it will modify the list items adding the generic values. Final step is to associate the ‘Microsoft.Office.Server.UserProfiles.BlogListEventReceiver’ handlers.

With this knowledge in hand a quick addition to the custom blog stapling feature had them also attaching the missing handlers.

That fixes the missing Activity events but the data still appears to be wrong. During the execution of site creation the MySite.aspx modifies the data in the lists and replaces it with versions it chooses. The only 100% accurate way of stopping is to implement all this functionality in a page/handler yourself and adjust the blog web part link to point to your custom code instead.

What’s next?

 

So now we’ve seen some of the MySite specific extras for Blog sites. In the next article we’ll examine using the MySite functionality as the default homepage for an intranet.