Jun
23
2008

(CodePlex) MOSS Faceted Search - HTML encode/decode problem fixed!

MOSS Faceted Search is a great SharePoint search extension project on CodePlex. It provides following features (copy from its CodePlex site)

  • Grouping search results by facet
  • Displaying a total number of hits per facet value
  • Refining search results by facet value
  • Update of the facet menu based on refined search criteria
  • Displaying of the search criteria in a Bread Crumbs
  • Ability to exclude the chosen facet from the search criteria
  • Flexibility of the Faceted search configuration and its consistency with MOSS administration

However, there are still some known issues exist due to this project does not update and release regularly.

HTML Encoding

An issue I experienced was HTML string encoding/decoding problem. After I added faceted search web part to search result page and searched for something, it returns result like following.

 

James Tsai .Net VSTO SharePoint Blog MOSS Faceted Search problem 

 

As you can see first facet value in both "Refine by Title" and "Refine by role" sections are empty. Only number of hits displayed. And it is because Faceted web part does not perform HTML encode/decode properly. It can be easily spotted by closer inspection of page's HTML source.


<!-- rendered html of facet web part -->


<td nowrap="nowrap" width="100%" style="vertical-align:middle"><a class="ms-navitem" href="javascript:__doPostBack(...)" style="vertical-align:middle;"><No Title></a><span> (2)</span></td>


<td nowrap="nowrap" width="100%" style="vertical-align:middle"><a title="&amp;lt;enter position title>" class="ms-navitem" href="javascript:__doPostBack(...)",style="vertical-align:middle;"><No Position defi...&lt;/a><span> (1)</span></td>

First empty facet value is causing by "<No Title>" rendered as html tag, which is not recognized by browser. And the second empty facet value is with half-open "<No Position defi..." also treated as malformed html tag.

To fix this problem, you have to modify Common\Templates.cs source code file. perform HttpUtility.HtmlEncode  for facet.DisplayName before assign it as link.Text

 

//Templates.cs source code

void link_DataBinding(object sender, EventArgs e)
{
            /******irrelevant code omitted*******/     
           

            if (facet.DisplayName.Length > _cropMax)
            {
                link.ToolTip = facet.Name;

                //HtmlEncode facet display name
                link.Text = HttpUtility.HtmlEncode(facet.DisplayName.Remove(_cropMax)) + "...";
            }
            else link.Text = HttpUtility.HtmlEncode(facet.DisplayName);  //HtmlEncode here too

            /******irrelevant code omitted*******/   
}

After you re-deploy fixed version of the feature and web part, the search result should now look like

 

James Tsai .Net VSTO SharePoint Blog MOSS Faceted Search fix encoding problem

 

Everything look fine now? If you now click on the link you just fixed as followed above steps. You will see this in returned refine search result page.

 

James Tsai .Net VSTO SharePoint Blog MOSS Faceted Search decoding problem

 

HTML Decoding

 

Yes, the problem is obvious. Whatever we encoded, we must decode it. Otherwise Faceted search web part will pass encoded query value to do refine search. And it will not return correct result (or no result at all, since encoded string will not look pretty).

To fix this problem you will need to modify Common\Utility.cs source code file. Do HttpUtility.HtmlDecode for query.Properties[property] before add it to keywords ArrayList.

 

public static string BuildQueryString(SearchQuery query)
{
             /******irrelevant code omitted*******/  

            foreach (string property in query.Properties)
            {
                // consider empty facets and don't add them to qs
                if (query.Properties[property] == Common.Constants.UNNAMED_FACET_VALUE) continue;
                ka.Add(string.Format("{0}:\"{1}\"",property, HttpUtility.HtmlDecode(query.Properties[property])));
            }

             /******irrelevant code omitted*******/  

            return qs;
}

 

If you followed all above steps you should now see correct refined search keyword and results.

 

James Tsai .Net VSTO SharePoint Blog MOSS Faceted Search decoding problem fix

Jun
12
2008

How to update properties of an already provisioned and in use page layout

Let's say you have a custom page layout and you want to give this page layout an unique Publishing Preview Image. To allow users to easily choose the right page layout among many different layouts.

This can be done easily if it is a NEW page layout. All you need to do is to define the PublishingPreviewImage element when you provisioning it.


<!-- PageLayout.xml -->



<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

  <Module Name="PageLayouts" Url="_catalogs/masterpage" Path="PageLayouts" RootWebOnly="TRUE">



<File Url="CustomPageLayout.aspx" Type="GhostableInLibrary" IgnoreIfAlreadyExists="TRUE"  >

      <Property Name="Title" Value="My Custom Page Layout" />

      <!-- new added description -->

      <Property Name="MasterPageDescription" Value="This page layout is used for new sites." />

      <!-- new added preview image -->

      <Property Name="PublishingPreviewImage" Value="~SiteCollection/_layouts/images/pagelayouts/custompagelayout.gif" />

      <Property Name="ContentType" Value="Custom Content Type" />

      <Property Name="PublishingAssociatedContentType"

                Value=";#Custom Content Type;#0x010100C568D1D2D3D0A14D9B2FDCC96666E9F2007123456EC3DB064584E219954237AF123456;#"/>

</File>



</Module>



</Elements>
 

Problem -

What if you want to do this for the page layout which already in use and provisioned?  The challenging part is that when page layout has already been provisioned and used across webs. Any metadata changes made in page layout provision feature will NOT update its metadata in SharePoint site.

That means even if you changed PublishingPreviewImage element value. The preview images for the already in use page layouts in CreatePage.aspx will still use default image (DefaultPageLayout.png)

 

This screen shot shows the page layout is still using default preview image

James Tsai .Net SharePoint Blog - Page Layout Preview Image Default 

 

Solution -

Create a SPFeatureReceiver class and register it with same feature that provisions page layout(s) . In the FeatureActivated, update each page layout metadata base on the page layout manifest file.

Every time you want to update custom page layout property, just update manifest file, deactivate and re-activate this page layout provisioning feature.

This screen shot shows after reactivated the feature page layout is now using new preview image and new description.

James Tsai .Net SharePoint Blog - Page Layout Preview Image Default

Note: Although you can update each individual layout manually , you would expect the update should be done automatically every time you change layouts manifest and provision them again. That is the main reason to warp the logic inside feature receiver class to mimic the automation.

 

Please see below for code samples


<!-- Page layout provision feature schema -->


<Feature

  Id="23451A4D-3645-4531-B3A1-E25C6740D321"

          Title="Custom page layouts provisioning feature"

          Description="Provisions custom page layouts."

          Version="1.0.0.0"

          Scope="Site"

          Hidden="FALSE" DefaultResourceFile="core"           

          xmlns="http://schemas.microsoft.com/sharepoint/"

          ReceiverAssembly="James.Tsai.Net.SharePoint, Version=1.0.0.0, Culture=neutral, PublicKeyToken=12345bf123eec123"

          ReceiverClass="James.Tsai.Net.SharePoint.PageLayoutProvisioningFeature"          

          >

  <ElementManifests>

    <ElementManifest Location="PageLayouts.xml"/>

  </ElementManifests>

</Feature>

The feature receiver class will look like following code sample.

//Feature receiver class

class PageLayoutProvisioningFeature : SPFeatureReceiver
{
    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        using (SPSite site = (SPSite)properties.Feature.Parent)
        {
            //Load manifest file as XmlDocument.
            XmlDocument manifestXml = LoadPageLayoutsManifestAsXml(properties);

            //Load all page layouts for the site into dictionary
            Dictionary<string, PageLayout> allLayouts = LoadSitePageLayouts(site);

            //Now we have an entire page layouts manifest as XmlDocument.
            //We also have a collection of all page layouts
            //We can now step through each page layout defined in manifest and check every propertie
            //against layouts in allLayouts Dictionary.

            //Update layout property in allLayouts Dictionary with value defined in manifest file

            //Check out page layout
            //Call pagelayoutsDic[pagelayoutName].Update()
            //Check in page layout
        }       
    }

    //LoadPageLayoutsManifestAsXml(){} ... see next section

    //LoadSitePageLayouts(){} .... see next section
}

LoadPageLayoutsManifestAsXml method

private XmlDocument LoadPageLayoutsManifestAsXml(SPFeatureReceiverProperties properties)
    {
        //Get feature definition as xml node
        XmlNode def = properties.Definition.GetXmlDefinition(System.Globalization.CultureInfo.CurrentCulture);
        //Create xml namespace manager
        string wss = "wss:";
        string xpath = wss + "ElementManifests/" + wss + "ElementManifest";
        XmlNameTable nameTable = new NameTable();
        XmlNamespaceManager manager = new XmlNamespaceManager(nameTable);
        manager.AddNamespace("wss", "http://schemas.microsoft.com/sharepoint/");
        //Create an enumerator and get ElementManifest node
        IEnumerator enumerator = def.SelectNodes(xpath, manager).GetEnumerator();
        XmlNode current = (XmlNode)enumerator.Current;

        //Get manifest file name PageLayouts.xml from ElementManifest node
        string PageLayoutsManifestFileName = current.Attributes["Location"].Value;

        //Load manifest file to XmlDocument object
        XmlDocument doc = new XmlDocument();
        doc.Load(feature.Definition.RootDirectory + "/" + PageLayoutsManifestFileName);

        return doc;
    }

 

LoadSitePageLayouts method

private Dictionary<string, PageLayout> LoadSitePageLayouts(SPSite site)
    {
        PublishingSite publishingSite = new PublishingSite(site);               
        PageLayoutCollection pageCollection = publishingSite.PageLayouts;
        Dictionary<string, PageLayout> pagelayoutsDic = new Dictionary<string, PageLayout>();
        foreach (PageLayout layout in pageCollection)
        {
            pagelayoutsDic.Add(layout.Name, layout);
        }              

        site.Dispose();       

        return pagelayoutsDic;
    }

Feb
11
2008

How to use event handler to add web part to web part page

When you are designing a new page layout and want to have default web parts to be included when user creates a new page off this page layout. You can accomplish this by several ways.

1.       Easiest way probably is to put web parts inside the page layout. – But you cannot change the web part settings without modifying page layout.

2.       Use <AllUsersWebPart/> within element manifest file where you provisioning the page layout. – It is good way to do it only if it can work properly. I am having issues with this method. Having Default Web Parts in new Pages Based Off Page Layouts in MOSS 2007 Publishing Sites have great post about how to do this.

Or you can use event handler to add default web parts to the page when page gets created and added to page library.

First step, create a page layout with empty WebPartZone.

 


<!-- page layout .aspx -->


<WebPartPages:WebPartZone id="DefaultWebPartZone" runat="server" title="Feature Zone">


</WebPartPages:WebPartZone>

Second step, create a list event handler and register it with “Page document library”


<!-- Feature element manifest .xml-->


<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

  <Receivers ListTemplateId="850">

    <Receiver>

      <Name>AddedEventHandler</Name>

      <Type>ItemAdded</Type>

      <SequenceNumber>10000</SequenceNumber>

      <Assembly>YourAssemblyNameHere, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0123456789ABCDEF</Assembly>

      <Class>YourNamespaceHere.YourClassNameHere</Class>

      <Data></Data>

      <Filter></Filter>

    </Receiver>

  </Receivers>

</Elements>
 

Thrid step, write your code to insert the web part to page.


 /*EventHandler.cs*/
public class WebPartPageBuilder : SPItemEventReceiver
{
        public override void ItemAdded(SPItemEventProperties properties)
        {    
            //we check if event fired from page library and page was created base on correct page layout (with web part zone).
            if (properties.ListTitle.Equals("Pages") && properties.ListItem.ContentType.Name.Equals("Page Layout Name")) 
            {
                SPFile thisFile = properties.ListItem.File;
                SPLimitedWebPartManager webPartManager = thisFile.GetLimitedWebPartManager(PersonalizationScope.Shared);
                ContentByQueryWebPart defaultCQWPWebPart = new ContentByQueryWebPart();
                /**web part settings ommited here**/ 
                webPartManager.AddWebPart(defaultCQWPWebPart, "DefaultWebPartZone", 1);
            }
            base.ItemAdded(properties);
        }
}

 Now, you have created a event handler that inserts web part to the page.