My career in technology

This is part eight of my exploration of a solution I recently completed, using an event handler to programmatically create and configure a site based on form data provided by users and a site template created and managed by a SharePoint power user.

The next thing on the To Do list is to modify some web part pages.

        private void DoAllSiteCreationSteps()
        {
            //Make sure this runs with appropriate privileges to actually run the commands
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                //ToDo #1: Create the site
                DoCreateSite();
                //make sure the site has finished baking before continuing
                for (int i = 0; i < 30; i++)
                {
                    System.Threading.Thread.Sleep(1000);
                }
                //ToDo #2:  Create the site user groups
                DoAllMembershipTasks();
                //ToDo #3:  Provision the site with the data about the project type for correct display
                DoAddControlEntries();
                DoActivateFeature_3rdPartyFeature();
                //ToDo #4:  Set the filters for the Eval Factors List in the new site
                DoChangeFactorFilters();
                //ToDo #5:  When a site is created from the template, the KPI List points to the template.  Change the KPIs to get their data from the site in which it resides.
                DoFixKPIListEntries();
                //ToDo #6:  Fix the top nav so it matches parent site
                DoFixNav();
                //ToDo #7:  Modify web part pages
                DoFixWebParts();

The landing page for the newly created site has some dashboard web parts with information specific to the project. As before, in the template some of these are configured with full URLs and need to be modified to pull information from the newly created site. Also, the audience for one of the web parts needs to be set.

But once I complete this, I discover a problem…


(Links to the other parts in the series are at the bottom of the post!)

Let’s start (as we should) with the specific requirements.

The landing page, Welcome.aspx, is a web part page contained in the Pages library. On it are 12 web parts:

  • 2 Content Editor web parts
  • 2 Data View web parts
  • 6 List View web parts
  • 1 User Tasks web part
  • 1 Key Performance Indicator web part

Requirements:

  1. One of the Content Editor web parts displays a Visio diagram. The URL for the source is not a relative URL and needs to be modified.
  2. One of the Data View web parts displays the data from the project site request. Since that list item doesn’t exist until the request is made, the web part needs to be configured to display the correct item in the list.
  3. One of the List View web parts is for the project managers (the site owners). Since these are not determined until request time, the audience needs to be configured for the web part.
  4. There is a content editor web part on another web part page that needs to be configured.
  5. There is a late requirement for a temporary change to the site template.

We start with the last requirement first. On to the code!

        private void DoFixWebParts()
        {
            //DoRemovePCPage();
            DoUpdateProjectProcessWebPart();
            DoFilterAudienceOnTaskWebPart();
            DoSetProjectDatabaseDisplayWebPart();
            DoFixProjKickoffContentEditorWebPart();

            //update BuildWeb
            BuildWeb.Update();
        }

As in other parts of this code, DoFixWebParts() is actually comprised of several steps, each of which is broken out in their own separate method.

Deleting a List Item

The first method called has been commented out. This is because it was made obsolete almost immediately after the first release of the code. But I thought it might be a good piece of code to go over anyway.

        private void DoRemovePCPage()
        {
            //The site template contains a page that is deprecated and adds a link to the navigation
            //that is undesired because of the deprecation.  Unitl the page is removed from the template,
            //run this deletion.
            SPList ListToGetInfoFrom = BuildWeb.Lists["Pages"];
            SPListItem ListItemToDelete = ListToGetInfoFrom.GetItemById(28);
            ListItemToDelete.Delete();
        }

During development, it was determined the one of the site’s web pages might no longer be required. The decision was made to leave the page in the template until a final decision was made, but that new sites were to be created without the page going forward. Once a final determination had been made, the template would be updated and the code disabled. But this procedure would work well in the reverse situation, in which a page was added to the template but was not quite “ready for prime time”, and needed to not be included in built sites until the final specs had been worked out.

In either case, commenting out a single line in DoFixWebParts() disabled the step from the running code.

I could have added code to check to see if the page exists first before trying to delete, and then the power user could have posted the updated site template on their own schedule without worrying about the event handler throwing an error and without involving me. But as stated in an earlier post, in the procedure we established for our environment, updating the template included full testing prior to deployment, which made adding that functionality unnecessary for our purposes.

In this case, I knew ahead of time the ID of the page to be deleted, so deleting the page was a simple process of getting the SPListItem by ID and then calling Delete(). I probably could have written it like this:

            BuildWeb.Lists["Pages"].GetItemById(28).Delete();

This would have been marginally more efficient, but it does make stepping through the code for troubleshooting a little less easy. Since this code is run a few times a week instead of dozens of times per hour, to me the tradeoff (lack of ultimate efficiency in exchange for ease of stepping through the code) was worth it.

Correcting A URL in a Content Editor Web Part

The next requirement is fixing the display of the Visio diagram. The file is located in the Shared Documents library of the site and contains an interactive process map for the project. The file itself and its configuration are beyond the scope of this post, not to mention beyond the scope of my work and outside my skill set (though I did end up helping the power user do some configuration within the file, mostly by guesswork and knowing the “Microsoft Way” of doing things – you know: “There is the right way, the wrong way, and the Microsoft way…”).

However, as with the KPI entries in Part Seven (Pointing The Way), the URL pointing to the content to be displayed is absolute, not relative, so the Visio diagram shown on the page of a newly created site defaults to the Visio file in the template site.

I had a bit of trouble figuring out where the data for the URL was stored and how to access it. I finally went to the template site and exported the web part and opened it in Notepad to examine the XML. And it hit me… just modify the XML of the web part.

        private void DoUpdateProjectProcessWebPart()
        {
            SPList ListToGetInfoFrom = BuildWeb.Lists["Pages"];
            SPListItem ListItemToGetInfoFrom = ListToGetInfoFrom.GetItemById(41);
            SPFile WebpartPageToUpdate = BuildWeb.GetFile(ListItemToGetInfoFrom.Url);

It helps that I know that the ID of the Welcome.aspx page in the template is 41. To get the SPFile itself, I use the URL of the SPList Item as the parameter in SPWeb.GetFile().

            try
            {
                WebpartPageToUpdate.CheckOut();
                using (SPLimitedWebPartManager MyWPM = WebpartPageToUpdate.GetLimitedWebPartManager(PersonalizationScope.Shared))
                {

There is some debate on whether or not it is actually necessary to use the Using statement for the SPLimitedWebpartManager, but there is no harm in disposing of the object when I’m done with it.

                    string WebPartTitle;
                    System.Web.UI.WebControls.WebParts.WebPart wp = MyWPM.WebParts[7];
                    WebPartTitle = wp.Title;
                    if (WebPartTitle == "Project Process")
                    {

I know the ID of the web part I want to change is 7, but just to be sure, I check to make sure the title matches. Again, I could have gone straight for if (wp.Title == “Project Process”), but for ease of stepping through the code, went for the slightly less elegant way.

                        ContentEditorWebPart ceWebPart = new ContentEditorWebPart();

                        // Create an XmlElement to hold the value of the Content property.
                        XmlDocument xmlDoc = new XmlDocument();
                        XmlElement xmlElement = xmlDoc.CreateElement("Content");
                        xmlElement.InnerText = ((ContentEditorWebPart)(wp.WebBrowsableObject)).Content.InnerText.ToString();

The System.Web.UI.WebControls.WebParts.WebPart.WebBrowsableObject needs to be cast as a ContentEditorWebPart to properly access the Content. (You’ll see this again momentarily.)

At this point, we have an XmlElement object containing the contents of the web part as a string. Anyone have a problem with a little string.Replace()?

                        xmlElement.InnerText = xmlElement.InnerText.ToString().Replace("http://SharePoint/projects/TemplateSite", serverURL + siteUrl);

                        //Set the Content property to the XmlElement.
                        ceWebPart.Content = xmlElement;
                        ((ContentEditorWebPart)(wp.WebBrowsableObject)).Content = ceWebPart.Content;

                        //Call SaveChanges method
                        MyWPM.SaveChanges(wp);
                    }

                }

In the code, “http://SharePoint/projects/TemplateSite&#8221; is the template location, and serverURL + siteUrl are, taken together, the address of the newly created site.  Remember that serverURL and siteUrl are both class-level variables set earlier in the process.

                WebpartPageToUpdate.CheckIn("Initial Site Setup - ProjectProcessWebPart");
                WebpartPageToUpdate.Publish("Initial Site Setup - ProjectProcessWebPart");
            }

The library enforces check in/out and publishing, so both of these methods need to be called to finalize the changes. The strings in the method call are the comments saved with the version.

That is the end of the Try block, so we follow up with a little error handling (again, the details of which will be covered in a later post).

            catch (Exception ex)
            {
                LogTheError("EventHandlerName", "Error in DoUpdateProjectProcessWebPart(): " + ex.Message + " Stack Trace: " + ex.StackTrace);
            }
        }

Setting an Audience Filter on a Web Part

As I mentioned back in Part Four, there was a reason to do the creation of Groups first. I’m about to set the Audience for a web part, and I’ll need that Audience to already exist.

        private void DoFilterAudienceOnTaskWebPart()
        {
            SPList ListToGetInfoFrom = BuildWeb.Lists["Pages"];
            SPListItem ListItemToGetInfoFrom = ListToGetInfoFrom.GetItemById(41);
            SPFile WebpartPageToUpdate = BuildWeb.GetFile(ListItemToGetInfoFrom.Url);
            WebpartPageToUpdate.CheckOut();
            try
            {
                using (SPLimitedWebPartManager MyWPM = WebpartPageToUpdate.GetLimitedWebPartManager(PersonalizationScope.Shared))
                {
                    string WebPartTitle;
                    System.Web.UI.WebControls.WebParts.WebPart wp = MyWPM.WebParts[5];
                    WebPartTitle = wp.Title;
                    if (WebPartTitle == "Tasks")
                    {
                        //wp.Title = "Tasks";
                        //Need to change AuthorizationFilter for this webpart
                        //If you know the ID, or want to use the ID, format the string this way
                        //wp.AuthorizationFilter = string.Format("{0},{1};;;;", am.GetAudience("Sales").AudienceID, am.GetAudience("Finance").AudienceID);
                        //If you are using the name, use this format
                        wp.AuthorizationFilter = ";;;;" + siteName + " Project Site Owners";
                        //Call SaveChanges method
                        MyWPM.SaveChanges(wp);
                    }
                }
                WebpartPageToUpdate.CheckIn("Initial Site Setup - FilterAudienceOnTaskWebPart");
                WebpartPageToUpdate.Publish("Initial Site Setup - FilterAudienceOnTaskWebPart");
            }
            catch (Exception ex)
            {
                LogTheError("EventHandlerName", "Error in DoFilterAudienceOnTaskWebPart(): " + ex.Message + " Stack Trace: " + ex.StackTrace);
            }
        }

The first section of the code looks suspiciously like the opening code to DoUpdateProjectProcessWebPart(), because the web part in question is on the same page.

At line 778, again, I already know the ID for the web part (in this case 5), but I verify we’re working on the right part by checking the title.

There is a little nugget in the comments about using the IDs of the groups and an Audience manager, which you can find out more about here. I went for using the group name instead. As I mentioned in Part Four, be sure the Group your Audience is based on has rights to the site in question or users will not see the web part. As administrator, I could see the web part after the filter was applied if I wasn’t a member of the group, but once I was added to the Audience I couldn’t see the web part! It was because the group hadn’t been given specific rights to the site.

Modify the Data Source of a Data View Web Part

        private void DoSetProjectDatabaseDisplayWebPart()
        {
            SPList ListToGetInfoFrom = BuildWeb.Lists["Pages"];
            SPListItem ListItemToGetInfoFrom = ListToGetInfoFrom.GetItemById(41);
            SPFile WebpartPageToUpdate = BuildWeb.GetFile(ListItemToGetInfoFrom.Url);
            WebpartPageToUpdate.CheckOut();
            using (SPLimitedWebPartManager MyWPM = WebpartPageToUpdate.GetLimitedWebPartManager(PersonalizationScope.Shared))
            {
                //Fixes connection to Project Request Database
                Microsoft.SharePoint.WebPartPages.DataFormWebPart dfwp = (DataFormWebPart)MyWPM.WebParts[8];
                try
                {
                    string mystring1 = dfwp.DataSourcesString;
                    dfwp.DataSourcesString = mystring1.Replace("SelectCommand=\"&lt;View&gt;&lt;/View&gt;\" ", "SelectCommand=\"&lt;View&gt;&lt;Query&gt;&lt;Where&gt;&lt;Eq&gt;&lt;FieldRef Name=&quot;Project_x0020_Name&quot;/&gt;&lt;Value Type=&quot;Text&quot;&gt;" + siteName + "&lt;/Value&gt;&lt;/Eq&gt;&lt;/Where&gt;&lt;/Query&gt;&lt;/View&gt;\" ");
                    MyWPM.SaveChanges(dfwp);
                }
                catch (Exception ex)
                {
                    LogTheError("EventHandlerName", "Error in DoSetProjectDatabaseDisplayWebPart(): " + ex.Message + " Stack Trace: " + ex.StackTrace);
                }
            }
            WebpartPageToUpdate.CheckIn("Initial Site Setup - ProjectDatabaseDisplayWebPart");
            WebpartPageToUpdate.Publish("Initial Site Setup - ProjectDatabaseDisplayWebPart");
        }

The by-now familiar getting the web part page (I think in my next revision I’ll make getting the web part page its own separate function, as I probably should have done in the first place, and pass in as parameters the library and ID) and the web part gotten by ID with a name check for validation. 

Notice in the code that the Data View web part is actually a DataFormWebPart.  You need to cast the web part explicitly as as a DataFormWebPart to get to the DataSourcesString property.  The web part in the template site is already pointing to the list containing the project requests, so all that needs to be done is to specify which record in the list we want to display in the newly created site.  A little string.Replace(), throw in some CAML code with the special characters encoded in the string, add the variable containing the siteName, and the data source is set to display the specific request in the request list.  Note that the column name in the request list is “Project Name”, but in the FieldRef parameter in the query the space needs to be replaced with ‘_x0020_”, making the value of the column name passed to the query “Project_x0020_Name”.

Modify the Appearance of a Web Part

        private void DoFixProjKickoffContentEditorWebPart()
        {
            //UPDATE the Web Part page at Lists/Project%20Eval%20Factors/ProjectKickoff.aspx
            SPFile WebpartPageToUpdate = BuildWeb.GetFile(parentSiteUrl + siteName + "/Lists/Project%20Eval%20Factors/ProjectKickoff.aspx");
            try
            {
                using (SPLimitedWebPartManager MyWPM = WebpartPageToUpdate.GetLimitedWebPartManager(PersonalizationScope.Shared))
                {
                    string WebPartTitle;
                    //System.Web.UI.WebControls.WebParts.WebPart wp = MyWPM.WebParts[1];
                    foreach (System.Web.UI.WebControls.WebParts.WebPart wp in MyWPM.WebParts)
                    {
                        if (wp.Title == "Content Editor Web Part")
                        {
                            wp.Title = "Page Guidance (Expand for additional instructions)";
                            wp.ChromeState = PartChromeState.Minimized;
                            MyWPM.SaveChanges(wp);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                LogTheError("EventHandlerName", "Error in DoFixProjKickoffContentEditorWebPart(): " + ex.Message + " Stack Trace: " + ex.StackTrace);
            }
        }

On another web part page, one of the web parts is supposed to be minimized.  For some reason, when a new site is created from the template, the web part becomes un-minimized and loses its title.  Yeah, I could have spent time troubleshooting it, but the power user had spent some time banging his head against the wall on this one, so I just added this fix.   Rather than go through getting the URL by finding the item in the library, I hard-coded the URL since this is a brute-force fix.

The web part title is changed to the proper title, and the ChromeState is set to minimized.

So that is it.  All the web parts that need changes are changed.  The web part page looks great.  Except…

Uh oh…. something happened to the KPI web part on the Welcome.aspx page!   Did you see anywhere in my code that I referenced the KPI web part?  Neither did I!  In the words of Bart Simpson, “Ididn’tdoit!”

A mystery in the next installment!

I’d love your feedback. Is there something you think I could be doing better? Are there questions about the code I haven’t answered? Am I full of it? Let me know! Feel free to use these code samples in accordance with my usage policy.

Check out Part One here (The Setup).
Check out Part Two here (The Decision).
Check out Part Three here (If You Built It…).
Check out Part Four here (…They Will Come).
Check out Part Five here (Bring ‘Em All In).
Check out Part Six here (Ride the CAML!).
Check out Part Seven here (Pointing The Way)
You are reading Part Eight.
Check out Part Nine here (Deconfigured).
Part Ten (The Log Blog) is coming!

(Did I mention that this took me a little while to build?)

More posts about SharePoint.

Advertisements

Comments on: "Automated SharePoint Site Provisioning Solution – Act Eight (Taking Part)" (1)

  1. […] There were no custom web parts on the page, just out-of-the box parts with some configuration (see my post Automated SharePoint Site Provisioning Solution – Act Eight). […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: