Archives

Archives / 2006 / June
  • NAnt task for SharePoint: Save SPWeb as site template to the filesystem

    I love NAnt as a tool to automate deployment processes. Kris Syverstad created a nice set of NAnt tasks for SharePoint that he uploaded to a GotDotNet workspace. My latest task was to save a site as a site template to the filesystem for deployment to another system. I decided to create a task in the same spirit to Kris his tasks. You can add it to the set of tasks that Kris already created. That is what I did.

    The code:

    //

    // NAnt.SharePoint Microsoft Sharepoint Server utility tasks.

    // Copyright (C) 2006 Macaw, Serge van den Oever

    //

    // This library is free software; you can redistribute it and/or

    // modify it under the terms of the GNU Lesser General Public

    // License as published by the Free Software Foundation; either

    // version 2.1 of the License, or (at your option) any later version.

    //

    // This library is distributed in the hope that it will be useful,

    // but WITHOUT ANY WARRANTY; without even the implied warranty of

    // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

    // Lesser General Public License for more details.

    //

    // You should have received a copy of the GNU Lesser General Public

    // License along with this library; if not, write to the Free Software

    // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

    //

    using System;

    using System.IO;

    using System.Globalization;

     

    using Microsoft.SharePoint;

     

    using NAnt.Core;

    using NAnt.Core.Attributes;

    using NAnt.Core.Types;

     

    namespace NAnt.SharePoint.Tasks

    {

        /// <summary>

        /// Save SPWeb as template.

        /// </summary>

        /// <remarks>

        ///   <para>

        ///   Save a template of the SPWebs pecified by a Url on the local machine.

        ///   </para>

        ///   <note>

        ///   If the <see cref="Url" /> specified does not exist, a

        ///   <see cref="BuildException" /> will be raised.

        ///   </note>

        /// </remarks>

        /// <example>

        ///   <para>Delete a SPSite.</para>

        ///   <code>

        ///    <![CDATA[

        /// <deletespsite Url="http://myserver/sites/mysite" />

        ///    ]]>

        ///   </code>

        /// </example>

        /// <example>

        ///   <para>

        ///   Save SPWeb as template. If the SPWeb does not exist, the task does nothing.

        ///   </para>

        ///   <code>

        ///    <![CDATA[

        /// <savespwebastemplate url="${url}" templatename="myTemplate" title="My Template" description="A Template Of My Site" savedata="true" tofile="c:\template.stp" failonerror="false" />

        ///    ]]>

        ///   </code>

        /// </example>

        [TaskName("savespwebastemplate")]

        public class SaveSPWebAsTemplateTask : Task

        {

     

            private string _url = "";

            private string _templatename = "";

            private string _title = "";

            private string _description = "";

            private bool _savedata = false;

            private string _tofile = null;

     

            /// <summary>

            /// The URL for the SPWeb to save as template.

            /// </summary>

            [TaskAttribute("url", Required = true)]

            public string Url

            {

                get { return _url; }

                set { _url = value; }

            }

     

            /// <summary>

            /// The name for the template.

            /// </summary>

            [TaskAttribute("templatename", Required = true)]

            public string TemplateName

            {

                get { return _templatename; }

                set { _templatename = value; }

            }

     

            /// <summary>

            /// The title for the template.

            /// </summary>

            [TaskAttribute("title", Required = false)]

            public string Title

            {

                get { return _title; }

                set { _title = value; }

            }

     

            /// <summary>

            /// The description for the template.

            /// </summary>

            [TaskAttribute("description", Required = false)]

            public string Description

            {

                get { return _description; }

                set { _description = value; }

            }

     

            /// <summary>

            /// Boolean specifying if data of the SPWeb should be included in the template.

            /// Only data up to 10Mb can be included.

            /// </summary>

            [TaskAttribute("savedata", Required = false)]

            public bool SaveData

            {

                get { return _savedata; }

                set { _savedata = value; }

            }

     

            /// <summary>

            /// The file to save the template to.

            /// </summary>

            [TaskAttribute("tofile", Required = false)]

            public string ToFile

            {

                get { return _tofile; }

                set { _tofile = value; }

            }

     

            /// <summary>

            /// Because ExecuteTask is protected added this function to debug the task.

            /// </summary>

            public void DebugTask()

            {

                ExecuteTask();

            }

     

            /// <summary>

            /// Task for saving a SPWeb as template.

            /// </summary>

            protected override void ExecuteTask()

            {

                string templateFilename = TemplateName + ".stp";

                try

                {

                    using (SPSite site = new SPSite(Url))

                    {

                        try

                        {

                            using (SPWeb web = site.OpenWeb(site.ServerRelativeUrl))

                            {

                                // Delete site template with name TemplateName if it exists

                                SPDocumentLibrary webTemplates;

                                try

                                {

                                    webTemplates = site.GetCatalog(SPListTemplateType.WebTemplateCatalog) as SPDocumentLibrary;

                                }

                                catch (Exception ex)

                                {

                                    throw new BuildException(

                                        string.Format("The site collection of the SPWeb '{0}' does not contain a Site Template Gallery.",

                                        Url), Location, ex);

                                }

                                SPFolder folder = web.Folders["_catalogs"];

                                SPFolder subfolder = folder.SubFolders["wt"];

                                subfolder.Files.Delete(templateFilename);

                            }

                        }

                        catch (Exception)

                        {

                            // ignore, no template to delete

                        }

     

                        // Save the SPWeb as template

                        using (SPWeb web = site.OpenWeb())

                        {

                            // If no title is specified, use same as template name

                            if (Title.Length == 0)

                            {

                                Title = TemplateName;

                            }

     

                            web.SaveAsTemplate(templateFilename, Title, Description, SaveData);

                        }

                    }

     

                    Log(Level.Info, LogPrefix + "Save SPWeb '{0}' as template with name '{1}'.", Url, TemplateName);

                }

                catch (Exception ex)

                {

                    // The SPS API will throw an exception when you try and create an

                    // instance of SPSite for a URL that doesn't exist. 

                    throw new BuildException(

                        string.Format("Cannot save SPWeb '{0}' as template. Either the SPWeb does not exist, or savedata is true and the size of the SPWeb > 10MB, or there is already a template by this name.",

                        Url), Location, ex);

                }

     

                if (ToFile != null)

                {

                    try

                    {

                        using (SPSite site = new SPSite(Url))

                        {

                            string templateUrl = site.Url + "/_catalogs/wt/" + templateFilename;

                            System.Net.WebClient objWebClient = new System.Net.WebClient();

                            objWebClient.Credentials = System.Net.CredentialCache.DefaultCredentials;

                            objWebClient.DownloadFile(templateUrl, ToFile);

                        }

                    }

                    catch(Exception ex)

                    {

                        throw new BuildException(

                            string.Format("The SPWeb '{0}' is saved as template '{1}' in the Site Template Gallery, but downloading the template to file '{2}' failed.",

                            Url, Name, ToFile), Location, ex);

                    }

                }

            }

        }

    }

  • SharePoint and objects disposal, the definitive guide!

    One of those difficult things to handle in the construction of SharePoint related code is the cleaning up of used objects. The problem is that if you don't clean up your mess, the garbage collector does its job, and nothing really bad seems to happen. Problems only occur when your site is on heavy load in the production environment, and you have no clue how to solve that problem where your processor hits the 100% and used memory goes sky high.

    Finally there is a great article out there that explains in great detail the best programming practices with SharePoint objects. Go grab it and start reviewing your code!

    Article: Best Practices: Using Disposable Windows SharePoint Services Objects

    Blog post with further discussions: http://blogs.msdn.com/krichie/archive/2006/06/15/632611.aspx

  • SharePoint 2007 - _layouts, pages in site context, help!

    NOTE: See this blogpost for updated information on this topic.

    SharePoint 2003 has a very powerful feature to run pages in the context of a site, this is especially used for the administrative pages. All these administrative pages live in the “/_layouts” folder. If you have two sites, http://server/A and http://server/B, and an administrative page admin.aspx, you can execute this page in site context as follows:

    http://server/A/_layouts/admin.aspx executes in the context of site A

    http://server/B/_layouts/admin.aspx executes in the context of site B

    Within those pages you can directly access the current site with the following line of code:

           SPWeb site = SPControl.GetContextWeb(Context);
     

    With this site object as starting point you can access all information that is in your site.

    This approach is used in all standard pages in /_layouts, with all pages containing inline code.

    All the years I have been working with SharePoint 2003 I had another approach to create web pages to run in the context of a site. I just create a web project, deploy the aspx pages, images, client scripts etc to a directory in /_layouts, and put the "code behind" dll's into /_layouts/bin.

    This approach no longer works, and a new adventure begins...

    THE ADVENTURE

    1. In SharePoint 2007 the _layouts virtual directory no longer contains a "bin" directory, all pages use inline code. First thing I tried was to add a "bin" directory, deploy my "code behind" dll's there and go to my page http://server/_layouts/myapp/default.aspx. Nothing happened, not even an error, just a blank page.
    2. I change the name of of the page to a non-existant name: http://server/_layouts/myapps/blabladefault.aspx. Again nothing happened. All url's requested in _layouts that don't exist just give a blank page!!
    3. After looking at the web.config in SharePoint 2003  I see that the line:

      <trust level="Full" originUrl="" />

      is missing, so I add that line. Still the same result, a blank page. But at the same time strange things start to happen to other parts of SharePoint. The other administrative pages return a blank page as well, and the pages in the SharePoint administration site start to loose their design. After looking at the source of the page it is not so strange: a lot of assets from the /_layouts path are referenced. But it is strange that with trust level "Full", the highest as far as I know, things stop working!

    4. I remove the "bin" directory, the web.config is as prestine as it was, and I try again. Something happens: I get an expected error, the "code behind" dll can't be found! A nice error is displayed in "SharePoint syle", with the message: Could not load type 'myapp._Default'. Ah, almost there I'm thinking, we just need a probing path.

    5. I add the probing path to give .Net direction on where to look for the dll's:

      <runtime>
          <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
              <probing privatePath="myapp\bin"/>
          </assemblyBinding>
      </runtime>

      and I hit the refresh button in great expectation... same error. the "code behind" dll can't be found.

    6. One last thing to try that must work, otherwise I'm getting mad: throw the "code behind" assembly into the GAC. I strong-sign my assembly, drag it to the GAC, do an IISRESET and... shoot! I'm mad! Still the same error!

    7. Back to the model that Microsoft uses in the _layouts pages: all code inline in the aspx pages, and this works... I hate this approach, but it seams the only way.

    QUESTIONS

    And now my questions!!! There must be someone out there who knows how to solve my problem.

    1. Is it possible to have pages with code behind in _layouts, and have the code behind assemblies in a bin directory?
    2. Is it possible to have our own custom"_layouts" like virtual directory that executes "in context" of a page?

    UPDATE: I managed to get a 2005 web project working, where the code is not precompiled. You don’t need to deploy your code-behind code deployed as a DLL. One thing to watch out for however: if you have a web.config file in your project, comment out the following line: <authentication mode="Windows"/>.

  • SharePoint 2007 ends the bucket nightmare.. move those sites!!!

    Do you still remember that good old bucket system in SharePoint Portal Server 2003 where the first 20 portal area's ended up with nice urls, but then the buckets started kicking in and provided you with urls like http:/server/C0/myarea/default.aspx? Those days are over.

    The reason behind the bucket system in Portal Server was understandable; Microsoft expected a huge number of areas, with deep hierarchies that could be moved around at will without breaking links, and probably performance was an issue. But the bucket solution is a really machine oriented solution, users want to be able to understand their urls. If not we could use GUID's instead of urls;-) But I think they calculated wrong, I never saw really deep hierarchies appearing, and in most cases not that many areas, because you don't want to do to many things in areas due to all the limitations imposed compared to the standard WSS sites: no list and document library security, no possibility to backup a single area, no possibility to create a template from an existing area, or create an area based on a template... should I continue?

    In SharePoint 2007 Microsoft got rid of the bucket system. You can move sites around, and the urls of the sites change!! But what happens to urls pointing to those sites? There is a good side to the story, and a bad side. The good side is that all urls in url fields in lists and metadata are rewritten. This is actually quite cool! Move your site and all lists like links lists are updated!! But the bad thing is that all urls NOT managed by SharePoint will NOT be updated (SharePoint has no control over them), and this will break a lot of links.

    I wonder about links within documents within document libraries in SharePoint... would be a nice excercise to update those as well;-)

    Another thing I heard, but not verified yet, is that if you put a url in a url field in a list, and this url points to something within SharePoint it is actually stored as a relative url. You can also store a releative url in a url field now, but if you edit the field again it shows the full url, but still stores the url in a relative format.

  • Web Deployment Projects and deploying web.config settings for multiple machines

    I wrote the following comment on a post http://weblogs.asp.net/dfindley/archive/2006/06/06/Frustrations-with-Web-Deployment-Projects.aspx about having machine dependent configurations in your web.config. DFindley expresses some frustrations with the web deployment projects on configuring per machine settings in the web.config, but he found the fix right away. This approach is powerful, but sometimes not clear where the settings are actually managed. I use another approach that works quite well for me. I posted this as a comment on the mentioned post.

    Scott mentions in his comment to have a web.config per machine, and copy the correct web.config on deployment.
    I prefer to only have replacements for the changing sections per machine. Often these settings are the appSettings, connections and an impersonation account.
    In this approach you can manage the other settings in a generic web.config that is used for all machines.
    So what I do is the following:

    Create appSettings files per machine:

    MACHINE1.appSettings.config
    MACHINE2.appsettings.config

    Create connections files per machine:

    MACHINE1.connections.config
    MACHINE2.connections.config

    On automated deployment check your machine name, and copy:

    MACHINEX.appSettings.config to appSettings.config
    MACHINEX.connections.config to connections.config

    In your web.config you refer to those external files as follows:

    <appSettings configSource="appSettings.Config"/>
    <connectionStrings configSource="connections.config"/>

    Another thing you often want to set is the impersonation account, we manage those in the registry per server in a secure way as follows:

    <!-- Impersonation identity is encrypted in the registry. Identity is set with the following command:
       
    aspnet_setreg.exe -k:SOFTWARE\MyApp\identity -u:"yourdomainname\username" -p:"password"
       
    Tool can be downloaded at:
      
    http://download.microsoft.com/download/2/9/8/29829651-e0f0-412e-92d0-e79da46fd7a5/aspnet_setreg.exe
    -->
    <identity impersonate="true"
              userName="registry:HKLM\SOFTWARE\MyApp\identity\ASPNET_SETREG,userName"
              password="registry:HKLM\SOFTWARE\MyApp\identity\ASPNET_SETREG,password" />