Challenges in SharePoint projects

SharePoint is a great platform for application development. However there exist some challenges that need to be kept in mind during planning, that are common across a range of SharePoint projects. Each topic is worthy of its own article. Some design aspects are easily changed on the fly, while other changes come at a great cost when discovered late, resulting in significant additional effort to remediate; hence having a seasoned architect early on in the design can be very cost effective, and reduce project risk.

Site Topology

The set of Site Collections needs to be designed for scalability and secure isolation. Too often projects attempt to save effort by unnecessarily restricting the system to function within a single Site Collection. Such a design can hit performance and scalability limits of single Site Collections. Peeling apart a Site Collection in a large production farm can be fraught with risk and take significant effort.

Design for upgrades

Design should include plans for upgrading, as patches and new versions come out; not just of SharePoint, but of SQL Server and 3rd party selected add-ons and operating systems.

3rd Party Software Dependency

Purchasing add-ons is often cost-effective and preferable to writing code, however the vendor dependency is a serious issue to be considered, including vendor viability. SharePoint 2013, for example, requires solutions (WSPs) to be rebuilt (using different DLL versions, deploying to a different hive location, using a different .NET version etc). If a vendor is not around when you upgrade, you may be forced to rewrite the application first a different way.

Flexible AMC

The full cost of a system needs to be taken into account, as well as a viable support model, for the AMC (Annual Maintenance Cost). This considers the full cost, not just for development but for Post deployment and Maintenance Support. A proper cost assessment in a business case should take into account a 3 or 5 year cost horizon.

Hardware

It’s not just the software costs that need to be taken into account, but harware costs as well, even in a multi-tenancy model. Proper costing takes into account the allocation utilized by a proposed system. One should complete an Assessment of the existing Hardware Infrastructure and Environment and provide necessary recommendations for Server Configuration and Farm Topology.

Governance

After doing the Requirement Gathering, some thought should be placed on the Governance Plan. This would identify business needs that can be achieved by OOTB features of SharePoint and which would require custom development, how the system would be authorized, users authenticated, policies, procedures, access levels, auditability and the like.

Market Awareness

This circles back to the concept of buy-vs-build. Too often developers reach into their coding bag-of-tricks, before considering what exists. All too often I’ve seen web parts written that duplicate what comes out of the box in SharePoint. Awareness of 3rd party solutions is also critical, to managing risk, timelines and project costs. Designers and developers need kept aware about market trends, new release, updates and patches. Senior team members in SharePoint can gain this through conferences, and just plain experience. Certifications are one indicator of knowledge, but that alone may not be sufficient.

Best practices

There is a wealth of knowledge in the industry on best-practices, starting from Microsoft. Development and Implementation should be done as per Microsoft suggested practices, but also considering leading authority opinions.

Scalability

Architecture and System design is done for scalability and high performance keeping in mind future data size and increase in the number of users. All too often a system that passes a demo, can’t scale to the planned level of usage. This includes handling geographic diversity, and concurrency.

Deployment Model

Now more than ever, the deployment model is key. Where once Sandbox solutions were promoted, these are now deprecated. Where once onPremises was the only option, now the App Model in Office 365 and Azure present not just viable options, but recommended approaches.

Awareness of roles

SharePoint team members should be included with specific expertise including crucially Administration, Development, Branding. A pure development approach may lead to taking a blind alley where a system is not easily maintained, or not easily branded.

Talent

Finding real SharePoint talent is a challenge. One needs to be aware that a purely .NET background is useful, but is not the complete skillset needed for successful projects.

How to start?

In short, one has to start and focus on the problem at hand. The solution comes only after the problem is crystal clear and understood. It is all too easy to jump into the technology, but must first start with the problem. One way to look at it is that there is no real solution without a specific problem.

Programmatically Configuring Metadata Navigation

Metadata navigation offers a quite clever way to navigate documents within a library. It cuts across folders, and allows drill-in for taxonomies as configured hierarchies, and also allows for a combination of fields for key filters. One can configure it manually for a library, but how can one do this programmatically? Below are the steps:

# get the SPWeb:
$web = Get-SPWeb "http://WhateverTheSPWebSiteIs"
 
# get the library:
$JPLib = $web.Lists.TryGetList("WhateverTheListIsCalled")
 
# Here's the XML object we are coding against:
$listNavSettings = [Microsoft.Office.DocumentManagement.MetadataNavigation.MetadataNavigationSettings]::GetMetadataNavigationSettings($JPLib)
 
# You can output the XML settings and easily see its configuration each step along the way with this:
$listnavSettings.SettingsXml
 
# Here's how to clear both Configured Hierarchies, and Key Filters:
$listNavSettings.ClearConfiguredHierarchies()
$listNavSettings.ClearConfiguredKeyFilters()
[Microsoft.Office.DocumentManagement.MetadataNavigation.MetadataNavigationSettings]::SetMetadataNavigationSettings($JPLib, $listNavSettings, $true)
 
# Let's get ready for a Content Type Hierarchy
$ctHierarchy = [Microsoft.Office.DocumentManagement.MetadataNavigation.MetadataNavigationHierarchy]::CreateContentTypeHierarchy()
$listnavSettings.AddConfiguredHierarchy($ctHierarchy)
 
# Add a configured Hierarchy:
$listNavSettings.AddConfiguredHierarchy($JPLib.Fields["Field Name"])
 
# Add a Content Type Key Filter; I chose this on purpose, as using "Content Type" will not work, the field to use here is "Content Type ID":
$listNavSettings.AddConfiguredKeyFilter($JPLib.Fields["Content Type ID"])
 
# Now the party ends happily with an update; note no $list.update() or $web.update() is needed:
[Microsoft.Office.DocumentManagement.MetadataNavigation.MetadataNavigationSettings]::SetMetadataNavigationSettings($JPLib, $listNavSettings, $true)

Checking for a specific permission for a specific user or group in SharePoint

While the UI allows one to easily check permissions for a given user, how can one do that iteratively?

Here’s the heart of the magic:

# first grab the user principal:
  $user = $TargetWeb.Groups[$GroupToAdd];
 
# Now let's get the Role Assignments for that user on the folder:
  $RA = $folder.RoleAssignments.GetAssignmentByPrincipal($user);
 
#Role bindings are useful
  $RoleDefBindings = $RA.get_RoleDefinitionBindings();
 
#Now let's grab the Role Definition for Contribute permission in this SPWeb:
  $roledef = $TargetWeb.RoleDefinitions["Contribute"];
 
Lastly we can check whether the role bindings for this user on this folder contains the Contribute Role Definition:
  if ($RoleDefBindings.Contains($roledef)) {...}

Some useful routines first. Note I like to predefine a “Write” permission that allows creation and editing but not deletion:

function PermRole([string] $RoleChar)
{
    switch ($RoleChar)
    {
    "R" {$res="Read"}
    "C" {$res="Contribute"}
    "W" {$res="Contribute wo delete"}
    "D" {$res="Manage Hierarchy"}  #aka design, for setting permissions
    default {$res=$null}
    }
    return $res;
}
 
# Routine for adding permission based on passing in a character for the role definition to be granted:
function AddPerm ([string] $RoleChar, [string] $RoleGroup)
{ #JPItem/f and TargetWeb are implied and not passed as parms for efficiency!
    if ((!$RoleChar) -or (!$RoleGroup))
    {
    return; #race to be efficient on NullOp
    }
 
    $RoleValue=PermRole($RoleChar);
    if (!$RoleValue) 
    {
    Write-Host -ForegroundColor -darkred "ok, expected Role, but got none, for $($RoleChar)"
    return; 
    }
 
    try
    {
    #CONTROVERSIAL!
    if ($RoleChar -eq "W")  #wipes out reads etc.
    {
    RemovePerm $RoleGroup
    }
 
    try
    {
        $user = $TargetWeb.ensureuser($RoleGroup)
    }
    catch  #if the above fails, user is likely not a user, but in fact a group, let's retry as group
    {
        $user = $TargetWeb.Groups[$RoleGroup]
    }
    $roledef = $TargetWeb.RoleDefinitions[$RoleValue]
    $roleass = New-Object Microsoft.SharePoint.SPRoleAssignment($user)
    $roleass.RoleDefinitionBindings.Add($roledef)
 
    $f1.RoleAssignments.Add($roleass)  #This is SPFolder specific in this routine
    }
    catch
    {
    Write-Host -ForegroundColor DarkRed "ERR: Can't Assign $($RoleGroup)"
    }
}

Let’s first establish the libraries to look at across all webs and site collections:

$libsArrStr="Library name 1|Library name 2"
$LibsArr=$libsArrStr.split("|")
$GroupToAdd = "Department Contributors"
$Site = "ht tp://SharePoint/sites/SiteOfInterest"
 
$TargetWeb=$web=get-spweb $Site;
 
Write-Host "==>working in $($web.url)"
 
for ($j=0; $j -lt $LibsArr.count; $j++)
    {
        $libStr=$LibsArr[$j];
        $list=$web.Lists.TryGetList($libStr)
 
        if ($list -eq $null)
        {
            Write-Host -ForegroundColor DarkRed "List not found"
        }
        else
        {
        for ($fi=0; $fi -lt $list.Folders.Count; $fi++)
        {
            $f1 = $list.Folders.get_Item($fi)
            $f = $f1.folder;
 
      write-host -f green "The Library $($listName) exists in the site $($web.url), about to set folder Perms" 
 
        try
        {
            #the rule is if this field has data, make the user a Contributor
            $f1.ResetRoleInheritance(); #badda-bing, security is inherited
            $isWritable = ($f.item["TargetMetadata"] -ne $null);
            if (!$isWritable)
            {
                # nul op, already inherited
            }
                else  #let's see whether to break perms, based on whether the group already has Contribute
                {
                #let's see if the user has Contributor rights already; if so, no need to break inheritence
                                             
                $user = $TargetWeb.Groups[$GroupToAdd]
 
                $RA = $f1.RoleAssignments.GetAssignmentByPrincipal($user)
                $RoleDefBindings = $RA.get_RoleDefinitionBindings()
                $roledef = $TargetWeb.RoleDefinitions["Contribute"]
                if ($RoleDefBindings.Contains($roledef))  # user is already a Contributor, let's do nothing
                {
                }
                else
                {
                    $f1.BreakRoleInheritance($true);  #minimalist approach
                    addPerm "C"     $GroupToAdd                            
                    }
            }
        }
        catch
        {
            Write-Host problems setting perms
        }
    } #Folder processing for loop $fi
    } # list found
} #for loop $j

Enhancing SharePoint breadcrumbs when navigating deeply nested folders

I had an interesting challenge to improve breadcrumbs in navigating a deeply folder nested library. The nice breadcrumb dropdown icon is actually gone in SP2013 by default; that can easily be re-enabled via CSS. an onMouseOver event action in JavaScript can eliminate the need to click on it. However to improve the breadcrumb navigation, I created an SPWeb feature to enhance breadcrumbs by overriding the OnPreRender event. The OnPreRender should only occur if the ‘Visible’ property of the web control is set to true. The PreRender event is raised just before the page is about to render its contents. This is the last chance we have to modify the page output before it is received by the browser.

I deployed this web Scoped solution that overrides the breadcrumbs with OnPreRender page processing, and provides the full folder path, clickable for each link. It recursively searches the page for the “TitleBreadcrumb” control, and builds a replacement, adding an a href, folder link name, and breadcrumb image iteratively. It only affects the page when you are navigating within a library.

namespace MyDocLibraryBreadcrumb
{
    public class MyDocLibBreadcrumbDelegateControl : WebControl 
    {
        protected override void OnPreRender(EventArgs e)
        {
 
            try
            {
                base.OnPreRender(e);
 
                // Get the path to the current folder
                string path = Context.Request.QueryString["RootFolder"];
 
                // if there's no path then there is nothing to do; it implies are are not in the context of the library 
                if (String.IsNullOrEmpty(path))
                {
                    return;
                }
 
                // Let's get the current folder
                SPWeb web = SPContext.Current.Web;
                SPFolder currentFolder = web.GetFolder(path);
 
                // Let's find the breadcrumb control on the current page - it's a ListProperty control where the property is  "TitleBreadcrumb". 
                Control c = Utils.FindRecursive(Page.Controls, ctrl => ctrl is ListProperty && ((ListProperty)ctrl).Property == "TitleBreadcrumb");
 
                // If not found, nothing to do, and we are not likely in a library. 
                if (c == null)
                    return;
 
                // Let's subsitute the OOTB breadcrumb control with our replacement enhanced one
                var parent = c.Parent;
                var index = parent.Controls.IndexOf(c);
                parent.Controls.RemoveAt(index);
                parent.Controls.AddAt(index, new LiteralControl { Text = GetReplacementBreadCrumbOutput(currentFolder) });
            }
            catch (Exception ex)
            {
                // log errors quietly 
                Utils.WriteMyLog(Utils.GetErrorInfo(ex));
            }
        }
 
 
        /// SPFolder is the parameter to create navigation to
        /// returns the HTML output
        private string GetReplacementBreadCrumbOutput(SPFolder folder)
        {
            List<BreadcrumbNodeData> nodes = new List<BreadcrumbNodeData>();
 
            // Collect a path from current folder to its root folder
            SPFolder nodeFolder = folder;
            while (nodeFolder != null)
            {
                // If we're in folder use the folder name as a title. If not use a library title enstead.
                BreadcrumbNodeData node  = new BreadcrumbNodeData();
                node.Url = nodeFolder.ServerRelativeUrl;
                if (string.IsNullOrEmpty(nodeFolder.ParentFolder.Url))
                {
                    if (nodeFolder.DocumentLibrary != null)
                        nodes.Add(new BreadcrumbNodeData
                                      {Title = nodeFolder.DocumentLibrary.Title, Url = nodeFolder.ServerRelativeUrl});
                }
                else
                {
                    nodes.Add(new BreadcrumbNodeData { Title = nodeFolder.Name, Url = nodeFolder.ServerRelativeUrl });
                }
 
                nodeFolder = string.IsNullOrEmpty(nodeFolder.ParentFolder.Url) ? null : nodeFolder.ParentFolder;
            }
 
            // Reverse the collected path because the root folder must be on the left in bredcrumb
            nodes.Reverse();
 
            // Create an HTML output similar to original. An arrow image we've created from the original
            string htmlOutput = String.Empty;
 
            foreach (var node in nodes)
            {
                if (node != nodes.Last())
                    htmlOutput +=
                        String.Format(
                            @"<A href=""{0}"">{1}</A> <IMG style=""vertical-align:middle"" alt=: src=""/_layouts/images/MyDocLibraryBreadcrumb/breadcrumb_arrow.png""/> ", node.Url, node.Title);
                else
                {
                    htmlOutput += node.Title;
                }
            }
 
            return htmlOutput;
        }
    }
 
 
    /// temporary class to holds navigation node data
 
    public class BreadcrumbNodeData
    {
        /// Title for URL (it will be a folder name)
        public string Title { get; set; }
 
        /// Url to navigate on click (it will be a server relative URL of the folder)
        public string Url { get; set; }
    }
 
    public class Utils
    {
        public static string GetErrorInfo(Exception ex)
        {
            string result = "ex.Message=" + ex.Message;
            result += ex.InnerException == null ? "|ex.StackTrace=" + ex.StackTrace : String.Empty;
 
            if (ex.InnerException != null)
                result += "[INNER EXCEPTION: ]" + GetErrorInfo(ex.InnerException);
 
            return result;
        }
 
        public static void WriteMyLog(string text)
        {
            SPDiagnosticsService.Local.WriteTrace(0,
                                                  new SPDiagnosticsCategory("MyDocLibBreadcrumbDelegateControl", TraceSeverity.High,
                                                                            EventSeverity.Error),
                                                  TraceSeverity.High, text, null);
        }
 
 
 
        /// Finds a control based on provided criteria inside controls hierarchy
 
        /// <param name="controls">A Controls collection to start search</param>
        /// <param name="criteria">A criteria to return control</param>
        /// <returns>The founded control </returns>
        public static Control FindRecursive(ControlCollection controls, Func<Control, bool> criteria)
        {
            foreach (Control control in controls)
            {
                if (criteria(control))
                    return control;
 
                var innerControl = FindRecursive(control.Controls, criteria);
                if (innerControl != null)
                    return innerControl;
            }
 
            return null;
        }
    }
}

Converting SQL for embedded use within VBA

Converting SQL for embedded use within VBA

After creating and testing SQL to embed within a VB or VBA application, it needs to be added to a VB project in usable strings. How to convert your SQL easily without introducing errors? Here’s a PowerShell script that takes in a SQL file with a header (SQL) and condenses it into 80+ character strings for copying into your VB code.

$MyFile=get-content -Path "sql.txt" 
$outfile = "sqlNew.txt"
Remove-Item $outfile -ErrorAction SilentlyContinue
$str=$null;
$firstLine=$true;
for ($i=0; $i-lt $MyFile.Count; $i++)
{
if ($str.length -gt 80)
{
    if ($firstLine)
    {
        $str = '"' + $str + '" _'
        $firstLine=$false;
    }
    else
    {
        $str = '& "' + $str + '" _'
    }
    Add-Content $outfile "$($str)`n"
    $str=$null;
}
$nextLine = $MyFile[$i]
$nextLine = $nextLine.Replace("`t"," ");
$nextLine = $nextLine.Replace("  "," ");$nextLine = $nextLine.Replace("  "," ");$nextLine = $nextLine.Replace("  "," ");
$idx = $nextLine.indexof("--");
if ($idx -ge 0)
{
    $nextLine = $nextLine.Substring(0,$idx)
}
 
$str = $str + ' ' + $nextLine;
 
}
 
if ($firstLine)
{
    $str = '"' + $str + ' "'
}
else
{
    $str = '& "' + $str + ' "'
}
Add-Content $outfile "$($str)`n"
$str = $null;

Jump starting a multi-server SharePoint install

Jump-starting a multi-server SharePoint installation

After a multiple server SharePoint install, I noticed some issues with SharePoint, including User Profile Service started, but the properties not appearing correctly. Enabling MMS (Managed Metadata Service and ensuring a proxy connection to the web applications was part of the solution. Clearing the SharePoint cache didn’t help. Neither did a Restart of IIS and the SP Timer service in Services. Msc.

When I tried to run a PSConfig, using the command below, it generated a SPUpdatedConcurrencyException, no matter how I tried, even after reboots:

PSCONFIG.EXE -cmd upgrade -inplace b2b -wait -force

Here’s what worked, the following command had to be run, note the “installfeatures” cmd:

PSConfig.exe -cmd upgrade -inplace b2b -force -cmd applicationcontent -install -cmd installfeatures

[/av_textblock]

Fixing Checked Out Files

Fixing Checked Out Files

I ran into a challenge this evening with a monstrously large library filled with 5,000+ folders with 47,000+ files.

What to do?

Firstly, things don’t work correctly until the list view threshold is temporarily lifted. Once that is done, we can iterate through the files, take the checked out ones over, and force the check in.

Here’s how:

$root = get-spweb "ht tp://SharePoint/sites/site"
$lib = $root.lists["LibraryName"]
$x = $lib.CheckedOutFiles
$count = $x.Count
for ($i=$count-1; $i -ge 0; $i--)
{
$Checkeditem = $x.get_Item($i)
$Checkeditem.TakeOverCheckOut()
 
$libitem = $lib.GetItemById($Checkeditem.listitemid)
$libitem.File.CheckIn("")
Write-Host -NoNewline "."
}

Custom SQL Reporting in MS-Project Server 2013

Custom SQL Reporting in MS-Project Server 2013

It is easy to navigate the database schema in MS-Project Server to generate reports.  The SQL can be embedded in an ODC, or can be used within PowerPivot.  If joining Task and Project data, there’s a challenge of rollups.  The first challenge is avoiding double-counting from summary tasks.  The solution is to exclude them on the join, adding this condition:

where TaskIsSummary=0

The next source for double-counting are external tasks; those exposed through cross-linking tasks in separate projects. We can exclude both this way:

where TaskIsSummary=0 and TaskIsExternal = 0

The next problem is if merging task and project tables, project values would roll up incorrectly, however such numeric fields can be pro-rated to the project work, as long as we avoid divide-by-zero errors, here’s how, referencing a custom field called “Budgeted Costs”; note how its value is proportionate to the task work:

, case
 when [MSP_EpmProject_UserView].[Budgeted Costs] = 0 THEN 0
 when MSP_EpmTask_UserView.TaskRegularWork = 0 THEN 0
 when MSP_EpmProject_UserView.ProjectWork = 0 THEN 0
 else
 [MSP_EpmProject_UserView].[Budgeted Costs] * ( MSP_EpmTask_UserView.TaskRegularWork/ MSP_EpmProject_UserView.ProjectWork )
 END as [Budgeted Costs]
 
FROM dbo.MSP_EpmProject_UserView INNER JOIN dbo.MSP_EpmTask_UserView
ON MSP_EpmProject_UserView.ProjectUID = MSP_EpmTask_UserView.ProjectUID
where TaskIsSummary=0 and TaskIsExternal = 0
ORDER BY MSP_EpmProject_UserView.ProjectName, MSP_EpmTask_UserView.TaskIndex, MSP_EpmTask_UserView.TaskName

One step further, we can do the same using task assignment data, here’s what that looks like using the assignment work:

, case
 when [MSP_EpmProject_UserView].[Budgeted Costs] = 0 THEN 0
 when MSP_EpmAssignment_UserView.AssignmentWork = 0 THEN 0
 when MSP_EpmProject_UserView.ProjectWork = 0 THEN 0
 else
 [MSP_EpmProject_UserView].[Budgeted Costs] * ( MSP_EpmAssignment_UserView.AssignmentWork/ MSP_EpmProject_UserView.ProjectWork )
 END as [Budgeted Costs]
 
,[MSP_EpmResource_UserView].[Cost Type]
 
,[MSP_EpmResource_UserView].[Resource Departments]
 ,[MSP_EpmResource_UserView].[RBS]
 ,[MSP_EpmResource_UserView].[Resource Title] FROM dbo.MSP_EpmProject_UserView INNER JOIN dbo.MSP_EpmTask_UserView ON MSP_EpmProject_UserView.ProjectUID = MSP_EpmTask_UserView.ProjectUID LEFT OUTER JOIN dbo.MSP_EpmAssignment_UserView ON MSP_EpmTask_UserView.TaskUID = MSP_EpmAssignment_UserView.TaskUID AND MSP_EpmTask_UserView.ProjectUID = MSP_EpmAssignment_UserView.ProjectUID LEFT OUTER JOIN dbo.MSP_EpmResource_UserView ON MSP_EpmAssignment_UserView.ResourceUID = MSP_EpmResource_UserView.ResourceUID
where TaskIsSummary=0 and TaskIsExternal = 0
ORDER BY MSP_EpmProject_UserView.ProjectName, MSP_EpmTask_UserView.TaskIndex, MSP_EpmTask_UserView.TaskName

Running SharePoint 2013 search within a limited RAM footprint

Running SharePoint 2013 search with limited RAM

SharePoint 2013 search is very powerful, however if you have limited server resources, it can easily get the better of your environment.  I’ve seen a small SharePoint 2013 environment go unstable, with w3p processes crashing, ULS logs filling with low RAM errors, and search index going into “Degraded” mode during a crawl, and end-user search attempts returning correlation errors, and even sites and Central Admin returning 500 errors; all for wont of a few more GB of RAM.  An IIS Reset gets the server responsive again, and an index reset will get SharePoint crawling again, but outside of tossing in precious RAM chips, what’s a caring administrator to do?  Let’s first see how to determine whether your search index is degraded:

Get-SPEnterpriseSearchServiceApplication | Get-SPEnterpriseSearchStatus
 
Name State Description
---- ----- -----------
IndexComponent1 Degraded
Cell:IndexComponent1-SPb5b3474c2cdcI.0.0 Degraded
Partition:0 Degraded
AdminComponent1 Active
QueryProcessingComponent1 Active
ContentProcessingComponent1 Active
AnalyticsProcessingComponent1 Active
CrawlComponent0 Active

In the example above, note the Index component is degraded. In Central Admin, simply do an Index Reset to get things back on foot, and restart the World Web Publishing to kick-start IIS and its app pools. In the command below, we’ll lower the priority of Search, so it doesn’t blow up our underresourced farm:

set-SPEnterpriseSearchService -PerformanceLevel Reduced

Next, let’s limit the RAM utilized by the NodeRunners; these are the processes that handle search crawling. You can find this on C: or perhaps a different drive letter on your system:

C:Program FilesMicrosoft Office Servers15.0SearchRuntime1.0

Open text file (notepad is fine, especially if your farm is wheezing from being RAM challenged, here’ the XML file: file noderunner.exe.CONFIG
Change value from 0 to 180. Note I would not run with less than 180MB per nodeRunner, as I’ve seen search components fail to start as a result.

 

Try another crawl, with a more RAM stable experience.

Here’s how to tell where your index is located on disk:

$ssi = Get-SPEnterpriseSearchServiceInstance
$ssi.Components

Here’s how to get the topology and the Index component:

$ssa = Get-SPEnterpriseSearchServiceApplication
$active = Get-SPEnterpriseSearchTopology -SearchApplication $ssa -Active
$iComponent = $active | Get-SPEnterpriseSearchComponent IndexComponent1

The SharePoint internals behind an Append text field

SharePoint internals of the Append text field

In configuring a SharePoint field, there’s an option to “Append Changes to Existing Text” for Multi-line text fields. This setting requires versioning be enabled. Let’s delve into why versioning must be enabled for this feature to be enabled.

In a nutshell, SharePoint puts the latest value within the property bag, which you can see if you grab the SPItem object, and dump the read-only SPItem.xml. First let’s examine in PowerShell.

# let's get the web:
$web=Get-SPWeb http ://SharePoint/sites/Site/web
 
# let's get the list:
$list = $web.Lists["Name of List"]
 
# let's get the SPItem:
$q=$list.GetItemById(4567)
 
#Let's set the internal field name for later reference:
$FName = "Internal Field Name"

A more elegant way is by addressing by URL directly:

$url="http ://SharePoint/sites/Site/web/Listname/4567_.000"
$item = $web.GetListItem($url)

Then we can peek into the property bag:

$item.xml

It is worth noting there are times when SharePoint does not put the latest value in the property bag. In fact the property is absent altogether, although the versions continue to capture the append text. One theory is that this occurs to SP2007 lists that have been upgraded. If you experience this behavior, read below for accessing all of the append text.

But where are the previous appends? How can we see them? The trick is to walk through the versions, grabbing the version SPItem, and then grab the field value.

foreach ($v in $item.versions)
{
 $v.get_Item($FName)
}

In C# we extract in a function such as this:

public static string GetVersionedMultiLineTextAsPlainText(int ID, string field,SPList list)
 {
     SPListItem item = list.GetItemById(ID);
     StringBuilder sb = new StringBuilder();
     foreach (SPListItemVersion version in item.Web.Lists[item.ParentList.ID].Items[item.UniqueId].Versions)
     {
         SPFieldMultiLineText CommentsField = version.Fields.GetFieldByInternalName(field) as SPFieldMultiLineText;
         if (CommentsField != null)
         {
             string comment = CommentsField.GetFieldValueAsText(version[field]);
             if (comment != null && comment.Trim() != string.Empty)
             {
                 sb.Append("");
                 sb.Append(version.CreatedBy.User.Name).Append(" (");
                 sb.Append(version.Created.ToString("MM/dd/yyyy hh:mm tt"));
                 sb.Append(") ");
                 sb.Append(comment);
             }
         }
     }
     return sb.ToString();
 }