FS4SP Branding Project

In this post I’ll describe a technique for branding a FAST Search for SharePoint Search Center.  There are three places where branding changes are often made:

  1. The master page.  The FAST Search Center is based on minimal.master and in this example I’ll just take a copy of that page and make a minor tweak to prove that it is my version that is being used.   Alternatively you can convert a v4 master page.
  2. The Search Main publishing layout page.  This defines the layout of the main search page with just the search box.
  3. The Search Results publishing layout page.  This defines the layout of the search results page.

We’ll build this all using Visual Studio 2010 into a site collection feature that can be applied to any search center.  When the feature is activated, the search center will be applied and the publishing pages made active.  On deactivation, we’ll roll back our changes so the solution can be properly retracted.   Lastly for good measure, we’ll add in some logging to the ULS.

This won’t show any actual snazzy branding (I’m no UI designer).  I’ll just be adding a custom css file and enough changes to the templates to prove that the customized versions are being used.

1. Open Visual Studio 2010 (as administrator), and create a new project. File > New > Project…, choosing the Empty SharePoint Project template.

clip_image001

Verify your local site and choose "Deploy as farm solution" (we'll be logging to the ULS which you can't do as a sandboxed solution)

image

2. Add the feature. Right click on MCSBranding\Features in the Solution Explorer and choose "Add Feature". By default it will be called "Feature1", right click and rename to "Branding" (or whatever you want to call it). In the Branding.feature designer, update the title and make sure to change the scope to "Site"

clip_image003

3. Add a module to hold the master page. Right click on the MCSBranding project in the Solution Explorer, Add > New Item…, choosing the "Module" template and naming it "MasterPage"

clip_image004

4. Add our master page. In the Solution Explorer, go to MCSBranding\MasterPage and delete the Sample.txt file that was created by the template. Right click on MasterPage and choose Add > Existing…, navigate to your …14\TEMPLATE\LAYOUTS directory and choose minimal.master. Right click on "minimal.master" and rename to "mcs_minimal.master" (or whatever makes sense for you). The Solution Explorer at this point should look like:

clip_image005

In the MasterPage\Elements.xml (which should have been opened when you added the module), you now need to add some magic bits to make this page available as a master page.

 <Module Name="MasterPage" Url="_catalogs/masterPage">
    <File Path="MasterPage\mcs_minimal.master" Url="mcs_minimal.master" Type="GhostableInLibrary">
        <Property Name="Title" Value="MCS Master Page"/>
        <Property Name="MasterPageDescription" Value="MCS Master Page"/>
        <Property Name="ContentType" Value="$Resources:cmscore,contenttype_masterpage_name;" />
    </File>

If you want to make any changes specific to either the search main or results pages you'll need to add those publishing templates as well. THERE MUST BE A BETTER WAY TO DO THIS BUT HERE'S WHAT HAS WORKED FOR ME… Launch SharePoint Designer 2010 and open a site that already has an Enterprise FAST Search Center. Choose "Master Pages" in the Site Objects and you should see SearchMain.aspx and SearchResults.aspx. For each of them do the following: Right click and choose "Edit file in Advanced Mode". Do not check the file out. Switch to "Code" mode and select everything. Copy and paste into your favorite text editor and save to C:\Projects\MCSSolution\MCSBranding\MasterPage adding a "mcs_" prefix.

Next add SearchMain.aspx and SearchResults.aspx for us to customize. Right click on MasterPage again, Add > Existing…, navigate to …\MCSBranding\MasterPage and select both files to add at once. Back to the MasterPage\Elements.xml and these files also need some extra properties:

 <File Path="MasterPage\mcs_searchmain.aspx" Url="mcs_searchmain.aspx" Type="GhostableInLibrary">
    <Property Name="Title" Value="MCS Search Main" />
    <Property Name="MasterPageDescription" Value="Search Main for MCS" />
    <Property Name="ContentType" Value="$Resources:cmscore,contenttype_pagelayout_name;"></Property>
    <Property Name="PublishingAssociatedContentType" Value=";#Welcome Page;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4;#"></Property>
</File>
<File Path="MasterPage\mcs_searchresults.aspx" Url="mcs_searchresults.aspx" Type="GhostableInLibrary">
    <Property Name="Title" Value="MCS Search Main" />
    <Property Name="MasterPageDescription" Value="Search Results for MCS" />
    <Property Name="ContentType" Value="$Resources:cmscore,contenttype_pagelayout_name;"></Property>
    <Property Name="PublishingAssociatedContentType" Value=";#Welcome Page;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4;#"></Property>
</File>

5. Add a place for your branding elements. Since your branding solution will undoubtedly have custom style sheets and images, we'll next add a module to house them. Click to highlight MCSBranding and press Ctrl-Shift-A and add another Module and name it "mcsBranding" (or whatever you want).

For this example, I'm just going to rename the sample.txt to mcs_branding.css which I'll add a reference to in my master page, inserting the following line just before the </head> tag:

 <SharePoint:CssRegistration name="<% $SPUrl:~SiteCollection/mcsBranding/mcs_branding.css %>" runat="server"/>

