Thursday, January 27, 2011

File provisioning: a hard-coded list id replacement

Howdy!
Today's topic might really help you in your SharePoint life. I will present to you a  technique to work with SPD workflow and page provisioning to make them portable from one environment to another. Not delaying any further - I will show you  real world examples with it's issue:
 Example #1. I have created SPD (SharePoint Designer ) workflow and want to export it and import to the another site created from the same site template (different enviroment: Dev, Qc, Staging).
 Issue: The exported wf holds hard-coded list id value
 Example #2. I have created a XSLT webpart and put it in a module <file> element. This feature should work on every site created from the site template to which this feature is belong to.
Issue: The xslt webpart has a hard-coded ListId.

Ok, I think it's convincing enough to read on to find a solution for described situations.
Here is our custom approach:
   We created a feature which we call SiteProvisioning. It consists of 2 elements:
1. A custom class: SiteProvisioning.cs which set as ReceiverClass for the feature:
<Feature Id="{4FA2AC90-321A-4d4c-B587-CE6D4A5B90FA}"
         Title="SiteProvisioning"
         Description=""
         Version="1.0.0.0"
         Scope="Web"
         Hidden="FALSE"
         DefaultResourceFile="core"
         ReceiverAssembly="Custom.SharePoint.Case, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a4607a0133bbaeb2"
         ReceiverClass="Custom.SharePoint.Case.SiteDefinition.SiteProvisioning"

         xmlns="http://schemas.microsoft.com/sharepoint/">
  <ActivationDependencies>
    <ActivationDependency FeatureId="43DA1A6A-0759-4093-B84D-9593555BC037"/>
  </ActivationDependencies>
( I will explain this element later)
  <ElementManifests>
    <ElementFile Location="SiteProvisioning.xml" />
  </ElementManifests>
</Feature>

2. And a file:
 <ElementManifests>
    <ElementFile Location="SiteProvisioning.xml" />
  </ElementManifests>
SiteProvisioning file is a list of elements which are originally have a hard-coded value and related list title;
<SiteSettings>
  <FixupFiles>
    <FixupFile DataViewInZone="true" DeleteWebPartsOnDeactivation="true" RelativePath="Lists/Discussion/DiscussionView.aspx" />
    <FixupFile DataViewInZone="true" DeleteOnDeactivation="true" RelativePath="default.aspx" />
    <FixupFile DataViewOutZone="true" DeleteOnDeactivation="true" RelativePath="default.aspx" />
  </FixupFiles>
  <ListInstances>
    <ListInstance Id="fd482aa7-9511-4b0f-abeb-79030bb681ca" Title="Case" />
    <ListInstance Id="85ac067e-e536-46f3-9a7b-032016cd032d" Title="Case Review Meeting" />
    <ListInstance Id="cde441a0-b34f-4f19-ba47-52b0b5c8d49e" Title="Action Items" />
    <ListInstance Id="dd764ce9-aa7a-41aa-aa2f-28841aee3356" Title="Claims" />
    <ListInstance Id="9b811048-cbd5-4e41-b526-703f1ddc3fae" Title="Disciplinary Actions" />
    <ListInstance Id="A8949622-30DB-4CE6-A6A6-B674DED51355" Title="Involved People" Web="/apps/systems/cases/" />
    <ListInstance Id="cbe513fb-ef61-45aa-9162-23398b8a4011" Title="Reports" />
    <ListInstance Id="4DF356F2-90CF-4D76-9B38-1B17F3B3D6E9" Title="Tasks" />
  </ListInstances>
</SiteSettings>

Every time we put a xslt web part in the module file, we need to be sure that we found a hard-coded value in it and added it to the SiteProvisioning also we need to put a file where we should replace this id during siteprovisioning.

The guy who will be in charge of the replacement is a  SiteProvisioning class
Here is an essence of it:

 public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            if (properties == null)
            {
                return;
            }

            SPWeb web = properties.Feature.Parent as SPWeb;         
            string filePath = this.GetProvisioinerFilePath(properties.Definition);

            var sz = new XmlSerializer(typeof(SiteSettings));
            SiteSettings settings = null;
            if (File.Exists(filePath)) {
                using (var sr = File.OpenRead(filePath)) {
                    try {
                        settings = sz.Deserialize(sr) as SiteSettings;
                    } catch { }
                }
            }

            if (settings != null) {
                this.RestoreDataViewInZone(web, settings);
                this.RestoreDataViewOutZone(web, settings);           
            }

            this.OnActivated(properties);
        }
  private void RestoreDataViewInZone(SPWeb web, SiteSettings settings)
        {
            if (settings.FixupFiles == null)
                return;

            foreach (var xFixupFile in settings.FixupFiles.Where(i => i.DataViewInZone))
            {
                var relativePath = xFixupFile.RelativePath;
                if (string.IsNullOrEmpty(relativePath))
                {
                    continue;
                }

                SPFile file = web.GetFile(relativePath);
                if (file == null)
                {
                    continue;
                }

                SPLimitedWebPartManager manager = file.GetLimitedWebPartManager(System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared);
                SPLimitedWebPartCollection pageWebParts = manager.WebParts;
                if (pageWebParts == null)
                {
                    continue;
                }

                foreach (System.Web.UI.WebControls.WebParts.WebPart webPart in pageWebParts)
                {
                    if (((webPart is DataFormWebPart) && (((DataFormWebPart)webPart).ParameterBindings != null)))
                    {
                        this.SubstituteGuidInZone(web, manager, webPart as DataFormWebPart, settings);
                        this.SubstituteIDInZone(web, manager, webPart, settings);
                    }
                    else
                    {
                        this.SubstituteIDInZone(web, manager, webPart, settings);
                    }
                }
                // http://msdn.microsoft.com/en-us/library/aa973248.aspx
                //
                //  Microsoft.SharePoint.WebPartPages.SPLimitedWebPartManager
                //  The SPLimitedWebPartManager class contains a reference to an internal SPWeb object that must be disposed.
              
                manager.Web.Dispose();
            }
        }

 private void SubstituteGuidInZone(SPWeb web, SPLimitedWebPartManager manager, DataFormWebPart dataForm, SiteSettings settings)
        {
            if (settings.ListInstances == null)
            {
                return;
            }

            foreach (var xListInstance in settings.ListInstances)
            {
                if (xListInstance.Id == null || xListInstance.Title == null) {
                    return;
                }

                SPList list = null;
                try
                {
                    if (string.IsNullOrEmpty(xListInstance.Web))
                    {
                        list = web.Lists[xListInstance.Title];
                    }
                    else
                    {
                        using (SPWeb listWeb = web.Site.OpenWeb(xListInstance.Web))
                        {
                            list = listWeb.Lists[xListInstance.Title];
                        }
                    }
                }
                catch (ArgumentException)
                {
                    continue;
                }

                if (list == null)
                {
                    continue;
                }
                string newId = list.ID.ToString();


                dataForm.ListName = newId;
                dataForm.ParameterBindings = Regex.Replace(dataForm.ParameterBindings, xListInstance.Id, newId, RegexOptions.IgnoreCase);
                dataForm.DataSourcesString = Regex.Replace(dataForm.DataSourcesString, xListInstance.Id, newId, RegexOptions.IgnoreCase);
                dataForm.Xsl = Regex.Replace(dataForm.Xsl, xListInstance.Id, newId, RegexOptions.IgnoreCase);

                manager.SaveChanges(dataForm);
            }
        }

     private void SubstituteGuidInZone(SPWeb web, SPLimitedWebPartManager manager, DataFormWebPart dataForm, SiteSettings settings)
        {
            if (settings.ListInstances == null)
            {
                return;
            }

            foreach (var xListInstance in settings.ListInstances)
            {
                if (xListInstance.Id == null || xListInstance.Title == null) {
                    return;
                }

                SPList list = null;
                try
                {
                    if (string.IsNullOrEmpty(xListInstance.Web))
                    {
                        list = web.Lists[xListInstance.Title];
                    }
                    else
                    {
                        using (SPWeb listWeb = web.Site.OpenWeb(xListInstance.Web))
                        {
                            list = listWeb.Lists[xListInstance.Title];
                        }
                    }
                }
                catch (ArgumentException)
                {
                    continue;
                }

                if (list == null)
                {
                    continue;
                }
                string newId = list.ID.ToString();


                dataForm.ListName = newId;
                dataForm.ParameterBindings = Regex.Replace(dataForm.ParameterBindings, xListInstance.Id, newId, RegexOptions.IgnoreCase);
                dataForm.DataSourcesString = Regex.Replace(dataForm.DataSourcesString, xListInstance.Id, newId, RegexOptions.IgnoreCase);
                dataForm.Xsl = Regex.Replace(dataForm.Xsl, xListInstance.Id, newId, RegexOptions.IgnoreCase);

                manager.SaveChanges(dataForm);
            }
        }

Whew, you have made it almost to the end. I hope that you have grasped the concept of id replacement: Regex.Replace(dataForm.ParameterBindings, xListInstance.Id, newId, RegexOptions.IgnoreCase); understand how to keep the hardcoded value for replacement.
The procedure pretty much the same for SPD WF. Once you have figured out where SPD hard codes the value - you write the class to replace id with current id of the list on the current site.
The only technique I haven't covered yet - how and when apply the site provisioning for the hardcoded webpart on the page. 
Here is a neat approach - use <ActivationDependencies> element in the site provisioning feature to call the module feature which holds hard-coded web parts.

Let's summarize:
1. We have a module feature with hardcoded  XSLT web parts.
2. We have a site provisioning feature which has activation dependency to the module feature.
It means every time the site provisioning gets activated, the module gets activated first. In such way we handle a fresh new pages and web parts on the site by SiteProvisioning.
3. We have a settings file for site provisioning which holds the hard coded ids and the related list name.
4. We have a custom class to perform the id replacement.


That's it! We are done! We are enlightened!