Second Life of a Hungarian SharePoint Geek

September 19, 2010

Publishing files stored in the file system through external list

Filed under: BCS, External list, SP 2010 — Tags: , , — Peter Holpar @ 01:35

Although it is rather common at companies that they store significant amount of the their files on SharePoint, there are always exceptions, files that are stored at the file system. We created special web parts and other alternative solutions even for SPS 2003 / WSS 2.0 and MOSS 2007 / WSS 3.0 to make these files accessible in a read-only way through the SharePoint portal user interface. As illustrated in this post, creating a similar solution is easily possible based on the external list feature of SharePoint 2010.

Unfortunately due to time and space limitations I can only describe the most interesting parts and ideas of the implementation and refer to another posts to help you better understand the process of customization.

Just to start at the beginning, if you are new to external lists that are built on .NET assemblies, I think reading and understanding the following articles will help at the first steps:

Creating .NET Assemblies That Aggregate Data from Multiple External Systems for Business Connectivity Services in SharePoint Server 2010

External list example demonstrating .NET connectivity assembly and custom field type

Notice (as usual): The goal of this sample only to illustrate the capabilities and the limitations of BCS and external lists. The code example is not intended to be used in production. If you would like to enhance it, you are free to use it at your own risk. You should definitely deal with error handling, concurrency and security issues I would not like to detail here due to lack of time.

For example, in this sample only makes it possible to display files from the local SharePoint front-end machine, that is typically not a file server role in a real-live scenario. Although the sample can be extended to make remote file server access available, describing the necessary steps is really beyond the scope of this post, similar to implementing the read-write access.

We want to create an instance of our external list on deployment, so added a new list instance item to our project having the following Elements.xml:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <ListInstance Title="Files"
  4.                 OnQuickLaunch="TRUE"
  5.                 TemplateType="600"
  6.                 FeatureId="00bfea71-de22-43b2-a848-c05709900100"
  7.                 Url="Lists/Files"
  8.                 CustomSchema="Files\Schema.xml"
  9.                 Description="Filesystem demo external list">
  10.     <DataSource>
  11.       <Property Name="LobSystemInstance" Value="FileSystemModel" />
  12.       <Property Name="EntityNamespace" Value="FileSystem.FileSystemModel" />
  13.       <Property Name="Entity" Value="FileSystemEntity" />
  14.       <Property Name="SpecificFinder" Value="ReadItem" />
  15.     </DataSource>
  16.   </ListInstance>
  17. </Elements>

Notice the CustomScheama attribute of the ListInstance node. It makes possible to deploy a custom list schema file for our list as described by Stefan Stanev:

SharePoint 2010 – ListInstance CustomSchema attribute

It is a great help as it allows you to configure values automatically on deployment instead of setting those values (for example, item order, menu binding) that you should otherwise configure using either the view settings UI or SharePoint Designer.

  1. <List Title="Files" QuickLaunchUrl="Lists/Files/GetFilesByFolder.aspx" Direction="none" Url="Lists/Files" BaseType="0" Type="600" DontSaveInTemplate="TRUE" DisableGridEditing="TRUE" NoCrawl="TRUE" DisallowContentTypes="TRUE" BrowserFileHandling="Permissive" FolderCreation="FALSE" DisableAttachments="TRUE" Catalog="FALSE" SendToLocation="|" ImageUrl="/_layouts/images/itebl.png" xmlns:ows="Microsoft SharePoint" xmlns:spctf="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms" xmlns="http://schemas.microsoft.com/sharepoint/">
  2.   <MetaData>
  3.     <ContentTypes>
  4.       <ContentType ID="0x01" Name="Item" Group="List Content Types" Description="Create a new list item." FeatureId="{695b6570-a48b-4a8e-8ea5-26ea7fc1d162}">
  5.         <Folder TargetName="Item" />
  6.         <FieldRefs>
  7.           <FieldRef ID="{c042a256-787d-4a6f-8a8a-cf6ab767f12d}" Name="ContentType" />
  8.           <FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title" Required="TRUE" ShowInNewForm="TRUE" ShowInEditForm="TRUE" />
  9.         </FieldRefs>
  10.       </ContentType>
  11.     </ContentTypes>
  12.     <Fields>
  13.       <Field DisplayName="BDC Identity" Hidden="FALSE" Name="BdcIdentity" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="BdcIdentity" Type="Text" />
  14.       <Field DisplayName="Path" Hidden="FALSE" Name="Path" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Path" Type="Text" />
  15.       <Field DisplayName="Name" Hidden="FALSE" Name="Name" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Name" Type="Text" />
  16.       <Field DisplayName="Icon" Hidden="FALSE" Name="Icon" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Icon" Type="Text" />      
  17.       <Field DisplayName="Size" Hidden="FALSE" Name="Size" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Size" Type="Integer" />
  18.       <Field DisplayName="Created" Hidden="FALSE" Name="Created" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Created" Type="DateTime" />
  19.       <Field DisplayName="Last modified" Hidden="FALSE" Name="LastModified" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="LastModified" Type="DateTime" />
  20.     </Fields>
  21.     <Views>
  22.       <View DisplayName="GetFilesByFolder" DefaultView="TRUE" BaseViewID="1" Type="HTML" MobileView="TRUE" MobileDefaultView="TRUE" ImageUrl="/_layouts/images/generic.png" XslLink="FileSystem.xsl" WebPartZoneID="Main" WebPartOrder="0" Url="GetFilesByFolder.aspx" SetupPath="pages\viewpage.aspx">
  23.         <XslLink>FileSystem.xsl</XslLink>
  24.         <Method Name="GetFilesByFolder">
  25.           <!– don't forget to alter the value of the path parameter to match your local settings! –>
  26.           <Filter Name="path" Value="C:\Data\Temp\SampleFiles" />
  27.         </Method>
  28.         <Query>
  29.           <OrderBy>
  30.             <!– folders come first, then list of files –>
  31.             <FieldRef Name="IsFolder" Ascending="FALSE"/>
  32.             <!– order by name –>
  33.             <FieldRef Name="Name" />
  34.           </OrderBy>
  35.         </Query>
  36.         <ViewFields>
  37.           <FieldRef Name="Icon" />
  38.           <FieldRef Name="Name" ListItemMenu="TRUE" LinkToItem="TRUE" />
  39.           <FieldRef Name="Size" />
  40.           <FieldRef Name="Created" />
  41.           <FieldRef Name="LastModified" />
  42.         </ViewFields>
  43.         <RowLimit Paged="TRUE">30</RowLimit>
  44.         <Aggregations Value="Off" />
  45.       </View>
  46.     </Views>
  47.   </MetaData>
  48. </List>

Furthermore, the View node in the list schema contains a link to the XSL file (see the XslLink attribute and the subnode having the same name) used on the rendering of the external list, so it is just another great way for making customizations on deployment.

So we added a custom Schema.xml file (Understanding Schema.xml Files describes the content of this file) to the list instance and also added an XSL file called FileSystem.xsl to the project to a mapped folder called XSL in the Layouts folder to deploy the file to the {SharePointRoot}\Template\Layouts\XSL\ path that is the standard location of the XSL files used by external lists (XsltListViewWebPart). See a bit more details about XSL templates in the following post on Yaroslav Pentsarskyy’s blog.

Using custom XSL list rendering templates in SharePoint 2010

The FileSystem.xsl file looks like this:

  1. <xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:d="http://schemas.microsoft.com/sharepoint/dsp" version="1.0" exclude-result-prefixes="xsl msxsl ddwrt" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:asp="http://schemas.microsoft.com/ASPNET/20" xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:SharePoint="Microsoft.SharePoint.WebControls" xmlns:ddwrt2="urn:frontpage:internal" xmlns:o="urn:schemas-microsoft-com:office:office">
  2.   <xsl:include href="/_layouts/xsl/main.xsl"/>
  3.   <xsl:include href="/_layouts/xsl/internal.xsl"/>
  4.   <xsl:param name="AllRows" select="/dsQueryResponse/Rows/Row[$EntityName = '' or (position() &gt;= $FirstRow and position() &lt;= $LastRow)]"/>
  5.   <xsl:param name="dvt_apos">'</xsl:param>
  6.   <xsl:variable name="BdcIdToken" select="'_BdcId_'" />
  7.   <xsl:template name="FieldRef_Text_body.Icon" ddwrt:dvt_mode="body" match ="FieldRef[@Name='Icon']" mode="Text_body" ddwrt:ghost="" xmlns:ddwrt2="urn:frontpage:internal">
  8.     <xsl:param name="thisNode" select="."/>
  9.     <xsl:variable name="text" select="$thisNode/@Icon" />
  10.     <xsl:choose>
  11.       <xsl:when test="contains($text, $BdcIdToken)">
  12.         <xsl:variable name="before" select="substring-before($text, $BdcIdToken)" />
  13.         <xsl:variable name="after" select="substring-after($text, $BdcIdToken)" />
  14.         <xsl:value-of select="concat($before, $thisNode/@BdcIdentity, $after)" disable-output-escaping ="yes"/>
  15.       </xsl:when>
  16.       <xsl:otherwise>
  17.         <xsl:value-of select="$text" disable-output-escaping ="yes"/>
  18.       </xsl:otherwise>
  19.     </xsl:choose>
  20.   </xsl:template>
  21.   <xsl:template name="FieldRef_body.Size" ddwrt:dvt_mode="body" match="FieldRef[@Name='Size']" mode="body" ddwrt:ghost="" xmlns:ddwrt2="urn:frontpage:internal">
  22.     <xsl:param name="thisNode" select="."/>
  23.     <xsl:if test="$thisNode/@IsFolder != 'Yes'">
  24.       <xsl:value-of select="format-number($thisNode/@Size, '###,###,###')"/>
  25.     </xsl:if>
  26.   </xsl:template>
  27. </xsl:stylesheet>

Of course, you can create these schema and .xsl files by editing XML directly if you want, but it is much easier to create the customizations through the SharePoint UI and SharePoint Designer, then exporting the result into a site template file (.wsp) that you can either import into Visual Studio 2010 or use its content directly. You can learn more about this process here:

Deploying an External List via Feature Using CAML

Based on my experience the method described in the post works only for subsites. For site collection roots I have not found the Save Site as Template option in Site Settings, so I made the customizations on a subsite.

One point I failed to configure this way was that my list does not appear on the Quick Launch, although I’ve set both the QuickLaunchUrl in the schema file and the correct value for the OnQuickLaunch attribute in the elements file.

I’ve also tried to set field attributes like ShowInDisplayForm in Schema.xml to alter the visibility of fields on form, but without success.

But let’s back to our business entity representing a file in the file system. The definition of our LOB system shown below:

  1. <LobSystem Name="FileSystemModel" Type="DotNetAssembly">
  2.   <LobSystemInstances>
  3.     <LobSystemInstance Name="FileSystemModel" />
  4.   </LobSystemInstances>
  5.   <Entities>
  6.     <Entity Name="FileSystemEntity" Namespace="FileSystem.FileSystemModel" EstimatedInstanceCount="1000" Version="1.0.0.35">
  7.       <Properties>
  8.         <Property Name="Class" Type="System.String">FileSystem.FileSystemModel.FileSystemEntityService, FileSystemModel</Property>
  9.       </Properties>
  10.       <Identifiers>
  11.         <Identifier Name="Path" TypeName="System.String" />
  12.       </Identifiers>

And this is the .NET class for the business entity:

  1. namespace FileSystem.FileSystemModel
  2. {
  3.     public class FileSystemEntity
  4.     {
  5.         public String Path { get; set; }
  6.         public String RootPath { get; set; }
  7.         public String Name { get; set; }
  8.  
  9.         public String Icon
  10.         {
  11.             get
  12.             {
  13.                 String icon = String.Empty;
  14.                 SPContext context = SPContext.Current;
  15.  
  16.                 if (context != null)
  17.                 {
  18.                     SPWeb web = context.Web;
  19.                     
  20.                     if (IsFolder)
  21.                     {
  22.                         String relativePath = String.IsNullOrEmpty(RootPath) ? String.Empty : Path.Substring(RootPath.Length);
  23.                         icon = String.Format("<DIV><a href='?path={0}'>" +
  24.                             "<img title='{1}' alt='{1}' src='{2}/_layouts/images/folder.gif' border='0'/></a></DIV>",
  25.                                                     relativePath,
  26.                                                     Name,
  27.                                                     SPContext.Current.Site.Url
  28.                                                     );
  29.                     }
  30.                     else
  31.                     {
  32.                         icon = String.Format("<DIV><a href='{2}/_layouts/DownloadExternalData.aspx" +
  33.                             "?EntityNamespace=FileSystem.FileSystemModel" +
  34.                             "&EntityName=FileSystemEntity" +
  35.                             "&LobSystemInstanceName=FileSystemModel" +
  36.                             "&StreamAccessorName=FileAccessor" +
  37.                             "&IsXmlEncodedStreamName=true&ItemId=_BdcId_'>" +
  38.                             "<img title='{1}' alt='{1}' src='{2}/_layouts/images/{0}' border='0'/></a></DIV>",
  39.                                                     SPUtility.MapToIcon(web, Name, String.Empty, IconSize.Size16),
  40.                                                     Name,
  41.                                                     SPContext.Current.Site.Url
  42.                                                     );
  43.                     }
  44.                 }
  45.                 return icon;
  46.             }
  47.         }
  48.  
  49.         // FileInfo.Length is type of long but
  50.         // Int64 (long) is not supported by external lists
  51.         public int? Size { get; set; }
  52.         public DateTime? Created { get; set; }
  53.         public DateTime? LastModified { get; set; }
  54.         public bool IsFolder { get; set; }
  55.     }
  56. }

Note the nullable properties. Null values will be rendered as empty cells by the external list.

You can ignore the relative complex content of the Icon property for now, you will understand its significance by the end of the post. One of the questions I had to decide the data type of the Size property. Since I wanted to be able to order the items by numerical values of the file size and not alphabetical order of the file size (for example 9 would be larger than 100 in this case) I decided to go with numerical values. There is however a limitation you should be aware of. The file size is handled by .NET as long (Int64) data type, but external lists do not support this data type, as stated in the following article:

Using the SharePoint List Object Model and the SharePoint Client Object Model with External Lists

“The following .NET Framework types are not supported by external lists: System.GUID, System.Object, System.URI, System.UInt64, and System.Int64. Therefore, if one of the fields of the external list are of the .NET Framework types listed here, these fields are omitted.”

So I selected Int32 for this example and hope nobody would like to publish and download files through SharePoint UI over this size limit.

To add some minor formatting to the file size, I’ve applied the following pattern to the numbers in the XSL:

<xsl:value-of select="format-number($thisNode/@Size, ‘###,###,###’)"/>

Also note in the XSL, that we disable output escaping for the Icon field. It causes the content of the field to be interpreted as HTML.

The definition of the Finder method in the model file:

  1. <Method Name="GetFilesByFolder" IsStatic="false">
  2.   <FilterDescriptors>
  3.     <FilterDescriptor Name="path" Type="Comparison" FilterField="path" />
  4.   </FilterDescriptors>
  5.   <Parameters>
  6.     <Parameter Name="path" Direction="In">
  7.       <TypeDescriptor Name="path" TypeName="System.String" AssociatedFilter="path" />
  8.     </Parameter>
  9.     <Parameter Name="returnParameter" Direction="Return">
  10.       <TypeDescriptor TypeName="System.Collections.Generic.IEnumerable`1[[FileSystem.FileSystemModel.FileSystemEntity, FileSystemModel]]" IsCollection="true" Name="FileSystemEntityList">
  11.         <TypeDescriptors>
  12.           <TypeDescriptor TypeName="FileSystem.FileSystemModel.FileSystemEntity, FileSystemModel" Name="FileSystemEntity">
  13.             <TypeDescriptors>
  14.               <TypeDescriptor TypeName="System.String" IdentifierName="Path" Name="Path" />
  15.               <TypeDescriptor TypeName="System.Boolean" Name="IsFolder" />
  16.               <TypeDescriptor TypeName="System.String" Name="Name" />
  17.               <TypeDescriptor TypeName="System.String" Name="Icon" />
  18.               <TypeDescriptor TypeName="System.Int32" Name="Size" />
  19.               <TypeDescriptor TypeName="System.DateTime" Name="Created">
  20.                 <Interpretation>
  21.                   <NormalizeDateTime LobDateTimeMode="Local" />
  22.                 </Interpretation>
  23.               </TypeDescriptor>
  24.               <TypeDescriptor TypeName="System.DateTime" Name="LastModified" DefaultDisplayName="Last modified">
  25.                 <Interpretation>
  26.                   <NormalizeDateTime LobDateTimeMode="Local" />
  27.                 </Interpretation>
  28.               </TypeDescriptor>
  29.             </TypeDescriptors>
  30.           </TypeDescriptor>
  31.         </TypeDescriptors>
  32.       </TypeDescriptor>
  33.     </Parameter>
  34.   </Parameters>
  35.   <MethodInstances>
  36.     <MethodInstance Name="GetFilesByFolder" Type="Finder" ReturnParameterName="returnParameter" ReturnTypeDescriptorPath="FileSystemEntityList" Default="false" DefaultDisplayName="GetFilesByFolder" />
  37.   </MethodInstances>
  38. </Method>

As you can see there is a parameter called path for this method. This parameter gets its default value from the Schema.xml file. Be sure to use a value that represents an existing folder on your SharePoint server you would like to display on the external list.

You can alter the root folder path value either in the schema file or at the Data Source Filters section of the view settings after the list is deployed.

image

image

The following method does the job for the finder:

  1. public IEnumerable<FileSystemEntity> GetFilesByFolder(string path)
  2. {
  3.     List<FileSystemEntity> entityList = new List<FileSystemEntity>();
  4.  
  5.     String relativePath = String.Empty;
  6.     String rootPath = path;
  7.  
  8.     HttpContext httpContext = HttpContext.Current;
  9.     if ((httpContext != null) && (httpContext.Request != null))
  10.     {
  11.         relativePath = httpContext.Request.QueryString["path"];
  12.     }
  13.  
  14.     if ((!String.IsNullOrEmpty(relativePath)) && (!relativePath.Contains("..")))
  15.     {
  16.         path = path + relativePath;
  17.     }
  18.  
  19.     if (Directory.Exists(path))
  20.     {
  21.         DirectoryInfo di = new DirectoryInfo(path);
  22.         FileInfo[] files = di.GetFiles();
  23.  
  24.         foreach (FileInfo file in files)
  25.         {
  26.             FileSystemEntity fileSystemEntity = GetFileSystemEntity(file, rootPath);
  27.             entityList.Add(fileSystemEntity);
  28.         }
  29.  
  30.         DirectoryInfo[] folders = di.GetDirectories();
  31.  
  32.         if (path != rootPath)
  33.         {
  34.             DirectoryInfo parentFolder = new DirectoryInfo(path).Parent;
  35.             FileSystemEntity fileSystemEntity = new FileSystemEntity
  36.             {
  37.                 IsFolder = true,
  38.                 Path = parentFolder.FullName,
  39.                 RootPath = rootPath,
  40.                 Name = "..",
  41.             };
  42.             entityList.Add(fileSystemEntity);
  43.         }
  44.  
  45.         foreach (DirectoryInfo folder in folders)
  46.         {
  47.             FileSystemEntity fileSystemEntity = GetFileSystemEntity(folder, rootPath);
  48.             entityList.Add(fileSystemEntity);
  49.         }
  50.  
  51.     }
  52.  
  53.     return entityList;
  54. }

We get the path query string parameter from the current HTTP context and append its value to the root path we configured for the view. I tried to limit the hacking options by eliminating values that contain “..” to disable users to change folders above the root.

If the folder is a subfolder of the root, we also add a “..” folder (without size and date properties) to the list to enable users to change to the parent folder.

Two helper methods to assemble the entity instances:

  1. private static FileSystemEntity GetFileSystemEntity(FileInfo fileInfo, String rootPath)
  2. {
  3.     FileSystemEntity fileSystemEntity = new FileSystemEntity
  4.     {
  5.         IsFolder = false,
  6.         Path = fileInfo.FullName,
  7.         RootPath = rootPath,
  8.         Name = fileInfo.Name,
  9.         // Int64 (long) is not supported by external lists
  10.         Size = (fileInfo.Length > Int32.MaxValue) ? Int32.MaxValue : (Int32)fileInfo.Length,
  11.         Created = fileInfo.CreationTime,
  12.         LastModified = fileInfo.LastWriteTime
  13.     };
  14.  
  15.     return fileSystemEntity;
  16. }
  17.  
  18. private static FileSystemEntity GetFileSystemEntity(DirectoryInfo directoryInfo, String rootPath)
  19. {
  20.     FileSystemEntity fileSystemEntity = new FileSystemEntity
  21.     {
  22.         IsFolder = true,
  23.         Path = directoryInfo.FullName,
  24.         RootPath = rootPath,
  25.         Name = directoryInfo.Name,
  26.         Size = 0,
  27.         Created = directoryInfo.CreationTime,
  28.         LastModified = directoryInfo.LastWriteTime
  29.     };
  30.  
  31.     return fileSystemEntity;
  32. }

Note, that for folders we do not set the size property.

The SpecificFinder method is defined in the model as illustrated below:

  1. <Method Name="ReadItem">
  2.   <Parameters>
  3.     <Parameter Direction="In" Name="path">
  4.       <TypeDescriptor TypeName="System.String" IdentifierName="Path" Name="Path" />
  5.     </Parameter>
  6.     <Parameter Direction="Return" Name="returnParameter">
  7.       <TypeDescriptor TypeName="FileSystem.FileSystemModel.FileSystemEntity, FileSystemModel" Name="FileSystemEntity">
  8.         <TypeDescriptors>
  9.           <TypeDescriptor TypeName="System.String" IdentifierName="Path" Name="Path" />
  10.           <TypeDescriptor TypeName="System.Boolean" Name="IsFolder" />
  11.           <TypeDescriptor TypeName="System.String" Name="Name" />
  12.           <TypeDescriptor TypeName="System.String" Name="Icon" />
  13.           <TypeDescriptor TypeName="System.Int32" Name="Size" />
  14.           <TypeDescriptor TypeName="System.DateTime" Name="Created">
  15.             <Interpretation>
  16.               <NormalizeDateTime LobDateTimeMode="Local" />
  17.             </Interpretation>
  18.           </TypeDescriptor>
  19.           <TypeDescriptor TypeName="System.DateTime" Name="LastModified" DefaultDisplayName="Last modified">
  20.             <Interpretation>
  21.               <NormalizeDateTime LobDateTimeMode="Local" />
  22.             </Interpretation>
  23.           </TypeDescriptor>
  24.         </TypeDescriptors>
  25.       </TypeDescriptor>
  26.     </Parameter>
  27.   </Parameters>
  28.   <MethodInstances>
  29.     <MethodInstance Type="SpecificFinder" ReturnParameterName="returnParameter" Default="true" Name="ReadItem" DefaultDisplayName="Read FileSystemEntity" />
  30.   </MethodInstances>
  31. </Method>

The corresponding method in the .NET class is really straightforward:

  1. public static FileSystemEntity ReadItem(String path)
  2. {
  3.     FileSystemEntity fileSystemEntity = null;
  4.     if (File.Exists(path))
  5.     {
  6.         FileInfo file = new FileInfo(path);
  7.         fileSystemEntity = GetFileSystemEntity(file, null);
  8.     }
  9.     else if (Directory.Exists(path))
  10.     {
  11.         DirectoryInfo folder = new DirectoryInfo(path);
  12.         fileSystemEntity = GetFileSystemEntity(folder, null);
  13.     }
  14.     else
  15.     {
  16.         // file deleted, create empty entity
  17.         fileSystemEntity = new FileSystemEntity();
  18.     }
  19.     return fileSystemEntity;
  20. }

The last method our model implements is the StreamAccessor.

  1. <Method Name="GetFile">               
  2.    <Parameters>
  3.      <Parameter Direction="In" Name="path">
  4.       <TypeDescriptor TypeName="System.String" IdentifierName="Path" Name="Path" />
  5.     </Parameter>
  6.      <Parameter Name="StreamData" Direction="Return">
  7.        <TypeDescriptor TypeName="System.IO.Stream" Name="FileContent" />
  8.     </Parameter>
  9.   </Parameters>
  10.    <MethodInstances>
  11.      <MethodInstance Name="FileAccessor" Type="StreamAccessor" ReturnParameterName="StreamData" ReturnTypeDescriptorName="FileContent">
  12.        <Properties>                    
  13.         <Property Name="FileNameField" Type="System.String">Name</Property>
  14.       </Properties>
  15.     </MethodInstance>
  16.   </MethodInstances>
  17. </Method>

We simply provide the read-only stream for the selected file:

  1. public static Stream GetFile(String path)
  2. {
  3.     return new FileStream(path, FileMode.Open);
  4. }

You might be a bit surprised that I’m implementing a StreamAccessor method for my external list. The following article describes, that a download link is displayed in the Business Data List Web Part for external content types implementing the StreamAccessor method, but “if an external list is created by using an external content type with a stream field, the list does not show the link to download the content.”

Accessing BLOB Data from External Systems Using Business Connectivity Services in SharePoint Server 2010

Fortunately that is true only for the default behavior of the external lists, but nobody stops you to learn from the example of the Business Data List Web Part and implement something similar.

This web part creates the link to the downloadable file through the DownloadExternalData.aspx, passing the properties of the BDC model as query string parameters. To identify the file (BDC entity) to download, we should also pass the ID of the BDC item.

The following code snippet illustrates how we assemble the link for the files in our code (note also, how we get the icon for the file extension using the SPUtility.MapToIcon method):

  1. icon = String.Format("<DIV><a href='{2}/_layouts/DownloadExternalData.aspx" +
  2.     "?EntityNamespace=FileSystem.FileSystemModel" +
  3.     "&EntityName=FileSystemEntity" +
  4.     "&LobSystemInstanceName=FileSystemModel" +
  5.     "&StreamAccessorName=FileAccessor" +
  6.     "&IsXmlEncodedStreamName=true&ItemId=_BdcId_'>" +
  7.     "<img title='{1}' alt='{1}' src='{2}/_layouts/images/{0}' border='0'/></a></DIV>",
  8.                             SPUtility.MapToIcon(web, Name, String.Empty, IconSize.Size16),
  9.                             Name,
  10.                             SPContext.Current.Site.Url
  11.                             );

We now the BDC model parameters, so they are hardcoded, but we don’t know the ID of the BDC entity. To reserve its place, we use the _BdcId_ token in the code, and replace it using the XSLT later:

  1. <xsl:when test="contains($text, $BdcIdToken)">
  2.   <xsl:variable name="before" select="substring-before($text, $BdcIdToken)" />
  3.   <xsl:variable name="after" select="substring-after($text, $BdcIdToken)" />
  4.   <xsl:value-of select="concat($before, $thisNode/@BdcIdentity, $after)" disable-output-escaping ="yes"/>
  5. </xsl:when>

For folders we simply create a link for the current page passing a path query string parameter that contains its relative path related to the root.

  1. String relativePath = String.IsNullOrEmpty(RootPath) ? String.Empty : Path.Substring(RootPath.Length);
  2. icon = String.Format("<DIV><a href='?path={0}'>" +
  3.     "<img title='{1}' alt='{1}' src='{2}/_layouts/images/folder.gif' border='0'/></a></DIV>",
  4.                             relativePath,
  5.                             Name,
  6.                             SPContext.Current.Site.Url
  7.                             );

Let’s see some screenshots about the working sample. On the first one you can see some folder and files. By default, the items are ordered by name, folders on the top, files below, as set by the Query/OrderBy nodes in the Schema.xml.

image

Note the icons. Clicking on them have different behavior based on the item type (folder vs. file). When clicking on file icons, a File Download dialog is displayed:

image

When you click on a folder icon, its relative path is included in the query string, and it causes the list to be refreshed with the content of the subfolder.

image

An extra folder icon is displayed in the subfolders. By clicking on the folder icon next to the “..” label you can navigate back to the parent folder.

You can also view an item either by clicking on its name or via selecting the View Item form the context menu. Note, that the menu is bound to the Name column (via <FieldRef Name="Name" ListItemMenu="TRUE" LinkToItem="TRUE" /> in Schema.xml), but it is also available for the first column (Icon) without explicitly specified. It seems to be a default behavior of external lists.

image 

The next screen is not from the current version but shows how the default view form would be displayed without customization:

image

In this case the form would be defined in the Schema.xml using the following settings:

  1. <Forms>
  2.   <Form Type="DisplayForm" Url="DispForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />      
  3. </Forms>

That display is definitely not what I would like to see.

Since the content of the display form is described by a BinarySerializedWebPart section when working with the site template exported earlier, it is not trivial to alter it in the Elements.xml that contains the form. Instead, we can easily alter the content using SharePoint Designer 2010 and extract the content again from the modified site.

image 