For customizations in this example all I'm going to add is: 

Search main line 153: 

 <div id="mcs_main">MCS_SEARCHMAIN</div>

Search results line 128:

 <div id="mcs_results">MCS_SEARCHRESULTS</div>

Mcs_minimal.master line 136: 

 <div id="mcs_master">MCS_MASTER_PAGE</div>

And for mcs_branding.css: 

 #mcs_master 
{
   font-weight: bold;
   color: Red;
}

#mcs_main 
{
   font-weight: bold;
   color: Green;
}

#mcs_results
{
   font-weight: bold;
   color: Blue;
}
  

6. Feature Activation. Now comes the fun part. Thanks to my colleague Jeff Lyttle for the starter event receiver code that finally got me going on this, here's the setup that worked for me. Right click on your Branding feature and choose "Add Event Reciever".

Right click on the MCSBranding project and choose "Add reference". Select .NET tab and add Microsoft.SharePoint.Publishing. Replace the existing block of using's with:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Publishing;
using Microsoft.SharePoint.Administration;

In the event reciever, select and delete the commented out FeatureActivated and FeatureDeactivating method stubs and replace with

 public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    LoggingService.LogMonitor("DarpaBranding entering FeatureActivated");

    SPSite site = properties.Feature.Parent as SPSite;
    SPList masterPageGallery = site.GetCatalog(SPListTemplateType.MasterPageCatalog);
    foreach (SPListItem li in masterPageGallery.Items)
    {
        if (li.File.Name.ToLower() == "mcs_minimal.master" ||
            li.File.Name.ToLower() == "mcs_searchmain.aspx" ||
            li.File.Name.ToLower() == "mcs_searchresults.aspx" )
        {
            if (!li.HasPublishedVersion)
            {
                LoggingService.LogInfo(String.Format("Publishing {0}", li.File.Name));
                li.File.CheckIn("Automatically checked in by MCS Branding feature",
                SPCheckinType.MajorCheckIn);
                li.File.Update();
                li.File.Approve("Automatically approved by MCS Branding feature");
                li.File.Update();
            }
        }
    }
    SPWeb web = site.RootWeb;
    if (web.CustomMasterUrl.EndsWith("minimal.master"))
    {
        LoggingService.LogInfo("Applying our master page");
        Uri masterUri = new Uri(web.Url + "/_catalogs/masterpage/mcs_minimal.master");
        web.CustomMasterUrl = masterUri.AbsolutePath;
        LoggingService.LogInfo("Applying custom publishing pages");
        // register our search layout pages 
        PageLayout[] allLayouts = AddPageLayout(web, new string[] { "mcs_searchresults.aspx",
                                                                    "mcs_searchmain.aspx" });
        // set them on the search pages
        SetPublishingPageLayout(web, allLayouts, "default.aspx", "mcs_searchmain.aspx");
        SetPublishingPageLayout(web, allLayouts, "results.aspx", "mcs_searchresults.aspx");
        web.Update();
    }
}

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    SPSite site = properties.Feature.Parent as SPSite;
    SPWeb web = site.RootWeb;
    // roll back master page if needed
    if (web.CustomMasterUrl.EndsWith("mcs_minimal.master"))
    {
        Uri masterUri = new Uri(web.Url + "/_catalogs/masterpage/minimal.master");
        web.CustomMasterUrl = masterUri.AbsolutePath;
    }
    // reset the page layouts so our customized files can be retracted
    PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
    PageLayout[] layouts = pubWeb.GetAvailablePageLayouts();
    SetPublishingPageLayout(web, layouts, "default.aspx", "SearchMain.aspx");
    SetPublishingPageLayout(web, layouts, "results.aspx", "SearchResults.aspx");
    web.Update();
}
  

You'll also need these helper methods. Paste at the bottom of the class: 

         private static PageLayout[] AddPageLayout(SPWeb currentWeb, string[] layoutNames)
        {
            PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(currentWeb);

            PageLayout[] layouts = pubWeb.GetAvailablePageLayouts();

            List<string> layoutsToAdd = new List<string>();

            foreach (string currentLayoutName in layoutNames)
            {
                var layoutExists = from PageLayout currentLayout in layouts
                                   where currentLayout.Name == currentLayoutName
                                   select currentLayout;

                if (layoutExists.Count() == 0)
                {
                    layoutsToAdd.Add(currentLayoutName);
                }
            }

            if (layoutsToAdd.Count > 0)
            {
                List<PageLayout> newLayouts = new List<PageLayout>();

                SPList masterPageList = currentWeb.GetCatalog(SPListTemplateType.MasterPageCatalog);

                List<string> j = new List<string>();
                foreach (SPListItem r in masterPageList.Items)
                {
                    j.Add(r.Name);
                }
                j.Sort();
                string[] z = j.ToArray();

                foreach (string currentLayoutToAdd in layoutsToAdd)
                {
                    SPListItem itemLayoutsToAdd = (from SPListItem currentItem in masterPageList.Items
                                                   where (currentItem.Name == currentLayoutToAdd)
                                                   select currentItem).First();

                    newLayouts.Add(new PageLayout(itemLayoutsToAdd));
                }

                List<PageLayout> newLayoutList = layouts.ToList();
                newLayoutList.AddRange(newLayouts);

                PageLayout[] newLayoutSet = newLayoutList.ToArray();

                pubWeb.SetAvailablePageLayouts(newLayoutSet, false);
                pubWeb.Update();
                currentWeb.Update();

                return newLayoutSet.ToArray();

            }
            else
            {
                return layouts.ToArray();
            }

        }

        private static void SetPublishingPageLayout(SPWeb currentWeb, PageLayout[] existingLayouts, string pageName, string layoutName)
        {
            PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(currentWeb);

            PublishingPageCollection pubPages = pubWeb.GetPublishingPages();

            var pages = from PublishingPage currentPage in pubPages
                                where currentPage.Name == pageName
                                select currentPage;

            if (pages.Count() == 0)
            {
                LoggingService.LogError(String.Format("Failed to find publishing page {0}", pageName));
                return;
            }
            PublishingPage page = pages.First();

            var layouts = from PageLayout currentLayout in existingLayouts
                                        where currentLayout.Name == layoutName
                                        select currentLayout;
            if (layouts.Count() == 0) 
            {
                LoggingService.LogError(String.Format("Failed to find page layout {0}", layoutName));
                return;
            }
            PageLayout layout = layouts.First();

            LoggingService.LogInfo(String.Format("Setting {0} layout to {1}", pageName, layoutName));
            page.CheckOut();
            page.Layout = layout;
            page.Update();
            page.CheckIn("Added in by feature");
            SPFile pageFile = page.ListItem.File;
            pageFile.Publish("Added in by feature");
            LoggingService.LogInfo(String.Format("Publishing {0}", pageName));
        }
    }
        




    class LoggingService : SPDiagnosticsServiceBase
    {
        // This is what shows up in the Product column of the ULS viewer
        public static string LOG_AREA = "MCS Feature";
        private static LoggingService _Current;
        public static LoggingService Current
        {
            get
            {
                if (_Current == null)
                {
                    _Current = new LoggingService();
                }
                return _Current;
            }

        }

        private LoggingService()
            : base("MCS Logging Service", SPFarm.Local)
        {
        }

        protected override IEnumerable<SPDiagnosticsArea> ProvideAreas()
        {
            List<SPDiagnosticsArea> areas = new List<SPDiagnosticsArea>
            {
                new SPDiagnosticsArea(LOG_AREA, new List<SPDiagnosticsCategory>
                {
                    new SPDiagnosticsCategory("ERROR", TraceSeverity.Unexpected, EventSeverity.Error),
                    new SPDiagnosticsCategory("INFO", TraceSeverity.Verbose, EventSeverity.Information),
                    new SPDiagnosticsCategory("monitoring", TraceSeverity.Medium, EventSeverity.Information)
                })
            };
            return areas;
        }

        public static void LogError(string errorMessage)
        {
            LogError(errorMessage, 0);
        }
        public static void LogError(string errorMessage, ushort id)
        {
            string categoryName = "ERROR";
            SPDiagnosticsCategory category = LoggingService.Current.Areas[LOG_AREA].Categories[categoryName];
            LoggingService.Current.WriteTrace(id, category, TraceSeverity.Unexpected, errorMessage);
        }
        public static void LogInfo(string infoMessage)
        {
            LogInfo(infoMessage, 0);
        }
        public static void LogInfo(string infoMessage, ushort id)
        {
            string categoryName = "INFO";
            SPDiagnosticsCategory category = LoggingService.Current.Areas[LOG_AREA].Categories[categoryName];
            LoggingService.Current.WriteTrace(id, category, TraceSeverity.Verbose, infoMessage);
        }
        public static void LogMonitor(string infoMessage)
        {
            LogMonitor(infoMessage, 0);
        }
        public static void LogMonitor(string infoMessage, ushort id)
        {
            string categoryName = "monitoring";
            SPDiagnosticsCategory category = LoggingService.Current.Areas[LOG_AREA].Categories[categoryName];
            LoggingService.Current.WriteTrace(id, category, TraceSeverity.Medium, infoMessage);
        }

    }
  

7. Deploy it! Right click on the MCSBranding project and choose Deploy. (note: if your test farm has multiple WFE servers this will error out with the error "Error occurred in deployment step 'Activate Features': Feature with Id 'GUID' is not installed in this farm, and cannot be added to this scope."… apparently that's a known issue and you'll have to then go to Central Admin > System Settings > Manage Farm Solutions and deploy the solution manually.)

Now over to your FAST Search Center and to Site Settings -> Site Collection Features and you should see your feature ready to be activated:

clip_image006

Click "Activate" and if everything works you should just see that it has now been activated. If you were running the ULS Viewer you can filter by the Product "MCS Feature" and should be rewarded with:

clip_image007

Now if you go to the search home page you should see your customizations.

clip_image008

And on the results page:

clip_image009