Wednesday, November 26, 2008

jQuery in a SharePoint Custom Field

One way of gaining new SharePoint functionality without "disturbing" the structure or content of lists and document libraries is to create custom fields that use jQuery to perform client-side processing.

AJAX calls to the built-in web services are simple once the SOAP message structure has been determined. The data returned by the call can then be made to interact with and modify controls on the page using the powerful selector mechanism in jQuery.

In this post I'll cover a few tips I learnt whilst creating one such custom field.

The Ajax Call

In a previous post I covered the structure of a jQuery AJAX call to a SharePoint web service. But how to derive the actual SOAP body for that call? One way I found to do this which removed lots of the guess work was as follows:
  1. Create a simple console application in Visual Studio and add a reference to the web service you want to call

  2. Write code in that console application to call the required method on that web service. Begin by using the simplest set of parameters in the call and making sure they return data as expected before adding additional parameters. For example, in my calls to the Lists web service (lists.asmx) my first tests used empty elements for the query, viewfields and queryoptions arguments in the GetListItems() call

  3. Start up Fiddler (or your favourite HTTP traffic logging tool). Ensure that the "Capture Traffic" option is ticked in the file menu.

  4. Run your console application, which will make the web service call.

  5. In Fiddler, view the HTTP call that was made by the console app by clicking on that listed call - it will be to the URL you set in the console app, and will have a result of '200'.

If the right-hand pane of Fiddler is showing the Statistics tab, click on the Inspectors tab - this displays the outgoing message from your console app in the top half, and the return message in the bottom half. Open the Raw tab within each half of the right-hand pane, and you will see the soap envelope that was sent in the web service call in the top pane. That is the XML to be used in the data parameter of the jQuery AJAX call.

One other item to note from the Raw content of the outgoing HTTP call is the SOAPAction, as that value needs to be used to set the soapaction in the beforesend anonymous function in the jQuery AJAX call.


Communicating from SharePoint to jQuery
One important point I have not yet covered was the fact that it is possible for a custom field to render in a SharePoint form, and yet to have no displayed elements. This is one way to use jQuery to modify controls and data in the form. My custom field outputs only script tags plus an empty span. It is still necessary to hide the table row in the form that contains this custom field, as the standard SharePoint rendering of a field in the new/edit list item form shows the field title in the left column and the field content in the right.

Hiding of the row is achieved by giving the empty span a class (for example 'hiddenfield'), and then rendering in the custom field some jQuery script that hides table rows that contain spans with the 'hiddenfield' class.

For the custom field to interact with data in other fields using jQuery, data needs to be passed from the server side into the client-side script in the custom field. AJAX web service calls can be used for this once the form has rendered, but what if you want your client-side script to have knowledge about the particular controls it is required to modify?

The first step is to ensure that the page on which the custom field will be rendered references the jQuery library. My approach is to add script elements to the RenderingTemplate in the custom field ASCX user control (which is deployed to the CONTROLTEMPLATES 12 hive folder), and to deploy the jQuery library to the LAYOUTS 12 hive folder.

Then, to enable the code in the custom field to set parameters in the client-side script (for example, to set the name of another field in the new/edit form that the script is to interact with), I added in the RenderingTemplate another script tag that contains an asp:Literal server control. Custom field code in the BaseFieldControl subclass can then output script into this literal control.

I created a custom field editor which derives the information to be rendered into that Literal control, and save this information in a custom property of my custom field. The technique for storing custom properties is explained well by Anton Myslevich in this forum thread (thanks, Anton!), particularly the difficulty in saving properties for a new instance of a custom field.

To summarise, the SharePoint list administrator adds a new field of type "MyCustomjQueryField" to a list and applies some custom configuration settings in the "Create New Field" form. These settings are processed and stored as custom properties of the custom field instance. When a list item is create or edited by a list user, the new or edit form is displayed, and the custom field uses its saved custom properties to render JavaScript in a hidden table row in that form. The JavaScript then uses the power of jQuery to interact with other fields in that form.

Displaying a Message During the AJAX Call

One small use of jQuery was to show a message in the new/edit form whilst a web service call initiated by the custom field was occuring. This is important given the asynchronous nature of the call and the fact that it is possible for the call to take a noticeable time to complete.

This was achieved using the 'after()' jQuery manipulation method to add a span directly after the HTML element in the form that was being modified by the web service call. Before the jQuery AJAX call is made, I add this span, and then remove the span in the event handlers for the error and success events triggered on completion of the AJAX call.

One small but pleasing touch was to fade out the message using the 'fadeOut()' jQuery method. Unnecessary perhaps, but less jarring to the eye than the text just disappearing instantly.

Wednesday, November 19, 2008

Silverlight in a Content Editor Web Part

First trial with Silverlight - getting Scott Gu's Silverlight DIGG client running in a SharePoint web part page with the simplest possible implementation.

The steps:
  1. Created a web part page document library
  2. Uploaded DiggSample.xap into the library
  3. Create a new web part page in the library
  4. Added a content editor web part to the page.
  5. Added an object tag pointing to the XAP file in the source code of the content editor web part

And, voila.... nothing displayed! Played with adding the Silverlight.js file to the library, and various other techniques. Finally found the key was the interaction of the object tag width and height with the rendered dimensions of the web part.


Solved by either:

  • Setting a fixed height and width in the web part appearance properties, and giving the object tag a height and width of 100%, OR
  • Setting an absolute height and width for the object tag and leaving the web part with non-fixed height and width

The former approaches works better as the latter approaches results in the Silverlight element's UI not matching the web part's width.


The content of the object tag is as shown below:

Using jQuery for AJAX User Profile Queries in SharePoint

Continuing on my vein of experimenting with customisation of SharePoint using jQuery, I wanted to see if I could use the AJAX methods in jQuery to call SharePoint web services. And it just so happens a suitable requirement arose - the need to use details from the profile for the current logged-in user of a web part page.

A data view web part was required to display staff located in the same office as the current logged-in user of the web part page. The office name for each staff member was being stored in the user profiles.

Of course this functionality could have been developed in a custom web part, but I am growing to like the rapidity of creating business functionality with data view web parts. Combined with jQuery, they offer a suprising amount of flexibility.

In SharePoint designer I created a data source pointing at the user profiles. Then I created a data form web part using this data source to display a table of information on each user profile. Also included in the web part was a hidden text field that contained the login name of the current user. The purpose of this hidden value was to provide the search text for the AJAX call to the profile web service, so that I could retrieve the details for the current user.

I needed the help of Fiddler and the SharePoint Search Service Tool to help craft the necessary QueryPacket XML. Once I had the working XML, I placed it in a JavaScript file referenced within the data form web part.

The jQuery went something like this:

$(document).ready(function() {
    startAjaxOfficeSearch();
});

function startAjaxOfficeSearch() {
    var userlogin = $("#CurrentUserLogin").text();
    if (userlogin != '') {
      var queryXml = 'urn:Microsoft.Search.Response.Document:Documentselect accountname, preferredname, firstname, lastname, workemail, workphone, Title, Path, pictureurl, description, write, rank, size, OfficeNumber from scope() where "scope"= \'People\' and accountname = \'' + userlogin + '\' order by preferredname asc';
      var queryXmlOptions = '110truetruetruetruetruetruetrue';
      var completeQueryXml = queryXml + queryXmlOptions;

      var regex = new RegExp(String.fromCharCode(60), 'g');
      queryXml = completeQueryXml.replace(/</G, '&lt;').replace(/>/g, '&gt;');

      var soapMessage = String.fromCharCode(60) + '?xml version="1.0" encoding="utf-8"?>' + queryXml + '';
      $.ajax({ type: "POST",
            url: "/_vti_bin/search.asmx",
            contentType: "text/xml",
            dataType: "xml",
            data: soapMessage,
            processData: false,
            beforeSend: function(req) {
              req.setRequestHeader("X-Vermeer-Content-Type", "text/xml; charset=utf-8");
              req.setRequestHeader("soapaction", http://microsoft.com/webservices/OfficeServer/QueryService/QueryEx);
            },
            error: function(XMLHttpRequest, textStatus, errorThrown) { ajaxError(XMLHttpRequest,textStatus, errorThrown); },
            success: function(xml) { ajaxFinish(xml); }
      });
    }
}

function ajaxFinish(xml) {
  $("RelevantResults", xml).each(function() {
    var selectedOfficeValue = $("OFFICENUMBER", this).text();
    //Do other stuff with this office value
  });
}   

function ajaxError(xmlObj,textStatus,errorThrown)   {
    alert("(Ajax error: "+textStatus+")");
    alert(xmlObj.statusText);
}

The login of the current user is contained in the hidden text box with ID of 'CurrentUserLogin', as created by the data form web part. The derived query seeks exact matches for that user login against the accountname property in the user profile records - you may find this needs to query against the preferredname property instead of accountname.

The ajaxfinish method gets all the XML returned from the web service call, and uses jQuery parsing to extract the office name from the returned profile.

Note that the url parameter in the ajax call really needs to be replaced by a variable that adjusts to the actual site collection root, and that the ajaxerror function is not production ready!

I've missed out a lot of detail in this post (for instance, how to create a data source in SharePoint designer that points at the user profile store), but hopefully have given a taster of another use for jQuery, and another way of approaching SharePoint customisations.

One other tip - to find out the property names to use in the query, go to the Shared Services Administration page for your farm, open the search settings page, and then open the Metadata property mappings page. That is where you will discover those strange names (such as OFFICENUMBER)!

Wednesday, November 12, 2008

jQuery in SharePoint Example - Rounded Corners

A recent project involved the branding of MOSS to incorporate custom design elements as supplied in a PhotoShop page mockup. One of these design requirements was rounding the external corners of the quick launch menu.

Hmmm, what to do. There are plenty of techniques discussed on the web for achieving this outcome, but often the basis for these techniques is to start with clean, web-standards compliant HTML. That's certainly not what SharePoint provides - I needed to work with the HTML that is created by the mixture of master pages and user controls, and did not want to override standard elements for this styling exercise. One aim was to minimize the changes, if any, to the master page.

Many of the rounded corner approaches use JavaScript. And I was, at the time of this project, starting to see the potential of jQuery combined with SharePoint. So a little more research located a neat way forward - the jQuery.Round plug-in.

After a few minutes (hours?) experimentation I derived the jQuery statements to apply rounded corners to the menu. This of course also required adding references to the jQuery and jQuery.round libraries in the master page - so it was necessary after all to change that page!

The statements were:

function AddQuickLaunchCorners()
{
    $(".ms-quicklaunchouter").corner("10px");
    $("div.ms-quickLaunch h3.ms-standardheader span div.ms-quicklaunchheader").css("background-color", "#c1ecff").corner("top 7px");
    $("table.ms-recyclebin:last td").corner("bottom 7px");
}

Note the assumption in the last JavaScript statement that the recycle bin will be the bottom row in the quick launch menu.

These statements, together with modifications to some of the other CSS styles, resulted in the following look for the quick launch menu:




But then I came across a very annoying behaviour. Imagine the following scenario:

  • I view a page containing this menu in one tab in IE7
  • Then I open another tab in IE7 and navigate around any page in that second tab

Not an unusual behaviour really. But on returning to the first tab displaying the SharePoint page with menu, this is what the menu then looked like:


Yeuch! More "minutes" of investigation lead to the inclusion of the following code in the JavaScript file applying the dynamic styling to the page:

window.onfocus = ReapplyQuickLaunchCorners;

function ReapplyQuickLaunchCorners()
{
    RemoveQuickLaunchCorners();
    AddQuickLaunchCorners();
}

function RemoveQuickLaunchCorners()
{
    $("div.ms-quicklaunchouter>div:not(.ms-quickLaunch)").remove();
    $("div.ms-quickLaunch h3.ms-standardheader span div.ms-quicklaunchheader>div").remove();
    $("table.ms-recyclebin:last td>div").remove();
}

Not a nice solution (OK, it's a hack!) but it works and I ran out of time.

So where am I leading with this post - well, just to illustrate the great things you can do with jQuery to mould SharePoint. Sometime I'll blog about using jQuery for AJAX calls to the MOSS profile search web services, but that's for another time.....

Sunday, November 2, 2008

jQuery Introduction Talk at Christchurch Code Camp

Gave a 5 minute lightning talk on "A Brief Introduction to jQuery" at the Christchurch Code Camp on Saturday - didn't quite get to my last slide in the 5 minutes and Dave Mateer took great delight in "riffing" me off the stage with his electric guitar turned up loud!

Really like the lightning talk concept - you don't need to be an expert on the subject, and they are very easy to prepare for, given that you can run through the slides lots of times beforehand (though having said that, I obviously didn't practice enough to get my talk short enough!).

I have published the slide deck from my talk - it includes an overview of a couple of ways in which I recently solved SharePoint UI requirements using jQuery, including a way of adding rounded corners to the quicklaunch menu using the jQuery.Corner library.

Amongst the talks on the day were:

Just wanted to say well done to the small team of organisers who did a great job in a short time - those half million listeners of Dot Net Rocks who heard the announcement about the camp but who didn't attend missed out on a treat!

Monday, October 20, 2008

Revisiting ddwrt and FormatDate

I have had some feedback on the my posting in May 2007 about the ddwrt FormatDate function, including a request to see additional formats.

Therefore I had a play with some reflection code, and have created a little command line application that can be used to see the available date formats output by the ddwrt utility.

Note that this code could be used to experiment with the output of some of the other ddwrt functions in the Microsoft.SharePoint.WebPartPages.DdwRuntime library - to see a list of the other functions, open up the Microsoft.SharePoint dll in Reflector.

One other tip - a list of the LCID values is available in this post on my blog


using System;

using System.Collections.Generic;

using System.Globalization;

using System.Linq;

using System.Reflection;

using System.Text;

 

namespace DDWRT.FormatDate

{

    class Program

    {

        static void Main(string[] args)

        {

            try

            {

                CultureInfo ci = new CultureInfo(1031);

                Console.WriteLine(RunDDWRT("05-09-2007 11:00", ci));

            }

            catch (Exception ex)

            {

                Console.WriteLine(ex.ToString());

            }

            Console.ReadLine();

        }

 

        public static string RunDDWRT(string szDate, CultureInfo ci)

        {

            string fullName = "Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c";

 

            StringBuilder sb = new StringBuilder();

 

            //Load the SharePoint Assembly

            Assembly assem = Assembly.Load(fullName);

 

            // Reference the DDWRT namespace

            Type type = assem.GetType("Microsoft.SharePoint.WebPartPages.DdwRuntime", true, true);

 

            //Find the sought method in DDWRT (the "FormatDateTime" method)

            MethodInfo methodInfo = null;

            MethodInfo[] methods = type.GetMethods();

            foreach (MethodInfo method in methods)

            {

                //Uncomment the next line to see a list of all the methods available in the ddwrt class

                //sb.AppendLine(string.Format("  {0}", method.Name));

 

                if (method.Name == "FormatDate")

                    methodInfo = method;

            }

 

            if (methodInfo != null)

            {

                object objectInstance = Activator.CreateInstance(type);

 

                sb.AppendLine("ddwrt.FormatDate Test");

                sb.AppendFormat("\r\n");

                sb.Append("Locale LCID:");

                sb.AppendFormat("\t");

                sb.Append(string.Format("{0} ({1})", ci.LCID.ToString(), ci.Name));

                sb.AppendFormat("\r\n");

                sb.Append("Date to format:");

                sb.AppendFormat("\t");

                sb.Append(szDate);

                sb.AppendFormat("\r\n");

                sb.AppendFormat("\r\n");

 

                sb.Append("FormatFlag");

                sb.AppendFormat("\t");

                sb.Append("Formatted Date");

                sb.AppendFormat("\r\n");

                sb.AppendFormat("\r\n");

                for (long formatFlag = 1; formatFlag < 16; formatFlag++)

                {

                    try

                    {

                        string formattedDateTime = (string)methodInfo.Invoke(objectInstance, new Object[] { szDate, ci.LCID, formatFlag });

                        sb.Append(formatFlag);

                        sb.AppendFormat("\t\t");

                        sb.Append(formattedDateTime);

                        sb.AppendFormat("\r\n");

                    }

                    catch

                    {

                        sb.Append(formatFlag);

                        sb.AppendFormat("\t\t");

                        sb.Append("--------");

                        sb.AppendFormat("\r\n");

                    }

                }

            }

            return sb.ToString();

        }

 

    }

}

Wednesday, October 1, 2008

Displaying the Raw Search Results

Here's a little tidbit I usually end up searching for when manipulating search results - so it must be time to link to it here:
How to: View Search Results XML Data
(Also see Customize the People Search Results)

This gives a rundown on the XSL to use to show the raw XML that is returned to a search results web part (such as the "CoreResultsWebPart").

Basically, use the following:
<xsl:stylesheet version="1.0" xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes">
<xsl:template match="/">
<xmp><xsl:copy-of select="*"></xmp>
</xsl:template>
</xsl:stylesheet>

Custom Web Part Pages Template Woes

Seems to have been plenty of people that have tried to extend the list Web Part Page templates made available on the spcf.aspx page. And it seems that the approach outlined in the Microsoft article "Creating Custom Web Part Page Templates for Microsoft SharePoint Products and Technologies" just doesn't work with SharePoint v3.

The approach of adding an additional option to the list of templates in a custom custspcf.aspx page seems to fail (with the "Invalid URL Parameter" message) for the following reason: according to the WSS SDK, the NewWebPage RPC method accepts a value for the WebPartPageTemplate of between 1 and 8.

That must explain why adding a ninth template to the list fails - the call to the NewWebPage method OWSSVR.DLL doesn't expect a number above 8. For the background, here is a discussion on OWSSVR by Joel Oleson.

So I guess the options if different Web Part Page templates are required are to either
  • Replace the standard ones that are located in 12\TEMPLATE\1033\STS\DOCTEMP\SMARTPGS, or to
  • Create a completely custom template selection page - but I am finding that there are some real oddities in the names of pages that appear in search results when web part pages are created in a custom manner.

Another area that needs attention in vNext!

Tuesday, August 5, 2008

Adding Default Content to a SharePoint Wiki

If you create a new Wiki through a list instance in a feature, through a site definition or programmatically, the default How To and Home page will not appear in that Wiki.

To add those page, the Microsoft.SharePoint.Utilities.SPUtility class offers the following public static method:

AddDefaultWikiContent(SPList wikiList)

Call this method to add those default pages. Or if you want more control over the content of the provisioned pages, use Reflector to disassemble the following DLL and have a look at the code in that method to see how it is done:

C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\ISAPI\Microsoft.SharePoint.dll

Another Way to Enrich Your SharePoint Forms - JQuery

I'm still enjoying that honeymoon period with a new tool in my SharePoint developer "bag of tricks" - JQuery. Such a powerful library!

As an example of its usefulness, in a recent project I needed to modify the behaviour of search scope checkboxes on an advanced search page in SharePoint (enforcing that the 'All Sites' checkbox is ticked if the user clears all other checkboxes).

Here's the steps I took using JavaScript plus JQuery:
1. Encapsulate the minified JQuery library in a SahrePoint feature in order to deploy across the farm.
2. Add a script tag referencing the JQuery JS file in the 'PlaceHolderTitleAreaClass' content placeholder of the search page.
3. Add a Content Editor Web Part to the search page, and open the source view for that web part.
4. Add the following JavaScript function to the content editor source (to enable custom JavaScript to be called on page load):

    1 function addLoadEvent(func){


    2     var oldonload = window.onload;


    3     if (typeof window.onload != 'function') {


    4         window.onload = func;


    5     } else {


    6         window.onload = function () {


    7             if (oldonload) {


    8                 oldonload();


    9             }


   10             func();


   11         }


   12     }


   13 }



5. Call a click handler processing function on page load with the following:
addLoadEvent(AddOnClickHandlers);

6. Add a function to assign onclick handlers to the targetted checkboxes. This is where the power of JQuery shines through - notice the short expression in line 5 that retrieves references to only the checkboxes I am seeking (these checkboxes are contained in table cells attributed with the class 'ms-advsrchText')

    1 //Add an onclick event handler for each of the checkboxes in the search form


    2 //(the checkboxes are identified by the 'ms-advsrchText' class applied to their containing table cells)


    3 function AddOnClickHandlers() {


    4   //Get the checkboxes in the form


    5   var chks = $('td.ms-advsrchText').find(':checkbox').get();


    6   for(var i=0;i<chks.length;i++)  {


    7     chks[i].onclick = SetFormState;


    8   }


    9 }



7. Write the onclick event handler - this needs to find the check box labelled "All Sites" and check its status. I have not included all the code, just a few lines showing more use of JQuery. Line 12 builds a selection expression to get the next sibling for each check box; the next sibling is a label whose text contains the scope name (e.g. "All Sites").

    1 function SetFormState() {


    2   //... OTHER CODE ...


    3 


    4   //Get the scope checkboxes in the search form


    5   var chks = $('td.ms-advsrchText').find(':checkbox').get();


    6 


    7   //Enumerate through the check boxes


    8   for(var i=0;i<chks.length;i++) {


    9 


   10     //Get the next sibling to the checkbox, which is a label element that contains the descriptive text.


   11     //This text is assumed to always be 'All Sites' for the main search scope


   12     var lbl = $('#' + chks[i].id + ' ~ label').get(0);


   13 


   14     //Save the reference to the All Sites checkbox


   15     if (lbl.innerText == 'All Sites') {




So, the actual code to get at the required elements on the page is very brief, and the range of JQuery selectors makes getting to the elements easy.

One tip - notice the use of the Get() method call at the end of the JQuery expressions. This returns DOM elements that can then be manipulated with normal DOM scripting.

Sunday, July 27, 2008

The SharePoint Paradox - Too Many Hammers

It happens with each new custom development I am involved with on the SharePoint platform - that initial temptation to write custom code, to head down the technical and complex route to a solution. Comes from having a background in software development, I guess!

The process of firing up Visual Studio and starting to code against the SharePoint APIs can thus become the "hammer" used to fix every "nail" (for "nail" read "business requirement").

But that is missing the point of SharePoint - there are plenty of built-in features that can greatly reduce the need for custom development. And hence reduce the cost to the client. Take one recent example from my work - a client required a web part that displayed documents created or modified by the current user, with some particular requirements around the way the metadata was displayed. Some of the functionality in this web part had me hovering the mouse over the Visual Studio start program link, but a little further design work revealed that the good ole' Data View (Data Form) web part could satisfy their needs.

The web part took around an hour to complete, with the necessary XSL jiggling. And one of the real benefits of taking this approach is the deployment - no WSP files to install on the server, no interruption to service. A simple text file to export and import.

Perhaps this is a paradox with SharePoint - as a development platform of significant breadth, it offers lots of different hammers for each nail. Me, I try (often have to remind myself!) to pick the lightest-weight hammer possible for each task. Lightweight = quicker development cycle = more agile and more responsive to customer requirements.

There is a proviso, of course (quite a major one) - the size of the client and their deployment/integration process may choose that hammer for you. The fast, lightweight approach that may be appropriate for clients with small, simple sites and a handful of users on a single server is unlikely to fit major organisations with strict control over the staging/QA/production environments. But even in that instance a Data View web part could be enveloped inside a SharePoint Feature and the deployment needs likely satisfied.

One step missing from the process when using this lightweight means of solving IT needs is the testing, or rather the inclusion of automated, repeateable testing. Someday I'll get a chance to look at the web test various frameworks around and to see whether NUnit combined with, say, WebAii or WATIN will help.

Wednesday, July 16, 2008

Getting that PDF Indexing to work in MOSS

Had a case where the latest Adobe IFilter had been installed, but the crawl log in MOSS was displaying "filtering process could not process this item" messages.

The key to fixing this issue was the information in this post from the Filter Center blog: checking the registry key value, and then adding the path to the Acrobat Reader folder were the solutions. With those in place, a full scan correctly indexed the PDF documents.

To save the search for the Adobe Acrobat PDF file icon logo for referencing in the DocIcons.xml file, here is the location of the GIF file on the Adobe site: http://www.adobe.com/misc/linking.html

Other useful aticles:

Tuesday, July 1, 2008

Hide the "View All Site Content" Quick Launch Link

To hide this link from all but those users with full control over a WSS site, open the site master page in SharePoint Designer and find the SPSecurityTrimmedControl element that contains a div with class ms-quicklaunchheader. The quickest route to this is to view the page in split view, and click on the "View All Site Content" link in the design pane.

The PermissionsString attribute of the SPSecurityTrimmedControl element determines what users can view this content. Change the value of this attribute to ManageWeb and only those users with rights to perform all admin tasks on the site will then be able to see the link on all pages in the site.

See this page for a list of all the possible permission string values.

Tuesday, June 17, 2008

WSS Search and Breadcrumb Links Fail over SSL

A site originally extended for extranet access needed to be now served over SSL, so I extended a new web application to enable this, and modified the external Alternate Access Mapping entry to be https rather than http.

The site was then successfully accessible on the internet, though I did notice that the full path had to be entered in the browser address bar, including the page name itself - for example, https://[siteurl]/default.aspx rather than just https://[siteurl].

Further testing revealed that the links in the breadcrumbs all led to a "Page cannot be diaplayed" IE error, and submitting a search on the site gave an error message on the results page that stated:
"If the URL should be serving existing content, the system administrator may need to add a new request url mapping to the intended application"


The cause was all in the AAM entry - I had a port number (80) in the extranet URLs from some early trials, and had not removed this port number when setting https. By setting the Extranet zone AAM internal URL and public URL to be simply https://[ssiteurl], these problems were solved.

Interesting that the pages were still accessible with an incorrect Access Mapping!

Monday, June 16, 2008

Double-check your hosting company's infrastructure

Story from the trenches today - a few weeks ago I customised a hosted WSS site, including UI changes and a small amount of functionality.

Somehow in one of the admin pages, the hosting company had exposed the capability to delete the complete site. So when my client did accidentally make use of that "feature", he called the hosting company to request a restore of the site.

Unfortunately, the hosting company discovered that they only actually have disaster recovery backup for an entire server farm. No application-level backup in place. It would take them 5 man days to retrieve the site.

Luckily, the site had not been used yet, so no data was lost. Just means that I have to rebuild from scratch... I had presumed that a hosted site would be safe, and so made no form of local backup.

So, in the words of Carl Franklin in Mondays (listen if you dare!), Things I have Learnt This Week:
  • Check the recovery options offered by a hosting company
  • Make your own backups of any customisations on hosted sites
  • Write reminders for yourself of any tricky little changes to make a site work as required
  • Take lots of screenshots

The latter point is worth reviewing. In this project I had taken a few screen shots of various pages within the site as development progressed, primarily to send to the client to show current state of play. Now I am using these screen shots to help recreate the site.

On the subject of screen shots. I use either Cropper or Faststone Capture for this task. Cropper is a free .net util, whereas Capture offers some nice easy image annotation options (plus ragged edges to images if required). Thoroughly recommend them both!

Wednesday, May 28, 2008

Site Definition Fails to Use My Custom Home Page

Here's a minor annoyance that doesn't causes any in-your-face error messages, but can lead to a few deep frowns.

A MOSS site created from my custom site definition failed to apply my custom master page to the default.aspx page. The MasterPageFile attribute in the default.aspx file in my Visual Studio project was certainly set to ~masterurl/custom.master, yet the deployed file in the associated SITETEMPLATES folder used the standard ~masterurl/default.master.

Turns out that I had missed an attribute from the node in the node within ONET.XML. What I had initially was
     Url="default.aspx" NavBarHome="true">
but what I needed was
     Url="default.aspx" Name="default.aspx" NavBarHome="true">

Yet the feature deployed, and the site was created. Could perhaps do with some "fail fast" philosophy in the implementation of some of these out-of-the-box operations rather than letting default values kick-in when there is a missing configuration value?

Tuesday, May 6, 2008

Override the Edit Form SaveButton - Part 1

An overview of how to override the action of the Save Button in a SharePoint form for a specific document library (in this case a Wiki library). The intention of this brief series of articles is to record how to add a custom behaviour to the Save button of a wiki edit form.

The elements in this solution can be all included in a single Visual Studio solution, but I suggest that they are compiled into several separate SharePoint features for ease of management/deployment. All the features can be referenced in a single Manifest.xml file.

1. Create a content type feature to define a custom content type for the document library. In the XmlDocument section of the ContentType definition, specify the name of a custom template for the edit form template:


<XmlDocuments>
<XmlDocument NamespaceURI="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
<FormTemplates xmlns="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
<Display>DocumentLibraryForm</Display>
<Edit>MyCustomWikiEditForm</Edit>
<New>WikiEditForm</New>
</FormTemplates>
</XmlDocument>
</XmlDocuments>

The result of this will be that any library using this content type will display the custom content (to be created in later steps) on the edit form.

2. Create a user control file containing a new RenderingTemplate with the
template custom ID matching that in the custom content type.

To achieve this, create the new ascx file in the Visual Studio solution (as an example, I'll name it MyCustomWikiEditForm.ascx). Copy the Assembly and Register tags from the top of DefaultTemplates.ascx (located in C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\CONTROLTEMPLATES).

Then find (in DefaultTemplates.ascx) the RenderingTemplate node for the template being replaced, and copy/paste this into the new ascx file - in this case, I would copy the complete RenderingTemplate with ID of WikiEditForm into my new ascx file. Rename the ID of the RenderingTemplate to match the custom ID from step 1 (i.e. to MyCustomWikiEditForm).

This new ASCX file will need to be deployed to the CONTROLTEMPLATES folder by stating the destination folder in a TemplateFiles node in the Manifest.xml for the solution, as in the following example snippet:

<TemplateFiles>
<TemplateFile Location="CONTROLTEMPLATES\MyCustomWikiEditForm.ascx" />
</TemplateFiles>

In the next installment, I'll look at modifying the edit template to include a custom save button.

Sunday, April 6, 2008

Whoops, Where are Those Site Themes Hiding?

I am trying to persuade the application pages in a WSS site to look similar to the site pages that are customised by a master page - my approach was to create a new theme, and modify the CSS in that theme using the CSS applied to the master page.

It almost worked, but had a nasty side-effect. Some part of the CSS caused the "select theme" list on the Site Theme admin page to be squashed out of screen space by the left-hand cells expanding too far across the page. The image of the still displayed, but there was no way to remove the bad theme.

Oh dear, what to do?

Then some playing in the IE Developer toolbar revealed a solution - the toolbar allows CSS styles to be edited and applied in the live page. I applied a width attribute to several of the cells in the affected table, and managed to reveal the theme list box in the displayed page, thereby allowing me to reapply the default theme before modifying the new theme.

This difficulty in modifying the application pages to match the site pages is an oft-quoted citicism of SharePoint. During a little research for this post, I have noticed that Microsoft has published a KB article on their recommended server-side fix for this.

Thursday, March 13, 2008

A Few Useful WSS Icons

Based on an idea in the AutoSponge blog, here are content editor web parts that display some of the icons available in WSS:
You can simply import these into any page on a WSS site to see the icons - right click on the links above, save the dwp files and then import 'em.

Thursday, February 28, 2008

Why Reflector is a Developer's Friend

If you have been wondering why some .net developers rave about Reflector as the one tool that rules them all, and have thought that maybe you should give it a try, here's a little tale about how it made my day better... (hope it encourages you)

Whilst attempting to bind an SPGridView to a LinqDataSource, one of the requirements was to use the filtering features of the SharePoint gridview (that's one of it's strengths above the standard GridView control).

I came across a post by Robert Fridén that described how to enable filtering when binding to an ObjectDataSource. An SPGridView property named FilteredDataSourcePropertyName needs to be set, and that must contain the name of the property on the bound datasource which will be called when filtering the data. In other words, it requires a string containing the name of a control property.

For an ObjectDataSource, that property is named "FilterExpression". I decided it was highly unlikely that the LinqDataSource would use the same property name, so I turned to Reflector.

In Reflector, I found and opened the System.Web.Extensions dll, browsed to the LinqDataSource class and then looked through the properties. Didn't need to right-click and disassemble the dll in this case, as one of the properties was named "Where". Sounded a good option for filtering. So I used that as the FilteredDataSourcePropertyName value, played around a little with the FilteredDataSourcePropertyFormat property of SPGridView, and hey presto the filtering worked. Sure, I could have looked through the MSDN documentation, but with Reflector I can browse inside the methods of the data source class if needed.

If you've not yet delved inside the .Net dlls with Reflector, give it a go. You'll be rewarded!

...and by the way, as tempting as it was I just couldn't bring myself to put the word"best" in the title ;-)

Sunday, February 24, 2008

VMWare Virtual Machine 'Failed to Lock the File'

Scary moment occurred last week when one of my virtual machines failed to start from VMWare Workstation. The reported error was "failed to lock the file". A bit of searching revealed that I needed to delete the .LCK file in the VM's folder.

The cause for this (I think) was that I had opened the virtual disk of this VM as a mapped drive on the host machine (TIP: very useful feature to get at files from the VM without powering it up - just right click on the .VMDK file in Windows Explorere, and you'll see a couple of options there to map the disk as a drive). That drive was still mapped when trying to start the VM, meaning that the host machine was accessing the VM's files.

Thursday, February 14, 2008

Some OWSSVR Parameters are Case-Sensitive

Experimenting with some owssvr URL protocol calls today, and kept getting error pages back when trying to retrieve data from a particular view by specifying a "View" parameter in the querystring

e.g. http://[site url]/_vti_bin/owssvr.dll?CS=109&List={list Guid}&View={View Guid}

Tried swapping various list ids, and removing the view id parameter. Well, it turns out that the View ID is CASE-SENSITIVE, yet the List ID value is not case-sensitive. Cunning way to throw me off the scent, there!

Monday, February 4, 2008

MOSS Search Box Missing Search Scopes

Had a situation where the search box on the SharePoint home page on a MOSS site was missing the "All Sites" and "People" options in the search scope dropdown.

Luckily John M. Cass has posted a fix - if the display group named "Search Dropdown" is missing (you only see the group named "Unused Scopes" on the Search Scopes admin page in the Site Settings), add a new group named "Search Dropdown" and add the two scopes to that group.

Thanks for that, John.

Wednesday, January 30, 2008

Incorrect Alternative Access Mapping Removes Search Scope Options

Experimenting a little with MOSS today, I found that if I set the Alternative Access Mapping of a MOSS site to be incorrect, then the search scope dropdown box in the associated site loses two of the standard search scope options.

After setting the AAM to the wrong value, the dropdown only offers the "This Site:..." option. The "All Sites" and "People" scopes are missing until I correct the AAM value.

Thursday, January 10, 2008

A Wrong (!) Exception Message when Applying SPWebConfigModification

Found that there is an error in an exception message associated with SPWebConfigModifications. The error message I was getting was as follows:

Failed to apply a web.config modification to file "configuration/runtime". The specified node "c:\inetpub\wwwroot\wss\virtualdirectories\100\web.config" was not found in the web.config file.

Just in case I was setting reversed values, I debugged for a while then opened up reflector. Seems that the author of SPWebConfigFileChanges got the parameter order wrong when deriving the exception message in ApplyModificationsWebConfigXmlDocument()!

Adding a BindingRedirect to Web.Config Using SPWebConfigModification

Had a need to remap some DLLs developed for a SharePoint site to different versions of third party libraries (the DLLs had been compiled against other versions of the libraries).

Found that this can be achieved by adding Binding Redirect nodes to the web.config file of the SharePoint web application. Actually getting this to work, however, is not straightforward; the main cause for complexity is that the parent node in the config file (the assemblyBinding node) has it's own namespace. This requires careful Xpath in the SPWebConfigModification name and path values.

Here are the necessary values:

string path "configuration/runtime/*[namespace-uri()='urn:schemas-microsoft-com:asm.v1' and local-name()='assemblyBinding']";

string 
name = string.Format("*[namespace-uri()='urn:schemas-microsoft-com:asm.v1' and local-name()='dependentAssembly']/*[namespace-uri()='urn:schemas-microsoft-com:asm.v1' and local-name()='assemblyIdentity'][@name='{0}']/parent::*", LibraryName);

Use these in conjunction with the following code to create the necessary value to be inserted into the web config file:

string WebConfigElement @"<dependentAssembly>
        <assemblyIdentity name='{0}' publicKeyToken='{1}' culture='neutral' />
        <bindingRedirect oldVersion='{2}' newVersion='{3}' />
      </dependentAssembly>"
;
string 
webConfigValue = string.Format(WebConfigElement, LibraryPublicToken, LibraryName, OldVersion, NewVersion);

Where LibraryPublicToken, LibraryName, OldVersion and NewVersion are the string values to be inserted into the bindingRedirect node that will be added to the web.config.

Set the owner, sequence and type for the SPWebConfigModification, and apply to create the new config file entry:

SPWebConfigModification mod = new SPWebConfigModification(name, path);
mod.Value = webConfigValue;
mod.Owner = string.Format("SetAsposeVersions.{0}", LibraryName);
mod.Sequence = 0;
mod.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;

webApp.WebConfigModifications.Add(mod);
webApp.Update();
SPFarm.Local.Services.GetValue<SPWebService>().ApplyWebConfigModifications();

Finding and Removing a Rogue SharePoint Web Config Modification

During development of a small tool to enable binding redirection of .net libraries referenced in a SharePoint site, I found that ApplyWebConfigModifications() calls to the SPWebService were failing due to an existing SPWebConfigModification. The existing modification was atempting to access a path that does not exist in the web.config file.

To find the modification that is failing, I added a breakpoint and whilst debugging opened the following statement in QuickWatch in Visual Studio:

SPFarm.Local.Services.GetValue<SPWebservice>()

Then I navigated through the following properties in the Quick Watch window:
WebApplications/base/base/Non-Public members/BackingList

This offers access to all the web applications on the farm. For each web application under that node, open the WebConfigModifications node and check each one for the path that is reported to be failing. Then you can delete that faulty WebCofigModification using the following code:


        foreach (SPWebApplication thisApp in SPFarm.Local.Services.GetValue<SPWebService>().WebApplications)
        {
            for (int thisApp.WebConfigModifications.Counti > 0i--)
            {
                SPWebConfigModification mod 
thisApp.WebConfigModifications[i - 1];
                if 
(mod.Path.ToLower().Contains("[TEXT IN BAD PATH]"))
                      thisApp.WebConfigModifications.Remove(mod)
;
            
}
            thisApp.Update()
;
        
}


By deleting the faulty modification from the collection, the ApplyWebConfigModifications() call will now run to completion without raising an exception.