Random Musings of Jeremy Jameson

  • Building SharePoint WSPs with Team Foundation Build

    As I noted in my previous post, I recently discovered that my approach for building Web Solution Packages (WSPs) in Microsoft Office SharePoint Server (MOSS) 2007 isn't compatible with Team Foundation Build.

    I'm actually a little embarrassed to say this, but when I created the original "DR.DADA" approach for MOSS 2007 development back on the Agilent Technologies project, we were using Visual SourceSafe -- not Team Foundation Server (TFS) -- and a "manual" build process.

    I'd used VSS and automated builds on other projects before (using NAnt), but never got around to automating our MOSS 2007 builds on the Agilent project because, honestly, there were just too many other higher priority items. Besides, each build only required a couple of minutes of actual human effort because most of the build was scripted.

    Still, an automated daily build (and deployment to DEV) is a really, really good thing to have.

    I've been fortunate to be on a few projects since then that have leveraged TFS.

    However, up until about a month ago, I hadn't used Team Foundation Build (outside of the Jameson Datacenter, of course) due to the fact that we are leveraging the extranet TFS instance hosted by Microsoft.

    Note that Microsoft IT makes it very easy for us to provision new TFS projects on either the extranet or one of several internal TFS instances. Configuring builds using Team Foundation Build on one of the intranet TFS instances is very easy (from what I hear), but I strongly prefer working off the extranet TFS instance because then I don't have to VPN into CorpNet in order to have access to source control.

    However, choosing the extranet TFS instance also means we can't configure builds using the out-of-the-box functionality in Team Foundation Server (at least not without setting up a build server on the extranet). Fortunately, I've found a way to schedule "manual" builds that look a lot like automated builds performed using Team Foundation Build.

    So, if you are building SharePoint WSPs -- regardless of whether you use the real Team Foundation Build or my "imitation Team Foundation Build" -- you need a way to build the WSPs without referring to referenced assemblies using relative paths.

    As I first mentioned in my previous post, relative paths work just fine when compiling from within Visual Studio or using MSBuild from the command line. However, they don't work at all when queuing the builds through Team Foundation Build.

    The problem with relative paths is that Team Foundation Build uses a different folder structure when compiling your projects. Specifically, it changes the output folder for all compiled items to be under a new Binaries folder -- not the location specified in the project settings within Visual Studio.

    In other words, if you refer to a referenced assembly using something like:

    ..\..\..\CoreServices\bin\%BUILD_CONFIGURATION%\Fabrikam.Demo.CoreServices.dll

    then you will find that this works just fine when building through Visual Studio -- or even when compiling using TFSBuild.proj from the command line (a.k.a. a "Desktop Build"). However, if you then queue the build through Team Foundation Server, you'll find your build fails because the referenced assembly was actually output to a different folder.

    If you dive into the log file for the build, you will find that Team Foundation Build modifies the OutDir variable and sets it to something like:

    C:\Users\svc-build\AppData\Local\Temp\Demo\Daily Build - Main\Binaries\Debug\

    So the trick to building WSPs with Team Foundation Build is to leverage the OutDir variable instead of relying on relative paths to referenced assemblies.

    Here is the updated DDF file based on my earlier sample:

    ;
    ; This ddf specifies the structure of the .wsp solution cab file.
    ;
    ; HACK: OPTION EXPLICIT cannot be used when specifying a variable with the /D option,
    ; otherwise MakeCAB aborts with an error similar to the following:
    ;
    ;        ERROR: Option Explicit and variable not defined: OUT_DIR
    ;
    ;.OPTION EXPLICIT    ; Generate errors for undefined variables
    
    .Set CabinetNameTemplate=Fabrikam.Demo.Publishing.wsp
    
    ; The following variable must be set when calling MakeCAB (using the /D option)
    ;.Define OUT_DIR=
    
    .Set DiskDirectoryTemplate=CDROM    ; All cabinets go in a single directory
    .Set CompressionType=MSZIP            ; All files are compressed in cabinet files
    .Set UniqueFiles=ON
    .Set Cabinet=ON
    .Set DiskDirectory1=%OUT_DIR%\Package
    
    DeploymentFiles\PackageFiles\manifest.xml
    
    .Set SourceDir=%OUT_DIR%            ; Copy assemblies from %OUT_DIR% folder
    
    Fabrikam.Demo.Publishing.dll
    Fabrikam.Demo.CoreServices.dll
    
    .Set SourceDir=                    ; Copy files relative to project folder
    
    .Set DestinationDir=Fabrikam.Demo.Publishing.DefaultSiteConfiguration
    DefaultSiteConfiguration\FeatureFiles\Feature.xml
    
    .Set DestinationDir=Fabrikam.Demo.Publishing.Layouts
    Layouts\FeatureFiles\Feature.xml
    Layouts\FeatureFiles\ProvisionedFiles.xml
    
    .Set DestinationDir=Fabrikam.Demo.Publishing.Layouts\MasterPages
    Layouts\MasterPages\FabrikamMinimal.master
    
    ;.Set DestinationDir=Fabrikam.Demo.Publishing.Layouts\PageLayouts
    ;Layouts\PageLayouts\MinimalPage.aspx
    
    .Set DestinationDir=Fabrikam.Demo.Publishing.Layouts\Images
    Layouts\Images\FabrikamLogo_32x32.png
    
    .Set DestinationDir=Fabrikam.Demo.Publishing.Layouts\Themes\Theme1
    Layouts\Themes\Theme1\BreadcrumbBullet.gif
    Layouts\Themes\Theme1\FauxColumn-Fixed-2Col.png
    Layouts\Themes\Theme1\FauxColumn-Fixed-3Col.png
    Layouts\Themes\Theme1\Fabrikam-Basic.css
    Layouts\Themes\Theme1\Fabrikam-Core.css
    Layouts\Themes\Theme1\Fabrikam-FixedLayout.css
    Layouts\Themes\Theme1\Fabrikam-IE.css
    Layouts\Themes\Theme1\Fabrikam-IE6.css
    Layouts\Themes\Theme1\Fabrikam-QuirksMode.css
    Layouts\Themes\Theme1\Tab-LeftSide.jpg
    Layouts\Themes\Theme1\Tab-RightSide.jpg
    
    .Set DestinationDir=ControlTemplates\Fabrikam\Demo\Publishing\Layouts
    Layouts\Web\UI\WebControls\GlobalNavigation.ascx
    Layouts\Web\UI\WebControls\StyleDeclarations.ascx
    
    .Set DestinationDir=Layouts\Fabrikam
    Layouts\MasterPages\FabrikamMinimal.master

    Note how I've replaced the BUILD_CONFIGURATION variable with the OUT_DIR variable. Not surprisingly, the OUT_DIR variable in the DDF is specified similar to how BUILD_CONFIGURATION was previously specified when calling makecab.exe. However, unlike the build configuration the OutDir variable will likely contain spaces as well as a trailing slash (which makecab.exe apparently doesn't like). Therefore we must quote the OutDir variable and append with "." if a trailing slash is found.

    Here is the corresponding update to the project file:

      <PropertyGroup>
        <BuildDependsOn>
          $(BuildDependsOn);
          CreateSharePointSolutionPackage
        </BuildDependsOn>
        <QuotedOutDir>"$(OutDir)"</QuotedOutDir>
        <QuotedOutDir Condition="HasTrailingSlash($(OutDir))">"$(OutDir)."</QuotedOutDir>
      </PropertyGroup>
      <Target Name="CreateSharePointSolutionPackage" Inputs="@(None);@(Content);$(OutDir)$(TargetFileName);" Outputs="$(ProjectDir)$(OutDir)Package\Fabrikam.Demo.Publishing.wsp">
        <Message Text="Creating SharePoint solution package..." />
        <Exec Command="makecab /D OUT_DIR=$(QuotedOutDir) /F &quot;$(ProjectDir)DeploymentFiles\PackageFiles\wsp_structure.ddf&quot;" />
      </Target>

    With these changes, the SharePoint WSP is successfully built regardless of whether it is compiled through Visual Studio, from the command line using MSBuild and the TFSBuild.proj file, or as an automated build using a Team Foundation Build server.

  • The "Copy Local" Bug in Visual Studio

    If you've ever worked with me on a Microsoft Office SharePoint Server (MOSS) 2007 project -- or if you've read my Sample Walkthrough of the DR.DADA Approach to SharePoint -- then you've probably seen the following comment:

    Note: Referenced assemblies must be specified with a path corresponding to the build configuration. If the path is not specified to the referenced assembly, then the build works fine as long as the referenced assembly is not in the GAC.

    However, when the referenced assembly is in the GAC (i.e. after a deployment) then MakeCAB will not be able to find the referenced assembly (since it is no longer copied to the current project's bin\Debug or bin\Release folder).

    It turns out that I was using a rather elaborate workaround for a problem that is actually much easier to solve.

    To workaround the "Copy Local" bug and force a referenced assembly to always be copied to the output folder (regardless of whether the referenced assembly is in the GAC):

    1. In the Solution Explorer window in Visual Studio, expand the References folder for the project and then select the referenced assembly.
    2. In the Properties window, change the value of Copy Local to False, and then change it back to True.

    Following these two simple steps explicitly adds <Private>True</Private> to the project file, as shown in the following example:

        <ProjectReference Include="..\CoreServices\CoreServices.csproj">
          <Project>{01C58D27-9818-45D6-A0B6-8EF765CA9397}</Project>
          <Name>CoreServices %28CoreServices\CoreServices%29</Name>
          <Private>True</Private>
        </ProjectReference>

    When you add a referenced assembly in Visual Studio using a project reference, Copy Local defaults to True, but Visual Studio doesn't explicitly state this in the MSBuild project file. Toggling the value of Copy Local forces this element to be added to the project file and consequently you no longer need any hacks to reference the assembly in its original output folder.

    At this point, you might be wondering why do I bring this up after all this time? After all, hasn't the hack I came up with for building SharePoint Web Solution Packages (WSPs) been working for several years? Well, yes, in most cases it works just fine.

    However, there's one fundamental problem that I only recently discovered back in early October: you can't use Team Foundation Build to build the WSP when specifying relative paths to assemblies in the DDF file.

    I'll cover this in my next post.

  • SketchPath - The XPath Tool

    I added another tool to my Toolbox yesterday: SketchPath.

    The SketchPath site labels it as "The XPath Tool" but I'd say it more like "The XPath Tool."

    I've seen a few other tools for quickly building and testing XPath expressions against an XML document, but they pale in comparison to SketchPath. There are a number of online tools that unquestionably require less effort to get started with, but you'll likely find them very limiting as well.

    I know I'm not the only one who thinks SketchPath rocks. Scott Hanselman included it in his 2009 Ultimate Developer and Power Users Tool List for Windows.

    If you do any significant amount of work with XML, I recommend you download SketchPath today.

  • SharePoint Web Part to Redirect from HTTP to HTTPS

    Yesterday, I detailed the steps I recommend for configuring SSL on sites built on Microsoft Office SharePoint Server (MOSS) 2007. I also mentioned that users won't automatically be redirected from HTTP to HTTPS, and how I've previously used a little bit of code to automatically perform this redirection.

    Here is a base class that contains the core logic for detecting when a redirect from HTTP to HTTPS is required and automatically redirecting as necessary:

    using System;
    using System.Diagnostics;
    using System.Globalization;
    using System.Text;
    using System.Web;
    using System.Web.UI.WebControls.WebParts;
    
    using Microsoft.SharePoint;
    using Microsoft.SharePoint.WebControls;
    
    using Fabrikam.Demo.CoreServices.Logging;
    using Fabrikam.Demo.Web.Properties;
    
    namespace Fabrikam.Demo.Web.UI.WebControls
    {
        /// <summary>
        /// Base class for Web Parts that require secure communication (i.e. HTTPS).
        /// </summary>
        public abstract class SslRequiredWebPart : WebPart
        {
            /// <summary>
            /// Redirect from HTTP to HTTPS (if required).
            /// </summary>
            /// <param name="e">An <see cref="System.EventArgs" /> object that
            /// contains the event data.</param>
            protected override void OnInit(
                EventArgs e)
            {
                base.OnInit(e);
    
                HttpContext context = HttpContext.Current;
    
                if (context == null)
                {
                    throw new InvalidOperationException(
                        "HttpContext.Current is null.");
                }
    
                HttpContextWrapper contextWrapper = new HttpContextWrapper(context);
    
                bool forceRedirect = IsSslRedirectRequired(contextWrapper);
                
                if (forceRedirect == false)
                {
                    return;
                }
    
                HttpRequest request = context.Request;
    
                Logger.LogDebug(
                    CultureInfo.InvariantCulture,
                    "SslRequiredWebPart - Redirecting from HTTP to HTTPS"
                        + " for URL ({0})...",
                    request.RawUrl);
    
                Debug.Assert(request.RawUrl.StartsWith(
                    "/",
                    StringComparison.OrdinalIgnoreCase) == true);
    
                StringBuilder urlBuilder = new StringBuilder();
    
                urlBuilder.Append(Uri.UriSchemeHttps);
                urlBuilder.Append(Uri.SchemeDelimiter);
                urlBuilder.Append(request.Url.Host);
                urlBuilder.Append(request.RawUrl);
    
                string redirectUrl = urlBuilder.ToString();
    
                Logger.LogDebug(
                    CultureInfo.InvariantCulture,
                    "SslRequiredWebPart - Redirecting to URL ({0})...",
                    redirectUrl);
    
                context.Response.Redirect(redirectUrl, false);
                context.ApplicationInstance.CompleteRequest();
            }
    
            /// <summary>
            /// Determines if an SSL redirect is required.
            /// </summary>
            /// <param name="context">Context information representing an individual
            /// HTTP request.</param>
            /// <returns><c>true</c> if a redirect from HTTP to HTTPS is required,
            /// otherwise <c>false</c>.</returns>
            protected virtual bool IsSslRedirectRequired(
                HttpContextBase context)
            {
                if (context == null)
                {
                    throw new ArgumentNullException("context");
                }
    
                Logger.LogDebug(
                    CultureInfo.InvariantCulture,
                    "SslRequiredWebPart - RedirectToHttpsWhenSslRequired = {0}",
                    Settings.Default.RedirectToHttpsWhenSslRequired);
    
                if (Settings.Default.RedirectToHttpsWhenSslRequired == false)
                {
                    return false;
                }
    
                HttpRequestBase request = context.Request;
    
                if (request.IsSecureConnection == true)
                {
                    Logger.LogDebug(
                       CultureInfo.InvariantCulture,
                       "SslRequiredWebPart - The connection is secure.",
                       request.RawUrl);
    
                    return false;
                }
    
                if (request.Url.Host.Contains(".") == false)
                {
                    Logger.LogDebug(
                        CultureInfo.InvariantCulture,
                        "SslRequiredWebPart - The hostname is not a fully-qualified"
                            + " domain name.");
    
                    return false;
                }
                else if (request.Url.Host.Contains("-local.") == true
                    || request.Url.Host.Contains("-dev.") == true)
                {
                    Logger.LogDebug(
                        CultureInfo.InvariantCulture,
                        "SslRequiredWebPart - LOCAL or DEV environment detected.");
    
                    return false;
                }
    
                if (SPContext.Current != null)
                {
                    SPControlMode formMode = SPContext.Current.FormContext.FormMode;
    
                    if (formMode == SPControlMode.Edit
                        || formMode == SPControlMode.New)
                    {
                        // Never redirect when editing a page                
                        Logger.LogDebug(
                            CultureInfo.InvariantCulture,
                            "SslRequiredWebPart - SSL redirect is not required"
                            + " because the page is being edited.");
    
                        return false;
                    }
                }
    
                return true;
            }
        }
    }

    Using this approach, users can browse the anonymous areas of your site using HTTP. However, as soon as they browse to a page that contains a Web Part that requires secure communication, they are automatically redirected from HTTP to HTTPS.

    Note how I use a custom application setting (RedirectToHttpsWhenSslRequired) to allow this feature to be turned off. The default value is True (meaning the feature is turned on). However, by changing the property setting to False in the Web.config file, the feature can be disabled as necessary for a particular environment or server (for troubleshooting purposes):

    <configuration>
      <configSections>
        <sectionGroup
          name="applicationSettings"
          type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
          <section name="Fabrikam.Demo.Web.Properties.Settings"
            type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
            requirePermission="false" />
        </sectionGroup>
      </configSections>
      <applicationSettings>
        <Fabrikam.Demo.Web.Properties.Settings>
          <setting name="RedirectToHttpsWhenSslRequired" serializeAs="String">
            <value>False</value>
          </setting>
        </Fabrikam.Demo.Web.Properties.Settings>
      </applicationSettings>
    </configuration>

    Also note that there are several scenarios in which a redirect is avoided:

    • When the connection is already secure (well, duh...we obviously don't want to cause an endless redirect loop)
    • When the request URL does not specify a fully qualified domain name, but rather an intranet URL (e.g. http://fabrikam). In this scenario, user are expected to be authenticated using Windows Authentication, which does not send credentials in clear text and therefore does not require SSL.
    • In LOCAL developer environments (e.g. http://www-local.fabrikam.com) and the Development Integration environment (DEV) -- e.g. http://www-dev.fabrikam.com -- since these environments don't typically have SSL certificates installed. This is an example of why a standard environment naming convention is important.
    • When the page where the Web Part resides is being edited (because we don't want to force a redirect immediately after someone adds the Web Part to a page). This scenario is not expected to occur, since content managers will typically use the intranet URL (e.g. http://fabrikam) for creating and editing pages. However, it is covered just in case the scenario is ever encountered.

    Assuming the IsSslRedirectRequired method returns true, the Web Part redirects to an HTTPS connection while preserving all of the query string parameters specified in the original request. Note that this simple approach only supports an HTTP GET on the original request. In other words, HTTP POST requests are "not supported" (however they are not expected to occur either). This is because any form parameters specified in the body of an HTTP POST request would be dropped in the redirect.

    Similar code can also be used in a page (e.g. /_layouts/Fabrikam/Login.aspx) if you prefer that approach instead of creating some kind of "Login" Web Part.

    Lastly, note that I allow the IsSslRedirectRequired method to be overridden in Web Parts that inherit from SslRequiredWebPart (e.g. LoginFormWebPart). I haven't yet found this to be necessary, but it's there just in case.

  • Configuring SSL on SharePoint Sites

    If you are using Basic Authentication or Forms-Based Authentication (FBA) with Microsoft Office SharePoint Server (MOSS) 2007 -- or any Web site, for that matter -- then you must configure secure communication (HTTPS) using SSL certificates.

    However, you probably didn't select to enable SSL when you originally created your Web application (after all, you most likely don't want to use Basic Auth or FBA exclusively, but rather only for users accessing the site externally). If you followed the process I recommend for customers deploying MOSS solutions, you created the Web application using an intranet URL (e.g. http://fabrikam) and subsequently extended the Web application to the Internet zone (e.g. http://www.fabrikam.com) or Extranet zone (e.g. http://extranet.fabrikam.com) -- utilizing the SharePoint alternate access mappings feature.

    Now that you need to enable secure communication, you might once again consider extending the Web application -- say to the Custom zone -- and select the option to use SSL (e.g. https://www.fabrikam.com). After all, this is the recommendation in the following TechNet article:

    Update a Web application URL and IIS bindings (Windows SharePoint Services). (Updated 2009-04-23).

    Specifically, here is the text that I am referring to:

    We do not recommend reusing the same IIS Web site for your HTTP and SSL hosting. Instead, extend a dedicated HTTP and a dedicated SSL Web site, each assigned to its own alternate access mapping zone and URLs.

    Wait a minute...let me see if I've got this right...you want me to extend my Web application -- thereby creating yet another copy of my Web.config files -- just to enable SSL? That doesn't seem like a good idea.

    After all, managing the multitude of Web.config files across multiple Web application and Web servers in a farm seems to be one of the most challenging aspects for the Release Management and Operations teams (at least in my experience during the past several years). [I can't tell you how many times I've compared Web.config files during the process of troubleshooting various issues and found discrepancies due to manual changes not getting applied consistently. Sure, the risk of this is drastically reduced if you leverage the SPWebConfigModification infrastructure, but there are still rare occasions when a little manual "tweaking" is necessary -- and these often lead to inconsistent Web.config files.]

    Therefore, one of my goals with any SharePoint solution is to minimize the number of Web.config files that are used. It seems reasonable to expect two different Web.config files to be used for a site that uses Windows Authentication internally and Forms Authentication externally, since this is configured using the <authentication> element.

    For example, in the Default zone you probably want to specify:

        <authentication mode="Windows" />

    Whereas in the Internet zone you instead need something like this:

        <authentication mode="Forms">
          <forms loginUrl="/Public/Pages/default.aspx" defaultUrl="/" />
        </authentication>

    Okay, so if we want to avoid creating a new Web application -- and the corresponding Web.config file(s) -- how can we support both HTTP and HTTPS on the same Web application? It turns out to be rather simple.

    For a starting point, let's assume you've just created your Web application using the default (i.e. intranet) URL -- say, http://fabrikam -- which uses Windows Authentication. This is the site that your content authors will use to manage the site.

    The first step is to create an alternate access mapping (AAM).

    To configure an alternate access mapping:

    1. On the SharePoint Central Administration home page, click the Application Management tab on the top link bar.
    2.  On the Application Management page, in the SharePoint Web Application Management section, click Create or extend Web application.
    3. On the Create or Extend Web Application page, in the Adding a SharePoint Web Application section, click Extend an existing Web application.
    4.  On the Extend Web Application to Another IIS Web Site page:
      1. In the Web Application section, select the Web application to extend (e.g. http://fabrikam).
      2. In the IIS Web Site section, in the Port and Host Header boxes, enter the corresponding values such as 80 and www.fabrikam.com, respectively.
      3. In the Security Configuration section, keep the default options (you can configure forms authentication, anonymous access, and SSL later).
      4. In the Load Balanced URL section, ensure the default value specified in the URL box is correct (e.g. http://www.fabrikam.com:80) and in Zone dropdown list, select Internet.
      5. Click OK.

    The next step is to install your SSL certificate on the site. Once you've procured your certificate and installed it through Internet Information Services (IIS) Manager, you then need to add a public URL in SharePoint for HTTPS and add an HTTPS binding in IIS.

    To add a public URL to HTTPS:

    1.  On the SharePoint Central Administration home page, click the Operations tab on the top link bar.
    2.  On the Operations page, in the Global Configuration section, click Alternate access mappings.
    3.  On the Alternate Access Mappings page, click Edit Public URLs.
    4.  On the Edit Public Zone URLs page:
      1. In the Alternate Access Mapping Collection section, select the Web application (e.g. http://fabrikam).
      2. In the Public URLs section, copy the URL from the Internet box to the Custom box, and change http:// to https://.
      3. Click Save.

    To add an HTTPS binding to the site in IIS:

    1. Click Start, point to Administrative Tools, and then click Internet Information Services (IIS) Manager.
    2. In Internet Information Services (IIS) Manager, click the plus sign (+) next to the server name that contains the Web application, and then click the plus sign next to Sites to view the Web applications that have been created.
    3. Click the name of the Web application corresponding to the Internet zone (e.g. SharePoint – www.fabrikam.com80). In the Actions section, under the Edit Site heading, click Bindings….
    4. In the Site Bindings window, click Add.
    5. In the Add Site Binding window:
      1. In the Type: dropdown, select https.
      2. In the SSL Certificate: dropdown, select the certificate corresponding to the site (e.g. www.fabrikam.com).
      3. Click OK.
      4. In the Site Bindings window, click Close.

    At this point, your SharePoint site supports Windows Authentication both internally (via http://fabrikam) and externally (via http://www.fabrikam.com and https://www.fabrikam.com). The final steps are to enable anonymous access and configure Forms Authentication.

    Note that after configuring Forms Authentication, users will not automatically be redirected from HTTP to HTTPS for the login page. To achieve this user experience, I've used a couple of techniques in the past that are based on the same fundmental concept. The first technique utilized code on the login page to automatically detect HTTP connections and redirect to HTTPS. The second technique was essentially the same logic, but rather than utilizing code embedded in the login page, it is encapsulated in a Login Form Web Part. I'll discuss this redirect code in a follow-up post.

  • A Simple Backup Solution

    As I've mentioned before, I don't spend much money or time maintaining the "Jameson Datacenter" (a.k.a. my home lab). However, that doesn't mean that I treat my infrastructure lightly.

    In previous posts, I've covered many of the Group Policy objects that I use to minimize the maintenance effort associated with running more than a dozen servers (mostly virtual). In this post, I'll provide the details on how I backup these servers.

    I should preface this by saying this is not meant to be an "enterprise-level" backup solution. Rather it is simply meant to provide a cheap (actually free) and easy solution to the problem of ensuring you can recover from data loss. Note that data loss rarely occurs through some sort of hardware failure or Act of God (as the insurance folks like to put it). Rather the majority of the time someone accidentally overwrites or deletes a file -- or, gasp, a complete folder hierarchy -- and you subsequently need to restore the data from a backup.

    For as long as I can remember, Windows Server has included the NTBackup utility. I'm guessing from the name that this has been around since the days of Windows NT 3.1, but honestly I don't believe I even started running Windows NT until version 3.5. Or was it 3.51? I can't remember. Anyway, I certainly haven't been running NTBackup since then.

    Here is the simple batch file that I use to perform scheduled backups:

    @echo off
    
    setlocal
    
    set BACKUP_TYPE=normal
    
    if ("%1") NEQ ("") set BACKUP_TYPE=%1
    
    for /f "tokens=2-4 delims=/ " %%i in ('date /t') do set currentDate=%%k-%%i-%%j
    for /f "tokens=1-2" %%i in ('time /t') do set currentTime=%%i %%j
    set BACKUP_TIMESTAMP=%currentDate%-%currentTime:~0,2%-%currentTime:~3,2%-%currentTime:~6,2%
    
    set BACKUP_FILE=D:\NotBackedUp\Backups\Backup-%BACKUP_TYPE%-%BACKUP_TIMESTAMP%.bkf
    
    :: ----------------------------------------------------------------------------
    call :LogMessage "Starting backup..."
    call :LogMessage "BACKUP_TYPE: %BACKUP_TYPE%"
    call :LogMessage "BACKUP_FILE: %BACKUP_FILE%"
    
    C:\WINDOWS\system32\ntbackup.exe backup C:\BackedUp /n "Backup created %BACKUP_TIMESTAMP%" /m %BACKUP_TYPE% /j "Backup (%BACKUP_TYPE%)" /f "%BACKUP_FILE%"
    if %ERRORLEVEL% neq 0 goto Errors
    
    call :LogMessage "Successfully completed backup."
    
    goto :eof
    
    :: ----------------------------------------------------------------------------
    ::
    :LogMessage
    
    REM Strip leading and trailing quotes and then display message with timestamp
    set MESSAGE=%1
    set MESSAGE=%MESSAGE:~1,-1%
    
    for /f "tokens=2-4 delims=/ " %%i in ('date /t') do set currentDate=%%k-%%i-%%j
    for /f "tokens=1-2" %%i in ('time /t') do set currentTime=%%i %%j
    echo %currentDate% %currentTime% - %MESSAGE%
    
    goto :eof
    
    :: ----------------------------------------------------------------------------
    ::
    :Errors
    
    echo Warning! One or more errors detected.

    If you've seen any of my scripts before, then you'll quickly notice the typical LogMessage "function" that I use to write messages prefixed with a timestamp. For example here's the output from the log for this morning's backup:

    2009-11-09 12:30 AM - Starting backup...
    2009-11-09 12:30 AM - BACKUP_TYPE: differential
    2009-11-09 12:30 AM - BACKUP_FILE: D:\NotBackedUp\Backups\Backup-differential-2009-11-09-12-30-AM.bkf
    2009-11-09 12:31 AM - Successfully completed backup.

    I use similar token parsing of the output from the date and time system commands to generate the name of the backup file (e.g. Backup-differential-2009-11-09-12-30-AM.bkf).

    Also note that the type of backup (e.g. normal or differential) can be specified as a parameter when running the batch file. This is really powerful for scheduling different types of backups on various schedules.

    Here are the scheduled backups on one of my servers (BEAST):

    Scheduled Backups on BEAST
    Name Schedule
    Daily Backup At 12:00 PM every day
    Differential Backup At 12:30 AM every day
    Full Backup At 1:00 AM every Sun of every week

    The Daily Backup task is configured as follows:

    • Run: C:\BackedUp\Backup.cmd daily >> Backup.log
    • Start in: C:\BackedUp
    • Run as: TECHTOOLBOX\svc-backup

    Note that I specifically chose the middle of the day to perform daily backups so that I could potentially recover a file that was created in the morning but mistakenly deleted in the afternoon. I suppose I could schedule incremental backups throughout the day, but honestly, I haven't seen the need given my situation.

    Also note that the service account that I use for backups (TECHTOOLBOX\svc-backup) is only a member of the Backup Operators group. It is not a member of the Administrators group.

    Consequently there's a known issue with running batch files using scheduled tasks due to out-of-the-box security restrictions on cmd.exe:

    "Access is denied" error message when you run a batch job on a Windows Server 2003-based computer

    Lastly, note that I am doing a simple disk-to-disk backup on my servers, so if there's a fire in the Jameson Datacenter (i.e. my basement) and I lose these servers completely then I'm going to be "hurtin' for certain." However should there ever be a fire in my basement (Heaven forbid), I'm going to be worried about a lot more than just restoring my data from backup. Note that I keep copies of the really important stuff (e.g. digital photos and home videos of my family) on DVDs at my parents' house.

    I've read that there's a new backup tool in Windows Server 2008, so I suppose one of these days I'll need to get around to upgrading my backup solution ;-)

  • AutoEventWireup Issue in MOSS 2007

    I recently promised to finish this blog post that has been sitting in "unpublished" status since June 2008, so here it is...

    Have you ever encountered the following error in Microsoft Office SharePoint Server (MOSS) 2007?

    An error occurred during the processing of . The attribute 'autoeventwireup' is not allowed in this page.

    I just searched for this using Bing and it seems like I'm not the only one who has ever experienced this issue. However, glancing through a few of the top search results, I didn't see any solutions to the error.

    The problem occurs when you have a custom master page which includes code and that master page subsequently becomes unghosted. I believe this happens with custom page layouts that are customized as well.

    I have to admit that I was completely stumped when I first encountered this error a few years ago while working on the Agilent Technologies project. I eventually tracked down the root cause to be unghosted pages, but we were not using SharePoint Designer to create or customize our master pages, so I couldn't understand why we would occasionally encounter this error.

    My speculation is that when the feature/solution containing the custom master page is deactivated, retracted, and deleted (as part of the "DR.DADA" process), SharePoint has some "smarts" within it that essentially equates to:

    • Hey, this master page (or page layout) is currently in use so removing it could really break the site.
    • Therefore, I'd better make a copy of it and store it in the database (i.e. unghost it).

    Unfortunately, when we subsequently added, deployed, and activated the solution/feature, SharePoint would still attempt to use the unghosted master page and summarily generate the error stating that "the attribute 'autoeventwireup' is not allowed in this page."

    Note that this is pure speculation on my part as to what was causing the master page to become unghosted.

    However, what I do know for sure is that once I reghosted the master page, the AutoEventWireup error would magically disappear.

    Here are the steps to reghost a master page or page layout:

    1. Browse to Site Settings page for your site. Note that if your master page is causing the AutoEventWireup error, you can explicitly specify the URL (e.g. http://fabrikam/_layouts/settings.aspx).
    2. On the Site Settings page, under the Look and Feel section, click Reset to site definition.
    3. On the Reset Page to Site Definition Version page:
      1. In the Reset to Site Definition section, ensure the option to he Local URL of the page box,
      2. Click Reset.
      3. In the confirmation dialog that appears stating that you will lose all customizations, including web part zones, custom controls, and in-line text, click OK.

    Note that in ASP.NET, the default value for the AutoEventWireup attribute is true. Therefore you might assume that you could simply remove the attribute from your custom master page in order to avoid the error when the master page is unghosted. After all, the error clearly states that the AutoEventWireup attribute is not allowed in this page, right?

    In other words, the solution to the problem would seem to be simply be a matter of changing something like this...

    <%@ Master Language="C#" AutoEventWireup="true" Codebehind="FabrikamMinimal.master.cs"
        Inherits="Fabrikam.Demo.Publishing.Layouts.MasterPages.FabrikamMinimal" %>

    ...to this:

    <%@ Master Language="C#" Codebehind="FabrikamMinimal.master.cs"
        Inherits="Fabrikam.Demo.Publishing.Layouts.MasterPages.FabrikamMinimal" %>

    Unfortunately -- at least in my experience -- this doesn't work. It only leads to other errors, such as:

    The event handler 'OnPreRender' is not allowed in this page.

    The above error occurs when the master page contains something like the following:

                <asp:SiteMapPath ID="BreadcrumbSiteMapPath" Runat="server"
                    SiteMapProvider="CurrentNavSiteMapProviderNoEncode"
                    RenderCurrentNodeAsLink="true"
                    SkipLinkText=""
                    OnPreRender="BreadcrumbSiteMapPath_OnPreRender">

    I attempted to resolve this by converting the BreadcrumbSiteMapPath_OnPreRender event handler to a method and invoking the method from the Page_PreRender event handler instead. However, that only led to yet another error:

    Code blocks are not allowed in this file.

    Sensing a very deep "rat hole" at this point, I decided it wasn't worth pursuing this issue any further.

    Fortunately, as I've stated before, I don't believe master pages and page layouts deployed through solutions and features should subsequently be customized through SharePoint Designer. In my opinion, these items should be tightly managed through your SCM (software configuration management) process -- in other words, versioned in your source control system and subsequently deployed through a formal change process.

    Of course, if your custom master pages and page layouts are very simple (i.e. no code-behind) then you probably will never encounter this problem.

  • Compiling C++ Projects with Team Foundation Build

    As I mentioned in my previous post, this week I incorporated Password Minder into my "Toolbox" Visual Studio solution that is scheduled to build daily through Team Foundation Server (TFS).

    It's not that I really need daily builds of Password Minder; rather it's just been something on my "TO DO" list for a long time and I finally got around to doing it. Unfortunately, it wasn't without issue.

    In case you are not familiar with Password Minder, it is mostly written in C#, but it includes one C++ project (for the NativeHelpers.dll).

    Thus when I added the Password Minder projects to my "Toolbox" solution, I woke up the next morning to a notification from Team Foundation Server that my build failed. [No, I don't have some sort of alarm that goes off when one of my builds break. By "notification" I am referring to the e-mail message that I found in my inbox when I sat down at the computer.]

    Clicking the build log referenced in the e-mail message, I quickly discovered the following:

    Using "VCBuild" task from assembly "Microsoft.Build.Tasks.v3.5, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a". Task "VCBuild"
    Locating vcbuild.exe: not found at "c:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE\..\..\vc\vcpackages\vcbuild.exe".
    Locating vcbuild.exe: Visual C++ Express is not installed on this computer.
    Locating vcbuild.exe: falling back to the system PATH variable.
    C:\Users\svc-build\AppData\Local\Temp\Toolbox\Automated Build - Main\Sources\Source\Toolbox.sln : error MSB3411: Could not load the Visual C++ component "VCBuild.exe". If the component is not installed, either 1) install the Microsoft Windows SDK for Windows Server 2008 and .NET Framework 3.5, or 2) install Microsoft Visual Studio 2008.

    That certainly is one of the best error messages I've seen in a long time. It told me exactly what I needed to do to fix the problem. Well, almost...

    Note that DAZZLER (a VM in the "Jameson Datacenter" that is my dedicated build server) does not have the full installation of Visual Studio 2008. Rather it only has the Team Foundation Build install. In keeping with best practices, I try to keep the build server as "clean" as possible. That means no Visual Studio, no SharePoint, etc.

    Following option #1 from the log file, I proceeded to install the Microsoft Windows SDK for Windows Server 2008 and .NET Framework 3.5 -- but, of course, I didn't bother to review the ReadMe file that comes with it (at least not at first).

    Consequently, I was greeted with the following error on the next build attempt:

    c:\Windows\Microsoft.NET\Framework\v3.5\Microsoft.Common.targets : warning MSB3428: Could not load the Visual C++ component "VCProjectEngine.dll". To fix this, 1) install the Microsoft Windows SDK for Windows Server 2008 and .NET Framework 3.5, 2) install Microsoft Visual Studio 2008 or 3) add the location of the component to the system path if it is installed elsewhere. System error code: 126.
    c:\Windows\Microsoft.NET\Framework\v3.5\Microsoft.Common.targets : warning MSB3425: Could not resolve VC project reference "..\NativeHelpers\NativeHelpers.vcproj".

    That's when I discovered the following from the release notes for the SDK:

    5.1.1 VCBuild fails to compile or upgrade projects

    In order for VCBuild to run properly, vcprojectengine.dll needs to be registered. If vcprojectengine.dll is not registered, VCBuild.exe will fail with errors such as:

    On compile: warning MSB3422: Failed to retrieve VC project information through the VC project engine object model. System error code: 127.

    On upgrade: Failed to upgrade project file ‘foo.vcproj'. Please make sure the file exists and is not write-protected.

    To workaround this issue, vcprojectengine.dll must be manually registered. From a Windows SDK command line window (as administrator in Vista:

    On an X86 machine, run:

    cd %mssdk%\VC\bin
    regsvr32 vcprojectengine.dll

    On an X64 machine, run:

    cd %mssdk%\VC\bin\X64
    regsvr32 vcprojectengine.dll

    Unfortunately, these instructions aren't quite right -- or at least they didn't work verbatim in my environment. The workaround stated above makes you think there's an environment variable (%mssdk%) that refers to the path where the SDK is installed. However, this wasn't configured on DAZZLER.

    Also, I didn't find my copy of VCProjectEngine.dll in an x64 folder, but rather in an amd64 folder:

    C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\amd64\VCProjectEngine.dll

    I know most of you out there are not doing much (if any) C++ work anymore, but I thought I should share this just in case you need it at some point.

  • Using Password Minder to Manage Your Passwords

    I started using Password Minder almost immediately after reading about it in the July 2004 edition of MSDN Magazine. I don't know about you, but trying to remember all of my various passwords for dozens of Web sites, numerous network accounts, VMs, etc. is really quite a nightmare. [I'm actually a little embarrassed to say that on more than one occasion I've had to hack one of my VMs in order to reset the password for a domain admin. This process definitely isn't for the faint of heart, but it was better than having to rebuild my FABRIKAM domain from scratch.]

    Instead I tend to use no more than three or four passwords that I can remember (and change every couple of months) and a whole bunch of randomly generated passwords that I rely on Password Minder to store securely.

    Note that all of the service accounts used in the "Jameson Datacenter" (a.k.a. my home lab) use random 20 character passwords.

    Hence, why I added Password Minder to my Toolbox years ago. [Thanks to Keith Brown for sharing this wonderful utility, as well as take the time to write about the internals of how it works.]

    However, there were a few problems I had with the original Password Minder.

    First, it didn't like the fact that my Documents folder is redirected to a server. In other words, my encrypted password file is:

    \\BEAST\Users$\jjameson\Documents\My Passwords.xml

    Consequently, I encountered the following error when trying to save my password file:

    Failed to save the password file. Here are the gory details:

    GetVolumeInformation failed:
    The filename, directory name, or volume label syntax is incorrect.

    I discovered that I could avoid this error by clearing the Let Password Minder control the DACL checkbox in the Password Minder Options dialog window.

    The next problem that I encountered was Password Minder throwing up on me (in other words, generating an unhandled IndexOutOfRangeException) when one of my passwords somehow ended up being an empty string (to this day I'm still not sure how that empty password managed to get saved to the file).

    Thus I made a small change to line 381 of CryptMaster.cs. The original code was:

                if (null == record.EncryptedUserId) {

    I changed this to:

                if (string.IsNullOrEmpty(record.EncryptedUserId) == true) {

    The third problem (which I didn't encounter until a few years later) was that Password Minder wouldn't run on my Windows Vista x64 desktop.

    Note that the original project settings specified Platform target: Any CPU and thus pwm.exe would generate a BadImageFormatException when trying to load the 32-bit NativeHelpers.dll.

    Could not load file or assembly 'NativeHelpers, Version=1.5.0.4, Culture=neutral, PublicKeyToken=null' or one of its dependencies. An attempt was made to load a program with an incorrect format.

    This was easy enough to fix simply by setting Platform target to x86 and then recompiling.

    So, why do I blog about Password Minder after all this time? Well, just this week I finally got around to incorporating it into my "Toolbox" Visual Studio solution that is scheduled to build daily using Team Foundation Server, and I discovered that my Team Foundation Build server wasn't setup to build C++ projects, which is what I will blog about next.

    Stay tuned...I need to eat some breakfast first ;-)

  • Using Robocopy to Move Files and Folders

    I use Robocopy a lot, and it's been in my Toolbox for so long that I hardly remember using anything else. I was glad to see that it is now included out-of-the-box (starting with Vista), because I typically use it instead of Windows Explorer to move or copy files, since I prefer the way it reports the progress of the lengthy file operations.

    Note that when you specify the "/S /MOV" or "/E /MOV" options (to recursively move files from one location to another), it may leave behind a bunch of empty folders depending on the contents of the source and what you specify to move.

    Over a year ago I went through the rather painful effort of organizing my thousands of digital photos. During the process of moving thousands of files around to various folders, I ended up with lots of empty folders in my source folder structure.

    As I discovered earlier this week, you can specify the /MOVE option (instead of /MOV) to move both the files and directories, and delete them from the source after they are copied.

    Perhaps now my blog post from earlier this week about deleting empty folders makes a little more sense ;-)

    Sometimes I'm amazed at what I learn while blogging. I had just been in such the habit of using the /MOV option that I never gave it any thought (that is, until this week).

  • Supportability Concerns with Custom HTTP Modules in SharePoint

    I am sure that I'm not the first one to tell you this, but you can't believe everything you read on the Internet these days ;-)

    Case in point...I've seen a number of sources claim that custom HTTP modules are not supported in Microsoft Office SharePoint Server (MOSS) 2007 and Windows SharePoint Services (WSS) v3.

    These statements make it sound like if you add your own custom processing to the pipeline of each page request, you will instantly teleport your Production SharePoint farm into that vast wasteland we generally refer to as "Unsupported" -- where your support calls to Microsoft will be summarily terminated shortly after getting your Service Request (SRX) number.

    Not so -- at least in this particular case.

    Thank goodness, at least for my sake, because I've worked on a number of MCS (Microsoft Consulting Services) projects where we've delivered custom HTTP modules. It's always a little scary that something we deliver as part of an MCS engagement will later be deemed "unsupported."

    In general, we are very good about knowing what is -- and is not -- supported, but there's always a chance that something "creative" we do in order to satisfy the business requirements for our customers will later be considered a problem by Microsoft Customer Service and Support (CSS).

    There are, in fact, lots of things you can do with SharePoint that will thrust you into the dreaded "unsupported" category, but a custom HTTP module isn't one of them.

    How can I say this with such conviction? Well, yesterday I came across the following on MSDN:

    Support Details

    A HTTP module assembly can be installed in the Web application’s \bin directory or in the global assembly cache.

    Because an HTTP module is always called as part of the page processing pipeline, a poorly designed or faulty module can have a detrimental effect on performance or perceived stability of the environment. Thoroughly test each module for performance before deploying it.

    If you want to see it for yourself, you can go straight to the source:

    Obviously you want to be very cautious about using custom HTTP modules, but there are definitely scenarios where this is a good approach.

  • Microsoft Translator Widget

    This week I stumbled upon the new (and very cool) Microsoft Translator Widget.

    Actually, this isn't really new (apparently it has been available for over six months now) but it was certainly new to me.

    A colleague of mine on a previous project pointed out the Microsoft Translator site back in March, but somehow I was oblivious to the Translator Widget until just this week. Wow...so many things to keep abreast of these days.

    I've added the Translator Widget to my MSDN blog this morning. Interesting tidbit - it required more time to tweak the Community Server CSS rules in order to make room for the 200px minimum width required by the Translator Widget than it did to actually integrate the feature onto my blog.

    All you have to do to integrate this feature on your site is:

    1. Enter the URL of your site
    2. Read and agree to the terms of use
    3. Click the Generate Code button
    4. Copy/paste a small amount of HTML into your site

    You can read more about the Microsoft Translator Widget on the Translation team's blog if you want more details.

  • Deleting Empty Folders

    For the sake of this post, let's assume that you have a directory that contains some empty folders you want to get rid of. How the empty folders got there isn't important; all that matters is that you have some and you want to get rid of them.

    A few years ago, I created the following script (starting from a sample I found in the Script Center on TechNet) to recursively enumerate a folder structure, identify any empty folders, and subsequently delete them.

    Option Explicit
    
    If (WScript.Arguments.Count <> 1) Then
        WScript.Echo("Usage: cscript DeleteEmptyFolders.vbs {path}")    
        WScript.Quit(1)
    End If
    
    Dim strPath
    strPath = WScript.Arguments(0)
    
    Dim fso
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    Dim objFolder
    Set objFolder = fso.GetFolder(strPath)
    
    DeleteEmptyFolders objFolder
    
    Sub DeleteEmptyFolders(folder)
        Dim subfolder
        For Each subfolder in folder.SubFolders
            DeleteEmptyFolders subfolder
        Next
        
        If folder.SubFolders.Count = 0 And folder.Files.Count = 0 Then
            WScript.Echo folder.Path & " is empty"
            fso.DeleteFolder folder.Path
        End If    
    End Sub

    As you can see, there's really nothing complex here. Nevertheless I still find it to be a very useful script from time to time, so I thought I should share it. I used it this morning and it occurred to me that I should throw it up on the blog.

    Just be sure to run it with CScript -- and not the default WScript -- or you'll find the WScript.Echo messages rather frustrating.

  • Analyzing My MSDN Blog

    According to my blog dashboard, this will be post #150 for my MSDN blog. So this morning I thought I would do something a little different by providing some analysis on my blog.

    I extracted the data from Community Server into an Excel spreadsheet so I could easily create some pivot tables and charts. [I don't believe that the MSDN blogs platform is running Harvest -- or if it is, I certainly don't have access to it.]

    Here's a quick summary of my initial analysis:

    Table 1: MSDN Blog Usage Analysis
    Metric Views AggViews Comments Combined Views
    Total 205,142 139,377 209 344,519
    Maximum 12,240 2,855 13 14,445
    Minimum 196 306 0 678
    Average 1,359 923 1.4 2,282

    Note that Combined Views is something I defined and is simply the sum of Views and AggViews.

    The following post provides a good explanation of the difference between "Views" and "AggViews" in Community Server.

    Eriksson, J-O (2006). Views statistics of your blog. 2006-07-24.

    Here's the gist of it:

    Generally speaking, "Views" is the number of times somone viewed a post on the web via a browser, and "AggViews" is the number of times someone viewed the post via the RSS and Atom feeds.

    More specifically, the web view count is only updated in the EntryView control. This is displayed when you are viewing a single post. If you are viewing a list of posts, such as on the blog home page, the view counts of the posts are not updated.

    Based on the second paragraph, the numbers shown above are not entirely accurate (specifically, they are less than the actual values) but my gut tells me they are reasonably close. In other words, while some people may simply browse to http://blogs.msdn.com/jjameson and start reading without ever clicking through on an individual post, I don't think this is a large number.

    After all, how many times do you find yourself truly "browsing" a site compared with how many times you start from a search in order to quickly locate content of interest? Then, of course, there's the case where someone starts reading a post but then decides something like "hmmm...interesting...but not really what I was looking for...time to move on..."

    Note that I had to do a little tweaking for the minimum values computed in my spreadsheet (in order to avoid showing all zeroes in the row) because I have a couple of unpublished posts that obviously haven't been viewed by anyone except me. I'll talk more about those in a moment.

    I have to say that I was a little surprised by the total number of combined views. While I'm sure 345,000 pales in comparison to the likes of Scott Hanselman, Scott Guthrie, and Joel Spolsky, it's still a lot more than I expected for my humble blog. Seeing the average number of RSS views for each post exceed the 900 mark also makes me feel a little warm and fuzzy. So, for the roughly 2,800 of you that have subscribed to my blog at one point or another, I want to shout out a big "thank you!"

    It's nice to know that the effort I put into writing blog posts are considered helpful for a number of people. Anyway, moving on...

    Here's a chart showing the combined views for each blog post that I've created so far.

    Figure 1: Combined views for each blog post

    I didn't show the labels (i.e. blog post titles) on the X-axis for obvious reasons. I find it interesting how some blog posts are really "hot" in comparison with the average. Nothing mysterious here, this is obviously due to Internet search engines such as Bing and Google. More on that in a moment.

    Looking at the chart above, one of the first things I was interested in identifying are my most popular posts, as well as my least popular posts.

    Let's start with the top 10 posts:

    Table 2: Top 10 Blog Posts
    Rank Post Combined Views
    1 Issues Deploying SharePoint Solution Packages 14,445
    2 The Case of the Disappearing Hosts File 13,029
    3 Dumping MOSS 2007 Variations - Part 1 9,417
    4 "Error Creating Control" when using Microsoft Office SharePoint Designer 2007 7,525
    5 Virtual Server Issues and Recommendations for MOSS Virtual Environments 6,908
    6 Dumping MOSS 2007 Variations - Part 2 6,813
    7 Creating a Site Template in MOSS 2007 that Works in WSS v3 5,753
    8 Scope Dependencies for SharePoint Features 5,607
    9 MOSS Development Environment and a Windows Update Bug 5,307
    10 Installing Visual Studio 2005 Service Pack 1 5,199

    Hmmm...nine of the top 10 posts are related to SharePoint. No big surprises there ;-)

    What about the bottom 10 posts?

    Table 3: Bottom 10 Blog Posts
    Rank Post Combined Views
    151 Adventures in Upgrading TFS 0
    150 AutoEventWireup Issue in MOSS 2007 0
    149 ArgumentNullException with Optional PublishingPage.Description Property
    (with some thoughts on breaking the build, too)
    678
    148 Eliminate MBSA Warnings Using Default Security Settings Policy 679
    147 Constraining Tables with CSS 728
    146 DataNavigateUrlFormatString Does Not Allow "javascript:" 773
    145 Add Rooler to Your Web Development Toolbox 826
    144 New MSDN Theme on My Blog 841
    143 KB 896861 and "Microsoft Fix it" 849
    142 Latest Version of Opera Ignores Hosts File 850

    I suppose that I really should have thrown out the first two items because these posts are not published.

    The Adventures in Upgrading TFS post was something I started back in March 2008 when I ran into a couple of snags upgrading the instance of Team Foundation Server 2005 in the "Jameson Datacenter" to TFS 2008. I thought I would eventually get around to finishing that post, but apparently not. Perhaps some other morning...however, I'm not sure it would help very many people after all this time. I certainly hope most organizations running TFS have upgraded by now.

    The AutoEventWireUp Issue in MOSS 2007 post is something I really should finish. I started it back in June 2008 but somehow it fell off my plate. Stay tuned...I'll try to get to that this week. No, I won't "try" -- I will get to that this week. I promise.

    In the meantime, back to this post.

    It's also not suprising to see that many of the items in the bottom 10 are recent posts. As such, one would expect them to have fewer hits than others.

    Since it's just so incredibly easy to do in Excel, I decided to see how my authoring of blog posts has varied over time.

    Let's start with the total number of posts by year:

    Figure 2: Blog posts created by year

    Hmmm...apparently 2008 wasn't a very good year for me from a blogging perspective. Fortunately, the number for 2009 is much better -- and we still have almost two months to go!

    Let's look at the total number of posts by month:

    Figure 3: Blog posts created by month

    Well, that certainly is, um...erratic. Oh, and where is December?!

    Ouch...it looks like for the last two years I have completely neglected my blog in that month.

    Will this year be any different? Hmmm...wait and see ;-)

    I can tell you that I'll be taking quite a bit of vacation again this year in December and my wife won't appreciate it very much if I blog when I should be working on refinishing our master bathroom. However, I'll try not to skip that month entirely once again.

    I read one of Scott Hanselman's posts a while ago that talked about calculating your PPM (Posts Per Month) -- or was it BPM? I can't remember. Anyway, it seems like I could definitely be a little more consistent throughout the year. Trying to maintain a steady rhythm when it comes to blogging is always a challenge when you consider how much else there is to do on any given day.

    The last area that I want to cover in this post is with regards to Internet search results -- which I briefly mentioned earlier. In other words, while it is interesting that blog posts like Issues Deploying SharePoint Solution Packages and The Case of the Disappearing Hosts File are at the top of the list, what I find more interesting is why they appear at the top of the list.

    Let's start with the most popular post. Drilling down on the 12,246 views in the Community Server dashboard (which does not include "AggViews") to see the list of referrals, I encountered the largest HTML table I've ever seen on a page (over 5,500 rows). After a couple of attempts, I managed to copy all of the data into my Excel workbook and then sort by Hits descending.

    Table 4: Top referrals by URL for post - Issues Deploying SharePoint Solution Packages
    URL Hits Last Date
    http://decatec.it/blogs/2007/06/18/sharepoint+deployment+tecniques.aspx 83 Oct 12 2009, 01:58 AM
    http://google.com/search?q=this+solution+contains+resources+scoped+for+a+web+application+and+must+be+deployed+to+one+or+more+web+applications.&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-us:official&client=firefox-a 76 Oct 12 2009, 10:02 AM
    http://stevepietrek.com/2007/06/17/links-6172007/ 69 Oct 28 2009, 02:41 AM
    http://google.com/search?q=this+solution+contains+resources+scoped+for+a+web+application+and+must+be+deployed+to+one+or+more+web+applications&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-us:official&client=firefox-a 45 May 04 2009, 03:04 PM
    http://google.com/search?q=this+solution+contains+resources+scoped+for+a+web+application+and+must+be+deployed+to+one+or+more+web+applications.&rls=com.microsoft:en-us&ie=utf-8&oe=utf-8&startindex=&startpage=1 44 Oct 28 2009, 02:03 PM
    http://google.com/search?q=this+solution+contains+resources+scoped+for+a+web+application+and+must+be+deploy&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-us:official&client=firefox-a 42 Oct 30 2009, 03:18 PM
    http://google.com/search?hl=en&q=this+solution+contains+resources+scoped+for+a+web+application+and+must+be+deployed+to+one+or+more+web+applications 37 Feb 04 2009, 04:07 PM
    http://google.co.uk/search?hl=en&q=this+solution+contains+resources+scoped+for+a+web+application+and+must+be+deployed+to+one+or+more+web+applications.&meta= 35 Oct 27 2009, 08:10 AM
    http://google.com/search?hl=en&q=this+solution+contains+no+resources+scoped+for+a+web+application+and+cannot+be+deployed+to+a+particular+web+application 34 Oct 27 2009, 11:56 PM
    http://google.com/search?q=this+solution+contains+no+resources+scoped+for+a+web+application+and+cannot+be+deployed+to+a+particular+web+application&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-us:official&client=firefox-a 33 Nov 01 2009, 06:57 PM
    http://social.technet.microsoft.com/forums/en-us/sharepointadmin/thread/0881326b-7cb6-4198-9bac-7df6eaed9dde 32 Oct 17 2009, 03:41 PM

    However, adding up the numbers for the top 10 referrals accounts for less than 5% of the 12,246 views for this post. Upon further inspection, I found that only 8,400 of the 12,200 views actually have referral data.

    Breaking down the referrals by domain name, I found the following:

    Table 5: Top referrals by domain for post - Issues Deploying SharePoint Solution Packages
    Domain Hits
    google.com 3,893
    google.co.uk 628
    google.co.in 519
    google.com.au 310
    google.ca 288
    google.nl 211
    google.de 198
    search.live.com 129
    google.fr 125
    decatec.it 99

    Note that the post was created back in June 2007, which helps explain why you see search.live.com instead of bing.com in the top 10 list.

    To help me get a better feel for the Google vs. Microsoft ratio, I grouped the results into categories and subcategories.

    Figure 4: Referrals by category for post - Issues Deploying SharePoint Solution Packages

    It turns out that the subcategories were really only interesting for the purposes of identifying the top-level categories.

    What's up with Alta Vista and AOL? Does anyone still use those search engines? Evidently a few people in the last couple of years ;-)

    I have a strong suspicion that if I went through the same exercise for my second most popular post, I would see very similar results.

    I'm not sure how useful this information might be to others, but I found it very enlightening. It gives me some great things to keep in mind when I sit down to work on my blog.

  • Constraining Tables with CSS

    Have you ever wanted to display data in a table but limit the size of the rows and columns within the table?

    For example, consider the classic master/detail view that we often find in software applications, in which items are shown in a summary table and each row provides a link to allow users to see more detail about the item.

    However, unlike the typical master/detail scenario, you need to limit the amount of real estate consumed on the page by the summary table. In addition, if text within a column is too long to fit within the constrained area, you want to show the full text when the mouse cursor hovers over the cell.

    The following figure illustrates the desired end result:

    Figure 1: Constrained table

    Here is the sample ASP.NET page that I created this morning to demonstrate this:

    <%@ Page Language="C#" AutoEventWireup="true"
    CodeBehind="ConstrainedTable.aspx.cs"
    Inherits="Fabrikam.Demo.Web.UI.ConstrainedTable" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Demo - Constrained Tabular Data</title> <link rel="Stylesheet" href="http://meyerweb.com/eric/tools/css/reset/reset.css" /> <style> /* Basic formatting ----------------------------------------------------------------------*/ body { color:#000000; font-family: Verdana, Arial, Helvetica; font-size: small; margin: 10px; } h1, h2, h3, h4, h5, h6, strong { font-weight: bold; } h1 { font-size: 1.5em; margin: .67em 0; } table caption { font-weight: bold; margin: 10px 0; } table.displayTable { border-left: 1px solid #36295E; border-right: 1px solid #36295E; margin: 1em 0; } table.displayTable th { text-align: left; } table.displayTable th, table.displayTable td { border-bottom: 1px solid #36295E; padding: 5px 10px; vertical-align: top; } table.displayTable thead th { background: #36295E; color: #FFF; padding: 8px 10px; border-bottom-width: 0; } table.displayTable tr.altRow { background: #F4F4F4; } /* =constrainedTable ----------------------------------------------------------------------*/ table.constrainedTable { table-layout: fixed; width: 540px; } table.constrainedTable th.nameColumn { width: 140px; } table.constrainedTable td { overflow: hidden; -o-text-overflow: ellipsis; /* Opera */ text-overflow: ellipsis; white-space: nowrap; } </style> </head> <body> <form id="form1" runat="server"> <h1>Demo - Constrained Tabular Data</h1> <asp:GridView ID="ConstrainedGrid" runat="server" AutoGenerateColumns="false" Caption="Constrained Table" CssClass="displayTable constrainedTable" OnRowDataBound="ConstrainedGrid_RowDataBound" EnableViewState="false" UseAccessibleHeader="true"> <AlternatingRowStyle CssClass="altRow" /> <RowStyle CssClass="row" /> <Columns> <asp:HyperLinkField DataTextField="Site" DataNavigateUrlFields="URL" HeaderText="Site" HeaderStyle-CssClass="nameColumn" /> <asp:BoundField DataField="Notes" HeaderText="Notes" HeaderStyle-CssClass="notesColumn" /> </Columns> </asp:GridView> </form> </body> </html>

    The most interesting parts of the ASP.NET page are the CSS rules for the constrainedTable class:

    table.constrainedTable {
        table-layout: fixed;
        width: 540px;
    }
    table.constrainedTable th.nameColumn {
        width: 140px;
    }        
    table.constrainedTable td {
        overflow: hidden;
        -o-text-overflow: ellipsis; /* Opera */
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    Changing the table-layout to fixed constrains the table to the specified width. Since I don't specify a width for the notesColumn it consumes the remaining width of the table.

    Next, I specify that all cells in the constrained table should truncate the text within each cell if it is too wide to fit within the width of the column. This is achieved using the combination of overflow: hidden and white-space: nowrap. Finally, I use the text-overflow CSS property to show ellipsis when text within a cell is clipped (as well as a slight variation for the Opera browser).

    Note
    Support for the text-overflow CSS property is somewhat limited. In particular, you will find that a clipped table cell renders without the ellipsis in Firefox (at least in version 3.5.3). Oddly enough, this appears to have been supported in Internet Explorer since version 6. In addition to Internet Explorer and Opera, this also appears as expected in Safari.

    Also note that constraining table cells using CSS does not automatically display the tooltip with the complete text. For that, you are on your own.

    When using an ASP.NET GridView control, I recommend using the RowDataBound event in order to set the ToolTip property of the table cell. Just be sure to decode the contents of the cell to avoid having it encoded twice:

            protected void ConstrainedGrid_RowDataBound(
                object sender,
                GridViewRowEventArgs e)
            {
                foreach (TableCell cell in e.Row.Cells)
                {
                    // Note: We need to decode the cell text in order to avoid
                    // having it encoded twice (e.g. "&amp;gt;")
                    cell.ToolTip = HttpUtility.HtmlDecode(cell.Text);
                }
            }

    This is somewhat of a "brute force" approach since it makes no attempt to determine if the text will be truncated, but rather sets the ToolTip (in other words, the title attribute on the HTML element) for every cell. However, the simplicity of this approach -- and the resulting user experience -- far outweighs any extraneous markup (at least in my opinion).

    Be aware that this simple approach for setting the ToolTip property doesn't support columns generated from a HyperLinkField, in which case cell.Text is empty (because the cell contains child controls, not simple text). In other words, it's a good thing my demo doesn't specify very long site names that don't fit within the 140 pixel width column constraint ;-)

    Here is the complete code-behind for my sample ASP.NET page so you can run it yourself:

    using System;
    using System.Data;
    using System.Globalization;
    using System.Web;
    using System.Web.UI.WebControls;
    
    namespace Fabrikam.Demo.Web.UI
    {
        public partial class ConstrainedTable : System.Web.UI.Page
        {
            protected void Page_PreRender(
                object sender,
                EventArgs e)
            {
                BindSampleData(this.ConstrainedGrid);
    
                this.ConstrainedGrid.HeaderRow.TableSection =
                    TableRowSection.TableHeader;
            }
    
            #region BindSampleData
    
            private static void BindSampleData(
                GridView grid)
            {
                DataTable sampleData = new DataTable();
                sampleData.Locale = CultureInfo.InvariantCulture;
    
                sampleData.Columns.Add("Site");
                sampleData.Columns.Add("URL");
                sampleData.Columns.Add("Notes");
    
                sampleData.Rows.Add(
                    new object[]
                    {
                        "Microsoft",
                        "http://www.microsoft.com",
                        "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
                            + "Morbi at sem lorem, ac blandit leo. Phasellus at"
                            + "ligula vitae enim dignissim tincidunt ornare nisl."
                    });
    
                sampleData.Rows.Add(
                    new object[]
                    {
                        "MSDN",
                        "http://msdn.microsoft.com",
                        "Sed posuere mattis egestas. Aliquam commodo dolor"
                            + " vulputate odio lacinia bibendum. Nullam bibendum,"
                            + " neque vitae ullamcorper elementum, ligula dolor"
                            + " mollis erat, ut ultricies mauris tortor ut eros."
                    });
    
                sampleData.Rows.Add(
                    new object[]
                    {
                        "TechNet",
                        "http://technet.microsoft.com",
                        "Vestibulum a leo nisl, sit amet porta eros. Proin vitae"
                            + " semper nunc. In facilisis nunc sit amet lacus"
                            + " accumsan mattis. Nulla facilisi. Pellentesque nisl"
                            + " sapien, dignissim ultrices semper et, mollis"
                            + " interdum sem. "
                    });
                
                grid.DataSource = sampleData;
                grid.DataBind();
            }
    
            #endregion
    
            protected void ConstrainedGrid_RowDataBound(
                object sender,
                GridViewRowEventArgs e)
            {
                foreach (TableCell cell in e.Row.Cells)
                {
                    // Note: We need to decode the cell text in order to avoid
                    // having it encoded twice (e.g. "&amp;gt;")
                    cell.ToolTip = HttpUtility.HtmlDecode(cell.Text);
                }
            }
        }
    }
More Posts Next page »

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker