Monday, October 6, 2014

The Treacherous SPFolder

Let's talk about SPFolder. This class is not as simple as it seems, and can cause a lot of headaches if you are not prepared. The reason is that a non-null object of the SPFolder class does not always point to a valid folder. First of all, the Exists property has to be set to true. This is a bit unusual for the SharePoint object model to have a class whose non-null objects are only valid if the Exists property is set to true. Lists, sites, and other SharePoint objects do not work this way.

But even if the Exists property happens to be set to true, the folder is not necessarily valid. It all depends on how you obtain the reference to the folder. Consider the GetFolder() method of the SPWeb class. You can pass a folder URL to this method and get a valid folder. Sort of. Because if you are not careful and are trying to add files to the folder whose reference you obtained in this fashion (and dutifully checked whether the Exists property is set to true), you will get the System.IO.DirectoryNotFoundException - and have no idea what is causing it. Let's see, then, what GetFolder() is and how to use it properly.

Suppose there is some site collection with a subsite - let's assume it is named "t." Suppose there is a document library there named Docs. This document library has a folder F1; the F1 folder, in turn, has a folder named F2. Our task is now to obtain an SPFolder reference to the F2 folder. If we call the GetFolder() method from the t subsite, the following simple PowerShell script:

cls
$s = Get-SPSite "https://main.mini.tws.com/sites/Test12345/"
$w = Get-Spweb "https://main.mini.tws.com/sites/Test12345/t"

$f2 = $w.GetFolder("Docs/F1/F2")
$f2.ServerRelativeUrl
$f2.ParentFolder

$w.Dispose()
$s.Dispose()


will return the valid folder:

 /sites/Test12345/t/Docs/F1/F2
EffectiveRawPermissions   : FullMask
EffectiveAuditMask        : 284
ProgID                    :
ParentFolder              : Docs
ParentWeb                 : t
Url                       : Docs/F1
UniqueId                  : bd2b9455-dc49-4378-854c-fb8d1077d881
ItemCount                 : 1
Name                      : F1
ServerRelativeUrl         : /sites/Test12345/t/Docs/F1
WelcomePage               :
Files                     : {}
SubFolders                : {Docs/F1/F2}
ContainingDocumentLibrary : 0f96ab7d-227f-422c-9c2b-6418a6b84125
RequiresCheckout          : False
DocumentLibrary           : Docs
Exists                    : True
Item                      : Microsoft.SharePoint.SPListItem
Properties                : {vti_folderitemcount, vti_level, vti_listname, vti_listbasetype...}
Audit                     : Microsoft.SharePoint.SPAudit
ParentListId              : 0f96ab7d-227f-422c-9c2b-6418a6b84125
UniqueContentTypeOrder    :
ContentTypeOrder          : {Document}


So far so good, right? As expected, ParentWeb is t, and the ContainingDocumentLibrary and ParentListId all have the valid list id. And, of course, Exists is set to True.

Suppose now that we call GetFolder from the root web, pointing to the folder URL, like in this script:

cls
$s = Get-SPSite "https://main.mini.tws.com/sites/Test12345/"
$w = Get-SPWeb "https://main.mini.tws.com/sites/Test12345/t"

$f = $s.RootWeb.GetFolder("t/Docs/F1/F2")
$f.ServerRelativeUrl
$f.ParentFolder

$w.Dispose()
$s.Dispose()


The result will be:

 /sites/Test12345/t/Docs/F1/F2
EffectiveRawPermissions   : FullMask
EffectiveAuditMask        : 284
ProgID                    :
ParentFolder              : t/Docs
ParentWeb                 : Test12345
Url                       : t/Docs/F1
UniqueId                  : bd2b9455-dc49-4378-854c-fb8d1077d881
ItemCount                 : 1
Name                      : F1
ServerRelativeUrl         : /sites/Test12345/t/Docs/F1
WelcomePage               :
Files                     : {}
SubFolders                : {}
ContainingDocumentLibrary : 00000000-0000-0000-0000-000000000000
RequiresCheckout          : False
DocumentLibrary           :
Exists                    : True
Item                      :
Properties                : {vti_folderitemcount, vti_level, vti_listname, vti_listbasetype...}
Audit                     : Microsoft.SharePoint.SPAudit
ParentListId              : 00000000-0000-0000-0000-000000000000
UniqueContentTypeOrder    :
ContentTypeOrder          : 


If you look carefully, something is not right. Even though Exists is set to True, ContainingDocumentLibrary and ParentListId are sent to empty guids - this basically means you cannot add items to this folder, as you will get the above-mentioned System.IO.DirectoryNotFound exception! Also, note that this strange behavior occurs in BOTH SharePoint 2010 and SharePoint 2013.

The bottom line is that if you call GetFolder() from the web other than the one that directly contains the library with the folders, you are not going to obtain a valid folder. So, what to do if your requirements call for adding new files to folders identified by their URLs?

Luckily, there is a way around it. First of all, here is an extension method that returns a containing web for the folder:

  public static SPWeb GetContainingWeb(this SPFolder folder)
  {
            SPWeb web = null;
            using (SPSite site = new SPSite(folder.ParentWeb.Site.ID))
            {
                using (SPWeb someweb = site.OpenWeb(folder.Url, false))
                {
                    web = someweb;
                }
            }
            return web;
  }


 We can now use it to obtain a reference to a corrected folder - we obtain it by parsing the url.

public static SPFolder GetCorrectedFolder(this SPFolder folder)
{
            SPWeb containingWeb = folder.GetContainingWeb();

            string folderUrl = folder.Url;

            SPFolder correctedFolder = containingWeb.GetFolder(folderUrl);

            string[] parts = folderUrl.Split(new char[] { '/' });

            for (int i = 0; i < parts.Length - 1 && !correctedFolder.Exists; i++)
            {
                int pos = folderUrl.IndexOf("/");
                folderUrl = folderUrl.Substring(pos + 1);
                correctedFolder = containingWeb.GetFolder(folderUrl);
            }
            return correctedFolder;
 }


Now we can obtain a valid reference to a folder no matter what SPWeb we use to call the GetFolder() method on.
 

No comments:

Post a Comment