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).

September 11, 2010

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

Filed under: BCS, SP 2010 — Tags: , — Peter Holpar @ 03:45

It is mentioned in several posts (like this one from  René Hézser) that you can use custom fields in BDC models an external links, but I do not really find any real downloadable sample project for that.

Since I like to have complete examples one can download and try in this post I will show you a sample .NET assembly that illustrates using custom field types in external list. For an introduction to .NET connectivity assembly, I suggest you to read and understand the following article:

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

To understand my code, you should know how we can read BDC metadata from code. The Reading metadata model properties section of the post How do I leverage Secure Store from within my .Net Connectivity Assembly? explains exactly that.

Notice: This 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.

Our example is a simple link collection, similar to the standard SharePoint Links list, but in this case our links come from the file system. You should create a local folder on the SharePoint server and copy a few links there from your Favorites folder. The sample code will help to list, read, modify and delete these URL files as well as create new ones from an external list on SharePoint UI.

To make a sample a little bit more exciting we will spice it with some XSL tampering (or let’s call it customization to be politically correct) using SharePoint Designer. It includes using the favicon property of the URL to be displayed next to the link and moving the list item menu to the second field of the view (that is not its standard position for an external list as we will see soon).

The topic of internal structure of URL files as well as reading and writing URL files are really beyond the scope of this post. If you need more info about that visit this page.

As you probably already guessed, the custom field we use in this example will be in this case the built-in (not-so-custom) Hyperlink or Picture field of SharePoint.

The Basics

Let’s see first the LobSytem and Entities sections:

  1. <LobSystems>
  2.   <LobSystem Name="ExternalLinks" Type="DotNetAssembly">
  3.     <LobSystemInstances>
  4.       <LobSystemInstance Name="ExternalLinks">
  5.         <Properties>
  6.           <!– modify the value for the Folder property to match to your local links folder path –>
  7.           <Property Name="Folder" Type="System.String">C:\Data\Temp\SampleLinks</Property>
  8.         </Properties>
  9.       </LobSystemInstance>
  10.     </LobSystemInstances>
  11.     <Entities>
  12.       <Entity Name="ExternalLink" Namespace="ExternalLinks" EstimatedInstanceCount="1000" Version="1.0.0.20">
  13.         <Properties>
  14.           <Property Name="Class" Type="System.String">ExternalLinks.ExternalLinkService, ExternalLinks</Property>
  15.         </Properties>
  16.         <Identifiers>
  17.           <Identifier Name="Title" TypeName="System.String" />
  18.         </Identifiers>

Note, that you should modify the value of the Folder property to match the path of your local links folder created earlier. If you alter the BDC model and deploy multiple times, don’t forget to delete the corresponding external system between as described in my former post.

