SharePoint DNS name considerations when using anonymous access

Last year we migrated our external SharePoint farm from SharePoint 2007 to SharePoint 2010. This envrionment has anonymous access turned on for the web application and certain sites have anonymous turned on. We’ve been having issues when accessing these sites with anonymous turned on where it would show us as being signed out for one request and then signed in for the next. See the video here to see what I’m talking about (please ingore the fact that the picture is not working, that’s an unrelated issue…).

We ended up opening a case with Microsoft and they were able to finally figured out what’s going on. It’s a cookie conflict problem with the WSS_KeepSessionAuthenticated cookie. This is the cookie that get’s set to let SharePoint know that you have already authenticated when on an anonymous site. The reason we have a conflict was because of how our dns names for are SharePoint sites are setup. We have a SP 2007 environment at https://company.com which sets the cookie to be 443 (https port #). Our anonymous accessible SP 2010 environment is at https://partners.company.com which is the one with the issues. This environment sets the cookie to be a guid.

Since partners.company.com is a sub-domain of company.com, both the guid and the 443 get sent to partners.company.com for the same cookie name because of sub-domain cookie sharing. I think the SP code in SP 2010 is only looking at the first value when checking for the cookie in the request and sometimes 443 is first and other times the guid is first in the cookie’s namevaluecollection. Thus it shows a user as being logged out when the 443 happens to be first in the namevaluecollection and logged in when the guid is first.

This never showed up before with us and SP 2007 because we used SSL everywhere so the cookie was being set to 443 for all of the SharePoint web applications. Now that SP 2010 uses the web application guid I see this being a bigger problem. Our ultimate solution is to change the dns names for our SharePoint sites. Until then, I was playing around with an HttpModule to “fix” the issue on our SP 2010 environment. Unfortunately, when I tried to remove the cookie that was not a guid, this also causes a race condition with the SharePoint HttpModule which checks the cookie and caused the same behavior even when the correct cookies were being sent. So the thought was to create a HttpModule for SP 2007 that would remove the cookie after it was being set because we aren’t using anonymous in our SP 2007 environment. Unfortunately, this didn’t work either because we use blob caching which exited out of the pipeline before I could remove the cookie. So my final code, which is below, was to always expire the cookie and hopefully, a later request will expire any that the blob cache sets.

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;

public class RemoveAuthCookie : IHttpModule
{

	/// <summary></summary>
	/// Initializes the module, and registers for application events.
	/// 
	public void Init(HttpApplication context)
	{
		context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute);
		context.PostAuthenticateRequest += new EventHandler(context_PostAuthenticateRequest);
	}

	void context_PostAuthenticateRequest(object sender, EventArgs e)
	{
		HttpApplication app = sender as HttpApplication;
		RemoveCookie(app);
	}

	void context_PreRequestHandlerExecute(object sender, EventArgs e)
	{
		HttpApplication app = sender as HttpApplication;
		RemoveCookie(app);
	}

	void RemoveCookie(HttpApplication app)
	{
		if (app == null) return;
		if (app.Response == null) return;
		if (app.Response.Cookies == null) return;

		try
		{

			if (app.Response.Cookies[&quot;WSS_KeepSessionAuthenticated&quot;] == null)
			{
				//WSS_KeepSessionAuthenticated cookie not found
			}
			else
			{

				app.Response.Cookies[&quot;WSS_KeepSessionAuthenticated&quot;].Expires = DateTime.Now.AddDays(-1D);
				//WSS_KeepSessionAuthenticated cookie EXPIRED
			}

			if (app.Response.Cookies[&quot;MSOWebPartPage_AnonymousAccessCookie&quot;] == null)
			{
				//MSOWebPartPage_AnonymousAccessCookie cookie not found
			}
			else
			{
				app.Response.Cookies[&quot;MSOWebPartPage_AnonymousAccessCookie&quot;].Expires = DateTime.Now.AddDays(-1D);
				//MSOWebPartPage_AnonymousAccessCookie cookie EXPIRED
			}                
		}
		catch { }
	}

	/// <summary></summary>
	/// Disposes of the resources (other than memory) used by the module.
	/// 
	public void Dispose()
	{
	}
}

So, this is not a fun situation to be in, so please keep this in mind when you are architecting your SharePoint solutions, a little thing such as the url used to access the site can be a hard thing to change later.

NTLM login with Anonymous fallback

At work we run a fairly large extranet SharePoint farm where some sites are anonymous access. Normally in SharePoint, when a domain user on a domain workstation hits a SharePoint site that requires authentication, it automatically logs them in. Unfortunately, in that same scenario but the site is anonymous, the user will not get logged in. The user then has to realize this and click sign in at the top right. Also because SharePoint security trims the user interface so you only see what you have access to see.  Many times users will complain they have lost their permissions or something because they won’t be able to find their normal way of doing things when they don’t realize they aren’t logged in.

Several years ago I set out on a mission to fix this. The best solution I found was posted here: http://blogs.claritycon.com/blogs/ryan_powers/archive/2007/06/12/3187.aspx. I was able to take what he had done and modify it for SharePoint. So since authentication is tied to the session, we only need to run our code once per session. So the trick is determining when a new session is starting and running the code. I was unsuccessful in getting the SessionStateModule.Start event to fire and not fire when I needed it to so I decided to use the PostAcquireRequestState event and check on each request if I have already run my code or not. I’m using a session variable to ensure I only run the code once per session. Essentially what the code does is the first time a new session is started instead of outputting the normal html requested, the httpmodule outputs some javascript.

This javascript makes a web call to /_layouts/authenticate.aspx which is the page used to login you into SharePoint. This call is done in a way so if an error happens (such as not being able to login) that error is trapped and the end user never sees the gray login box. Whether the login is successful or not the javascript then refreshes the page. At this point the httpmodule will not change the html output because it’s already been run on the current session so the real requested html is sent and if the authentication was successful, the user is shown as being logged into the site.

Also notice I added the querystring Source=%2F%5Flayouts%2Fimages%2Fblank%2Egif to the authenticate url. Normally when authenticate.aspx is called without this querystring it will redirect the user to the current site’s homepage. Since this causes extra execution time running webparts and rendering the page as well as the end user never sees this page because the call is being done in javascript, I found it was much faster to send the user to an image after authenticating. So i chose the blank.gif which is available on any SharePoint installation. This requires much less resources on the server as well as showing the actual page faster to the browser.

I also do a few checks before I output the javascript. Since automatic ntlm login only works in IE and windows, I check the useragent for that condition. I also check to make sure the request isn’t for infopath form services (/_layouts/formserver.aspx) because we had some issues with the module not playing nice with those services.

Anyway, here’s the code:

public class MixedAuthenticationScreeningModule : IHttpModule
{
    private const string NO_SCRIPT_MESSAGE = "Your browser does not support JavaScript or has scripting disabled, which prevents credentials screening from working.";
    private string _requiresAuthenticationUrl = "/_layouts/Authenticate.aspx?Source=%2F%5Flayouts%2Fimages%2Fblank%2Egif";
 
    void IHttpModule.Init(HttpApplication context)
    {
        context.PostAcquireRequestState += new EventHandler(context_PostAcquireRequestState);
    }
 
    void context_PostAcquireRequestState(object sender, EventArgs e)
    {
        MixedModeLogin();
    }
 
    private void MixedModeLogin()
    {
        HttpContext context = HttpContext.Current;
        if (context.Session == null) return;
        if (context.Request.RequestType != "GET") return;
        if (context.Session["MixedModeAuth"] != null) return;
        context.Session["MixedModeAuth"] = false;
        if (IsWin32Ie(context) == false) return;
        if (context.Request.RawUrl.ToLower().Contains("/_layouts/formserver.aspx")) return;
        context.Session["MixedModeAuth"] = true;
        RenderScreeningHtml(context.Request.RawUrl, context.Response);
    }
 
    private bool IsWin32Ie(HttpContext context)
    {
        string userAgent = context.Request.UserAgent;
        return userAgent != null && userAgent.IndexOf("MSIE") >= 0 && userAgent.IndexOf("Windows") >= 0;
    }
 
    private void RenderScreeningHtml(string currentUrl, HttpResponse response)
    {
        string screeningFailedUrl = currentUrl;
        response.Cache.SetCacheability(HttpCacheability.NoCache);
        response.Cache.SetExpires(DateTime.Now.AddDays(-1)); //or a date much earlier than current time
        response.Write("<!--DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"--><html></html><head></head><script type="text/javascript" language="javascript">  function canAuthenticate() { try { var dom = new ActiveXObject("Msxml2.DOMDocument");  dom.async = false; dom.load("" + _requiresAuthenticationUrl + "");} catch(e) { return false; }  return true;}  canAuthenticate(); window.location.href=window.location.href; </script><noscript></noscript>");
        try
        {
            response.End();
        }
        catch { }
    }
 
    void IHttpModule.Dispose()
    {
    }
}

Here is what the user’s see when the javascript code is trying to log them in:

Here is what shows up in fiddler when someone is logged in successfully. Notice the first time default.aspx is called it’s only 763 bytes and the second time 61 KB. That’s because the first time only the javascript was sent to the browser, but the second time the whole html for the page was sent to the browser. You can also see where the authentication happens and since it was successful you can see where the blank.gif was loaded.

 

Here is what shows up in fiddler when someone is not logged in successfully, thus it falls back to anonymous. You can see that the authentication doesn’t happen and blank.gif is never called.

PowerPoint 2010 Save As Web Page

Recently a need came up for a user that wanted to post a PowerPoint slide with links embedded in it onto a SharePoint 2007 teamsite. Unfortunately, in Office 2010 the ability to Save As Web Page doesn’t exist anymore. It seems they are really promoting their PowerPoint services (Office Web Apps) and decided to remove this capability from the user interface. Fortunately, the backend code is still there to perform this function.

I found this forum post: http://social.technet.microsoft.com/Forums/en/officeappcompat/thread/89d70894-b455-4d3e-a801-f2574c3a0f5a talking about a quick way to save as html through the visual basic editor in PowerPoint. This was good but not very user friendly.

I decided to write my own PowerPoint Add-In to add a button in the ribbon that saves the presentation to html. Since this is my first add-in, it did take me a little while to get it working but I was really happy with the development experience.  Hitting F5 compiled, built and deployed my add-in and then started powerpoint for me.  This made debuging and testing really quick.

To get started I fired up Visual Studio 2010 and created a new PowerPoint 2010 Add-In. I then added a new item and selected Ribbon. I also added a button to the ribbon from the toolbox and created an on-click event for it. Below is the code for the on-click event:

SaveFileDialog saveFileDialog1 = new SaveFileDialog();

saveFileDialog1.Filter = "htm files (*.htm)|*.htm|All files (*.*)|*.*";
saveFileDialog1.FilterIndex = 0;
saveFileDialog1.RestoreDirectory = true;
saveFileDialog1.AddExtension = true;
saveFileDialog1.AutoUpgradeEnabled = true;
saveFileDialog1.CheckFileExists = false;
saveFileDialog1.CheckPathExists = true;
saveFileDialog1.DefaultExt = "htm";

if (saveFileDialog1.ShowDialog() != DialogResult.OK) return;

string filepath = saveFileDialog1.FileName;

Globals.SaveToHtml.Application.ActivePresentation.SaveAs(filepath, Microsoft.Office.Interop.PowerPoint.PpSaveAsFileType.ppSaveAsHTML, Microsoft.Office.Core.MsoTriState.msoFalse);

 

Here’s a screenshot of what it looks like:

 

One of the reasons it took me a while was I had issues getting it installed on another machine.  It turns out I didn’t select Microsoft Visual Studio 2010 Tools for Office Runtime as a prerequisite to install under the publish settings.  Once that was checked it installed fine on other machines.

SPCalendarView not working as expected in SharePoint Foundation 2010

I’m currently working to make sure all of my custom code works correctly in SharePoint Foundation 2010. One of my webparts utilized the SPCalendarView and I would set the datasource to be a SPCalendarItemCollection and it would render a calendar with the events I specified in the collection. Well, this no longer works in SharePoint Foundation 2010. I was able to achieve a workaround with a little help from reflector. You’ll need to create two classes:

public class MySPCalendarDataSource : Control, System.Web.UI.IDataSource
    {
        private MyCalendarSourceView _view;
        private static readonly object EventDataSourceChanged = new object();
        private SPCalendarItemCollection _data = null;

        public MySPCalendarDataSource(SPCalendarItemCollection data)
            : base()
        {
            _data = data;
        }

        event EventHandler System.Web.UI.IDataSource.DataSourceChanged
        {
            add
            {
                base.Events.AddHandler(EventDataSourceChanged, value);
            }

            remove
            {
                base.Events.RemoveHandler(EventDataSourceChanged, value);
            }
        }

        protected DataSourceView GetView(string viewName)
        {
            return this.internalGetView(viewName);
        }

        protected ICollection GetViewNames()
        {
            return new string[] { this.internalGetView(null).Name };
        }

        private MyCalendarSourceView internalGetView(string viewName)
        {
            if (this._view == null)
            {
                this._view = new MyCalendarSourceView(this, viewName, _data);
            }
            return this._view;
        }

        private void OnDataSourceChanged(EventArgs e)
        {
            EventHandler handler = (EventHandler)base.Events[EventDataSourceChanged];
            if (handler != null)
            {
                handler(this, e);
            }
        }

        protected virtual void RaiseDataSourceChangedEvent(EventArgs e)
        {
            this.OnDataSourceChanged(e);
        }

        DataSourceView System.Web.UI.IDataSource.GetView(string viewName)
        {
            return this.GetView(viewName);
        }

        ICollection System.Web.UI.IDataSource.GetViewNames()
        {
            return this.GetViewNames();
        }        
    }
public class MyCalendarSourceView : DataSourceView
{
	SPCalendarItemCollection _data;

	public MyCalendarSourceView(System.Web.UI.IDataSource ds, string viewName, SPCalendarItemCollection data) : base(ds, viewName)
	{
		_data = data;
	}

	protected override System.Collections.IEnumerable ExecuteSelect(DataSourceSelectArguments arguments)
	{
		return _data;
	}
}

And where in SharePoint 2007 you could do this:

SPCalendarItemCollection items = GetCalendarItems();
SPCalendarView calView = new SPCalendarView();
calView.DataSource = items;
calView.DataBind();

You now have to do this:

SPCalendarItemCollection items = GetCalendarItems();
SPCalendarView calView = new SPCalendarView();
calView.EnableV4Rendering = false;
calView.DataSource = new MySPCalendarDataSource(items);
calView.DataBind();

Also, please note that for this to work, we do need to disable the V4 ajax rendering.

Adding Custom Activities for the Colleague Tracker in SP 2007

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.