span.sup { vertical-align:text-top; }

Office Space

Automated SharePoint Site Branding

Ted Pattison

Code download available at:OfficeSpace2008_07.exe(1,315 KB)

Contents

Introducing the LitwareBranding Solution
Creating a Utility Class for Brand Management
Swapping Out the Master Page
Registering an HttpModule in the Web.config File
Initializing Branding during Feature Activation

There is one request I have heard often since I began working with SharePoint® technologies. Rather than using standard SharePoint sites, companies often want to create unique sites. Whether you are using SharePoint technologies to create your company's Internet-facing Web sites or an intranet environment with hundreds of team collaboration sites, you might be asked to manage the visual aspects of your SharePoint sites such as page layout and the use of fonts and colors.

Windows® SharePoint Services (WSS) 3.0 provides branding through themes. A user can apply a WSS theme to a site to change its appearance. Note that themes in WSS are entirely different from themes in ASP.NET. WSS implements its infrastructure for themes by copying a cascading style sheet (CSS) file from the file system of the Web server into the context of the current site and dynamically linking to this CSS file from each page.

WSS themes have limitations that make them less interesting to developers. The first limitation is that WSS themes cannot integrate a custom Master Page to customize the page layouts used within a site. Furthermore, a WSS theme can only be applied on a per-site basis as opposed to across a site collection. There is no support to perform a single action that applies a theme to all the sites within a site collection. Instead, you would have to go to each site separately and apply the same theme to maintain a consistent appearance and functionality across a site collection.

The SharePoint Designer provides another means for branding sites in WSS or Microsoft® Office SharePoint Server (MOSS) 2007. You can use the SharePoint Designer to customize a site's Master Page and to edit and integrate custom CSS files. However, when working with a site collection based on standard WSS sites, the SharePoint Designer doesn't provide a way to synchronize all the sites within a site collection to use the same version of a customized Master Page. This can possibly lead to scenarios where you are forced to copy and paste your Master Page edits across multiple Master Page files.

Things get better when you use the SharePoint Designer together with MOSS because MOSS provides a Web Content Management (WCM) infrastructure through the use of publishing portals. A MOSS publishing portal is a site collection containing publishing sites that can automatically synchronize pages across all the sites to use the same Master Page and the same CSS file.

However, it is important to keep in mind that using the SharePoint Designer has some noteworthy limitations when it comes to creating a branding solution intended for use across multiple site collections or in enterprise scenarios. Remember that changes made with the SharePoint Designer are always made as one-off customizations within the context of a particular site. This means a customization made with the SharePoint Designer can be tricky or impossible to migrate between site collections or between farms. Also, it's impractical to integrate site customizations made with SharePoint Designer into a source code management system such as Visual Studio® Team System.

This month I am going to walk through developing a custom branding solution for SharePoint sites that leverages the capabilities of WSS to integrate custom Master Pages and custom CSS files at the level of the site collection. I am going to walk through the code within a sample branding solution named LitwareBranding that has been developed using a Visual Studio project and that is deployed using a solution package. You can find all the code in the code download for this issue.

Employing Visual Studio and generating a solution package makes it possible to deploy your branding solution to any farm running WSS 3.0 or MOSS 2007. It also makes it easy to create a single source file for each Master Page and CSS file that can be reused across multiple site collections or across farms. Furthermore, the Visual Studio approach can be far more appealing than using the SharePoint Designer because it allows you to check all your source files into a source code management system. You can also more easily move your work from a development farm to a staging farm for testing and then into production.

Introducing the LitwareBranding Solution

This month's column is accompanied by a sample Visual Studio project named LitwareBranding. Figure 1 shows the structure of the LitwareBranding project within Visual Studio. As you can see, the solution has been designed using three different features whose purpose and implementation will be explained throughout this month's column. The central feature within this solution is named LitwareBranding, which has been designed to be activated by a user within the scope of a WSS or MOSS site collection. Activation of the LitwareBranding feature forces the LitwareBranding solution to apply all the branding techniques that will be covered in this month's column.

fig01.gif

Figure 1 LitwareBranding Solution in Visual Studio

Let's begin our code walkthrough by looking at the feature.xml file for the central feature named LitwareBranding, shown in Figure 2. The LitwareBranding feature has been defined with a Scope attribute value of Site, which means it is scoped to the site collection level. The idea is that a site collection owner should be able to activate a single feature to apply the Litware corporate branding to all the sites within the current site collection.

Figure 2 Feature.xml

<Feature
  Id="065E2243-B968-4F14-BAAE-610BB975EFB7"
  Title="A sample feature: LitwareBranding"
  Description="Demoware created for Ted Pattison's OfficeSpace column"
  Hidden="FALSE"
  Scope="Site"
  ImageUrl="LitwareBranding\AfricanPith32.gif"
  ReceiverAssembly="LitwareBranding, [4-part assembly name]"
  ReceiverClass="LitwareBranding.FeatureReceiver" 
  xmlns="https://schemas.microsoft.com/sharepoint/">  

  <ElementManifests>
    <ElementManifest Location="elements.xml"/>
    <ElementManifest Location="stapling.xml" />
    <ElementFile Location="LitwareBranding.master"/>
  </ElementManifests>

</Feature>

The LitwareBranding feature contains a Master Page template named LitwareBranding.master and an element manifest named elements.xml. Within the elements.xml manifest there is declarative XML provisioning logic to create an instance of this Master Page template. This logic is defined by a Module element with an inner File element:

<Module Name="MasterPages" 
        Path="" List="116" 
        Url="_catalogs/masterpage" >

  <File Url="LitwareBranding.master" 
        Type="GhostableInLibrary" />

</Module>

During feature activation, this Module element forces WSS to provision an instance of the Master Page template named LitwareBranding.master in the Master Page gallery of the top-level site. After this Master Page instance has been provisioned, it's then possible to supply code that programs against the WSS object model to synchronize pages through the site collection to link to this Master Page, as I will show in the next section.

Creating a Utility Class for Brand Management

The LitwareBranding project contains a class named BrandManager. This utility class encapsulates all the code that programs against the WSS object model to apply and remove various branding elements. There are static methods exposed by the Brand­Manager class that accomplish the following tasks:

  • Synchronizing site pages to link to LitwareBranding.master
  • Synchronizing pages to use an alternate CSS file
  • Synchronizing pages to use a custom graphic for the site logo
  • Adding support to swap out the Master Page for application pages instead

Before drilling down into specific members, take a look at Figure 3, which shows all the members of the BrandManager class. This class contains several static properties that parse together URLs pointing to various resources such as Master Pages and a CSS file. For example, the SiteCollectionUrl property returns the Web application-relative path to the current site collection. The DefaultMasterPageUrl returns a Web application-relative path to the default master page named default.master in the Master Page gallery of the current site collection's top-level site. This path is parsed together using the SiteCollectionUrl property together with the following site-relative path:

_catalogs/masterpage/default.master

Figure 3 BrandManager Class

public class BrandManager {
  // read-only properties
  public static string SiteCollectionUrl 
  public static string DefaultMasterPageUrl 
  public static string CustomMasterPageUrl 
  public static string CustomCssUrl
  public static string CustomSiteLogoUrl
  // utility branding methods
  public static void ConfigureMasterUrl(bool ApplyMasterUrl) {}
  public static void ConfigureCustomMasterUrl(bool ApplyCustomMasterUrl) {}
  public static void ConfigureAlternateCss(bool ApplyCustomCss) {}
  public static void ConfigureSiteLogo(bool ApplySiteLogo) {}
  public static void ConfigureApplicationPageMaster(bool ApplyApplicationPageMaster) {}
}

The CustomMasterPageUrl property returns a Web app relative path to the Master Page instance named LitwareBranding.master in the Master Page gallery of the current site collection's top-level site. This path is parsed together using the SiteCollectionUrl property together with the following site-relative path:

_catalogs/masterpage/litwarebranding.master

Now, let's examine the ConfigureMasterUrl method, which enumerates through every site in the current site collection and updates each site's MasterUrl property to point to either the custom Master Page or the default Master Page:

public static void ConfigureMasterUrl(bool ApplyMasterUrl) {
  // determine MasterUrl property setting
  string MasterUrlPath = 
    (ApplyMasterUrl ? 
    CustomMasterPageUrl : 
    DefaultMasterPageUrl);

  // update MasterUrl property for all sites
  foreach (SPWeb site in SPContext.Current.Site.AllWebs) {
    site.MasterUrl = MasterUrlPath;
    site.Update();
  }
}

Note that the MasterUrl property affects all site pages that have been created with a MasterPageFile property setting of ~masterurl\default.master. This includes the standard default.aspx page template that provides the home page for many SharePoint site templates including Team Site and Blank site. It also includes the form pages such as AllItems.aspx and NewItem.aspx that are used by standard WSS lists types.

Page layouts in a MOSS publishing site will not be affected when you update the MasterUrl property. This includes all the content pages within the Pages library of a MOSS publishing site.

Instead, these content pages are designed to use a dynamic token for the MastePageFile attribute, which follows the form of ~masterurl\custom.master. This token is different from the ~master­url\custom.master token because it is switched out using the CustomMasterUrl property instead of the MasterUrl property. Therefore, the BrandManager class provides a second method named Configure­CustomMasterUrl that can be used to switch out the Master Page for content pages created in the Pages library of MOSS publishing sites:

public static void ConfigureCustomMasterUrl(
  bool ApplyCustomMasterUrl) {
  // determine MasterUrl property setting
  string CustomMasterUrlPath = 
    (ApplyCustomMasterUrl ? 
    CustomMasterPageUrl : 
    DefaultMasterPageUrl);

  // update MasterUrl property for all sites
  foreach (SPWeb site in SPContext.Current.Site.AllWebs) {
    site.CustomMasterUrl = CustomMasterUrlPath;
    site.Update();
  }
}

In addition to swapping out Master Pages, the Litware­Branding solution also integrates a custom CSS file named styles.css. The LitwareBranding solution uses a strategy of deploying the CSS file named styles.css in a directory nested inside the LAYOUTS directory so that it can be deployed a single time on the file system of each front-end Web server. The value of deployment within the LAYOUTS directory is that a file such as style.css can be deployed once on the Web server's file system and yet still be accessible from any site within the current farm using a standard URL that is returned by the CustomCssUrl property:

public static string CustomCssUrl {
  get {
    return "/_layouts/1033/STYLES/LitwareBranding/styles.css";
  }
}

The BrandManager class provides a method named ConfigureAlternateCss that uses the CustomCssUrl property to assign the URL to the AlternateCSS property of each site within the current site collection:

public static void ConfigureAlternateCss(bool ApplyCustomCss) {
// determine MasterUrl property setting
  string AlternateCssUrl = 
    (ApplyCustomCss ? 
    CustomCssUrl : 
    string.Empty);

  // update AlternateCssUrl for all sites
  foreach (SPWeb site in SPContext.Current.Site.AllWebs) {
    // make sure no theme is enabled
    site.ApplyTheme(string.Empty);
    // apply custom CSS file
    site.AlternateCssUrl = AlternateCssUrl;
    site.Update();
  }
}

The branding capabilities built into WSS also provide an easy way for you to replace the standard WSS site logo that displays on the top-left portion of its pages. The LitwareBranding solution takes advantage of this capability by including a custom graphics file named SiteLogo.gif, which is deployed in a directory nested inside the IMAGES directory. Like the CSS file named styles.css, the graphics file named SiteLogo.gif is also deployed in such a way that it is accessible from any site in the current farm using the URL returned by the CustomSiteLogoUrl property:

public static string CustomSiteLogoUrl {
  get {
    return "/_layouts/images/LitwareBranding/SiteLogo.gif";
  }
}

The ConfigureSiteLogo method of the BrandManager class has been written to enumerate through each site of the current site collection and to update the SiteLogoUrl property:

public static void ConfigureSiteLogo(bool ApplySiteLogo) {
  // determine SiteLogoUrl property setting
  string SiteLogoUrl = 
    (ApplySiteLogo ? 
    CustomSiteLogoUrl : 
    string.Empty);

  // update AlternateCssUrl for all sites
  foreach (SPWeb site in SPContext.Current.Site.AllWebs) {
    site.SiteLogoUrl = SiteLogoUrl;
    site.Update();
  }
}

Swapping Out the Master Page

While SharePoint makes it relatively easy to swap out the Master Page for site pages, it does not provide an obvious way to swap out the Master Page for built-in application pages running out of the LAYOUTS directory such as the standard WSS Site Settings page (settings.aspx). These application pages are linked to a standard WSS Master Page named application.master that WSS deploys within the LAYOUTS directory. However, if you create a branding solution that swaps out the Master Page for all the site pages within a site collection, you might want to create a consistent appearance by swapping out the Master Page for application pages such as settings.aspx so that they also use a customized Master Page.