I opened the DispForm.aspx file of the external list for editing and removed the rows of the view I don’t need, the ones for fields Path, IsFolder and Icon. After saving the results, I saved the site as a template, exported the site template as .wsp file, renamed the file to .cab. This time I had to open the Elements.xml file in the MySiteTemplateNameModules folder and to look up the Module that describes the DispForm.aspx form of my list. Since my list is called Files, the Module node has the Name attribute value ListsFiles_pages. Since my “work” list was on a subsite to enable saving the site as a template, and my target site was a root site, I had to alter a few URLs and file locations before importing the content of this section into Elements.xml in the FileSystemForms module of my project.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <Module Name="FileSystemForms" Url="Lists/Files" RootWebOnly="FALSE" SetupPath="pages">
  4.       <FileUrl="DispForm.aspx" Type="Ghostable" Path="form.aspx">
  5.       <BinarySerializedWebPart>
  6.         <GUIDMap>
  7.           <GUID Id="33ff2881_489d_4ce2_ac94_e81d64689d2a" ListUrl="Lists/Files" />
  8.         </GUIDMap>
  9.         <WebPart ID="{035cec7d-5f69-4dbf-a551-0b8203467c41}" WebPartIdProperty="" List="{$ListId:Lists/Files;}" Type="4"
  10.                  Flags="0" DisplayName="" Version="4" Url="Lists/Files/DispForm.aspx" WebPartOrder="1" WebPartZoneID="Main"
  11.                  IsIncluded="True" FrameState="0" WPTypeId="{feaafd58-2dc9-e199-be37-d6cdd7f84690}"
  12.                  SolutionId="{00000000-0000-0000-0000-000000000000}" Assembly="" Class="" Src=""
  13.                  AllUsers="B6Dt/kMAAAABAAAAAAAAAAIAAAAvX2xheW91dHMvaW1hZ2VzL2l0ZWJsLnBuZwAvRjFTaXRlL0xpc3RzL0ZpbGVzAP8BFCsAJQICAgMCAwEEAAICAhICFAEBAAIEBQtDb250cm9sTW9kZQspiAFNaWNyb3NvZnQuU2hhcmVQb2ludC5XZWJDb250cm9scy5TUENvbnRyb2xNb2RlLCBNaWNyb3NvZnQuU2hhcmVQb2ludCwgVmVyc2lvbj0xNC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj03MWU5YmNlMTExZTk0MjljAQUIRm9ybVR5cGUCBAEAAAIWAoYBCyo0U3lzdGVtLldlYi5VSS5XZWJDb250cm9scy5XZWJQYXJ0cy5XZWJQYXJ0RXhwb3J0TW9kZQICggEFGi9fbGF5b3V0cy9pbWFnZXMvaXRlYmwucG5nAn0FEy9GMVNpdGUvTGlzdHMvRmlsZXMFCFBhZ2VUeXBlCyl3TWljcm9zb2Z0LlNoYXJlUG9pbnQuUEFHRVRZUEUsIE1pY3Jvc29mdC5TaGFyZVBvaW50LCBWZXJzaW9uPTE0LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTcxZTliY2UxMTFlOTQyOWMEBQdMaXN0VXJsZQUGTGlzdElkKClYU3lzdGVtLkd1aWQsIG1zY29ybGliLCBWZXJzaW9uPTIuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OSQzM2ZmMjg4MS00ODlkLTRjZTItYWM5NC1lODFkNjQ2ODlkMmEFD0xpc3REaXNwbGF5TmFtZWUClQEFJnszM0ZGMjg4MS00ODlELTRDRTItQUM5NC1FODFENjQ2ODlEMkF9BQ1YbWxEZWZpbml0aW9uBcUPDQo8VXNlckNvbnRyb2wgeDpDbGFzcz0iRm9ybVhtbFRvWGFtbC5Vc2VyQ29udHJvbDIiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIiB4bWxuczpTaGFyZVBvaW50PSJNaWNyb3NvZnQuU2hhcmVQb2ludC5XZWJDb250cm9scyIgeG1sbnM6c3lzdGVtPSJjbHItbmFtZXNwYWNlOlN5c3RlbTthc3NlbWJseT1tc2NvcmxpYiI+PFN0YWNrUGFuZWwgeDpOYW1lPSJGb3JtIj4NCjxTdGFja1BhbmVsLlJlc291cmNlcz4NCjxzeXN0ZW06U3RyaW5nIHg6S2V5PSJGb3JtTW9kZSI+RGlzcGxheTwvc3lzdGVtOlN0cmluZz4NCjxzeXN0ZW06U3RyaW5nIHg6S2V5PSJGb3JtVHlwZSI+TGlzdEZvcm08L3N5c3RlbTpTdHJpbmc+DQo8L1N0YWNrUGFuZWwuUmVzb3VyY2VzPg0KPFN0YWNrUGFuZWwgeDpOYW1lPSJNYWluU2VjdGlvbnMiPjxHcmlkPjxHcmlkLkNvbHVtbkRlZmluaXRpb25zPg0KPENvbHVtbkRlZmluaXRpb24gU3R5bGU9IntTdGF0aWNSZXNvdXJjZSBtcy1mb3JtbGFiZWx9Ii8+DQo8Q29sdW1uRGVmaW5pdGlvbiBTdHlsZT0ie1N0YXRpY1Jlc291cmNlIG1zLWZvcm1ib2R5fSIvPg0KPC9HcmlkLkNvbHVtbkRlZmluaXRpb25zPjxHcmlkLlJvd0RlZmluaXRpb25zPg0KPFJvd0RlZmluaXRpb24gLz4NCjxSb3dEZWZpbml0aW9uIC8+DQo8Um93RGVmaW5pdGlvbiAvPg0KPFJvd0RlZmluaXRpb24gLz4NCjwvR3JpZC5Sb3dEZWZpbml0aW9ucz4NCjxTaGFyZVBvaW50OkZpZWxkTGFiZWwgR3JpZC5Db2x1bW49IjAiIEdyaWQuUm93PSIwIiBDb250cm9sTW9kZT0iRGlzcGxheSIgRmllbGROYW1lPSJOYW1lIiAvPg0KPENvbW1lbnQgRmllbGROYW1lPSJOYW1lIiBGaWVsZEludGVybmFsTmFtZT0iTmFtZSIgRmllbGRUeXBlPSJUZXh0IiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMCIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTmFtZSIgSW5jbHVkZURlc2NyaXB0aW9uPSJUcnVlIi8+DQo8U2hhcmVQb2ludDpGaWVsZExhYmVsIEdyaWQuQ29sdW1uPSIwIiBHcmlkLlJvdz0iMSIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iU2l6ZSIgLz4NCjxDb21tZW50IEZpZWxkTmFtZT0iU2l6ZSIgRmllbGRJbnRlcm5hbE5hbWU9IlNpemUiIEZpZWxkVHlwZT0iSW50ZWdlciIgLz4NCjxTaGFyZVBvaW50OkZvcm1GaWVsZCBHcmlkLkNvbHVtbj0iMSIgR3JpZC5Sb3c9IjEiIENvbnRyb2xNb2RlPSJEaXNwbGF5IiBGaWVsZE5hbWU9IlNpemUiIEluY2x1ZGVEZXNjcmlwdGlvbj0iVHJ1ZSIvPg0KPFNoYXJlUG9pbnQ6RmllbGRMYWJlbCBHcmlkLkNvbHVtbj0iMCIgR3JpZC5Sb3c9IjIiIENvbnRyb2xNb2RlPSJEaXNwbGF5IiBGaWVsZE5hbWU9IkNyZWF0ZWQiIC8+DQo8Q29tbWVudCBGaWVsZE5hbWU9IkNyZWF0ZWQiIEZpZWxkSW50ZXJuYWxOYW1lPSJDcmVhdGVkIiBGaWVsZFR5cGU9IkRhdGVUaW1lIiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMiIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iQ3JlYXRlZCIgSW5jbHVkZURlc2NyaXB0aW9uPSJUcnVlIi8+DQo8U2hhcmVQb2ludDpGaWVsZExhYmVsIEdyaWQuQ29sdW1uPSIwIiBHcmlkLlJvdz0iMyIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTGFzdE1vZGlmaWVkIiAvPg0KPENvbW1lbnQgRmllbGROYW1lPSJMYXN0IG1vZGlmaWVkIiBGaWVsZEludGVybmFsTmFtZT0iTGFzdE1vZGlmaWVkIiBGaWVsZFR5cGU9IkRhdGVUaW1lIiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMyIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTGFzdE1vZGlmaWVkIiBJbmNsdWRlRGVzY3JpcHRpb249IlRydWUiLz4NCjwvR3JpZD4NCjwvU3RhY2tQYW5lbD4NCjwvU3RhY2tQYW5lbD4NCjwvVXNlckNvbnRyb2w+AktkBRFQYXJhbWV0ZXJCaW5kaW5ncwXoAg0KPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iZHZ0X2Fwb3MiIExvY2F0aW9uPSJQb3N0YmFjaztDb25uZWN0aW9uIi8+DQogICAgICAgIDxQYXJhbWV0ZXJCaW5kaW5nIE5hbWU9IlVzZXJJRCIgTG9jYXRpb249IkNBTUxWYXJpYWJsZSIgRGVmYXVsdFZhbHVlPSJDdXJyZW50VXNlck5hbWUiLz4NCiAgICAgICAgPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iVG9kYXkiIExvY2F0aW9uPSJDQU1MVmFyaWFibGUiIERlZmF1bHRWYWx1ZT0iQ3VycmVudERhdGUiLz4NCiAgICAgICAgPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iTGlzdEl0ZW1JZCIgTG9jYXRpb249IlF1ZXJ5U3RyaW5nKElEKSIgRGVmYXVsdFZhbHVlPSIwIi8+DQogICAgICAgIA==" />
  14.       </BinarySerializedWebPart>
  15.     </File>
  16.   </Module>
  17. </Elements>

The next image shows the result.

image

If you forget to remove the Forms node from the schema file, both the form set in the Schema.xml and the one deployed using the module will be displayed, so be careful.

image

You can download the sample project here (including the sample file structure).

Advertisements

6 Comments »

  1. […] Second Life of a Hungarian SharePoint Geek If your sword is too short, take one step forward « Publishing files stored in the file system through external list […]

    Pingback by Creating the customization XSL for your external list « Second Life of a Hungarian SharePoint Geek — October 1, 2010 @ 22:31

  2. Thank you, Peter for a good article and an excellent solution. Your insights were invaluable to our effort on a similar solution. I wanted to give you some feedback on some of your findings. First, you mentioned that using the ShowInDisplayForm attribute in schema.xml to hide fields didn’t work. We saw similar behavior. One way to accomplish this is to remove any fieldref elements that you don’t want to appear from the Views element further down in the schema.xml.

    You also mention that your list didn’t appear in QuickLaunch. We saw this behavior too. Instead, we call into the CreateSPNavigationNode method of the Microsoft.SharePoint.Publishing.Navigation.SPNavigationSiteMapNode class to add a navigation grouping and the Microsoft.SharePoint.Navigation.SPNavigationNode class to add links to the collection of links in the navigation grouping.

    Last but not least, another possible approach for updating the View Item dialog would be to use a custom InfoPath form. Admittedly, InfoPath forms do make the solution a bit more cumbersome because of absolute paths that you have to modify in the XSF file of the InfoPath XSN to deploy the solution to other farms.

    Thanks again for your great piece of work!

    Comment by EthanW — October 13, 2010 @ 13:36

  3. […] When you save your SharePoint site as a template, the files within the generated WSP package contain the customized web parts as BinarySerializedWebPart, a format not easy to work with (see an example in my former post: Publishing files stored in the file system through external list). […]

    Pingback by Decoding the content of the BinarySerializedWebPart – The theory « Second Life of a Hungarian SharePoint Geek — October 17, 2010 @ 23:33

  4. Great post, it was very useful and not available anywhere else on the web. Thanks.

    Comment by Amit — October 20, 2010 @ 19:14

  5. Great post!
    and, i have a question about crawl blobs via BCS.
    the indexer can’t read file content by a right ifilter! except the file is a txt file.
    i’ve already set the FieldNameField and MimetypeField of the StreamAccessor.

    Comment by Eric — March 11, 2011 @ 09:46

  6. I am getting the following error when implemented this solution.
    “Web Part Error: The request could not be completed because the specified solution was not found.”
    Did anyone else got the same error?

    Comment by mk — April 16, 2012 @ 14:10


RSS feed for comments on this post. TrackBack URI

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

Create a free website or blog at WordPress.com.

%d bloggers like this: