In all den Jahren, in denen ich nun erfolgreich SharePoint-Projekte umgesetzt habe, stand ich immer wieder vor der Herausforderung das SharePoint-Portal in meiner Entwicklungsumgebung so aufzusetzen, das es mit dem Test-, dem Staging- und dem Live-System möglichst identisch ist.

Dieses Prozedere kann unter Umständen sehr viel Zeit und Nerven in Anspruch nehmen, da gerade im SharePoint-Umfeld eine Entwicklungsmaschine durchaus öfters wieder “sauber” aufgesetzt werden muss, weil man sich das System zerschossen hat. Zudem führen schon kleine Abweichungen in der Konfiguration der Systeme zu unterschiedlichen Verhalten.

Aus diesem Grund nehme ich vor allem bei den größeren Projekten etwas (vermeidlich) mehr Aufwand in kauf und versuche so viele Konfigurationsschritte wie möglich zu automatisieren. Im Idealfall steht die Entwicklungsumgebung nach dem ersten Deployment komplett, so wie man sie für das Projekt benötigt. So ein Mechanismus ist dann auch für den initialen Aufbau des Test-, Staging- und Live-Systems Gold wert.

Für den initialen Strukturaufbau bei WebCMS (Publishing) Portalen habe ich mir bspw. einen kleinen, nützlichen Helfer implementiert, der anhand einer konfigurierbaren XML-Definition die Website-Struktur und die Seiten je nach Projektanforderung automatisch erstellen kann.

Die Struktur wird via XML folgendermaßen definiert. Hier ein Beispiel:

<?xml version="1.0" encoding="utf-8" ?>
<content>
  <web isRoot="true">
    <webs>
      <web Name="HanseVision" Title="HanseVision">
        <webs>
          <web Name="Demos" Title="Demos">
            <pages>
              <page Name="Demo1.aspx" Title="Demo 1" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
              <page Name="Demo2.aspx" Title="Demo 2" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
            </pages>
          </web>
          <web Name="Tools" Title="Tools">
            <pages>
              <page Name="Tool1.aspx" Title="Tool 1" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
              <page Name="Tool2.aspx" Title="Tool 2" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
              <page Name="Tool3.aspx" Title="Tool 3" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
            </pages>
          </web>
          <web Name="Books" Title="Books">
            <pages>
              <page Name="Book1.aspx" Title="Book 1" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
              <page Name="Book2.aspx" Title="Book 2" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
              <page Name="Book3.aspx" Title="Book 3" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
            </pages>
          </web>
          <web Name="References" Title="References">
            <pages>
              <page Name="Reference1.aspx" Title="Reference 1" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
              <page Name="Reference2.aspx" Title="Reference 2" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4" ></page>
              <page Name="Reference3.aspx" Title="Reference 3" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4" ></page>
            </pages>
          </web>
          <web Name="Location" Title="Location">
            <pages>
            </pages>
          </web>
        </webs>
        <pages>
          <page Name="Page1.aspx" Title="Page 1" LayoutName="WelcomeSplash.aspx" CtId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"></page>
        </pages>
      </web>
      <web Name="Areas" Title="Areas">
        <webs>
          <web Name="Area1" Title="Area1">
            <webs/>
          </web>
          <web Name="Area2" Title="Area 2">
            <webs/>
          </web>
          <web Name="Area3" Title="Area 3">
            <webs/>
          </web>
          <web Name="Area4" Title="Area 4">
            <webs/>
          </web>
          <web Name="Area5" Title="Area 5">
            <webs/>
          </web>
          <web Name="Area6" Title="Area 6">
            <webs/>
          </web>
          <web Name="Area7" Title="Area 7">
            <webs/>
          </web>
        </webs>
        <pages>
        </pages>
      </web>
      <web Name="News" Title="News Room">
        <webs>
          <web Name="News1" Title="News 1">
            <webs/>
          </web>
          <web Name="News2" Title="News 2">
            <webs/>
          </web>
          <web Name="News3" Title="News 3">
            <webs/>
          </web>
          <web Name="News4" Title="News 4">
            <webs/>
          </web>
        </webs>
        <pages>
        </pages>
      </web>
    </webs>
  </web>
</content>

Die oben definiere Portal-Struktur ist zwar nicht sehr komplex; diese aber von Hand über die SharePoint UI zu erstellen, kostet dennoch immens viel Zeit und Nerven. Und Entwickler wollen ja schließlich entwickeln und nicht Inhalte pflegen!

Mein Code, mit dem die Struktur in SharePoint erzeugt wird, sieht wie folgt aus:

/// <summary>
    /// Privides publishing portal provisioning
    /// </summary>
    public class ContentProvisioner
    {
        /// <summary>
        /// Creates the publishing portal structure
        /// </summary>
        /// <param name="rootWeb"></param>
        /// <param name="contentDefinitionFilePath"></param>
        /// <returns></returns>
        public bool CreatePortalStructureFromXmlDefinition(SPWeb rootWeb,string contentDefinitionFilePath)
        {
            bool successful = true;
            try
            {
                if (File.Exists(contentDefinitionFilePath))
                {
                    XmlDocument contentDoc = new XmlDocument();
                    contentDoc.Load(contentDefinitionFilePath);

                    foreach (XmlNode node in contentDoc.DocumentElement.ChildNodes)
                    {
                        ProcessContentDefinitionNode(node, rootWeb);
                    }
                }
            }
            catch (Exception ex)
            {
                PortalLog.LogString("ContentProvisioner:Error->In ContentProvisioner.CreatePortalStructureFromXmlDefinition: {0}", ex);
                successful = false;
            }

            return successful;
        }

        /// <summary>
        /// Process a node from the content definition xml file
        /// </summary>
        /// <param name="node"></param>
        /// <param name="parentWeb"></param>
        private static void ProcessContentDefinitionNode(XmlNode node, SPWeb parentWeb)
        {
            try
            {
                string websiteName = String.Empty;
                string websiteTitle = String.Empty;
                foreach (XmlNode xmlContentDefinitionChildNode in node.ChildNodes)
                {
                    //process website node
                    if (xmlContentDefinitionChildNode.Name.Equals("webs"))
                    {
                        foreach (XmlNode webNode in xmlContentDefinitionChildNode.ChildNodes)
                        {
                            websiteName = webNode.Attributes["Name"].Value;
                            websiteTitle = webNode.Attributes["Title"].Value;

                            using (SPWeb childWeb = EnsureChildWebByName(parentWeb, websiteName, websiteTitle, webNode))
                            {
                                ProcessContentDefinitionNode(webNode, childWeb);
                            }
                        }
                    }// process page node
                }// foreach
            }
            catch (Exception ex)
            {
                PortalLog.LogString("ContentProvisioner:Error->In ContentProvisioner.ProcessNode: {0}", ex);
                throw ex;
            }
        }

        /// <summary>
        /// Gets or creates the sub website defined in the content definition xml
        /// </summary>
        /// <param name="parentWeb">The parent website object</param>
        /// <param name="websiteName">The name of the sub website to get or create</param>
        /// <param name="websiteTitle">The tile of the sub website to get or create</param>
        /// <param name="xmlContentDefinitionChildNode">The configuration node representing the website</param>
        /// <returns></returns>
        private static SPWeb EnsureChildWebByName(SPWeb parentWeb, string websiteName, string websiteTitle, XmlNode xmlContentDefinitionChildNode)
        {
            SPWeb childWeb = null;
            foreach (SPWeb web in parentWeb.Webs)
            {
                if (web.ServerRelativeUrl.Equals(parentWeb.ServerRelativeUrl + "/" + websiteName))
                {
                    childWeb = web;                    
                    break;
                }
                else
                {
                    if (web != null)
                    {
                        web.Dispose();
                    }
                }
            }

            if (childWeb == null)
            {
                //sub website not found so generate it
                uint lcid = parentWeb.Language;
                string templateName = "BLANKINTERNET";
                childWeb = parentWeb.Webs.Add(websiteName, websiteTitle, "", lcid, templateName, false, false);
                foreach(XmlNode definitionNodeChildNodes in xmlContentDefinitionChildNode.ChildNodes)
                {
                     if (definitionNodeChildNodes.Name.Equals("pages"))
                     {
                        childWeb = ProcessPagesNodes(childWeb, definitionNodeChildNodes);
                        break;
                     }
                }
               
            }

            return childWeb;
        }

        /// <summary>
        /// Creates the pages defined in the content definition xml
        /// </summary>
        /// <param name="web">The website to create the pages in</param>
        /// <param name="xmlContentDefinitionChildNode">The content definition node representing the pages collection for the web</param>
        /// <returns></returns>
        private static SPWeb ProcessPagesNodes(SPWeb web, XmlNode xmlContentDefinitionChildNode)
        {
            PublishingWeb publishingWeb = PublishingWeb.GetPublishingWeb(web);
            string pageName = String.Empty;
            string pageTitle = String.Empty;
            string pageContentTypeId = String.Empty;
            string pageLayoutName = String.Empty;

            //for each page defined
            foreach (XmlNode pageNode in xmlContentDefinitionChildNode.ChildNodes)
            {
                pageName = pageNode.Attributes["Name"].Value;
                pageTitle = pageNode.Attributes["Title"].Value;
                pageContentTypeId = pageNode.Attributes["CtId"].Value;
                pageLayoutName = pageNode.Attributes["LayoutName"].Value;

                //check to use default
                if (String.IsNullOrEmpty(pageContentTypeId))
                {
                    pageContentTypeId = "0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D4"; //use welcome page
                }

                //check to use default
                if (String.IsNullOrEmpty(pageLayoutName))
                {
                    pageLayoutName = "WelcomeSplash.aspx"; //use default layout
                }

                SPContentTypeId welcomePageCtId = new SPContentTypeId(pageContentTypeId);

                PageLayout layout = publishingWeb.GetAvailablePageLayouts(welcomePageCtId).First(l =&gt; l.Name == pageLayoutName);
                if (layout != null)
                {
                    PublishingPageCollection pages = publishingWeb.GetPublishingPages();
                    bool pageExists = false;
                    foreach (PublishingPage page in pages)
                    {
                        if (page.Name.Equals(pageName))
                        {
                            pageExists = true;
                            break;
                        }
                    }
                    if (!pageExists)
                    {
                        PublishingPage page = pages.Add(pageName, layout);
                        page.Title = pageTitle;
                        page.Update();
                    }
                }
                else
                {
                    throw new InvalidOperationException(String.Format("Page layout {0} not found in website!", pageLayoutName));
                }
            }
      
            publishingWeb.CustomMasterUrl.SetInherit(true, true);
            publishingWeb.Update();
            return web;
        }

        /// <summary>
        /// Removes the out-of-the-box created website "Press Releases"
        /// </summary>
        /// <param name="rootWeb">The root website to remove the out of the box website</param>
        public void RemovePressReleasesWebsite(SPWeb rootWeb)
        {
            try
            {
                string pressUrl = "PressReleases";

                SPWeb deleteWeb = null;
                foreach (SPWeb childWeb in rootWeb.Webs)
                {
                    if (childWeb.Url.EndsWith(pressUrl))
                    {
                        deleteWeb = childWeb;
                    }
                }

                if (deleteWeb != null)
                {
                    deleteWeb.Delete();
                }
            }
            catch (Exception ex)
            {
                PortalLog.LogString("ContentProvisioner:Error->In ContentProvisioner.RemovePressReleaseWeb: {0}", ex);
                throw ex;
            }
        }
    }

Ich verwende den Mechanismus in der Regel mit einem Feature Event Receiver und beziehe dann das Content Definition XML aus einer Datei, die ich ebenfalls mit dem WSP als “Template File” in das Layouts-Verzeichnis ausrolle.

Als Ergebnis habe ich dann ein vorkonfiguriertes Portal, in dem ich direkt mit der Entwicklung der Komponenten fortfahren kann.

image

 

Wer sich also wie ich die aufwändige Inhaltspflege sparen möchte, kann mit dieser oder einer Weiterentwicklung dieser Komponente die Inhaltspflege einfach automatisieren.

Leave a comment

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Time limit is exhausted. Please reload the CAPTCHA.