Author Archive

Enable Custom Search Scopes from code

Thursday, July 2nd, 2009

I was looking for some code to Enable Custom Scopes on a Site-Collection. I want to enable this from a feature instead of doing it manually for each site.

image

I couldn't find it fast on Google, so I tried to find out how `they' did it by using Reflector. It turned out it's easier than expected, just store the URL of the Search Center in a site-property and you're done:

[RootWeb].AllProperties["SRCH_ENH_FTR_URL"] = [URL to Search Center];[RootWeb].Update();

To disable the Custom Search Center:

[RootWeb].AllProperties["SRCH_ENH_FTR_URL"] = null;[RootWeb].Update();

Ensuring Infopath forms to open in Forms Services

Wednesday, June 10th, 2009

You might have ran into the following issue; you've deployed your InfoPath forms to Forms Services, and (of course) it seems to works like a charm.

It seems? Yes, there some exceptions in which case Forms Services does not do what you expect it to do, for example:

- Opening a form from the Search results
- Opening a form from a link (in an email, spreadsheet export, Content Query Webpart or anywhere else)

In these cases you'll notice that the form will try to open in the Infopath client application instead of the browser. The reason is that you're missing some smart javascript that is triggered when you click the link. This script is embedded in the Forms Library views, and redirects you to the browser version of the form.

 The box asking you to open the form in the client

We've created a catch all solution that is quite easy to implement as well. It's based on a HTTP Module that will pick up every request and check whether it's a request for an Infopath Forms Services document. If yes, it redirects the user to the equivalent page that renders the browser version of the form.

The HTTP Module looks like this (sorry about the empty lines, could not get rid of them..):

   1:  /// <summary>
   2:  
   3:      /// Checks whether this is a call to a web-enabled InfoPath form which should be opened in Forms Services
   4:  
   5:      /// </summary>
   6:  
   7:      public class DisplayFormInBrowserHandler : IHttpModule
   8:  
   9:      {
  10:  
  11:  
  12:  
  13:          /// <summary>
  14:  
  15:          /// Attach the PreRequestHandlerExecute event
  16:  
  17:          /// </summary>
  18:  
  19:          /// <param name="context"></param>
  20:  
  21:          public void Init(HttpApplication context)
  22:  
  23:          {
  24:  
  25:              context.PreRequestHandlerExecute += PreRequestHandlerExecute;
  26:  
  27:          }
  28:  
  29:  
  30:  
  31:          /// <summary>
  32:  
  33:          /// Attach the PreInit event
  34:  
  35:          /// </summary>
  36:  
  37:          /// <param name="sender"></param>
  38:  
  39:          /// <param name="e"></param>
  40:  
  41:          static void PreRequestHandlerExecute(object sender, EventArgs e)
  42:  
  43:          {
  44:  
  45:              Page page = HttpContext.Current.CurrentHandler as Page;
  46:  
  47:  
  48:  
  49:              if (page != null) // check if this is a page or document
  50:  
  51:              {
  52:  
  53:                  page.PreInit += PreInit;
  54:  
  55:              }
  56:  
  57:          }
  58:  
  59:  
  60:  
  61:          /// <summary>
  62:  
  63:          /// Checks whether this request should be redirected to Forms Services
  64:  
  65:          /// </summary>
  66:  
  67:          /// <param name="sender"></param>
  68:  
  69:          /// <param name="e"></param>
  70:  
  71:          static void PreInit(object sender, EventArgs e)
  72:  
  73:          {
  74:  
  75:              try
  76:  
  77:              {
  78:  
  79:                  Page page = (Page)sender;
  80:  
  81:  
  82:  
  83:                  // Check whether this is an Infopath form and whethers it's not already opening in Forms Services
  84:  
  85:                  if (page.GetType().ToString().ToLower(CultureInfo.InvariantCulture).Contains("formserver") &amp;&amp; !page.ClientQueryString.Contains("DefaultItemOpen"))
  86:  
  87:                  {
  88:  
  89:                      string url = string.Format("http://{0}{1}&DefaultItemOpen=1", page.Request.Url.Host, page.Request.RawUrl);
  90:  
  91:                      page.Response.Redirect(url, false);
  92:  
  93:                  }
  94:  
  95:              }
  96:  
  97:              catch (Exception ex)
  98:  
  99:              {
 100:  
 101:                  ExceptionPublisher.PublishInternalException(ex);
 102:  
 103:              }
 104:  
 105:          }
 106:  
 107:  
 108:  
 109:          public void Dispose() // nothing to dispose here
 110:  
 111:          {
 112:  
 113:          }
 114:  
 115:      }

