Wednesday, March 5, 2008

Programmatically Inherit Master Page and CSS in MOSS 2007

So recently I have been working on two small MOSS features:

  • Change the master page and stylesheet settings for a site

  • Staple the first feature to all site templates, so it is activated upon site creation

I was able to create both features and deploy them with little difficulty. See the following resources for tips on solutions, features, and stapling:

The problem that I ran into was this little radio button:





Brief rundown of SharePoint Object Model:

  • SPSite is a Site Collection

  • SPWeb is a Site within a Site Collection


Within the SharePoint Object Model, there are a couple of properties of the SPWeb object which we will be using:

  • [web_object].MasterUrl

  • [web_object].CustomMasterUrl

  • [web_object].AlternateCssUrl

These values are read/write, so you can set the values to your own strings and the values in the database will be changed accordingly.



Note: all of the following is done within the FeatureActivated function of a custom class inheriting from the SPFeatureReceiver class. Now let's dive into some code (these are all different methods I tried to no avail)

  • Created some variables and hard coded the locations within the feature:

    string MasterUrl = "/_layouts/custom.master";

    string CustomMasterUrl = "/_layouts/custom.master";

    string AlternateCssUrl = "/StyleLibrary/Custom/CSS/stylesheet.css";



    SPWeb web = (SPWeb)properties.Feature.Parent;

    try {

      web.MasterUrl = MasterUrl;

      web.CustomMasterUrl = CustomMasterUrl;

      web.AlternateCssUrl = AlternateCssUrl;

      web.Update();

    }

    catch { }


  • Set the values of the current site to the values from the root site:

    SPWeb web = (SPWeb)properties.Feature.Parent;

    try {

      web.MasterUrl = web.Site.RootWeb.MasterUrl;

      web.CustomMasterUrl = web.Site.RootWeb.CustomMasterUrl;

      web.AlternateCssUrl = web.Site.RootWeb.AlternateCssUrl;

      web.Update();

    }

    catch { }


  • Set the values of the current site to the values from it's parent site:

    SPWeb web = (SPWeb)properties.Feature.Parent;

    try {

      web.MasterUrl = web.ParentWeb.MasterUrl;

      web.CustomMasterUrl = web.ParentWeb.CustomMasterUrl;

      web.AlternateCssUrl = web.ParentWeb.AlternateCssUrl;

      web.Update();

    }

    catch { }

None of these methods, upon going to the "Site Master Page Settings" page within a browser, showed the little radio button next to "Inherit site master page from parent of this site" as being checked. They did correctly set the master page and stylesheet links, but if I went to a parent site and changed the master page, the master page did not get changed on the site *unless* the "inherit" radio button was selected.



I decided to take this to the Content Database for the portal in which I was working. Looking in the dbo.Webs table, I found that if the "Inherit" radio button is selected, there are still values in the DB for AlternateCssUrl, MasterUrl, and CustomMasterUrl. Running the above code, would put the same values in these fields, but the "Inherit" radio button would not be selected. I then performed the following steps:

  • Copy a row (one site) from the database to a text file

  • Change each of the 3 settings (Site Master, System Master, and Alternate CSS) from "Inherit" to "Specify a ..."

  • Copy the same row from the database to the second row of the text file

  • Change each of the 3 settings back to "Inherit"

  • Copy the same row from the database to the third row of the text file

  • Change each of the 3 settings back to "Specify a ..."

  • Copy the same row from the database to the fourth row of the text file

I then moved the side-scroll-bar all the way to the right (to confirm something in each lines was different) and they did not end at the same character, so I went to the beginning and compared vertically until I found different characters. There were a total of 3 instances of different characters (all 3 instances had the same characters) all within the same field, 'MetaInfo'. These values were:

  • "Inherit": 547275

  • "Specify a ...": 46616C73


Going back to the SharePoint Object Model, I discovered that there are two member properties to the SPWeb object which seem to correlate to site properties: Properties and AllProperties. I then tossed together a quick console app to output all Key/Value pairings for these two collections, and this is what came out:

  • SPWeb.Properties (C# type SPPropertyBag)


    • vti_extenderversion: 12.0.0.4518

    • vti_associatevisitorgroup: 4

    • vti_defaultlanguage: en-us

    • vti_associategroups: 5;4;3;6;7;8;9;10;11;14;15;17;18;28

    • vti_associateownergroup: 3

    • vti_associatemembergroup: 5

  • SPWeb.AllProperties (C# type Hashtable)


    • vti_extenderversion: 12.0.0.4518

    • __InheritsCustomMasterUrl: False

    • vti_associatevisitorgroup: 4

    • vti_categories: Business Competition Expense\ Report Goals/Objectives Ideas In\ Process Miscellaneous Planning Schedule Travel VIP Waiting

    • vti_associatemembergroup: 5

    • vti_defaultlanguage: en-us

    • vti_associateownergroup: 3

    • vti_associategroups: 5;4;3;6;7;8;9;10;11;14;15;17;18;28

    • __InheritsAlternateCssUrl: False

    • vti_approvallevels: Approved Rejected Pending\ Review

    • __InheritsMasterUrl: False

Fancy that, there are three properties which interest me most at this point (indicated in red).



I simply set these values to "True" (note, that is a string of "True" and not a 1 or C# true), which resulted in this:



However, it did not pull the correct values to begin with. Therefore, it would appear that SharePoint uses these settings for when the parent's master pages/css are changed, and not relying on these settings for everytime the site is accessed.



All in all, here is the final code I came up with for my Feature (inside FeatureActivated function):



SPWeb web = (SPWeb)properties.Feature.Parent;

Hashtable hash = web.AllProperties;

try {

  web.MasterUrl = web.ParentWeb.MasterUrl;

  hash["__InheritsMasterUrl"] = "True";

  web.Update();

}

catch { }

try {

  web.CustomMasterUrl = web.ParentWeb.CustomMasterUrl;

  hash["__InheritsCustomMasterUrl"] = "True";

  web.Update();

}

catch { }

try {

  web.AlternateCssUrl = web.ParentWeb.AlternateCssUrl;

  hash["__InheritsAlternateCssUrl"] = "True";

  web.Update();

}

catch { }



I hope this helps you, as I know I was pulling my hair out for a long time over this.



Enjoy!

--andrew



Correction [06.11.2008]: I just realized that in my final code, I had the following line:

  web.MasterUrl = web.ParentWeb.CustomMasterUrl;

This works just fine, except I am pulling the wrong master page setting from the parent site for the current setting. This should read:

  web.MasterUrl = web.ParentWeb.MasterUrl;

I have corrected this above in the code.

15 comments:

  1. AWESOME!
    you are a life saver don't stop blogging!

    ReplyDelete
  2. Agreed with Ishai. Thanks for a useful post.

    ReplyDelete
  3. Hey Andrew - I may have done something wrong, but I could only get your hash solution to work on a MOSS site, and it didn't work on WSS sites. I believe its because WSS sites don't have the masterpage inheritance available through the UI under look and feel. A bugger though, since that is specifically what I'm trying to work around.

    Thanks though!

    ReplyDelete
  4. Phil,
    Thanks for pointing out this only works with MOSS installations. You might try iterating through the AllProperties hash to see what keys and values it contains in a WSS deployment. You can always add custom keys/values into the hash, but then you would need to develop functionality to take advantage of your custom properties.

    ReplyDelete
  5. I use the publishingweb object to
    set the master page url, Custom master page url and style.


    PublishingWeb publishingWeb = PublishingWeb.GetPublishingWeb(oWebImp);
    publishingWeb.MasterUrl.SetValue(MasterPageName,, false);
    publishingWeb.Update();

    ReplyDelete
  6. Hey Andrew - Fantastic effort on uncovering this code. There's loads of posts out there that do features where all the URL's are specified, this is a very fresh approach as it's true to the SharePoint idea of inheritance. I've managed to shoehorn this into a feature and kick it off with a feature staple. One thing i'd like to now do is inherit the parent's SiteLogoUrl , do you know if this is possible with the way you acheived the above.

    ReplyDelete
  7. Thank you !

    Tippu, using PublishingWeb object model works only with... publishing webs :-). Since portals are often WSS and MOSS web template mixed, AllProperties seem to be the only generic solution.

    ReplyDelete
  8. AJ,
    Unfortunately I am unaware of a similar method for forcing inheritance of the SiteLogoUrl.

    You might be able to add a new link to the "Site Settings" page which could offer an option "Inherit Logo from Parent" (value to be stored in SPWeb.AllProperties hash), and a button to fire off a one-time SPTimerJob to traverse your SPSite and set the SiteLogoUrl for all SPWeb objects with your custom property defined.

    ReplyDelete
  9. Andrew,
    Thanks for this great work, I've been struggling out this and you sorted it. Great post.

    ReplyDelete
  10. Andrew,

    I am having a feature that is having scope "Web" and its working fine, means I am able to chnage master page & custom css for top level site after feature activation. but here If I would like to actiavte for all webs then I have to activate for each. I tried with HashTable Inherits that you mentioned in blog, but no luck. can you please guide me how can I use that hastable thing?

    ReplyDelete
  11. Rao,
    Feature Stapling only takes effect on newly created webs. It will not auto-activate on webs you have already created.

    To change existing webs and newly created webs, you can create 2 separate Features:
    1. A "Web" scoped Feature, and an associated Feature Staple, so that all newly created webs receive the correct master page, css, and inheritance settings.
    2. A "Site" scoped Feature, which traverses your Site Collection and activates the "Web" scoped feature for all sub-webs.**

    **IMPORTANT NOTE: Be sure not to activate the "Web" feature on the root web of the top level Site Collection, as I am unsure what would happen if you attempt to inherit on a site that has no parent!

    ReplyDelete
  12. Thanks for the post!
    It's exactly what I needed.

    ReplyDelete
  13. Thanks for the information, it was exactly what I needed to complete my project.

    ReplyDelete
  14. Thanks dude, that helped me a lot !

    ReplyDelete
  15. Hours of searching and this was what I needed. Thank you so much

    ReplyDelete