The LitwareBranding solution demonstrates a technique for swapping out the Master Page for all application pages that link to the standard Master Page file named application.master. The technique used for swapping out the Master Page involves implementing a custom HttpModule that registers an event handler for one of the events in the ASP.NET page lifecycle named PreInit. This is the event that the ASP.NET programming model requires you to implement whenever you want to swap out the Master Page during the initial processing of a page request.

The class LitwareBrandingHttpModule provides the Http­Module used in the LitwareBranding solution to swap out the Master Page (see Figure 4). This HttpModule class registers a handler for the PreRequestHandlerExecute event inside the Init method. Within the method implementation of the PreRequestHandlerExecute event handler, the HttpModule class determines whether the request is based on an HttpHandler object that derives from the ASP.NET Page class. Only in cases where the request is based on a Page-derived object will the HttpModule class register an event handler from the PreInit event. The code in the PreRequestHandler­Execute event handler also checks to make sure the incoming request targets a page inside the _layouts directory, which will always be the case when processing an application page in WSS.

Figure 4 Swap the Master Page with an App Page

using System;
using System.Web;
using System.Web.UI;
using Microsoft.SharePoint;

namespace LitwareBranding {
  public class LitwareBrandingHttpModule : IHttpModule {

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

    void context_PreRequestHandlerExecute(object sender, EventArgs e) {
      Page page = HttpContext.Current.CurrentHandler as Page;
      if (page != null) {         
        // register handler for PreInit event
        page.PreInit += new EventHandler(page_PreInit);
      }
    }

    void page_PreInit(object sender, EventArgs e) {
      // IF – requested page links to application.master
      // THEN – dynamically modify page to link to custom .master file
    }  

    public void Dispose() { /* empty implementation */ }

  }
}

It's important to remember that an HttpModule cannot be deployed in a WSS farm for an individual site collection. Instead, an HttpModule must be configured as an all-or-nothing proposition at the Web application level. However, a Web application may contain hundreds of site collections and only certain site collections might have enabled the LitwareBranding feature. Therefore, the PreInit event handler for the HttpModule class must be able to determine whether the current site collection has been configured with the behavior to swap out the Master Page for its application pages.

The LitwareBranding solution solves this problem by creating a custom property in the site collection's top-level site to indicate that swapping out the Master Page for application pages should be enabled. Examine the implementation of the ConfigureApplicationPageMaster method defined within the BrandManager class:

public static void ConfigureApplicationPageMaster(
  bool ApplyApplicationPageMaster) {
  SPWeb TopLevelSite = SPContext.Current.Site.RootWeb;
  if (ApplyApplicationPageMaster) {
    TopLevelSite.Properties["UseCustomApplicationPageMaster"] = "True";
  }
  else {
    TopLevelSite.Properties["UseCustomApplicationPageMaster"] = "False";
  }
  TopLevelSite.Properties.Update();
}

This method creates a custom property named UseCustom­ApplicationPageMaster on the top-level site and assigns the property a value of True. Now, the method implementation for the PreInit event handler in the HttpModule can look for this property within the current site collection to see whether it has been configured to enable swapping out the application page master. Note that the PreInit event handler also performs several other checks to determine whether it is appropriate to swap out the Master Page within the context of the current request (see Figure 5).

Figure 5 PreInit Event Handler

void page_PreInit(object sender, EventArgs e) {
  Page page = sender as Page;
  if ((page != null) &&
      (page.MasterPageFile != null) &&
      (page.Request.Url.AbsolutePath.Contains("_layouts")) &&
      (SPContext.Current != null)  ) {
    // inspect UseCustomApplicationPageMaster property
    SPWeb site = SPContext.Current.Site.RootWeb;
    string UseCustomApplicationPageMaster =
      site.Properties["UseCustomApplicationPageMaster"];
    if ((!string.IsNullOrEmpty(UseCustomApplicationPageMaster)) &&
       (UseCustomApplicationPageMaster.Equals("True"))) {
      // now replace application.master with customized version
      if (page.MasterPageFile.Contains("application.master")) {
        page.MasterPageFile = "/_layouts/LitwareBranding/application.master";
      }          
    }
  }
}

Once the PreInit event handler determines the current site collection has been configured to enable swapping out the Master Page for application pages and also that the current page links to application.master, it modifies the MasterPageFile property of the current ASP.NET Page object to use a custom Master Page located within a solution-specific directory within the LAYOUTS directory at the following path:

/_layouts/LitwareBranding/application.master

Registering an HttpModule in the Web.config File

When deploying a business solution with WSS and MOSS, it is good to distribute your development efforts within a solution package. For example, the Visual Studio project for Litware­Branding builds a single-solution package named LitwareBranding.wsp, which makes it easy and reliable to deploy this solution out to any farm running WSS 3.0 or MOSS 2007. However, the LitwareBranding solution requires adding an HttpModule entry to the web.config file for each Web application that will be running site collections that activate the LitwareBranding feature.

To update the web.config file within specific Web applications, the LitwareBranding solution uses a second feature named Litware­BrandingWebApplication. This feature is scoped to the Web application level and is configured with a feature receiver class to fire event handlers as it is activated and deactivated within the scope of a particular Web application:

<Feature 
  Id="FF739C76-0B08-4bc2-A3A2-F61524B492D8"
  Title="Litware Branding Support Feature (WebApplication)"
  Scope="WebApplication" 
  Hidden="False"
  ReceiverClass="LitwareBranding. FeatureReceiverWebApplication"
  ReceiverAssembly="LitwareBranding, [4-part assembly name]"
  xmlns="https://schemas.microsoft.com/sharepoint/">

  <!-- no declarative elements -->
  <ElementManifests />    

</Feature>

Like any other feature receiver class, the FeatureReceiver­WebApplication class inherits from the SPFeatureReceiver class and overrides the four handler methods named FeatureInstalled, FeatureActivated, FeatureDecactivating and FeatureUninstalling. The implementation for FeatureActivated adds the HttpModule entry to the web.config file for the current Web application. The implementation for FeatureDeactivating reverses that operation by removing the HttpModule entry.

When you need to add an entry to the web.config file, it is best to use the SPWebConfigModification class from the WSS object model. The FeatureReceiverWebApplication class contains a utility method named CreateHttpModuleModification that creates and initializes an instance of SPWebConfigModification and passes it back as its return value (see Figure 6).

Figure 6 CreateHttpModuleModification

public SPWebConfigModification CreateHttpModuleModification() {
  SPWebConfigModification modification;
  string ModName = "add[@name='LitwareBrandingModule']";
  string ModXPath = "configuration/system.web/httpModules";
  modification = new SPWebConfigModification(ModName, ModXPath);
  modification.Owner = "LitwareBranding";
  modification.Sequence = 0;
  modification.Type = 
    SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
  modification.Value = 
    @"<add name=""LitwareBrandingModule"" " + @ "type="
    "LitwareBranding.LitwareBrandingHttpModule, [4-part assembly name]"
    " />";
  return modification;
}

Once you have a utility method such as CreateHttpModuleModification that returns an initialized SPWebConfigModification object, you can simply call this method from event handlers such as FeatureActivated and FeatureDeactivating to add the required HttpModule entry to or remove it from the web.config file for the current Web application (see Figure 7).

Figure 7 Enabling and Disabling the HttpModule

public override void FeatureActivated(
  SPFeatureReceiverProperties properties) {
  SPWebApplication WebApp = 
    (SPWebApplication)properties.Feature.Parent;
  WebApp.WebConfigModifications.Add(
    CreateHttpModuleModification());
  WebApp.WebService.ApplyWebConfigModifications();
  WebApp.WebService.Update();      
}

public override void FeatureDeactivating(
  SPFeatureReceiverProperties properties) {
  SPWebApplication WebApp = 
    (SPWebApplication)properties.Feature.Parent;
  WebApp.WebConfigModifications.Remove(
    CreateHttpModuleModification());
  WebApp.WebService.ApplyWebConfigModifications();
  WebApp.WebService.Update();
}

Initializing Branding during Feature Activation

It's time to put all these pieces together. When a user activates the LitwareBranding feature within a specific site collection, there is a feature receiver with a FeatureActivated event handler that uses the BrandManager class to apply all the various branding elements. There is also a FeatureDeactivating event handler that removes all the branding elements during feature deactivation (see Figure 8).

Figure 8 Applying and Removing Branding Elements

