Jun28

Taking your Sharepoint production site offline

 Categories: Configuration, Publishing, Utility

You got your Sharepoint site running smoothly but are now ready to e.g. deploy a new Solution to the site. In this situation there can be at least two good reasons to take your Sharepoint site offline and show a user friendly message.
  1. The update goes awfully wrong and the site is showing nasty exceptions.
  2. You know the update will take a long time (> 1 min) and would like to show this to the users.

ASP.Net 2.0 (and therefore MOSS 2007/WSS 3.0) provides a neat way of showing a user friendly web page to the end-users while you can take it easy and work on your site behind the scene. The solution is to put a file called "app_offline.htm" (not .html) in the root of your Sharepoint site (e.g. C:\Inetpub\wwwroot\wss\VirtualDirectories\80). This will close the App Pool for the site and instead render the content of app_offline.htm to the end-user.

When ready to take your site online just rename/delete the file and your are up again.

 
 
Jun19

Quickly verify your Sharepoint Portal in Internet Explorer 5.5-8.0

 Categories: Theme, User Interface

When developing Sharepoint Solutions (as well as other web-based interfaces) you will most likely want your Portal to look good in all modern browsers like Firefox 1.5+, Safari, Opera and Internet Explorer 5.5-8.0. The former (like Firefox) is easy to check since you can just download the browser and check your pages, but when we need to check cross-IE compatibility it suddenly becomes much more difficult. This is due to the fact that Internet Explorer (5.5-8.0) is tightly integrated into you Windows installation. The only solution is often to have several Virtual Machines running different version of IE. This is quite obviously not very efficient.

Introducing IETester 0.2.2 which allows you to view you solution side-by-side or in a full window verifying your (of course) robust XHTML/CSS design. Take a look at the screenshot below of my own blog.

Hmmmm. Someone has not done his job very good (no one mentioned) :)

IETester

Go grab the program here: http://www.my-debugbar.com/wiki/IETester/HomePage

 
 
May18

Relevance Ranking in Sharepoint Enterprise Search

 Categories: Search

The solution
I am currently working on a Sharepoint project which is a purely search-based portal, that is, all information is to be found by performing some searches queries. Having said that, it should be quite clear that good/excellent relevance ranking is a very fundamental requirement.
 
The solution is contains some interesting aspects regarding how the search is performed and visualized, but this is for another post.
 
Optimize Indexing
Sharepoint Enterprise Search allows you to adjust a vararity of settings that affects the search results. First of all you need to get your content indexed properly. There are several things you can adjust to optimize this.
 
First of all you will want to make all "valuable" Meta Data (Site Columns) mapped as Managed Properties. When the indexer travels through your content it creates a large set of "Crawled Properties". These properties does nothing by them self; they need to be mapped to Managed Properties. When the Crawled Property is mapped as a Managed Property they become searchable and "optimizeable".
 
The two most important properties you can adjust on Managed Properties to optimize the ranking results is "Weight" and "Length Normalization". To adjust these you can take a starting point in the following code (C#):

float weight= 3f;
float lenNorm= 0.5f;

Schema
sspSchema = new Schema
(
SearchContext.GetContext(new SPSite(http://xxx)));

ManagedPropertyCollection
properties = sspSchema.AllManagedProperties;

string pName = "MyManagedProperty";

if (!properties.Contains(pName))
{
  Console.WriteLine(strPropertyName + " property does not exist.");
return;
}

foreach (ManagedProperty property in properties)
{
  if (property.Name.ToLower() == strPropertyName.ToLower())
  {
    property.Weight = weight;
    property.LengthNormalization = lenNorm;

    property.Update();

   Console.WriteLine("Weight on " + property.Name + " was changed to: " + property.Weight.ToString());
   Console.WriteLine("Length Normalization on " + property.Name + " was changed to: " + property.LengthNormalization.ToString()); 

  }
}

The weight can be from 0 => infinite. If you set the weight to e.g. "1" it means: whenever I see the search term 2 times in the indexed text we actually want to weight it as it occurs 2 times. If the weight is set to 5 it means: whenever I see the search term 2 times in the indexed text we actually want to weight it as it occurs 10 times (2 x 5).
 
So you need to decide how much weight a given property need to have. But do not set it to high since it could down-prioritize other, just as relevant, Managed Properties. 
 
The Length Normalization can be from 0 => 1. This property basically makes large amount of text sections more relevant. For "titles" and so on leave as default (0), for short texts use 0.3f=>0.5f. For large amount of text use 0.6f=>0.8F.
 
References
Other resources about the subject can be found here:
 
 
Apr22

Problem with a Custom Content Type when performing a Check-In

 Categories: CAML

Yesterday I developed some custom Content Types via a Feature (CAML). The Content Type inherited from "Page" and everything seemed to work just fine. I could Edit and Publish the page from the page itself but when I went to the "Pages" Document Library and performed the check in from there it failed with the following exception:
 
Value does not fall within the expected range.   at Microsoft.SharePoint.SPFieldCollection.GetFieldByInternalName(String strName, Boolean bThrowException)
   at Microsoft.SharePoint.SPFieldCollection.GetFieldByInternalName(String strName)
   at Microsoft.SharePoint.SPListItem.get_MissingRequiredFields()
   at Microsoft.SharePoint.ApplicationPages.Checkin.OnLoad(EventArgs e)
   at System.Web.UI.Control.LoadRecursive()
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

Ehmmm...what? It workes everywhere else...
 
It turns out that the "/_layouts/checkin.aspx" seems to be more picky than the Publishing Page. My exact problem was that I had included a <Fieldref> to the "LinkTitle" field (besides "Title"). It seems that it is not allowed to define the LinkTitle field in your custom Content Types. Anyway it works as expected when removing this .
 
 
Mar28

Dynamic Sharepoint Rebranding Module (DRSM) 0.0.3 Beta

 Categories: DSRM, Master page, Publishing, Theme, HTTPModule

Problems in Beta 0.02

After feedback from serveral users serveral bugs was discovered. Most noticeable was that it did not wotk with Publishing Pages. These pages are somewhat a pain in the a** to brand since Sharepoint eats all Page events like PreInit, Load, PreRender etc. This makes it quite hard to do anything actual.

Problems solved...in Beta 0.0.3

But I have now found ways to circumvent this; both for Master and CSS. To change the Master on Publishing Pages I use SPContext.Current.Web.CustomMaster property to change it instead of the Page.MasterUrl (which I can not access on Publishing Pages). Regarding CSS injection I need to insert the CSS link just before rendering, that is, in PreRender. But since I can not hook this event up on Publishing Pages I have implemented a Custom HTTPResponse Filter module that manipulats the raw HTML content to inject the CSS link.

Go grab the fresh releases at: http://www.codeplex.com/DSRM

Some general advice using DSRM on application pages.

You can use the same Master for both as default master and as application master. It is however not recommendable to do so since some pages problably will break. So here come some recommendations on how to create a compatible Application.master. (Is is not somethign DSRM can do much about).

1. Missing Place Holders

First of all masters based on default.master will miss the following two Place Holders. These must be present to support most Application Pages:

<asp:ContentPlaceHolder id="PlaceHolderPageDescriptionRowAttr2" runat="server"/>
<asp:ContentPlaceHolder id="PlaceHolderPageDescriptionRowAttr" runat="server"/>

2. Search box must be removed

Secondly. Some pages (like Checkin.aspx) will not work if a search box is present in the master.  

3. Change Bread Crump Navigation

You would problably also want to change default breadcrumbs code in your new applicaiton master to match the original application.master. Replace:

<asp:ContentPlaceHolder id="PlaceHolderTitleBreadcrumb" runat="server">
<asp:SiteMapPath SiteMapProvider="SPContentMapProvider" id="ContentMap" SkipLinkText="" NodeStyle-CssClass="ms-sitemapdirectional" runat="server"/>
</asp:ContentPlaceHolder>

with:

<asp:ContentPlaceHolder id="PlaceHolderTitleBreadcrumb" runat="server">
<asp:SiteMapPath SiteMapProvider="SPXmlContentMapProvider" id="ContentMap" SkipLinkText="" NodeStyle-CssClass="ms-sitemapdirectional" runat="server"/>
</asp:ContentPlaceHolder>

(Thanks: Steve Lineberry)

 
 
Mar25

Dynamic Sharepoint Rebranding Module (DRSM) 0.0.2 Beta released!

 Categories: HTTPModule, DSRM, Branding

About DSRM 0.02 Beta

I am happy to announce that I have finally polished of the roughest edges of my previously announced Dynamic Sharepoint Rebranding Module; and now the first release is here (released on CodePlex)! I have fixed several bugs in my first internal release and the code is now in semi beta state. Many features are already included.

Please refere to my last post for detailed configuration details here.

It currently supports:

  • Dynamic re-branding of Master Pages via URL pattern matching.
  • Dynamic appending of custom CSS
  • Replacing one Master Page with another like default.master => company.master.
  • Compatible with most Application Pages and Simple.master pages.
  • Highly configurable from the web.config with a somewhat intuitive configuration model including priorities on matching.
  • Rebranding of Master/CSS via query string parameters (like ?PrinterFriendly=true).
  • Must be compatible with Publishing Pages (many HTTPModules branding attempts out there are not)
  • Supports Add-on model for Interceptors like QueryStringInterceptor (included in Beta 0.0.2).

Short future road map:

  • Bug fixing :)
  • Performance optimization
  • Configuration from Csutom Site Collection Site Settings Page to avoid editing web.config for those of you who get a bad taste in their mouth doing this. (xml-syntax will problably remain the same).

Go grap the binaries or have a look at the source code at: http://www.codeplex.com/DSRM

Web.Config configuration sample

The following example shows a fictive scenario that must do the following: (higher priority has precedence).

Priority 1: All other pages must use Common.master
Priority 2: All default.aspx (frontpages) pages must use MasterFront.master.
Priority 3: All _/layouts/ pages (Application pages) must use MasterApp.master.
Priority 4: All _/layouts/ pages that has Simple.Master as current master must use MasterSimple.master.
Priority Above all: Whenever a qurystring of the form ?PrinterFriendly=true is present allways use Printer.master.


<
VisualBranding>
 <
RebrandPaths
>
  <
RebrandPath Path="" IncludeLayoutsFolder="false" Priority="1"
>
    <
ApplyMaster File="/_catalogs/masterpage/Common.master" ApplyOnlyTo="default.master"
/>
    <
ApplyCss File="/_layouts/Custom/Common.css"
/>
  </
RebrandPath
>

  <RebrandPath Path="default.aspx" IncludeLayoutsFolder="false" Priority="2" >
    <
ApplyMaster File="/_catalogs/masterpage/MasterFront.master" ApplyOnlyTo="default.master"
/>
    <
ApplyCss File="/_layouts/Custom/MasterFront.css"
/>
  </
RebrandPath
>

  <
RebrandPath Path="/_layouts/" IncludeLayoutsFolder="true" Priority="3"
>
    <
ApplyMaster File="/_layouts/Custom/MasterApp.master"
/>
    <
ApplyCss File="/_layouts/Custom/MasterApp.css"
/>
  </
RebrandPath
>

<RebrandPath Path="/_layouts/" IncludeLayoutsFolder="true" Priority="4" >
    <
ApplyMaster File="/_catalogs/masterpage/MasterSimple.master" ApplyOnlyTo="simple.master"
/>
    <
ApplyCss File="/_layouts/Custom/MasterSimple.css"
/>
  </
RebrandPath
>

  
 <QuerystringBrandings>
     <
QuerystringBranding Parameter="PrinterFriendly" Value="true"
>
    <ApplyMaster File="/_catalogs/masterpage/Printer.master"
/>
    </
QuerystringBranding
>
 </
QuerystringBrandings
>

 <
BrandingInterceptors
>
    <
Interceptor Priority="1" Assembly="AKJ.Sharepoint.Branding, Version=1.0.0.0, Culture=neutral, PublicKeyToken=adda199c4fc7387f" Type="AKJ.Sharepoint.Branding.BrandingInterceptor.QuerystringInterceptor"
/>´
    
    <
Interceptor Priority="2" Assembly="AKJ.Sharepoint.Branding, Version=1.0.0.0, Culture=neutral, PublicKeyToken=adda199c4fc7387f" Type="AKJ.Sharepoint.Branding.BrandingInterceptor.RebrandingPathInterceptor"
/>
 </
BrandingInterceptors
>
</
VisualBranding>

 
 
Mar9

Dynamic Sharepoint Rebranding Module

 Categories: HTTPModule, Theme, Branding, DSRM

I have finally taken the time to assemble a real-life Dynamic Sharepoint Rebranding Module (DSRM). The idea is based on some of my previous post (here and here) where I use a HTTPModule to dynamically change the Master Page. This approach has several advantages but my initial requirements were that it should support:

  • dynamic re-branding of Master Pages
  • dynamic appending of custom CSS
  • replacing one Master Page with another like default.master => company.master
  • configuration from the web.config with a somewhat intuitive configuration model.
  • activation from  the query string  ….yes…we will see why later.

Let’s jump right into what it can do. We start with the web.config configuration. When installing the supplied wsp-solution file everything should be configured in the web.config by for the matter of completeness let’s start from the beginning.

1.  First you need to install the AKJ.Sharepoint.Branding.dll in the GAC

2. Enable the custom web.config section by the following into the web.config’s <configSections>.

<configuration>
 <configSections>
  
 
<
section name=
"VisualBranding" type="AKJ.Sharepoint.Branding.VisualBrandingSection, AKJ.Sharepoint.Branding, Version=1.0.0.0, Culture=neutral, PublicKeyToken=adda199c4fc7387f"/>
 <!-- ...other section omitted... -->
 </configSections
>

 <httpModules>
  <add name="DSRM" type="AKJ.Sharepoint.Branding.HTTPModules.SharepointBrandingModule,  AKJ.Sharepoint.Branding, Version=1.0.0.0, Culture=neutral, PublicKeyToken=adda199c4fc7387f"/>
 </httpModules>
</
configuration
>

3. Finally we need to add the <VisualBranding> section where all the configuration data should be located. The following is probably the simplest working configuration DSRM supports.

<VisualBranding>
<RebrandPaths
>
  <RebrandPath Path=
"default.aspx" >
    <ApplyMaster File="/_catalogs/masterpage/BlueBand.master" />
  </RebrandPath>
</
RebrandPaths
>
</VisualBranding
>

Master Page and CSS branding through Web.Config

The above configuration will change the master page on ALL instances of pages called default.aspx with the BlueBand.master. The Path expression is implicitly using wildcard search which means it would also match /news/default.aspx. We could be less specific and target all pages under the news site.

<
RebrandPath Path="/news/" >
    <ApplyMaster File="/_catalogs/masterpage/BlueBand.master" />
</RebrandPath>

All right. This is all fine. But we could somewhat archive this now by changing the master pages our self. What we can’t, however, is to change the Master Page for e.g. the applications pages located under the /_layouts folder. By default DSRM does not target layouts pages because we must be more careful when affecting these pages. Setting this fact aside we can affect /_layouts by using the IncludeLayoutsFolder parameter.

<RebrandPath Path="/_layouts/CreatePage.aspx" IncludeLayoutsFolder="true" >
  <ApplyMaster File="/_catalogs/masterpage/Blackband.master" />
</RebrandPath>

The above changes the Master Page on the “Create Page” wizard page only. We could have targeted every application page with Path=”/_layouts/” of course. OK. Now we are talking. This could potentially raise the visual experience for the end-customer since it gives a uniform look-n-feel both front-end and backend. And it is not only limited to support branding default.master and application.master but also the tricky simple.master! But this brings up a concern. Do we always want to replace pages with the same master? Probably not (although simple.master pages does support rebranding by default.master pages, that actually great!). Anyway. How do we do this? We use the <ApplyOnlyTo> tag.

<VisualBranding>
 <
RebrandPaths
>
   <RebrandPath Path=
"/layouts/" IncludeLayoutsFolder="true" Priority="1" >    
    
<ApplyMaster File="MyCustom.master" ApplyOnlyTo="default.master" />
   </RebrandPath>
  <
RebrandPath Path=
"/layouts/" IncludeLayoutsFolder="true" Priority="2" >
    <ApplyMaster File="MySimple.master" ApplyOnlyTo="simple.master" />
  </RebrandPath>
 </
RebrandPaths
>
</VisualBranding
>

As you see we restrict the rebranding to only work on e.g. the simple.master page. Great! Also notice the newly introduced Priority parameter. This specify what <RebrandPath> should take precedence of conflicts arises. OK. But changing the Master Page is usually not enough. We usually need to change the CSS as well. This is quite easy actually. As part of a <RebrandPath> we can apply a CSS just like a master.

<RebrandPath Path="default.aspx" >
  <ApplyMaster File="/_catalogs/masterpage/BlueBand.master" />
  <
ApplyCss File="/_layouts/AKJ/akj.css"
/>
</RebrandPath>

Master Page and CSS branding through Querystrings
As a added bonus DSRM also supports rebranding through querystrings. This can be a very simple but powerful way to quickly and non-intrusive brand a single page. A very common scenario would be to make a Printer Friendly master page / CSS style sheet combo that can show a simplified view of any given page. Introducing <QuerystringBranding> as shown below.

<VisualBranding>
   <RebrandPaths
>
    ...
   </RebrandPaths
>
     <QuerystringBrandings
>
      
<QuerystringBranding Parameter=
"PrinterFriendly" Value="true" >
        
<ApplyMaster File="printerfriendly.master" />
      
</QuerystringBranding>
     </QuerystringBrandings
>
</VisualBranding
>

This specifies that when a page has the query string PrinterFriendly=true we should rebrand it with the specified Master and CSS. Tags like <ApplyOnlyTo> is also available. So to get a printer friendly view of the frontpage of the portal we use: http://myportal/default?PrinterFriendly=true.

I will release the source code etc. shortly (within the week) when I have done some further testing. If anyone has suggestions to the branding syntax or have ideas to other branding options please write a comment.

 
 
Jan3

Finally feature-rich support of Active Directory in .Net 3.5

 Categories: Active Directory, Security

It seems that Microsoft finally managed to include a powerfull and programmer-friendly API to interact with a LDAP repository like AD (Active Directory). This is good news for Sharepoint developers that often needs to interact with the AD.
 
Even though there have been countless attempts to make wrapper libraries for the .Net 2.0 DirectoryServices many problems still exist; especially when using D for authentication. The following citation is from MSDN talking about DS and authentication.
 
However, be careful: it is exceedingly easy to write poor versions of such code that are not secure, that are slow, or that are just plain clunky. Additionally, ADSI itself is not designed for this type of operation and can fail under high-use conditions due to the way it caches LDAP connections internally.
 
.Net 3.5 to the rescue! The low-level LDAP-classes has now been nicely wrapped into a very programmer-friendly API that allows for easy, querying, authentication, password management, user/group/computer CRUD operations (like creating and deleting a AD user or group). Besides that, it is powerfull and solid implemented so that strange behaving Active Directory code now now belongs to past. Now let's take a look at some simple code examples.
 
Create a new user
PrincipalContext principalContext = new PrincipalContext(
    ContextType.ApplicationDirectory,"myserver","ou=xxx Users,o=xxxx",
    ContextOptions.SecureSocketLayer,
    "CN=administrator,OU=xxx Users","asecretpassword");

UserPrincipal user = new UserPrincipal(principalContext,
    "NewUserAccountName", "asecretpassword", true);

user.GivenName = "My firstname";
user.Surname = "My lastname";

user.Save();

Searching the Active Directory
// create a principal object representation to describe what will be searched
UserPrincipal user = new UserPrincipal(adPrincipalContext);

// define the properties of the search (this can use wildcards)
user.Enabled = true;
user.Name = "myproject_user*";

// create a principal searcher for running a search operation
PrincipalSearcher pS = new PrincipalSearcher();

// assign the query filter property for the principal object you created
pS.QueryFilter = user;

// run the query
PrincipalSearchResult<Principal> results = pS.FindAll();

Console.WriteLine("Enabled accounts starting with 'myproject_user'");
foreach (Principal result in results)
{
    Console.WriteLine("name: " + result.Name);
}

As you can see the Active Directory has become much more accessible than with .Net 2.0 (and of course 1.1). Take a look at MSDN for more details about the new possibilities.
 
 
Nov23

Powerful console programming

 Categories: Utility, SPConstantGen

When developing Sharepoint solutions you (or at least I) often develope small C# console utilities to perform tasks like post-deployment scripts, security/user administration, dummy data generator....you name it.
 
These consoles programs is often of more or less poor quality since they are developed for you or your team only. Parameters is not documented and error handling is bad or none existing. This is most likely due to the fact that it takes to much time to programme it problably. The solution? Plossum!

The following screenshot is form one of my latest small utilities where I used Plossum to do the parameter collection/parsing/error-handling.

SPConstant
 
When creating consoles with many parameters where some will be optional, some mandatory and other needs to adhere a specific pattern, manual parsing can quickly become ugly and non-maintainable. I will in the next section show how a simple console program is written using Plossum. The (simplified) examples used in the next section is based on the handy utility called SPConstants. The following will however only scratch the surface of Plossum so do go visit the hompage.
 
The first step is to create a class that defines the parameters which our Console applications needs/can to collect.
 
[CommandLineManager(ApplicationName = "SPConstantGen", Copyright = 
"Copyright (c) Anders Jacobsen 2007")] [CommandLineOptionGroup("General")] class CmdOptions { [CommandLineOption(Description = "Displays this help text"
,GroupId="General")] public bool Help = false; [CommandLineOption(Description = "The Namespace used in the
generated file (Default is 'AKJ.Sharepoint')."
, MinOccurs = 0,
DefaultAssignmentValue = "AKJ.Sharepoint", RequireExplicitAssignment = true)] public string Namespace { get { return mNamespace; } set { if (value == null) throw new InvalidOptionValueException( "The name must not be empty", false); mNamespace = value; } } [CommandLineOption(Description = "[True:False]. If set
or True the generated class will be outputted to screen.
Can be VERY long!)."
, MinOccurs = 0,
DefaultAssignmentValue = false, RequireExplicitAssignment = true)] public bool Verbose { get { return mVerbose; } set { mVerbose = value; } } private bool mVerbose; private string mPath; }
 
 
As we see, the class is decorated with the [CommandLineManager] attribute which allows the class to act as a "Parameter Definition class". The class is also decorated with a [CommandLineOptionGroup] attribute. This basically allows us to group the options in logically groups (e.g. when showing the help screen). Now each option is defined as a public property decorated with the [CommandLineOption] attribute. The usage is more or less self explanatory and there is many more switches (like the MinOccurs = 0) available to tweak the rules of the parameter collection. The boolean handling is e.g. quite sophisticated.
 
When the CommandLineManager class (CmdOptions) is defined we can use it together with the parser class as shown below. By the way; the (78) parameter passed to the parser in some of the methods defines the width if the output text block.
 
class Program
{
    static void Main(string[] args)
    {
        // Used to parse and hold the collected parameters
        CmdOptions options = new CmdOptions();
        
        // Create the parser and parse the console input.
        CommandLineParser parser = new CommandLineParser(options);            
        parser.Parse();

        // Allways print the header (Program name, Author, Copyrights etc.)
        Console.WriteLine(parser.UsageInfo.GetHeaderAsString(78));

        // Did the parser encounter a problem?
        if (parser.HasErrors)
        {
            // Print the parsing error and show the available options.
            Console.WriteLine(parser.UsageInfo.GetErrorsAsString(78));
            Console.WriteLine(parser.UsageInfo.GetOptionsAsString(78));                
            return;
        }

        SPSecurity.RunWithElevatedPrivileges(delegate()
        {
            using (SPSite site = new SPSite(options.Url))
            {
                using (SPWeb web = site.RootWeb)
                {
// Use the option options.Namespace GenereateClass(web, options.Namespace); } } } ); } }
 
As you see, creating the initial (and most borring part) has suddenly become easy, fun and most importantly; maintainable and solid. Go download the library immediatly here.
 
 
Nov4

Change Master Page on the fly (via HTTPModule) version 2

 Categories: HTTPModule, Master page

This is an updated post replacing: http://www.pings.dk/blog/archive/2007/10/11/change-master-page-on-the-fly-via-httpmodule.aspx 

Sharepoint does not come with a build-in mechanism to dynamically change masterpage. This could come in handy when implementing eg a "Printer Friendly" view via a simple master page. You could also create a "Night" and "Day" master and change between those according to the time of day.

The solution is to create a HTTPModule which intercepts the Request and change the master page. This could be done directly on a asp.net page in code behind but you would then have to implemente it on every page (or through inheritance).

Solving the problem with version 1

As Keutmann commented in my first post about changing Master Page via a HTTPModule, the implementation worked fine with standard WSS 3.0 pages but failed when the page was a PublishingPage. First it sounded strange to me but after testing it on a Publishing Page he was of course right. It does not work on Publishing Pages.

So why is that? Let us take a look at the PublishingLayoutPage implementation (via Reflector) which standard Publishing Pages inherit from.

protected override void OnPreInit(EventArgs e)
{
    base.OnPreInit(e);
    SPContext current = SPContext.Current;
    
    // Rubbish deleted for clarity :)
    
    SPWeb web = current.Web;
    this.MasterPageFile = web.CustomMasterUrl;
}

The PublishingLayoutPage overrides the the OnPreInit event. This explains why my first implementation (which always set the this.MasterPageFile) in the PreInit did not work on publishing pages. It was simply overriding later by the PublishingLayoutPage.

Now the solution. Take a look at the OnPreInit(..) method above again. We can't set the MasterPageFile property since it will be overriden. By we can however set the current SPWeb's CustomMasterUrl to point to our new temporary MasterPageUrl. We of course should not persist this change since it would be...persisted..and would change the master page for all users. If we just change the CustomMasterUrl property on the current instance of the SPWeb object it will only be applied for this particular request which is exactly what we want.
SPContext.Current.Web.CustomMasterUrl = GetMaster(parameter);

Implementation of version 2

Compile the code below into a strong named assembly and place in GAC (remember SafeControls tag in web.config). Finally insert the following in web.config to register the HTTPModule.

<add name="DynamicMasterPageModule " type="DynamicMasterPageModule , DynamicMasterPageModule , Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx" />

public class DynamicMasterPageModule : IHttpModule
{
    public DynamicMasterPageModule()
    {}

    #region IHttpModule Members
    public void Dispose()
    {}

    public void Init(HttpApplication context)
    {
        // Hook into preRequestHandler
        context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute);            
    }

    /// <summary>
    /// In the PreRequestHandlerExecute we change the master page for PublishingPages.
    /// For "normal" WSS 3.0 pags we let the PreInit Handler do the job.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void context_PreRequestHandlerExecute(object sender, EventArgs e)
    {
        Page page = HttpContext.Current.CurrentHandler as Page;
        
        // If the request originates from a Publishing Page then we have to
        // change the master page on the current Sharepoint Web instance object
        // (of course we don't want to persist this change since it would
        // change the master page permanently..for all users).
        if (page is PublishingLayoutPage || page is TemplateRedirectionPage)
        {
            HttpApplication currentApp = sender as HttpApplication;
            string parameter = currentApp.Context.Request.QueryString["page"];
                            
            if (parameter != null)
            {  
                SPContext.Current.Web.CustomMasterUrl = GetMaster(parameter);
            }
        }
        else
        {
            // Now, if this is a normal wss 3.0 page hook into
            // the PreInit event (just before the ordinary Page event
            // cycle starts.            
            if (page != null)
                page.PreInit += new EventHandler(page_PreInit);
        }
    }

    /// <summary>
    /// First event where the Request instance is initialized
    /// (and before the Control structure is build so MasterPage
    /// can still be changed.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void page_PreInit(object sender, EventArgs e)
    {
        Page page = sender as Page;

        string parameter = page.Request.QueryString["page"];

        if (parameter != null)
        {
            page.MasterPageFile = GetMaster(parameter);
        }
    }

    /// <summary>
    /// Get the Master page according to the queryId
    /// </summary>
    /// <param name="queryId"></param>
    /// <returns></returns>
    private string GetMaster(string queryId)
    {
        queryId = queryId.ToLower();

        if (queryId == "1")
        {
            return "/_catalogs/masterpage/default.master";
        }

        if (queryId== "2")
        {
            return "/_catalogs/masterpage/BlackBand.master";
        }

        throw new ArgumentException("Master page ID not supported. Please use 1 or 2");
    }

    #endregion
 
 
 Next >>