You'll need to register the event-handler in the web.config to enable it. You can do this manually or use the following code. I'm calling it from a simple feature, but that's up to you to implement.

   1:          /// <summary>
   2:  
   3:          /// Adjust the web.config to use the HttpHandler to make sure that all 
   4:  
   5:          /// InfoPath forms are opened in the browser instead of the client.
   6:  
   7:          /// </summary>
   8:  
   9:          /// <param name="webApp">The web application</param>
  10:  
  11:          /// <param name="add">Add or remove the HttpHandler to/from the web.config</param>
  12:  
  13:          public static void ActivateDesignFeatureForWebApp(SPWebApplication webApp, bool add)
  14:  
  15:          {
  16:  
  17:              //Add ore remove the HTTPHandler to redirect a page that tries to open an InfoPath form 
  18:  
  19:              //in the InfoPath client
  20:  
  21:              switch (add)
  22:  
  23:              {
  24:  
  25:                  case true:
  26:  
  27:                      webApp.WebConfigModifications.Add(CreateHttpModuleModification());
  28:  
  29:                      break;
  30:  
  31:                  case false:
  32:  
  33:                      webApp.WebConfigModifications.Remove(CreateHttpModuleModification());
  34:  
  35:                      break;
  36:  
  37:              }
  38:  
  39:  
  40:  
  41:              webApp.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
  42:  
  43:              webApp.Update();
  44:  
  45:          }
  46:  
  47:  
  48:  
  49:          /// <summary>
  50:  
  51:          /// Creates the SPWebConfigModification for the HTTPHandler
  52:  
  53:          /// </summary>
  54:  
  55:          /// <returns></returns>
  56:  
  57:          private static SPWebConfigModification CreateHttpModuleModification()
  58:  
  59:          {
  60:  
  61:              string ModName = "add[@name='DisplayFormInBrowserHandler']";
  62:  
  63:              // modName must be equal to the name attribute of the childnode!
  64:  
  65:              string ModXPath = "configuration/system.web/httpModules";
  66:  
  67:  
  68:  
  69:              SPWebConfigModification modification = new SPWebConfigModification(ModName, ModXPath);
  70:  
  71:              modification.Owner = "[Admin name]";
  72:  
  73:              modification.Sequence = 0;
  74:  
  75:              modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
  76:  
  77:              modification.Value =
  78:  
  79:                  @"<add name=""DisplayFormInBrowserHandler"" type=""[Namespace].DisplayFormInBrowserHandler, [Namespace], Version=[1.0.0.0], Culture=neutral, PublicKeyToken=[1234567890.]"" />";
  80:  
  81:              return modification;
  82:  
  83:          }

Don't forget to replace the [brackets].

One important aspect of this solution is whether it is supported. I've been hearing different stories about using HTTP Modules in SharePoint, some say they are supported, other say they're not. Please keep that in mind if you're dealing with MS support – disabling the module should be good enough.

Backup strategy for BPOS

Thursday, June 4th, 2009

I’ve been using the MS Business Productivity Online Suite for 2 months now. I’m more than happy about it, it really saved me a lot of time setting up the basic needs for my own company. For your info, I’m using Exchange (both OWA and Outlook), SharePoint Online and Live Meeting.

I’ve always beep quite serious on making backups en to ensure the availability of my data. Not that I don’t trust MS, but at minimum I think you should have a clear understanding of what you can an cannot expect from them.

Luckily there’s a very clear text in the Online Services Help concerning data Backup and Recovery. You can find it here: http://www.microsoft.com/online/help/en-us/bpos/html/7b8f6b68-625d-4f45-98d8-b178c1f3f27d.htm

There are some interesting things in there, this is a short summary;

For Outlook you can be quite confident that your data will be available. 
- Deleted items are recoverable up to 14 days old
- Corrupted items are recoverable up to 14 days old, The newest recovered item may be at least one business day old
- Deleted or corrupted mailboxes or any other disaster recovery will attempt to recover a mailbox up to 14 or 30 days after the problem occurred, the data may be at least one business day old

Now that you’re aware of this you can determine your own backup strategy. The most important thing is that Deleted and Corrupted items are recoverable up to 14 days old. This means for any item older than 14 days backup/restore is up to yourself. I choose to adjust my Auto-archive settings to archive every 14 days, and make a regular backup of the archive files.

For SharePoint things are quite different, just look at this:

- Items deleted by mistake will go through the regular Recycle Bin process, this gives you 60 days to recover a deleted item. If versioning is enabled you can also restore previous versions of documents.
- If the SharePoint site is deleted by mistake or is corrupted recovery is not supported
- In case of disaster recovery MS will attempt to recover a SharePoint site up to 14 days after a disaster. The service may take six business days to be completed. The newest recovered item may be at least seven business days old.

Especially the second item is quite concerning. I think it is strange as well if you consider that in case of disaster recovery MS will attempt to recover the site. According to the disaster recovery there are weekly backups. In practice I assume this might mean they will help you recover a deleted site, but apparently it is not guaranteed in any way.

Personally I have made some precautions to prevent data loss. As a SharePoint consultant I should be able to restore a site-template to another environment. Unluckily my site was already to large to make a site-template with content included, I think the limit is 10mb (it was in SP2003). Other option is to use SharePoint Designer, although I have not tried the restore process.

The option I’m most confident about is to simply use the Office client tools. Right now I use the following tools for the different elements in the site:

Lists Access (Open with Access)
Tasks Outlook (Connect to Outlook)
Documents Outlook (Connect to Outlook)

The advantage of this method is that you store your data in a readable for and as a bonus you automatically have an off-line copy of it. I’ve made a scheduled task that will open the Access file once a day to allow it to retrieve the data from the BPOS site.

Overall I think backup/restore needs some more refinement and improvement, especially the SharePoint Online part. Sure, MS will use a reliable and redundant back-end, but as a client I would like to have my own physical copy of the bits (for as far as you can call a bit physical).

I’m quite interested in how you are addressing this topic, please comment your methods and opinions!

Backwards compatibility of Windows 7 VHD's

Wednesday, June 3rd, 2009

A great new feature of Windows 7 is Windows Virtual PC, the next version of Virtual PC 2007. It has all sorts of advantages like dual-core and USB support.

You can use your existing Virtual PC vhd's in Windows 7. But after a while I got tired of some driver issues and reverted back to Windows Vista, and Virtual PC of course. When booting the vhd it made Virtual PC crash right after the login box. The reason for this are the Integration Components/Virtual Machine Additions which are obviously incompatible between the platforms.

An easy fix is to boot in Safe Mode and disable the “Virtual PC Integration Components Services Application” service. After rebooting you can uninstall them.

Changing the save to `My SharePoint Sites' experience?

Wednesday, June 3rd, 2009

If you’re saving a document directly from an Office application to SharePoint, you will get the nice option to save it to “My SharePoint Sites”. Apart from some confusion on how these links are created and especially when they are synchronized with the client there are some other behaviors that keep my client from using them.

image

The first behavior is the error message that is shown when you are saving a document which has required fields. The user gets prompted with a big scary red cross, like he is doing something wrong or his document just went up in smoke. And that is while the user has not done anything wrong yet, he or she didn’t even got the change to enter the required fields yet..

image

We would like to alter this message and especially replace the red cross to make it a bit more friendly to the user. Has anyone have any experience doing this?

Second behavior is that when you close a document, you’ll get the message “Do you want to save this document?”. Nice, but if you are saving it directly to SharePoint you’ll never get the change to enter the metadata, after saving the Office application will close right away. One positive thing, this is not the case if you have any required fields. Of course, this is more by design; the user asked to close the document so that is just what it is going to do.

 image

But I remember in SharePoint 2003 (check the screenshots), you were always prompted with a dialog, even when the library did not have any required fields. In my opinion this dialog was for more effective than the Document Information Panel, since you really had to submit it before continuing:

image image

Does anyone had any good results in altering this experience. In the end we want to gather as much metadata as possible, and that’s what SharePoint is about right?

Figuring out a alternative to Cross-Site Lookups

Thursday, May 14th, 2009

In my Twitter feed I posted some tweets on building a workaround to create cross-site lookups. I’ve got some replies of people -thanks!- mentioning alternatives like the one of Tony Bierman (in the Netherlands that would be a name to be proud of!), found on his blog: http://tonybierman.blogspot.com/2008/07/free-custom-cross-site-lookup-column.html

Now this is a valid approach for sure, but in my opinion it has one flaw; it uses a custom field type. I’ve had some bad experiences using custom field types, and that’s because they are terribly limited. You can’t use them in Infopath, in workflows, in the Document Information Panel, in Excel; well, just about anywhere outside the browser (that is, when you use more complex types. If you stick to a plain text field storing only text your safe, but hey, why not use a text field instead?).

I’m trying to build something that is less intrusive, something that is based on the standards of the whole SharePoint platform. My solution would be to create one central site with all kinds of “Fact” lists, these are the lists you would like to reuse and share within the SharePoint environment. In the background I create a SPTimerJob which is going to copy and synchronize all these lists to all the site collections. By creating a real instance of those lists in all site-collections, you can treat it like a regular lookup field. And lookup’s are a basic element in the platform. It even allows you to create a multi-value lookup, although that is a bit less accepted by older clients like Excel 2003.

The solution is not complicated, this is a schematic overview:

 

image

 

Of course you want to add some control to manage the TimerJob, for example I add a feature to each Site Collection to include or exclude it from the sync. The sync itself has to be a bit clever, so it has to recognize the copied item so it can do an update if the original has changed. We don’t want to be cruel and truncate the whole list each time because this could potentially break links and lookups.

That said, what do you think of this approach – is it feasible? I’ll post back my results;

The impact of browser speed on your SharePoint app

Tuesday, May 12th, 2009

To me the browser discussion has always been on standards, some additional buttons and open-source ideals. Rendering speed has never been an issue.

The other way around, the browser had never been a factor in the performance of my applications. I tested load times of a certain page or application in a single browser, no more than that.

A year ago this changed for the first time when it appeared that IE7 was dramatically faster in displaying InfoPath forms than IE6 (/koning53/archive/2007/10/15/avoiding-performance-issues-in-infopath-forms-servers.aspx). Since then I advice clients using Forms Services to upgrade to IE7 or to install the IE6 hotfix for javascript performance.

More recently I've been hearing end users complaining that SharePoint is utterly slow and unresponsive. Slow?! Per user we've got an average of 3 Xeon-Cores and 8gb RAM desperately waiting to spit out content to the network (we've got a small group of people testing our new server farm). Those pages should be flying by the speed of light from SQL server to the screen.

It turned out that 80% of the people where still running IE6. Last week I've done some rough estimates using a stopwatch, but apart from not being a real test setup, the results are quite shocking.

Load times for a `large' list-view page (350 items with 7 columns):

IE6: 34 seconds (this is a clean system, just installed, no other apps running)
IE7: 21 seconds (this is a VPC, so limited to 1 CPU core and limited on memory)
IE8 in IE7 mode: 6,4 seconds (the same system as the IE6 test)
IE8: 6 seconds (the same system as the IE6 test)

This morning I've put Safari 4 to the same test (on the same system), it passed the test in less than 4 seconds. On the other side, the rendering failed in many ways so this is no alternative. But it shows there is still space for improvement.

Conclusion? The browser has become a large factor in the user-experience for your SharePoint environment; the perception of the user can be totally determined by the speed of the browser. So instead investing heavily in more servers for more speed, try to investigate the other (cheaper) alternatives for performance.

Determine the current number of SharePoint users

Wednesday, October 15th, 2008

Just a quick one, there might be better ways – but this is quite easy to do.

1. Open perfmon (run -> perfmon or Administrative Tools -> Performance monitor)
2. Create a new counter set (icon on the top left)
3. Add a new Counter (the button with the + sign)
4. Fill-in your front-end server in "Select counters from computer"
5. Select "Web service" as the performance object
6. Select the counter "Current Connections" and choose the websites you want to monitor

Repeat step 4 to 6 for every front-end.

"This site does not host personal sites" error with CreatePersonalSite()

Monday, September 8th, 2008

Quick note on this error, it is not documented and I did not find any answers on Google;

When you create a site by calling ProfileUser.CreatePersonalSite() you can get the error "This site does not host personal sites". To resolve this, create a root sitecollection on your MySite web application.

btw. make sure you use the "MySite Host" template for this rootsite.

Centralize Master page and Alternate CSS settings

Friday, August 29th, 2008

Goals:

  • Control all Master pages, Application Master pages and the Alternate CSS url's from code and being able to change these settings at run-time
  • Have only a single copy of the Master page(s) and the alternate stylesheet(s) in your whole farm (ideally on the file system in the _/layouts folder)
  • Apply a custom Master page and stylesheet to Non-Publishing teamsites
  • Apply a different Master page and stylesheet to the application pages (_/layouts pages)

To achieve this I want to use a HttpModule to set the Masterpage and alternate CSS settings at run time. This is probably the only way to change the application.master (do some Googling on it if you don't believe me).

This HttpModule should determine for every request what combination of stylesheet and master should be used.

To update the Masterpage you can easily tell the current page to use another master at runtime:

Page.MasterPageFile = "~/layouts/custom.master";

This works for both regular pages and application pages, although you probably want to use different master pages for the two.

Updating the Alternate CSS is quite a bit harder. This is because the CSS link is rendered by a separate control inside the masterpage:

<SharePoint:CssLink runat="Server"/>

If you dive into the source of this control by using Reflector you'll find a Render() method in which the actual CSS links are written to the screen.

This Render method will write several CSS links from several sources:

1st: It will ask the current SPContext for its CssReferences. This is an internal collection; I didn't find a way to modify this collection

2nd: It writes a link from its own private string m_primaryCssUrl. This is probably the link to core.css. Because it is private you cannot and you do not want to change it. I believe this string is assigned in a obfuscated method called SetDefaults() because it is not assigned anywhere else.

3th:  It writes a link from its own pubic property DefaultURL, finally a public property we can set! Setting this property can look like:

<SharePoint:CssLink runat="Server" DefaultUrl="~/layouts/custom.master"/>

You'll hard-code this property in the Masterpage, meaning that every Masterpage is connected to a specific Stylesheet. If you want to be able to dynamically attach different stylesheets from code (for example based on the user or based on the time of the day, the weather, etc), this is not the way for you.

So you really want to control the Stylesheet on run-time? Fine, but I'll warn you – it's not going to be nice.

First thing, you're not able to access the SharePoint:CssLink  control programmatically to set the DefaultUrl. This is because it has no Id and is not registered in the code-behind. Looping to all controls does not help as well; I can't get a hook to the control…

Luckily the Render() method of the CssLink control also has a 4th source from which it retrieves CSS links;

4th: It writes a link which is defined in one of the current SPWeb properties: m_alternateCssUrl. This property is also accessible from the public property SPWeb.AlternateCssUrl.

So if we set this property it will automatically be rendered by the SharePoint:CssLink control :-)

The downsize to this is that you are actually changing SPWeb properties. If you don't save the SPWeb these changes are discarded after the request, but if the SPWeb is saved somewhere along the page execution path, this setting will be saved as well. Not very nice.  

Custom CssLink Control?

So this SharePoint:CssLink control is not very easy to manipulate at run-time. For the moment I am fine setting the DefaultUrl property in the control itself. This leaves me with a bit less flexibility, but it works and it is actually default functionality. Due to the limited time I have available it's a nice trade-off.  

If you are ready to take more serious matters, there are 2 options I want to consider:

  • 1. Overriding the SharePoint:CssLink control and add our own logic on top of it
  • 2. Creating our own SharePoint:CssLink control
  • 3. Adding the CSS Link directly from the HttpModule.

The code if have so far look like (simplified):

public class MasterPageModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.PreRequestHandlerExecute += context_PreRequestHandlerExecute;
    }

    static void context_PreRequestHandlerExecute(object sender, EventArgs e)
    {
        Page page = HttpContext.Current.CurrentHandler as Page;
        if (page != null)
        {
            page.PreInit += page_PreInit;
        }
    }

    static void page_PreInit(object sender, EventArgs e)
    {
        Page page = sender as Page;

        if (page.MasterPageFile.ToLower().Contains("application.master"))
        {
            page.MasterPageFile = "~/_layouts/customApplication.master";
        }
        else if (page.MasterPageFile.ToLower().Contains("default.master"))
        {
            page.MasterPageFile = "~/_layouts/custom.master";
        }

    }

    public void Dispose()
    {
    }
}
 
[update] The solution to set the masterpage as shown above does not work for publishing pages. I'm still investigating this issue.