public override void FeatureActivated(SPFeatureReceiverProperties properties) {  
  EnsureWebApplicationFeatureEnabled();
  BrandManager.ConfigureMasterUrl(true);
  BrandManager.ConfigureCustomMasterUrl(true);
  BrandManager.ConfigureAlternateCss(true);
  BrandManager.ConfigureSiteLogo(true);
  BrandManager.ConfigureApplicationPageMaster(true);
}

public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {
  BrandManager.ConfigureMasterUrl(false);
  BrandManager.ConfigureCustomMasterUrl(false);
  BrandManager.ConfigureAlternateCss(false);
  BrandManager.ConfigureSiteLogo(false);
  BrandManager.ConfigureApplicationPageMaster(false);
}

Note the call to the EnsureWebApplicationFeatureEnabled method at the beginning of the FeatureActivated event handler. This method has been written to ensure that the Web application-level feature named LitwareBrandingWebApplication has been activated so that the HttpModule, which swaps out the Master Pages for application pages, is properly registered with ASP.NET:

public void EnsureWebApplicationFeatureEnabled() {
  // make sure feature which adds HttpModule to web.config is active
  SPSecurity.RunWithElevatedPrivileges(delegate() {
    using (SPSite siteCollection = new SPSite(SPContext.Current.Site.ID)) {
      try {
        Guid FeatureId = new Guid("FF739C76-0B08-4bc2-A3A2-F61524B492D8");
        siteCollection.WebApplication.Features.Add(FeatureId);
      }
      catch { }
    }
  });      
}

WSS and MOSS pose another design problem when creating a site collection-scoped branding solution: you must decide how to deal with properly initializing the branding for child sites as they are created. The code inside the BrandManager class only affects the branding properties of existing sites. Therefore, the LitwareBranding solution includes a third feature named LitwareBranding­ChildSiteInitializer that contains the following event handler for the FeatureActivated event:

// fired whenever a new site is created
public override void FeatureActivated(SPFeatureReceiverProperties properties) {
  SPWeb ChildSite = (SPWeb)properties.Feature.Parent;
  SPWeb TopLevelSite = ChildSite.Site.RootWeb;
  ChildSite.MasterUrl = TopLevelSite.MasterUrl;
  ChildSite.CustomMasterUrl = TopLevelSite.CustomMasterUrl;
  ChildSite.AlternateCssUrl = TopLevelSite.AlternateCssUrl;
  ChildSite.SiteLogoUrl = TopLevelSite.SiteLogoUrl;
  ChildSite.Update();
}

This event handler fires during feature activation and copies the top level site's branding properties into the current child site. Now you must figure out how to get this feature to activate automatically whenever a new child site is created inside a site collection in which the LitwareBranding feature has been activated.

The answer is feature stapling. The LitwareBranding feature provides a FeatureSiteTemplateAssociation element that staples the LitwareBrandingChildSiteInitializer feature to the GLOBAL site definition:

<!-- staple GLOBAL site definition to   itwareBrandingChildSiteInitializer -->
<FeatureSiteTemplateAssociation
  Id="1204A425-D105-46c5-BB2C-473A2F27B563"
  TemplateName="GLOBAL" />

This stapling technique forces automatic feature activation. By stapling the LitwareBrandingChildSiteInitializer feature to the GLOBAL site definition, you are in effect configuring the feature to activate automatically whenever a new site is created no matter what site template has been used.

Note that the standard WSS site template named Blank Site does not participate in features stapling to the GLOBAL site definition. This behavior was added to SharePoint so that sites created from the Blank Site template will work with the MOSS content deployment strategy. If you want to automate the activation of the Litware­BrandingChildSiteInitializer feature in sites created from the Blank Site template, you must add explicit stapling instructions for that site definition and configuration as well:

<!-- staple blank site template to LitwareBrandingChildSiteInitializer -->
<FeatureSiteTemplateAssociation
  Id="1204A425-D105-46c5-BB2C-473A2F27B563"
  TemplateName="STS#1" />

Send your questions and comments for Ted to mmoffice@microsoft.com.

Ted Pattison is an author, trainer, and SharePoint MVP who lives in Tampa, Florida. Ted has just completed his book Inside Windows SharePoint Services 3.0 for Microsoft Press. He also delivers advanced SharePoint training to professional developers through his company, Ted Pattison Group (www.TedPattison.net).