I just got back from the SharePoint Conference in Las Vegas a few weeks ago. One of the things that I learned about and am excited about is in SharePoint 2010 you can have custom activities that show up in the colleague tracker for My Sites. In SharePoint 2007, you were stuck with the out of the box (OOTB) activities such as: birthday, blog, profile attribute change, etc. When we rolled out our My Sites at work, we had two other lists that we wanted to add to people’s colleague tracker when a colleague of theirs added something to these lists. I was able to figure out a solution but I will warn you ahead of time, it’s a little hacky. Not only is the ootb Microsoft code for blog activities hacky, my solution sits on top of that and is more hacky than their code. This is all because Microsoft marked Internal several objects I wanted to use.
There are two parts to this solution. Part 1 is an eventreceiver that will add items to a user’s activity feed. Part 1 adds items the same way that blogs are added to the activity feed. Part 2 extends the colleague tracker webpart so that these new items in the activity feed don’t say blog beside them, and will actually say what you want them to say.
So for part 1 I used Reflector to see the code for the OOTB eventreceiver, Microsoft.Office.Server.UserProfiles.BlogListEventReceiver. Examining this code revealed that MS is calling some stored procedures in the SharedServices database to add items to the activity feed. There were two objects in this class that were marked Internal that I needed to figure out a way around. These objects allowed sql code to be executed on the SharedServices database. Since I couldn’t use these, the main hack involved hard coding the sharedservices database name into the event receiver. If someone knows how to get the name of the sharedservices database through the api, let me know, that would be a much better solution.
My solution below uses the author column to determine the user which this activity is for. Our solution at work required us to use a custom column. But if all you need is for the activity to be for the person that is adding the item and the list you are wanting to use is a blog, then you might just want to use the OOTB Microsoft.Office.Server.UserProfiles.BlogListEventReceiver.
Here’s the part 1 code or you can download it here:
using System.Data.SqlClient;
using System.Data;
using Microsoft.SharePoint;
using System;
using System.IO;
using System.Xml;
using System.Globalization;
using Microsoft.Office.Server;
using Microsoft.Office.Server.UserProfiles;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.Administration;
namespace EventHandlers
{
public class AddListChangesToUserProfile : SPItemEventReceiver
{
// Methods
public override void ItemAdded(SPItemEventProperties properties)
{
if (properties == null) return;
if (properties.ListItemId <= 0) return;
using (SPSite site = new SPSite(properties.SiteId))
{
ServerContext serverContext = ServerContext.GetContext(site);
UserProfile userProfile = new UserProfileManager(serverContext).GetUserProfile(properties.UserLoginName);
using (SPWeb web = properties.OpenWeb())
{
SPList list = web.Lists[properties.ListId];
string title = list.GetItemById(properties.ListItemId).Title;
AddNewEntry(serverContext, userProfile.RecordId, title, string.Format(CultureInfo.InvariantCulture, GetFormUrl(list, PAGETYPE.PAGE_DISPLAYFORM) + "?ID={0}", new object[] { properties.ListItemId }));
}
}
base.ItemAdded(properties);
}
public override void ItemDeleting(SPItemEventProperties properties)
{
if (properties == null) return;
if (properties.ListItemId <= 0) return;
using (SPSite site = new SPSite(properties.SiteId))
{
using (SPWeb web = properties.OpenWeb())
{
SPList list = web.Lists[properties.ListId];
SPListItem item = list.GetItemById(properties.ListItemId);
string title = item.Title;
SPUser user = GetUserFromItemValue(web, item["Author"]);
ServerContext serverContext = ServerContext.GetContext(site);
UserProfile userProfile = new UserProfileManager(serverContext).GetUserProfile(user.LoginName);
RemoveEntry(serverContext, userProfile.RecordId, title, string.Format(CultureInfo.InvariantCulture, GetFormUrl(list, PAGETYPE.PAGE_DISPLAYFORM) + "?ID={0}", new object[] { properties.ListItemId }));
}
}
}
public override void ItemUpdating(SPItemEventProperties properties)
{
this.ItemDeleting(properties);
base.ItemUpdating(properties);
}
public override void ItemUpdated(SPItemEventProperties properties)
{
this.ItemAdded(properties);
base.ItemUpdated(properties);
}
public static string MakeXml(string subject, string permaLink)
{
StringWriter w = new StringWriter(CultureInfo.InvariantCulture);
XmlTextWriter writer2 = new XmlTextWriter(w);
writer2.WriteStartDocument();
writer2.WriteStartElement("WebLog");
writer2.WriteElementString("Title", subject);
writer2.WriteElementString("PermaLink", permaLink);
writer2.WriteEndElement();
writer2.WriteEndDocument();
writer2.Close();
return w.ToString();
}
public static void AddNewEntry(ServerContext context, long userID, string subject, string permaLink)
{
SqlCommand command = new SqlCommand("dbo.Profile_AddWebLogEvent");
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@RecordId", SqlDbType.BigInt).Value = userID;
command.Parameters.Add("@Data", SqlDbType.Variant).Value = MakeXml(subject, permaLink);
//try
//{
ExecuteNonQuery(command);
//}
//catch (SqlException exception)
//{
//}
}
public static void ExecuteNonQuery(SqlCommand command)
{
SPWebService service = SPFarm.Local.Services.GetValue<SPWebService>();
SPDatabaseServiceInstance defaultDatabaseInstance = service.DefaultDatabaseInstance;
string databaseServer = SPHttpUtility.NoEncode(defaultDatabaseInstance.NormalizedDataSource);
string connectionString = "Data Source=" + databaseServer + ";Initial Catalog=SharedServices_DB;User ID=uid;Password=pwd;";
SqlConnection connection = new SqlConnection();
connection.ConnectionString = connectionString;
command.Connection = connection;
using (connection)
{
connection.Open();
command.ExecuteNonQuery();
connection.Dispose();
}
}
public static void RemoveEntry(ServerContext context, long userID, string subject, string permaLink)
{
SqlCommand command = new SqlCommand("dbo.Profile_RemoveWebLogEvent");
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@RecordId", SqlDbType.BigInt).Value = userID;
command.Parameters.Add("@Data", SqlDbType.Variant).Value = MakeXml(subject, permaLink);
try
{
ExecuteNonQuery(command);
}
catch (SqlException exception)
{
}
}
public static string GetFormUrl(SPList list, PAGETYPE type)
{
using (SPWeb web = list.ParentWeb)
{
string Url = web.Url + "/" + list.Forms[type].Url;
Url = Url.Replace(" ", "%20");
return Url;
}
}
public static SPUser GetUserFromItemValue(SPWeb web, object columnVal)
{
SPUser user = null;
try
{
string[] userarr = columnVal.ToString().Split('#');
using (SPSite site = web.Site)
{
using (SPWeb rootWeb = site.RootWeb)
{
user = rootWeb.AllUsers.GetByID(Convert.ToInt32(userarr[0].Replace(";", string.Empty)));
}
}
}
catch { }
return user;
}
}
}
So part 2 involved extending the colleague tracker webpart, Microsoft.SharePoint.Portal.WebControls.ContactLinksMicroView, and overriding the method GenerateHtmlOneRowForOneItem and doing some find and replace magic. From what I understand, the GenerateHtmlOneRowForOneItem method returns all the html output for one colleague in the colleague tracker. Since a colleague could have a blog entry and any of our other custom activities, it made the find and replace a little more difficult. Basically, the code searchs in the html for part of the url to one of the custom activities. Once it finds that location, it finds the string “Blog:” that is the first instance before that location and replaces it with the name of the activity.
My solution only replaces the text “Blog:” with the appropriate name. If you wanted to extend my solution you can also add some additional find and replace magic to change the icon as well. My solution includes two different custom activities, List and Comments. You can add more as you see fit.
Here’s the code for part 2 or you can download it here:
using System;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Xml.Serialization;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebPartPages;
using System.Web.UI.WebControls.WebParts;
using System.Data;
using System.Text;
namespace WebParts
{
/// <summary>
/// Description for PrintPage.
/// </summary>
[DefaultProperty("Text"),
ToolboxData("<{0}:ContactLinksMicroView runat=server></{0}:ContactLinksMicroView>"),
XmlRoot(Namespace="WebParts")]
public class ContactLinksMicroView : Microsoft.SharePoint.Portal.WebControls.ContactLinksMicroView
{
#region Variables
private const string LIST_URL = "/mysite/Lists/MyList/";
private const string COMMENT_URL = "/mysite/Lists/MyComments/";
private const string BLOG = "Blog:";
private const string LIST = "List:";
private const string COMMENT = "Comment:";
private string _listUrl = LIST_URL;
private string _commentUrl = COMMENT_URL;
#endregion
#region Properties
[Browsable(true),
Category("Administration"),
DefaultValue(LIST_URL),
WebPartStorage(Storage.Personal),
FriendlyName("List URL"),
Description("URL to the List")]
public string ListUrl
{
get
{
return _listUrl;
}
set
{
_listUrl = value;
}
}
[Browsable(true),
Category("Administration"),
DefaultValue(COMMENT_URL),
WebPartStorage(Storage.Personal),
FriendlyName("Comments URL"),
Description("URL to the Comments")]
public string CommentUrl
{
get
{
return _commentUrl;
}
set
{
_commentUrl = value;
}
}
[WebPartStorage(Storage.None)]
public override string TitleUrl
{
get
{
return string.Empty;
}
set
{
}
}
#endregion
public ContactLinksMicroView()
{
this.ChromeType = PartChromeType.None;
}
protected override void GenerateHtmlOneRowForOneItem(DataRow objectDataRow, System.Text.StringBuilder sbRenderRowHtml, int rowID, string strStyleClass, int iIndexOfItemInDataSet, int iIndexOfItemInGroup)
{
StringBuilder temp = new StringBuilder();
base.GenerateHtmlOneRowForOneItem(objectDataRow, temp, rowID, strStyleClass, iIndexOfItemInDataSet, iIndexOfItemInGroup);
string temp2 = temp.ToString();
temp2 = FixHtml(temp2, LIST, ListUrl);
temp2 = FixHtml(temp2, COMMENT, CommentUrl);
sbRenderRowHtml.Append(temp2);
}
private string FixHtml(string html, string title, string url)
{
if (html.IndexOf(url) < 0) return html;
string needsToBeSearched = html;
string alreadySearched = string.Empty;
while (needsToBeSearched.Length > 0)
{
Split(ref needsToBeSearched, ref alreadySearched, url);
Split(ref needsToBeSearched, ref alreadySearched, BLOG);
if (alreadySearched.StartsWith(BLOG))
{
alreadySearched = title + alreadySearched.Substring(BLOG.Length);
}
}
return alreadySearched;
}
private void Split(ref string needsToBeSearched, ref string alreadySearched, string search)
{
int pos = needsToBeSearched.LastIndexOf(search);
if (pos < 0)
{
alreadySearched = needsToBeSearched + alreadySearched;
needsToBeSearched = string.Empty;
return;
}
alreadySearched = needsToBeSearched.Substring(pos) + alreadySearched;
needsToBeSearched = needsToBeSearched.Substring(0, pos);
}
}
}
Now that we have a working colleague tracker webpart that displays the correct text for our activity, I needed to replace the current colleague tracker webpart on everyone’s My Site with our custom one. This is a pretty straightforward feature but if you want me to post the feature code, leave a comment below.
Also, don’t forget to also attach either my eventreceiver above or the OOTB Microsoft.Office.Server.UserProfiles.BlogListEventReceiver eventreceiver to the list you want to show up as a custom activity. This can be done a few different ways. I ended up creating a webservice where I input the url to the list, and the name of the eventreceiver and it attached them.