Publishing files stored in the file system through external list
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:
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:
- <?xml version="1.0" encoding="utf-8"?>
- <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
- <ListInstance Title="Files"
- OnQuickLaunch="TRUE"
- TemplateType="600"
- FeatureId="00bfea71-de22-43b2-a848-c05709900100"
- Url="Lists/Files"
- CustomSchema="Files\Schema.xml"
- Description="Filesystem demo external list">
- <DataSource>
- <Property Name="LobSystemInstance" Value="FileSystemModel" />
- <Property Name="EntityNamespace" Value="FileSystem.FileSystemModel" />
- <Property Name="Entity" Value="FileSystemEntity" />
- <Property Name="SpecificFinder" Value="ReadItem" />
- </DataSource>
- </ListInstance>
- </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.
- <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/">
- <MetaData>
- <ContentTypes>
- <ContentType ID="0×01" Name="Item" Group="List Content Types" Description="Create a new list item." FeatureId="{695b6570-a48b-4a8e-8ea5-26ea7fc1d162}">
- <Folder TargetName="Item" />
- <FieldRefs>
- <FieldRef ID="{c042a256-787d-4a6f-8a8a-cf6ab767f12d}" Name="ContentType" />
- <FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title" Required="TRUE" ShowInNewForm="TRUE" ShowInEditForm="TRUE" />
- </FieldRefs>
- </ContentType>
- </ContentTypes>
- <Fields>
- <Field DisplayName="BDC Identity" Hidden="FALSE" Name="BdcIdentity" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="BdcIdentity" Type="Text" />
- <Field DisplayName="Path" Hidden="FALSE" Name="Path" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Path" Type="Text" />
- <Field DisplayName="Name" Hidden="FALSE" Name="Name" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Name" Type="Text" />
- <Field DisplayName="Icon" Hidden="FALSE" Name="Icon" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Icon" Type="Text" />
- <Field DisplayName="Size" Hidden="FALSE" Name="Size" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Size" Type="Integer" />
- <Field DisplayName="Created" Hidden="FALSE" Name="Created" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Created" Type="DateTime" />
- <Field DisplayName="Last modified" Hidden="FALSE" Name="LastModified" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="LastModified" Type="DateTime" />
- </Fields>
- <Views>
- <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">
- <XslLink>FileSystem.xsl</XslLink>
- <Method Name="GetFilesByFolder">
- <!– don't forget to alter the value of the path parameter to match your local settings! –>
- <Filter Name="path" Value="C:\Data\Temp\SampleFiles" />
- </Method>
- <Query>
- <OrderBy>
- <!– folders come first, then list of files –>
- <FieldRef Name="IsFolder" Ascending="FALSE"/>
- <!– order by name –>
- <FieldRef Name="Name" />
- </OrderBy>
- </Query>
- <ViewFields>
- <FieldRef Name="Icon" />
- <FieldRef Name="Name" ListItemMenu="TRUE" LinkToItem="TRUE" />
- <FieldRef Name="Size" />
- <FieldRef Name="Created" />
- <FieldRef Name="LastModified" />
- </ViewFields>
- <RowLimit Paged="TRUE">30</RowLimit>
- <Aggregations Value="Off" />
- </View>
- </Views>
- </MetaData>
- </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:
- <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">
- <xsl:include href="/_layouts/xsl/main.xsl"/>
- <xsl:include href="/_layouts/xsl/internal.xsl"/>
- <xsl:param name="AllRows" select="/dsQueryResponse/Rows/Row[$EntityName = '' or (position() >= $FirstRow and position() <= $LastRow)]"/>
- <xsl:param name="dvt_apos">'</xsl:param>
- <xsl:variable name="BdcIdToken" select="'_BdcId_'" />
- <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">
- <xsl:param name="thisNode" select="."/>
- <xsl:variable name="text" select="$thisNode/@Icon" />
- <xsl:choose>
- <xsl:when test="contains($text, $BdcIdToken)">
- <xsl:variable name="before" select="substring-before($text, $BdcIdToken)" />
- <xsl:variable name="after" select="substring-after($text, $BdcIdToken)" />
- <xsl:value-of select="concat($before, $thisNode/@BdcIdentity, $after)" disable-output-escaping ="yes"/>
- </xsl:when>
- <xsl:otherwise>
- <xsl:value-of select="$text" disable-output-escaping ="yes"/>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:template>
- <xsl:template name="FieldRef_body.Size" ddwrt:dvt_mode="body" match="FieldRef[@Name='Size']" mode="body" ddwrt:ghost="" xmlns:ddwrt2="urn:frontpage:internal">
- <xsl:param name="thisNode" select="."/>
- <xsl:if test="$thisNode/@IsFolder != 'Yes'">
- <xsl:value-of select="format-number($thisNode/@Size, '###,###,###')"/>
- </xsl:if>
- </xsl:template>
- </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 theSave 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 theQuickLaunchUrl in the schema file and the correct value for theOnQuickLaunch attribute in the elements file.
I’ve also tried to set field attributes like ShowInDisplayForm inSchema.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:
- <LobSystem Name="FileSystemModel" Type="DotNetAssembly">
- <LobSystemInstances>
- <LobSystemInstance Name="FileSystemModel" />
- </LobSystemInstances>
- <Entities>
- <Entity Name="FileSystemEntity" Namespace="FileSystem.FileSystemModel" EstimatedInstanceCount="1000" Version="1.0.0.35">
- <Properties>
- <Property Name="Class" Type="System.String">FileSystem.FileSystemModel.FileSystemEntityService, FileSystemModel</Property>
- </Properties>
- <Identifiers>
- <Identifier Name="Path" TypeName="System.String" />
- </Identifiers>
And this is the .NET class for the business entity:
- namespace FileSystem.FileSystemModel
- {
- public class FileSystemEntity
- {
- public String Path { get; set; }
- public String RootPath { get; set; }
- public String Name { get; set; }
- public String Icon
- {
- get
- {
- String icon = String.Empty;
- SPContext context = SPContext.Current;
- if (context != null)
- {
- SPWeb web = context.Web;
- if (IsFolder)
- {
- String relativePath = String.IsNullOrEmpty(RootPath) ? String.Empty : Path.Substring(RootPath.Length);
- icon = String.Format("<DIV><a href='?path={0}'>" +
- "<img title='{1}' alt='{1}' src='{2}/_layouts/images/folder.gif' border='0'/></a></DIV>",
- relativePath,
- Name,
- SPContext.Current.Site.Url
- );
- }
- else
- {
- icon = String.Format("<DIV><a href='{2}/_layouts/DownloadExternalData.aspx" +
- "?EntityNamespace=FileSystem.FileSystemModel" +
- "&EntityName=FileSystemEntity" +
- "&LobSystemInstanceName=FileSystemModel" +
- "&StreamAccessorName=FileAccessor" +
- "&IsXmlEncodedStreamName=true&ItemId=_BdcId_'>" +
- "<img title='{1}' alt='{1}' src='{2}/_layouts/images/{0}' border='0'/></a></DIV>",
- SPUtility.MapToIcon(web, Name, String.Empty, IconSize.Size16),
- Name,
- SPContext.Current.Site.Url
- );
- }
- }
- return icon;
- }
- }
- // FileInfo.Length is type of long but
- // Int64 (long) is not supported by external lists
- public int? Size { get; set; }
- public DateTime? Created { get; set; }
- public DateTime? LastModified { get; set; }
- public bool IsFolder { get; set; }
- }
- }
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 theSize 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 Iconfield. It causes the content of the field to be interpreted as HTML.
The definition of the Finder method in the model file:
- <Method Name="GetFilesByFolder" IsStatic="false">
- <FilterDescriptors>
- <FilterDescriptor Name="path" Type="Comparison" FilterField="path" />
- </FilterDescriptors>
- <Parameters>
- <Parameter Name="path" Direction="In">
- <TypeDescriptor Name="path" TypeName="System.String" AssociatedFilter="path" />
- </Parameter>
- <Parameter Name="returnParameter" Direction="Return">
- <TypeDescriptor TypeName="System.Collections.Generic.IEnumerable`1[[FileSystem.FileSystemModel.FileSystemEntity, FileSystemModel]]" IsCollection="true" Name="FileSystemEntityList">
- <TypeDescriptors>
- <TypeDescriptor TypeName="FileSystem.FileSystemModel.FileSystemEntity, FileSystemModel" Name="FileSystemEntity">
- <TypeDescriptors>
- <TypeDescriptor TypeName="System.String" IdentifierName="Path" Name="Path" />
- <TypeDescriptor TypeName="System.Boolean" Name="IsFolder" />
- <TypeDescriptor TypeName="System.String" Name="Name" />
- <TypeDescriptor TypeName="System.String" Name="Icon" />
- <TypeDescriptor TypeName="System.Int32" Name="Size" />
- <TypeDescriptor TypeName="System.DateTime" Name="Created">
- <Interpretation>
- <NormalizeDateTime LobDateTimeMode="Local" />
- </Interpretation>
- </TypeDescriptor>
- <TypeDescriptor TypeName="System.DateTime" Name="LastModified" DefaultDisplayName="Last modified">
- <Interpretation>
- <NormalizeDateTime LobDateTimeMode="Local" />
- </Interpretation>
- </TypeDescriptor>
- </TypeDescriptors>
- </TypeDescriptor>
- </TypeDescriptors>
- </TypeDescriptor>
- </Parameter>
- </Parameters>
- <MethodInstances>
- <MethodInstance Name="GetFilesByFolder" Type="Finder" ReturnParameterName="returnParameter" ReturnTypeDescriptorPath="FileSystemEntityList" Default="false" DefaultDisplayName="GetFilesByFolder" />
- </MethodInstances>
- </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.
The following method does the job for the finder:
- public IEnumerable<FileSystemEntity> GetFilesByFolder(string path)
- {
- List<FileSystemEntity> entityList = new List<FileSystemEntity>();
- String relativePath = String.Empty;
- String rootPath = path;
- HttpContext httpContext = HttpContext.Current;
- if ((httpContext != null) && (httpContext.Request != null))
- {
- relativePath = httpContext.Request.QueryString["path"];
- }
- if ((!String.IsNullOrEmpty(relativePath)) && (!relativePath.Contains("..")))
- {
- path = path + relativePath;
- }
- if (Directory.Exists(path))
- {
- DirectoryInfo di = new DirectoryInfo(path);
- FileInfo[] files = di.GetFiles();
- foreach (FileInfo file in files)
- {
- FileSystemEntity fileSystemEntity = GetFileSystemEntity(file, rootPath);
- entityList.Add(fileSystemEntity);
- }
- DirectoryInfo[] folders = di.GetDirectories();
- if (path != rootPath)
- {
- DirectoryInfo parentFolder = new DirectoryInfo(path).Parent;
- FileSystemEntity fileSystemEntity = new FileSystemEntity
- {
- IsFolder = true,
- Path = parentFolder.FullName,
- RootPath = rootPath,
- Name = "..",
- };
- entityList.Add(fileSystemEntity);
- }
- foreach (DirectoryInfo folder in folders)
- {
- FileSystemEntity fileSystemEntity = GetFileSystemEntity(folder, rootPath);
- entityList.Add(fileSystemEntity);
- }
- }
- return entityList;
- }
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:
- private static FileSystemEntity GetFileSystemEntity(FileInfo fileInfo, String rootPath)
- {
- FileSystemEntity fileSystemEntity = new FileSystemEntity
- {
- IsFolder = false,
- Path = fileInfo.FullName,
- RootPath = rootPath,
- Name = fileInfo.Name,
- // Int64 (long) is not supported by external lists
- Size = (fileInfo.Length > Int32.MaxValue) ? Int32.MaxValue : (Int32)fileInfo.Length,
- Created = fileInfo.CreationTime,
- LastModified = fileInfo.LastWriteTime
- };
- return fileSystemEntity;
- }
- private static FileSystemEntity GetFileSystemEntity(DirectoryInfo directoryInfo, String rootPath)
- {
- FileSystemEntity fileSystemEntity = new FileSystemEntity
- {
- IsFolder = true,
- Path = directoryInfo.FullName,
- RootPath = rootPath,
- Name = directoryInfo.Name,
- Size = 0,
- Created = directoryInfo.CreationTime,
- LastModified = directoryInfo.LastWriteTime
- };
- return fileSystemEntity;
- }
Note, that for folders we do not set the size property.
The SpecificFinder method is defined in the model as illustrated below:
- <Method Name="ReadItem">
- <Parameters>
- <Parameter Direction="In" Name="path">
- <TypeDescriptor TypeName="System.String" IdentifierName="Path" Name="Path" />
- </Parameter>
- <Parameter Direction="Return" Name="returnParameter">
- <TypeDescriptor TypeName="FileSystem.FileSystemModel.FileSystemEntity, FileSystemModel" Name="FileSystemEntity">
- <TypeDescriptors>
- <TypeDescriptor TypeName="System.String" IdentifierName="Path" Name="Path" />
- <TypeDescriptor TypeName="System.Boolean" Name="IsFolder" />
- <TypeDescriptor TypeName="System.String" Name="Name" />
- <TypeDescriptor TypeName="System.String" Name="Icon" />
- <TypeDescriptor TypeName="System.Int32" Name="Size" />
- <TypeDescriptor TypeName="System.DateTime" Name="Created">
- <Interpretation>
- <NormalizeDateTime LobDateTimeMode="Local" />
- </Interpretation>
- </TypeDescriptor>
- <TypeDescriptor TypeName="System.DateTime" Name="LastModified" DefaultDisplayName="Last modified">
- <Interpretation>
- <NormalizeDateTime LobDateTimeMode="Local" />
- </Interpretation>
- </TypeDescriptor>
- </TypeDescriptors>
- </TypeDescriptor>
- </Parameter>
- </Parameters>
- <MethodInstances>
- <MethodInstance Type="SpecificFinder" ReturnParameterName="returnParameter" Default="true" Name="ReadItem" DefaultDisplayName="Read FileSystemEntity" />
- </MethodInstances>
- </Method>
The corresponding method in the .NET class is really straightforward:
- public static FileSystemEntity ReadItem(String path)
- {
- FileSystemEntity fileSystemEntity = null;
- if (File.Exists(path))
- {
- FileInfo file = new FileInfo(path);
- fileSystemEntity = GetFileSystemEntity(file, null);
- }
- else if (Directory.Exists(path))
- {
- DirectoryInfo folder = new DirectoryInfo(path);
- fileSystemEntity = GetFileSystemEntity(folder, null);
- }
- else
- {
- // file deleted, create empty entity
- fileSystemEntity = new FileSystemEntity();
- }
- return fileSystemEntity;
- }
The last method our model implements is the StreamAccessor.
- <Method Name="GetFile">
- <Parameters>
- <Parameter Direction="In" Name="path">
- <TypeDescriptor TypeName="System.String" IdentifierName="Path" Name="Path" />
- </Parameter>
- <Parameter Name="StreamData" Direction="Return">
- <TypeDescriptor TypeName="System.IO.Stream" Name="FileContent" />
- </Parameter>
- </Parameters>
- <MethodInstances>
- <MethodInstance Name="FileAccessor" Type="StreamAccessor" ReturnParameterName="StreamData" ReturnTypeDescriptorName="FileContent">
- <Properties>
- <Property Name="FileNameField" Type="System.String">Name</Property>
- </Properties>
- </MethodInstance>
- </MethodInstances>
- </Method>
We simply provide the read-only stream for the selected file:
- public static Stream GetFile(String path)
- {
- return new FileStream(path, FileMode.Open);
- }
You might be a bit surprised that I’m implementing aStreamAccessor 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 theStreamAccessor 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.”
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 theDownloadExternalData.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):
- icon = String.Format("<DIV><a href='{2}/_layouts/DownloadExternalData.aspx" +
- "?EntityNamespace=FileSystem.FileSystemModel" +
- "&EntityName=FileSystemEntity" +
- "&LobSystemInstanceName=FileSystemModel" +
- "&StreamAccessorName=FileAccessor" +
- "&IsXmlEncodedStreamName=true&ItemId=_BdcId_'>" +
- "<img title='{1}' alt='{1}' src='{2}/_layouts/images/{0}' border='0'/></a></DIV>",
- SPUtility.MapToIcon(web, Name, String.Empty, IconSize.Size16),
- Name,
- SPContext.Current.Site.Url
- );
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:
- <xsl:when test="contains($text, $BdcIdToken)">
- <xsl:variable name="before" select="substring-before($text, $BdcIdToken)" />
- <xsl:variable name="after" select="substring-after($text, $BdcIdToken)" />
- <xsl:value-of select="concat($before, $thisNode/@BdcIdentity, $after)" disable-output-escaping ="yes"/>
- </xsl:when>
For folders we simply create a link for the current page passing apath query string parameter that contains its relative path related to the root.
- String relativePath = String.IsNullOrEmpty(RootPath) ? String.Empty : Path.Substring(RootPath.Length);
- icon = String.Format("<DIV><a href='?path={0}'>" +
- "<img title='{1}' alt='{1}' src='{2}/_layouts/images/folder.gif' border='0'/></a></DIV>",
- relativePath,
- Name,
- SPContext.Current.Site.Url
- );
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 theQuery/OrderBy nodes in the Schema.xml.
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:
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.
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.
The next screen is not from the current version but shows how the default view form would be displayed without customization:
In this case the form would be defined in the Schema.xml using the following settings:
- <Forms>
- <Form Type="DisplayForm" Url="DispForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
- </Forms>
That display is definitely not what I would like to see.
Since the content of the display form is described by aBinarySerializedWebPart section when working with the site template exported earlier, it is not trivial to alter it in theElements.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.
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 fieldsPath, 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 theModule that describes the DispForm.aspx form of my list. Since my list is called Files, the Module node has the Name attribute valueListsFiles_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 FileSystemFormsmodule of my project.
- <?xml version="1.0" encoding="utf-8"?>
- <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
- <Module Name="FileSystemForms" Url="Lists/Files" RootWebOnly="FALSE" SetupPath="pages">
- <FileUrl="DispForm.aspx" Type="Ghostable" Path="form.aspx">
- <BinarySerializedWebPart>
- <GUIDMap>
- <GUID Id="33ff2881_489d_4ce2_ac94_e81d64689d2a" ListUrl="Lists/Files" />
- </GUIDMap>
- <WebPart ID="{035cec7d-5f69-4dbf-a551-0b8203467c41}" WebPartIdProperty="" List="{$ListId:Lists/Files;}" Type="4"
- Flags="0" DisplayName="" Version="4" Url="Lists/Files/DispForm.aspx" WebPartOrder="1" WebPartZoneID="Main"
- IsIncluded="True" FrameState="0" WPTypeId="{feaafd58-2dc9-e199-be37-d6cdd7f84690}"
- SolutionId="{00000000-0000-0000-0000-000000000000}" Assembly="" Class="" Src=""
- AllUsers="B6Dt/kMAAAABAAAAAAAAAAIAAAAvX2xheW91dHMvaW1hZ2VzL2l0ZWJsLnBuZwAvRjFTaXRlL0xpc3RzL0ZpbGVzAP8BFCsAJQICAgMCAwEEAAICAhICFAEBAAIEBQtDb250cm9sTW9kZQspiAFNaWNyb3NvZnQuU2hhcmVQb2ludC5XZWJDb250cm9scy5TUENvbnRyb2xNb2RlLCBNaWNyb3NvZnQuU2hhcmVQb2ludCwgVmVyc2lvbj0xNC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj03MWU5YmNlMTExZTk0MjljAQUIRm9ybVR5cGUCBAEAAAIWAoYBCyo0U3lzdGVtLldlYi5VSS5XZWJDb250cm9scy5XZWJQYXJ0cy5XZWJQYXJ0RXhwb3J0TW9kZQICggEFGi9fbGF5b3V0cy9pbWFnZXMvaXRlYmwucG5nAn0FEy9GMVNpdGUvTGlzdHMvRmlsZXMFCFBhZ2VUeXBlCyl3TWljcm9zb2Z0LlNoYXJlUG9pbnQuUEFHRVRZUEUsIE1pY3Jvc29mdC5TaGFyZVBvaW50LCBWZXJzaW9uPTE0LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTcxZTliY2UxMTFlOTQyOWMEBQdMaXN0VXJsZQUGTGlzdElkKClYU3lzdGVtLkd1aWQsIG1zY29ybGliLCBWZXJzaW9uPTIuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OSQzM2ZmMjg4MS00ODlkLTRjZTItYWM5NC1lODFkNjQ2ODlkMmEFD0xpc3REaXNwbGF5TmFtZWUClQEFJnszM0ZGMjg4MS00ODlELTRDRTItQUM5NC1FODFENjQ2ODlEMkF9BQ1YbWxEZWZpbml0aW9uBcUPDQo8VXNlckNvbnRyb2wgeDpDbGFzcz0iRm9ybVhtbFRvWGFtbC5Vc2VyQ29udHJvbDIiIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIiB4bWxuczpTaGFyZVBvaW50PSJNaWNyb3NvZnQuU2hhcmVQb2ludC5XZWJDb250cm9scyIgeG1sbnM6c3lzdGVtPSJjbHItbmFtZXNwYWNlOlN5c3RlbTthc3NlbWJseT1tc2NvcmxpYiI+PFN0YWNrUGFuZWwgeDpOYW1lPSJGb3JtIj4NCjxTdGFja1BhbmVsLlJlc291cmNlcz4NCjxzeXN0ZW06U3RyaW5nIHg6S2V5PSJGb3JtTW9kZSI+RGlzcGxheTwvc3lzdGVtOlN0cmluZz4NCjxzeXN0ZW06U3RyaW5nIHg6S2V5PSJGb3JtVHlwZSI+TGlzdEZvcm08L3N5c3RlbTpTdHJpbmc+DQo8L1N0YWNrUGFuZWwuUmVzb3VyY2VzPg0KPFN0YWNrUGFuZWwgeDpOYW1lPSJNYWluU2VjdGlvbnMiPjxHcmlkPjxHcmlkLkNvbHVtbkRlZmluaXRpb25zPg0KPENvbHVtbkRlZmluaXRpb24gU3R5bGU9IntTdGF0aWNSZXNvdXJjZSBtcy1mb3JtbGFiZWx9Ii8+DQo8Q29sdW1uRGVmaW5pdGlvbiBTdHlsZT0ie1N0YXRpY1Jlc291cmNlIG1zLWZvcm1ib2R5fSIvPg0KPC9HcmlkLkNvbHVtbkRlZmluaXRpb25zPjxHcmlkLlJvd0RlZmluaXRpb25zPg0KPFJvd0RlZmluaXRpb24gLz4NCjxSb3dEZWZpbml0aW9uIC8+DQo8Um93RGVmaW5pdGlvbiAvPg0KPFJvd0RlZmluaXRpb24gLz4NCjwvR3JpZC5Sb3dEZWZpbml0aW9ucz4NCjxTaGFyZVBvaW50OkZpZWxkTGFiZWwgR3JpZC5Db2x1bW49IjAiIEdyaWQuUm93PSIwIiBDb250cm9sTW9kZT0iRGlzcGxheSIgRmllbGROYW1lPSJOYW1lIiAvPg0KPENvbW1lbnQgRmllbGROYW1lPSJOYW1lIiBGaWVsZEludGVybmFsTmFtZT0iTmFtZSIgRmllbGRUeXBlPSJUZXh0IiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMCIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTmFtZSIgSW5jbHVkZURlc2NyaXB0aW9uPSJUcnVlIi8+DQo8U2hhcmVQb2ludDpGaWVsZExhYmVsIEdyaWQuQ29sdW1uPSIwIiBHcmlkLlJvdz0iMSIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iU2l6ZSIgLz4NCjxDb21tZW50IEZpZWxkTmFtZT0iU2l6ZSIgRmllbGRJbnRlcm5hbE5hbWU9IlNpemUiIEZpZWxkVHlwZT0iSW50ZWdlciIgLz4NCjxTaGFyZVBvaW50OkZvcm1GaWVsZCBHcmlkLkNvbHVtbj0iMSIgR3JpZC5Sb3c9IjEiIENvbnRyb2xNb2RlPSJEaXNwbGF5IiBGaWVsZE5hbWU9IlNpemUiIEluY2x1ZGVEZXNjcmlwdGlvbj0iVHJ1ZSIvPg0KPFNoYXJlUG9pbnQ6RmllbGRMYWJlbCBHcmlkLkNvbHVtbj0iMCIgR3JpZC5Sb3c9IjIiIENvbnRyb2xNb2RlPSJEaXNwbGF5IiBGaWVsZE5hbWU9IkNyZWF0ZWQiIC8+DQo8Q29tbWVudCBGaWVsZE5hbWU9IkNyZWF0ZWQiIEZpZWxkSW50ZXJuYWxOYW1lPSJDcmVhdGVkIiBGaWVsZFR5cGU9IkRhdGVUaW1lIiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMiIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iQ3JlYXRlZCIgSW5jbHVkZURlc2NyaXB0aW9uPSJUcnVlIi8+DQo8U2hhcmVQb2ludDpGaWVsZExhYmVsIEdyaWQuQ29sdW1uPSIwIiBHcmlkLlJvdz0iMyIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTGFzdE1vZGlmaWVkIiAvPg0KPENvbW1lbnQgRmllbGROYW1lPSJMYXN0IG1vZGlmaWVkIiBGaWVsZEludGVybmFsTmFtZT0iTGFzdE1vZGlmaWVkIiBGaWVsZFR5cGU9IkRhdGVUaW1lIiAvPg0KPFNoYXJlUG9pbnQ6Rm9ybUZpZWxkIEdyaWQuQ29sdW1uPSIxIiBHcmlkLlJvdz0iMyIgQ29udHJvbE1vZGU9IkRpc3BsYXkiIEZpZWxkTmFtZT0iTGFzdE1vZGlmaWVkIiBJbmNsdWRlRGVzY3JpcHRpb249IlRydWUiLz4NCjwvR3JpZD4NCjwvU3RhY2tQYW5lbD4NCjwvU3RhY2tQYW5lbD4NCjwvVXNlckNvbnRyb2w+AktkBRFQYXJhbWV0ZXJCaW5kaW5ncwXoAg0KPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iZHZ0X2Fwb3MiIExvY2F0aW9uPSJQb3N0YmFjaztDb25uZWN0aW9uIi8+DQogICAgICAgIDxQYXJhbWV0ZXJCaW5kaW5nIE5hbWU9IlVzZXJJRCIgTG9jYXRpb249IkNBTUxWYXJpYWJsZSIgRGVmYXVsdFZhbHVlPSJDdXJyZW50VXNlck5hbWUiLz4NCiAgICAgICAgPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iVG9kYXkiIExvY2F0aW9uPSJDQU1MVmFyaWFibGUiIERlZmF1bHRWYWx1ZT0iQ3VycmVudERhdGUiLz4NCiAgICAgICAgPFBhcmFtZXRlckJpbmRpbmcgTmFtZT0iTGlzdEl0ZW1JZCIgTG9jYXRpb249IlF1ZXJ5U3RyaW5nKElEKSIgRGVmYXVsdFZhbHVlPSIwIi8+DQogICAgICAgIA==" />
- </BinarySerializedWebPart>
- </File>
- </Module>
- </Elements>
The next image shows the result.
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.
You can download the sample project here (including the sample file structure).