You can reach the value of the Folder property from code as illustrated below:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.IO;
  6. using IContextProperty = Microsoft.BusinessData.SystemSpecific.IContextProperty;
  7. using Microsoft.BusinessData.MetadataModel;
  8. using Microsoft.BusinessData.Runtime;
  9.  
  10.  
  11. namespace ExternalLinks
  12. {
  13.     public class ExternalLinkService : IContextProperty
  14.     {
  15.  
  16.         #region IContextProperty implementation
  17.  
  18.         public IMethodInstance MethodInstance { get; set; }
  19.  
  20.         public ILobSystemInstance LobSystemInstance { get; set; }
  21.  
  22.         public IExecutionContext ExecutionContext { get; set; }
  23.  
  24.         #endregion
  25.  
  26.         #region BDC parameters
  27.  
  28.         public String FolderPath
  29.         {
  30.             get
  31.             {
  32.                 return (String)LobSystemInstance.GetProperties()["Folder"];
  33.             }
  34.         }
  35.  
  36.         #endregion

We had to reference the Microsoft.BusinessData assembly to make this code work.

The ExternalLink class used to represent our links in this sample is defined as:

  1. namespace ExternalLinks
  2. {
  3.     public partial class ExternalLink
  4.     {
  5.         public String Title { get; set; }
  6.         public SPFieldUrlValue Link { get; set; }
  7.         public String IconUrl { get; set; }
  8.     }
  9. }

Title will be read from the file name of the URL, this value will be the same as the Description property of the Link (type of SPFieldUrlValue), the Url property of the Link and the IconUrl will be read from the content of the URL file.

Note, that we use the SPFieldUrlValue type for the Link value.

We want our IconUrl property to be displayed as Icon field, so we have to add the DefaultDisplayName attribute to the TypeDescriptor.

Finder

The definition of the finder method in the model:

  1. <Method Name="ReadList">
  2.   <Parameters>
  3.     <Parameter Direction="Return" Name="returnParameter">
  4.       <TypeDescriptor TypeName="System.Collections.Generic.IEnumerable`1[[ExternalLinks.ExternalLink, ExternalLinks]]" IsCollection="true" Name="ExternalLinkList">
  5.         <TypeDescriptors>
  6.           <TypeDescriptor TypeName="ExternalLinks.ExternalLink, ExternalLinks" Name="ExternalLink">
  7.             <TypeDescriptors>
  8.               <TypeDescriptor TypeName="System.String" Name="IconUrl" DefaultDisplayName="Icon"/>
  9.               <TypeDescriptor TypeName="System.String" IdentifierName="Title" Name="Title" ReadOnly="true" />
  10.             </TypeDescriptors>
  11.           </TypeDescriptor>
  12.         </TypeDescriptors>
  13.       </TypeDescriptor>
  14.     </Parameter>
  15.   </Parameters>
  16.   <MethodInstances>
  17.     <MethodInstance Type="Finder" ReturnParameterName="returnParameter" Default="true" Name="ReadList" DefaultDisplayName="ExternalLink List" />
  18.   </MethodInstances>
  19. </Method>

The methods defined in the ExternalLinkService class do nothing more than forwarding the calls to the corresponding static utility methods defined in the Utils class:

  1. public IEnumerable<ExternalLink> ReadList()
  2. {
  3.     return Utils.GetItems(FolderPath);
  4. }
  5.  
  6. public ExternalLink ReadItem(String title)
  7. {
  8.     return Utils.GetItem(title, FolderPath);
  9. }
  10.  
  11. public void Update(ExternalLink externalLink)
  12. {
  13.     Utils.SaveChanges(externalLink, FolderPath);
  14. }
  15.  
  16. public void Delete(String title)
  17. {
  18.     Utils.Delete(title, FolderPath);
  19. }
  20.  
  21. public ExternalLink Create(ExternalLink newExternalLink)
  22. {
  23.     return Utils.Create(newExternalLink, FolderPath);            
  24. }

For the ReadList method the work is done by these methods:

  1. public static IEnumerable<ExternalLink> GetItems(String folderPath)
  2. {
  3.     List<ExternalLink> externalLinks = new List<ExternalLink>();
  4.     // check for existing folder
  5.     // if missing, returns an empty list
  6.     // modify the Folder property in the view to match your local folder name
  7.     if (Directory.Exists(folderPath))
  8.     {
  9.         foreach (String path in Directory.GetFiles(folderPath, "*.url"))
  10.         {
  11.             FileInfo fileInfo = new FileInfo(path);
  12.             ExternalLink externalLink = Utils.GetLink(fileInfo, true);
  13.             externalLinks.Add(externalLink);
  14.         }
  15.     }
  16.     return externalLinks;
  17. }
  18.  
  19. private static ExternalLink GetLink(FileInfo fileInfo, bool forList)
  20. {
  21.     String url = GetLinkData("URL", String.Empty, fileInfo);
  22.     // splitting .url extension from the end
  23.     String title = fileInfo.Name.Substring(0, fileInfo.Name.Length – 4);
  24.     String iconUrl = GetLinkData("IconFile", String.Empty, fileInfo);
  25.  
  26.     // some HTML hacking for the list view
  27.     if (forList)
  28.     {
  29.         iconUrl = String.Format("<DIV><a href='{0}' target='_blank'><img title='{1}' alt='{1}' src='{2}' border='0'/></a></DIV>",
  30.                                         url,
  31.                                         title,
  32.                                         String.IsNullOrEmpty(iconUrl) ? "/_layouts/images/icgen.gif" : iconUrl);
  33.     }
  34.  
  35.     return new ExternalLink
  36.     {
  37.         Title = title,
  38.         Link = new SPFieldUrlValue(String.Format("{0}, {1}", url, title)),
  39.         IconUrl = iconUrl
  40.     };
  41. }
  42.  
  43. private static String GetLinkData(String key, String defaultValue, FileInfo fileInfo)
  44. {
  45.     String linkData = defaultValue;
  46.     StringBuilder sb = new StringBuilder(1000);
  47.     uint callResult = GetPrivateProfileString("InternetShortcut", key, defaultValue, sb, (uint)sb.Capacity, fileInfo.FullName);
  48.     if (callResult > 0)
  49.     {
  50.         linkData = sb.ToString();
  51.     }
  52.     return linkData;
  53. }

It is important to note that the GetLink method does some kind of HTML manipulation of data if the link is requested for the finder method (forList is true). It assembles the HTML using the URL of the favicon or the URL of the general icgen.gif if no favicon specified.

Reading and writing the URL file is made by these methods:

  1. [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  2. [return: MarshalAs(UnmanagedType.Bool)]
  3. private static extern bool WritePrivateProfileString(string lpAppName,
  4.    string lpKeyName, string lpString, string lpFileName);
  5.  
  6. [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
  7. private static extern uint GetPrivateProfileString(
  8.    string lpAppName,
  9.    string lpKeyName,
  10.    string lpDefault,
  11.    StringBuilder lpReturnedString,
  12.    uint nSize,
  13.    string lpFileName);

After deployment the list should look like this:

image

If you failed to alter the value of the Folder property in the BDC model to match the path of your local links folder earlier, or if the folder contains no valid URL file, then the list is empty.

As you can see, we do not include the custom Link field in this view, I will give you the answer for this later.

Further things to note:

  • The value of the Icon field is not rendered as HTML, so it is no user-friendly.
  • The item menu is assigned to the Icon field.
  • The list is ordered by the Icon field, not by the Title.

SpecificFinder

The definition of the specific finder method in the model:

  1. <Method Name="ReadItem">
  2.   <Parameters>
  3.     <Parameter Direction="In" Name="title">
  4.       <TypeDescriptor TypeName="System.String" IdentifierName="Title" Name="Title" PreUpdaterField="true" />
  5.     </Parameter>
  6.     <Parameter Direction="Return" Name="returnParameter">
  7.       <TypeDescriptor TypeName="ExternalLinks.ExternalLink, ExternalLinks" Name="ExternalLink">
  8.         <TypeDescriptors>
  9.           <TypeDescriptor TypeName="System.String" IdentifierName="Title" Name="Title" ReadOnly="true" />
  10.           <TypeDescriptor TypeName="Microsoft.SharePoint.SPFieldUrlValue" Name="Link">
  11.             <Properties>
  12.               <Property Name="SPCustomFieldType" Type="System.String">URL</Property>
  13.             </Properties>
  14.           </TypeDescriptor>
  15.           <TypeDescriptor TypeName="System.String" Name="IconUrl" DefaultDisplayName="Icon"/>
  16.         </TypeDescriptors>
  17.       </TypeDescriptor>
  18.     </Parameter>
  19.   </Parameters>
  20.   <MethodInstances>
  21.     <MethodInstance Type="SpecificFinder" ReturnParameterName="returnParameter" Default="true" Name="ReadItem" DefaultDisplayName="Read ExternalLink" />
  22.   </MethodInstances>
  23. </Method>

Note the value of the TypeName attribute for the TypeDescriptor having Name “Link”. It is the full name (namespace and class name) of the type used for the custom field. It should match to the type defined in the entity class (see above the type of the  Link property of the ExternalLink class).

You can define the custom field type for your type in BDC model using the SPCustomFieldType property of the corresponding TypeDescriptor. I should note, that although at the description of the BDC Custom Properties it is stated that “this property has no effect on methods other than the SpecificFinder”, fortunately it is not exact since you can use it in the case of Updater and Creator methods as well, as you will see soon. But it is true, that the value of this field is not displayed when included in the finder method. If you included the field in the finder, the field header would be displayed in the view but column would contain no value. This is why we have not included this field in the finder method.

I feel this limitation rather sad. If you get used to play with the different RenderPattern elements of your custom fields to customize the item rendering in the list view, you can feel your creativity somewhat restrained.

It is important, that you should use the TypeAsString    property of the custom field type class ("URL" in this case) and not the TypeDisplayName  property (would be "Hyperlink or Picture") when setting the value of the SPCustomFieldType property.

What if you want to use a custom field and set its properties before using?

Well, then I think you are in a trouble, as the BDC model provides no declarative way to set custom field properties.

You can try to run this code from a console application:

  1. SPListCollection lists = web.Lists;
  2. SPList extList = lists["ExternalLinks"];
  3. SPFieldCollection fields = extList.Fields;
  4. SPFieldUrl link = (SPFieldUrl)fields["Link"];
  5. link.DisplayFormat = SPUrlFieldFormatType.Image;
  6. link.Update();

But running the code will result an SPException:

The field ‘Link’ cannot be updated. The field is in a field collection that cannot be updated, such as one returned as part of a query.

If you try to play with the SchemaXml property of the field, setting its value will throw an NotSupportedException.

You can probably workaround this (not yet tested) via inheriting your own custom control from the specific custom control and set properties to the desired values in your class. For example, you can derive an ImageLink custom field type from SPFieldUrl and have its default  DisplayFormat set to Image. But creating and deploying a custom field for every and each customization request is rather painful.

For the ReadItem method specified for the SpecificFinder BDC method type the work is done by the GetItem method:

  1. public static ExternalLink GetItem(String title, String folderPath)
  2. {
  3.     ExternalLink externalLink = null;
  4.     String path = String.Format(@"{0}\{1}.url", folderPath, title);
  5.  
  6.     if (File.Exists(path))
  7.     {
  8.         FileInfo fileInfo = new FileInfo(path);
  9.         externalLink = Utils.GetLink(fileInfo, false);
  10.     }
  11.  
  12.     return externalLink;
  13. }

Displaying a link will be similar to this image, assuming there is a favicon specified in the URL.

image 

Things to note:

  • The value of the Icon field is more user-friendly than in the case of the list view (finder method). This is due to the little trick with the forList parameter in the GetLink method.
  • The caption of the Title and Link fields are the same.

To tell the truth I’ve tried to remove the Title field from this view but without success. I found no declarative nor programmatic way for that.

For example, let’s see the following code:

  1. SPListCollection lists = web.Lists;
  2. SPList extList = lists["ExternalLinks"];
  3. SPFieldCollection fields = extList.Fields;
  4. SPField title = fields["Title"];
  5. title.ShowInViewForms = false;
  6. title.Update();

But running this code will result an NotSupportedException:

Setting property ‘ShowInViewForms’ to value ‘False’ is not supported on Microsoft.SharePoint.SPFieldText for external lists.

Updater

The updater method is defined like this:

  1. <Method Name="Update">
  2.   <Parameters>
  3.     <Parameter Name="externalLink" Direction="In">
  4.       <TypeDescriptor Name="ExternalLink" TypeName="ExternalLinks.ExternalLink, ExternalLinks">
  5.         <TypeDescriptors>
  6.           <TypeDescriptor TypeName="System.String" IdentifierName="Title" Name="Title" PreUpdaterField="true" />
  7.           <TypeDescriptor Name="Link" TypeName="Microsoft.SharePoint.SPFieldUrlValue" UpdaterField="true">
  8.             <Properties>
  9.               <Property Name="SPCustomFieldType" Type="System.String">URL</Property>
  10.             </Properties></TypeDescriptor>
  11.           <TypeDescriptor Name="IconUrl" TypeName="System.String" DefaultDisplayName="Icon" UpdaterField="true" />
  12.         </TypeDescriptors>
  13.       </TypeDescriptor>
  14.     </Parameter>
  15.   </Parameters>
  16.   <MethodInstances>
  17.     <MethodInstance Name="Update" Type="Updater" />
  18.   </MethodInstances>
  19. </Method>

The following methods in the Utils class will do the job for the Update method:

  1. public static void SaveChanges(ExternalLink externalLink, String folderPath)
  2. {
  3.     String path = String.Format(@"{0}\{1}.url", folderPath, externalLink.Title);
  4.  
  5.     if (File.Exists(path))
  6.     {
  7.         FileInfo fileInfo = new FileInfo(path);
  8.         String newUrl = externalLink.Link.Url;
  9.         String newTitle = externalLink.Link.Description;
  10.  
  11.         SetLinkData("URL", newUrl, path);
  12.         SetLinkData("IconFile", externalLink.IconUrl, path);
  13.  
  14.         // Title property set by renaming
  15.         if (externalLink.Title != newTitle)
  16.         {
  17.             String newPath = String.Format(@"{0}\{1}.url", fileInfo.Directory, newTitle);
  18.  
  19.             // move only if there is no file with that name
  20.             if (!File.Exists(newPath))
  21.             {
  22.                 fileInfo.MoveTo(newPath);
  23.             }
  24.         }
  25.     }
  26. }
  27.  
  28. private static void SetLinkData(String key, String value, String path)
  29. {
  30.     WritePrivateProfileString("InternetShortcut", key, value, path);
  31. }

In the SaveChanges method we change the filename itself (that means the Title field used as the identifier in our model) if the Description of the Link is changed. If there is already a file with the new name, the Title is not changed and it means that the Descriprion remains the same as well.

image

Note that the Title field is hidden due to the PreUpdaterField="true" setting in the TypeDescriptor.

Deleter

The deleter method is the simplest one:

  1. <Method Name="Delete">
  2.   <Parameters>
  3.     <Parameter Name="title" Direction="In">
  4.       <TypeDescriptor Name="Title" TypeName="System.String" IdentifierEntityName="ExternalLink" IdentifierEntityNamespace="ExternalLinks" IdentifierName="Title" /></Parameter>
  5.   </Parameters>
  6.   <MethodInstances>
  7.     <MethodInstance Name="Delete" Type="Deleter" />
  8.   </MethodInstances>
  9. </Method>

And in the Utils class:

  1. public static void Delete(String title, String folderPath)
  2. {
  3.     String path = String.Format(@"{0}\{1}.url", folderPath, title);
  4.  
  5.     if (File.Exists(path))
  6.     {
  7.         File.Delete(path);
  8.     }
  9. }

Creator

The last method defined is the creator:

  1. <Method Name="Create">
  2.   <Parameters>
  3.     <Parameter Name="returnExternalLink" Direction="Return">
  4.       <TypeDescriptor Name="ReturnExternalLink" TypeName="ExternalLinks.ExternalLink, ExternalLinks">
  5.         <TypeDescriptors>
  6.           <TypeDescriptor Name="Title" IdentifierName="Title" ReadOnly="true" TypeName="System.String" />
  7.           <TypeDescriptor Name="Link" TypeName="Microsoft.SharePoint.SPFieldUrlValue">
  8.             <Properties>
  9.               <Property Name="SPCustomFieldType" Type="System.String">URL</Property>
  10.             </Properties></TypeDescriptor>
  11.           <TypeDescriptor Name="IconUrl" TypeName="System.String" />
  12.         </TypeDescriptors>
  13.       </TypeDescriptor>
  14.     </Parameter>
  15.     <Parameter Name="newExternalLink" Direction="In">
  16.       <TypeDescriptor Name="NewExternalLink" TypeName="ExternalLinks.ExternalLink, ExternalLinks">
  17.         <TypeDescriptors>
  18.           <TypeDescriptor Name="Title" IdentifierName="Title" PreUpdaterField="true" TypeName="System.String" CreatorField="false" />
  19.           <TypeDescriptor Name="Link" TypeName="Microsoft.SharePoint.SPFieldUrlValue" CreatorField="true">
  20.             <Properties>
  21.               <Property Name="SPCustomFieldType" Type="System.String">URL</Property>
  22.             </Properties></TypeDescriptor>
  23.           <TypeDescriptor Name="IconUrl" TypeName="System.String" CreatorField="true" />
  24.         </TypeDescriptors>
  25.       </TypeDescriptor>
  26.     </Parameter>
  27.   </Parameters>
  28.   <MethodInstances>
  29.     <MethodInstance Name="Create" Type="Creator" ReturnParameterName="returnExternalLink" ReturnTypeDescriptorPath="ReturnExternalLink" />
  30.   </MethodInstances></Method>

The Create method is used for URL file creation in the back end:

  1. public static ExternalLink Create(ExternalLink newExternalLink, String folderPath)
  2. {
  3.     String newUrl = newExternalLink.Link.Url;
  4.     String newTitle = newExternalLink.Link.Description;
  5.  
  6.     String path = String.Format(@"{0}\{1}.url", folderPath, newTitle);
  7.  
  8.     SetLinkData("URL", newUrl, path);
  9.     SetLinkData("IconFile", newExternalLink.IconUrl, path);
  10.  
  11.     ExternalLink createdExternalLink = new ExternalLink
  12.     {
  13.         Title = newExternalLink.Link.Description,
  14.         Link = newExternalLink.Link,
  15.         IconUrl = newExternalLink.IconUrl,
  16.     };
  17.     return createdExternalLink;
  18. }

In this method we should return an ExternalLink entity based on the new URL file.

image

Note that the Title field is hidden due to the ReadOnly="true" setting in the TypeDescriptor.

Deploying the External List

The solution contains a list instance for our sample as well:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <ListInstance Title="ExternalLinks"
  4.                 OnQuickLaunch="TRUE"
  5.                 TemplateType="600"
  6.                 FeatureId="00BFEA71-9549-43f8-B978-E47E54A10600"
  7.                 Url="Lists/ExternalLinks"
  8.                 Description="Sample external list">
  9.     <DataSource>
  10.       <Property Name="LobSystemInstance" Value="ExternalLinks" />
  11.       <Property Name="EntityNamespace" Value="ExternalLinks" />
  12.       <Property Name="Entity" Value="ExternalLink" />
  13.       <Property Name="SpecificFinder" Value="ReadItem" />
  14.     </DataSource>
  15.   </ListInstance>
  16. </Elements>

For the details of deploying an external list from Visual Studio 2010 solutions you can read more in this post of Yaroslav Pentsarskyy.

Final Modifications

We completed a lot of things, but you may remember that our view created based on the Finder method is not really user-friendly yet. It is time to correct that.

Start SharePoint Designer, and on the Navigation bar (on the left) click on  List and Libraries then select our external list called ExternalLinks on the right bottom side in the External Lists group.

Open the ExternalLink List view by double-clicking on it.

image

In the code view look for this code snippet:

  1. <Query>
  2.   <OrderBy>
  3.     <FieldRef Name="IconUrl"/>
  4.   </OrderBy>
  5. </Query>
  6. <ViewFields>
  7.   <FieldRef Name="IconUrl" ListItemMenu="TRUE" LinkToItem="TRUE"/>
  8.   <FieldRef Name="Title"/>
  9. </ViewFields>

Alter it as illustrated below to change the ordering and to add item context menu to Title field:

  1. <Query>
  2.   <OrderBy>
  3.     <FieldRef Name="Title"/>
  4.   </OrderBy>
  5. </Query>
  6. <ViewFields>
  7.   <FieldRef Name="IconUrl"/>
  8.   <FieldRef Name="Title" ListItemMenu="TRUE" LinkToItem="TRUE"/>
  9. </ViewFields>

Note that even we set the menu to Title, the first field (IconUrl) will still have the menu.

In the design view, select the content of the Icon field cell. If you selected correctly, you should see the <xsl:value-of> tag. Select the Edit Tag… option.

image

Add the disable-output-escaping="yes" attribute (be sure to use standard quotation marks!) to the tag as shown below and apply the changes:

image

You can read more about the purpose of this action here.

Save the changes you made on the page, and refresh the view in the browser.

image

Voila.

The favicon is shown where it is specified, where it is missing, a standard icon is shown.

When you position the mouse over the icon, the Title of the item is shown as a tooltip.

By clicking the link you open the target URL in a new browser window. The item context menu is bound to the Title, and the items are ordered by the Title as well.

BTW, if you need to change only the order, of course you don’t need to use SPD, simply modify the view on the SharePoint UI.

That is for today, and sorry for the long post. I hope it was worth reading so much and I plan to be back soon with another – bit more complex – BCS sample. You can download this sample from here (including the URL files used in the post).

Lessons learned when working on this sample:

You can use the built-in and custom field types in your BDC model. When setting the SPCustomFieldType property for the TypeDescriptor you should use the TypeAsString property of the custom field type. Use the corresponding field value type for these fields in the BDC model.

Using custom field types is not supported in the Finder views of the external lists.

The custom field support is limited another way also: you cannot set properties of the custom field declaratively, for example from your BDC model. Seems that there is no trivial workaround for this issue.

There are further developer oriented limitations when working with external lists, for example you cannot hide fields on specific view types, like edit or display view.

If you would like to rename your field on the UI, you should apply the DefaultDisplayName attribute with the same value to all instances of the same TypeDescriptor in the model file. If you set it for example only in the Finder method, it seems to have no effect. In this case the Name attribute is displayed.

You can display different values for the same item in the same field in different views (like Finder(s), SpecificFinder).

You can enable HTML rendering by altering the XSL of the XsltListViewWebPart in the .aspx file generated for the view using SPD (or custom code, not covered in this post). It requires circumspection as making HTML errors (like missing closing tags or extra quotes) may cause your view to be rendered totally wrong on the external list UI.

You can set the ordering field both by altering the views on the SharePoint UI or via XSL modification in SPD.

By default, the item menu is linked to the first field in the Finder view, but you can alter that by modifying the rendering XSL using SPD (or custom code, not covered in this post).

LobSystemInstance properties provide a great way to configure your BDC application. You can access these properties even from managed code.

You can alter even the value of the field you used as the identifier of your entity model.

You can deploy your external list declaratively from your Visual Studio solution.

September 7, 2010

I hate “Go To Reflector” in Visual Studio

Filed under: Reflector, Visual Studio — Tags: , — Peter Holpar @ 00:09

If you follow my posts on this blog, you already know that Reflector is one of my favorite tools. A few days ago I was to look up again something in a SharePoint assembly but found that all of my registered assemblies are lost, and to make things even worse, all of my bookmarks, that served me as a map to the SharePoint libraries I worked with, disappeared also.

I remembered that the other day during debugging I accidentally chose the Go To Reflector menu in Visual Studio instead of its neighbor Run To Cursor (yes, you are right, I should have pressed simply Ctrl+F10). At that time I thought it has no consequence at all, but unfortunately I was wrong.

As it turned out, selecting this menu item will clean the registered (non-standard) assemblies from Reflector and registers only the assembly you selected in Visual Studio for investigation. I can reproduce this behavior and think it is not a very developer friendly feature.

Regarding my lost bookmarks – that is really a pain in my heart – I was not able to repro to clean them simply by using this menu, but somehow I suspect it has a relation to this mistake.

Lesson learned: one should backup the Reflector.cfg file regularly.

The Shocking Blue Green Theme. Create a free website or blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 54 other followers