65.9K
CodeProject 正在变化。 阅读更多。
Home

禅与 XSLT 渲染字段的艺术(SharePoint 2010)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2012 年 7 月 20 日

CPOL

16分钟阅读

viewsIcon

101744

downloadIcon

623

示例:如何在文档库中将ECB(快速访问菜单)从文件名移至标题,同时保留所有原生功能。

引言

SharePoint 2010 包含了很多 MOSS 2007 中没有的新功能,并且在很多方面都远远领先于之前的版本。然而,有些在 MOSS 中曾经很直接(尽管有时显得笨拙和不完善)的功能,在最新的——可能很快就会过时——版本中却变得晦涩难懂。它们使得开发自定义字段类型及其自定义字段类以便在编辑/新建/显示表单中渲染字段变得容易,但是……如果你习惯了旧的方式,要在列表视图中修改计算字段的渲染方式,会相当令人困惑。

注意:当我提到“计算”时,我并不是指使用“类似 Excel”公式的“已计算”列。那些都相当容易设置。

背景

我总是认为用一个例子来解释复杂的事情是最好的。  

假设你刚刚将 2000 个文件从以前的内容管理/版本控制系统(比如 FileNET?)导入到 SharePoint 中,而文件名都是晦涩难懂的乱码,与人类可读的标题大相径庭。当用户去获取文件时,看到一堆 xhggsdf12.docx 和 dfdfkkii.xslx 文件名,会怎么想?当然,第一个答案可能是……“哦,这很简单,我把标题列放在旁边显示!” 然后……你可能会遇到一个问题,文件名仍然是乱码,而旁边是标题。你可能仍然想显示最后修改人、修改日期,以及其他一两个信息,这样一来,视图就会显得有点拥挤。让标题列成为“关注点”,但菜单却固定在垃圾文件名上,这会给用户体验带来相当大的挫败感。如果你不相信我,试试看。即使是经验丰富的 SharePoint 用户(我知道,他们其实不存在),也会开始抱怨他们看一个方向,却点击另一个方向。这实在是不对劲。

你可能还会尝试在导入 SharePoint 之前用脚本重命名文件,或者在导入之后用脚本重命名。但这相当繁琐,而且坦白说,这不是一篇关于重命名文件的文章。(是的,也许我的例子有点牵强。这就是为什么我称之为例子。)

你可能还会尝试 SharePoint Designer 中的一个“快速修复”,它在列表(非文档库)中效果几乎完美,使用了闪亮的“TitleLink”列。但如果你将菜单放在“Name”(又名文件)列上,它就会破坏“在某个程序中打开……”功能。不仅如此,点击它甚至不会打开文档。它会显示文档的属性(就像列表项一样——明白了吗?)这比让标题显示在晦涩的文件名旁边还要糟糕。

到这个时候,你可能已经意识到这不会很简单。至少,表面上不会。

但别担心!我们是开发者,这意味着我们不必等待下一个版本!我们可以制定规则!(某种程度上。)我们可以将菜单/链接功能从文件名列移动到标题列,并且让它像文件名列一样工作!包括菜单和点击!我所要做的就是……别,开玩笑的。

答案是使用 SharePoint 自己使用的东西……XSLT 样式表!

什么是样式表?

X-S-L 转换样式表。在 SharePoint 2010 中,列表视图的渲染已经放弃使用协作应用程序标记语言(CAML)的“RenderPatterns”,转而使用 XSLT。(CAML 仍然存在,只是不再用于渲染列表视图。)事实上,ListViewWebPart 已被 XsltListViewWebPart 取代——你可以在 SharePoint Designer 中进行操作来验证这一点。我没有时间详细介绍这两种不同的技术是什么,但你可以通过跟随这里的步骤来学习你需要知道的。(或者你可以阅读我在这段落开头链接的教程。懒得往上找?链接在这里再次提供。

如果你以前从未见过 XSLT,这看起来会很吓人!XSLT 主要根据一个或多个样式表的规则,将 XML 从一种形式转换为另一种形式。这些 .xsl 文件就是样式表。它们包含一个或多个(有时是很多个)模板,这些模板是转换的细节指令。你可能还认为 XSLT 不完全算是一种“语言”,而是一种 XML 的方言。(有人猜测 XSLT 确实是图灵完备的,所以你可以在喝咖啡时和我争论,它是否和 Visual Basic 或 Beatnik 一样是一门语言,在我喝第二杯咖啡之前,我可能不得不同意。)样式表可以导入其他样式表。每个样式表包含一个或多个模板,这些模板可以通过匹配一个规则和一个XPath 表达式,或通过模板名称来调用。你可以使用参数调用模板。SharePoint 列表视图喜欢参数。(是真的。我在城市各处树的侧面刻的爱心上看到过它们的名字。)

SharePoint 本身如何使用这些东西

如果你在 SharePoint 服务器上打开 %ProgramFiles%\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\XSL 目录,你会看到一堆 XSL 文件。渲染你的列表视图的文件叫做 main.xsl。

我们来看看!

main.xsl

 <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">
  <xsl:import href="https://codeproject.org.cn/_layouts/xsl/fldtypes.xsl"/>
  <xsl:import href="https://codeproject.org.cn/_layouts/xsl/vwstyles.xsl"/>
  <xsl:output method="html" indent="no"/>
  <xsl:decimal-format NaN=""/>
  <xsl:param name="NavigateForFormsPages" />
  <xsl:param name="MasterVersion" select="3"/>
  <xsl:param name="TabularView"/>
  <xsl:param name="NoAJAX"/>
  <xsl:param name="WPQ"/>
  <xsl:param name="RowLimit" select="5"/>
  <xsl:param name="dvt_sortdir" select="'ascending'"/>
  <xsl:param name="dvt_sortfield" />
  <xsl:param name="WebPartClientID"/>
  <xsl:param name="dvt_filterfields" />
  <xsl:param name="dvt_partguid" />
  <xsl:param name="dvt_firstrow" select="1"/>
  <xsl:param name="dvt_nextpagedata" />
  <xsl:param name="dvt_prevpagedata" />
  <xsl:param name="XmlDefinition" select="."/>
  <xsl:param name="ViewCounter" select="'1'"/>
  <xsl:param name="View" />
  <xsl:param name="ListUrlDir"/>
  <xsl:param name="List" />
  <xsl:param name="Project"/>
  <xsl:param name="WebTitle"/>
  <xsl:param name="ListTitle"/>
 <!-- I DELETED A LOT OF STUFF -->
 </xsl:stylesheet>

就 XSL 样式表而言,这个相当简单。开头的命名空间垃圾你可以暂时忽略。实际样式表主体的前两行只是导入其他样式表。接下来的几行只是对 XSLT 处理器的一些简单格式化指令。文件其余部分只包含参数声明,这些参数会在转换运行之前由 XsltListViewWebPart 自动填充。真是太多的参数了!

这个文件的最重要部分,除了你在样式表中可以使用非常方便的参数之外,就是导入了 fldtypes.xsl 文件。

fldtypes.xsl 是 SharePoint 准备的用于渲染其所有内置字段的通用模板集。没错,孩子们,如果 SharePoint 想在列表视图中渲染一个字段,它会像我们一样做:使用 XSLT!当你显示一个带有列表视图 Web 部件的页面时,SharePoint 会执行视图定义中编码的 CAML 查询,生成一个 dsQueryResponse 元素的原始 XML。然后,main.xsl 会使用 Microsoft 的 XSLT 处理引擎 (MSXLT) 对这个 dsQueryResponse 进行处理,将其转换为我们都熟悉和喜爱的漂亮 HTML 和 JavaScript。你可以在这里看到这个过程的“之前和之后”的例子。

这种“大家都一样”的好处是,如果你想知道 X 功能是如何渲染的,你可以在 fldtypes.xsl 中搜索你想要使用的片段,看看 Redmond(微软)是如何做的。

那么,我们在找什么?

我之前提到过,我们希望菜单(称为编辑控件块菜单或简称“ECB 菜单”)出现在标题列上,而不是仅仅是文件名。所以我们最好的办法是弄清楚文件名是如何做到这一点的。

我们需要找出文件名字段是如何做到的!

让我们稍微后退一步,先思考一下。在转换输出之前,我们需要确保输入是正确的。文件名字段到底是什么?它如何告诉 SharePoint 将必要的数据读入 dsQueryResponse?如果你深入研究 MSDN 文档,你会最终发现文件名文本,也就是我们喜欢称之为文件名的部分,是一个叫做 FileLeafRef 的查找属性。但那只是一个单一的值。它不是菜单!它甚至不在一个表格单元格里!这个东西是如何工作的?

你在页面上看到的内容是使用字段定义创建的。当然,这些可以使用 CAML 创建。带有菜单的文件名列的字段定义是什么? 带有菜单的文件名列的字段定义是什么?再次翻阅文档,我们发现,在 Web 界面中显示为“Name (linked to document with menu)”的特定字段定义叫做 LinkFilename。它是一个“Computed”(计算)类型的字段。这是一个关键的陈述。我来解释一下。

计算字段到底是什么?

计算字段总是在渲染时“计算”得出。它们本身不存储任何值在数据库中,但可以使用同一项中一个或多个其他字段的信息。(这与已计算字段不同,已计算字段实际上将其值存储在数据库中,并且仅在项目更改时更新。)它们使用 XSLT 样式表在列表视图上渲染——无论是 SharePoint 内置在 fldtypes.xsl 中的,还是同一目录中出现的其他 *.xsl 文件。它们通过在字段定义中“引用”其他字段的内容来包含它们。当执行 CAML 视图查询时,它会通过检查所有被引用的字段的聚合列表(使用 CAML 中的 FieldRef 子元素声明)来确定它需要加载哪些字段。计算字段可以引用包含实际数据在数据库中的字段和/或其他的计算字段。这可能会变得相当递归。

获取所有正确的字段

这种可能的“包含递归”意味着,跟踪模板中使用的特定数据可能会比一开始看起来更复杂。你必须找到所有必要的元素并引用所有必要的字段,以确保它们出现在查询响应中,以便你能够将它们作为输出进行转换。幸运的是,我们有一个指南,就是内置字段的字段定义。

打开 SharePoint Manager(一个很棒的工具,对开发者来说是必备的!),查看 LinkFilename 字段的字段定义,我们会发现它只引用了几个字段。 

<Field ID="{5cc6dc79-3710-4374-b433-61cb4a686c12}" Name="LinkFilename" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="LinkFilename" Group="Base Columns" ReadOnly="TRUE" Type="Computed" DisplayName="Name" DisplayNameSrcField="FileLeafRef" Filterable="FALSE" ClassInfo="Menu" AuthoringInfo="(linked to document with edit menu)">
  <FieldRefs>
    <FieldRef ID="{687c7f94-686a-42d3-9b67-2782eac4b4f8}" Name="FileLeafRef" />
    <FieldRef ID="{1344423c-c7f9-4134-88e4-ad842e2d723c}" Name="_EditMenuTableStart2" />
    <FieldRef ID="{2ea78cef-1bf9-4019-960a-02c41636cb47}" Name="_EditMenuTableEnd" />
    <FieldRef ID="{9d30f126-ba48-446b-b8f9-83745f322ebe}" Name="LinkFilenameNoMenu" />
  </FieldRefs>
 ... 

请注意,它确实引用了 FileLeafRef 字段,但只有另外 3 个字段……这让我觉得这是之前提到的“递归”字段引用之一。负责 ECB 菜单核心功能的是 _EditMenuTableStart2_EditMenuTableEnd。渲染文件名作为可点击链接,用漂亮的图标打开相应程序的那个部分是 LinkFilenameNoMenu。我们可以推断,在查看 XSL 之前,它会渲染 LinkFilename,并将其夹在 _EditMenuTableStart2_EditMenuTableEnd 字段之间。 

现在总结一下,我深入研究了每一个,并递归地追踪了结果字段列表,在进行一些筛选和复制粘贴到记事本后,我得到了这个字段引用列表: 

      <FieldRef ID="{687c7f94-686a-42d3-9b67-2782eac4b4f8}" Name="FileLeafRef" />
      <FieldRef ID="{56605df6-8fa1-47e4-a04c-5b384d59609f}" Name="FileDirRef" />
      <FieldRef ID="{30bb605f-5bae-48fe-b4e3-1f81d9772af9}" Name="FSObjType" />
      <FieldRef ID="{1d22ea11-1e32-424e-89ab-9fedbadb6ce1}" Name="ID" />
      <FieldRef ID="{105f76ce-724a-4bba-aece-f81f2fce58f5}" Name="ServerUrl" />
      <FieldRef ID="{0c5e0085-eb30-494b-9cdd-ece1d3c649a2}" Name="HTML_x0020_File_x0020_Type" />
      <FieldRef ID="{39360f11-34cf-4356-9945-25c44e68dade}" Name="File_x0020_Type" />
      <FieldRef ID="{9d30f126-ba48-446b-b8f9-83745f322ebe}" Name="LinkFileNameNoMenu" />
      <FieldRef ID="{BA3C27EE-4791-4867-8821-FF99000BAC98}" Name="PermMask" />
      <FieldRef ID="{1df5e554-ec7e-46a6-901d-d85a3881cb18}" Name="Author" />
      <FieldRef ID="{3881510a-4e4a-4ee8-b102-8ee8e2d0dd4b}" Name="CheckedOutUserId" />
      <FieldRef ID="{c63a459d-54ba-4ab7-933a-dcf1c6fadec2}" Name="_SourceUrl" />
      <FieldRef ID="{26d0756c-986a-48a7-af35-bf18ab85ff4a}" Name="_HasCopyDestinations" />
      <FieldRef ID="{6b4e226d-3d88-4a36-808d-a129bf52bccf}" Name="_CopySource" />
      <FieldRef ID="{fdc3b2ed-5bf2-4835-a4bc-b885f3396a61}" Name="_ModerationStatus" />
      <FieldRef ID="{7841bf41-43d0-4434-9f50-a673baef7631}" Name="_UIVersion" />
      <FieldRef ID="{03e45e84-1992-4d42-9116-26f756012634}" Name="ContentTypeId" />  

“啊!”你说……“但我们也需要 Title!”你注意到这一点真是太聪明了!既然我们要使用标题字段的值,最好确保它在列表中!

<FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title"/>
  

所以我们会把它加到列表中。现在我们就可以创建我们的计算字段定义了。我们将创建两个,一个仅用于带菜单的文档标题链接,另一个用于不带菜单的链接。  启动 Visual Studio 2010…… 

创建 Visual Studio 项目

创建一个新的项目,选择 SharePoint 2010 -> Empty SharePoint Project 

确保在第二个屏幕上选择“Deploy as a farm solution”(部署为场解决方案),因为我们需要将一个文件发布到 Layouts 中的 XSL 目录。(注意:这意味着你不能在沙盒解决方案中这样做,因此也无法将其部署到 SharePoint Online。) 

现在,向项目中添加一个新项,选择 **Content Type**(内容类型)模板。内容类型的名称以及其基类型是什么并不重要。我们无论如何都会删除这部分,只是添加一些站点字段定义,但这会为我们设置好 Feature(功能)定义。 

现在你的解决方案资源管理器应该看起来像这样

你可以通过右键单击 Feature1 并选择“Rename”(重命名)来重命名该功能。(无论你将它重命名为什么,部署后它都会成为 TEMPLATES\Features 中你的功能文件夹的名称。)我选择了“DocLinkTitles”。 

然后双击重命名的文件进行编辑,你可以将功能名称更改为更有用的名称,例如“DocLinkTitles Feature”,并在描述中添加一些说明。我选择了“Feature containing the ability to move the ECB Menu to the Title column in document libraries.”(包含将 ECB 菜单移动到文档库标题列的功能的特性。)

现在我们将编辑 StupidContentType 节点下的 Elements.xml 文件。 选择从 <ContentType> 到 </ContentType> 的所有内容并删除它。粘贴以下内容代替:

    <Field ID="{F936A51C-9E26-45AA-A3A5-63357913E7E9}" 
         Name="DocTitleOrNameNoMenu" 
         StaticName="DocTitleOrNameNoMenu" 
         DisplayName="Document"
         Type="Computed" 
         ShowInDisplayForm="FALSE" 
         ShowInEditForm="FALSE" 
         ShowInNewForm="FALSE"
         Description="Link to Title or FileName"
         AuthoringInfo=" (linked to document)"
         Overwrite="TRUE">
    <FieldRefs>
      <!-- just so we get all of the required fields in the view query and the xsl has all the goodies we need will work -->
      <FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title"/>
      <FieldRef ID="{687c7f94-686a-42d3-9b67-2782eac4b4f8}" Name="FileLeafRef" />
      <FieldRef ID="{56605df6-8fa1-47e4-a04c-5b384d59609f}" Name="FileDirRef" />
      <FieldRef ID="{30bb605f-5bae-48fe-b4e3-1f81d9772af9}" Name="FSObjType" />
      <FieldRef ID="{1d22ea11-1e32-424e-89ab-9fedbadb6ce1}" Name="ID" />
      <FieldRef ID="{105f76ce-724a-4bba-aece-f81f2fce58f5}" Name="ServerUrl" />
      <FieldRef ID="{0c5e0085-eb30-494b-9cdd-ece1d3c649a2}" Name="HTML_x0020_File_x0020_Type" />
      <FieldRef ID="{39360f11-34cf-4356-9945-25c44e68dade}" Name="File_x0020_Type" />
      <FieldRef ID="{9d30f126-ba48-446b-b8f9-83745f322ebe}" Name="LinkFileNameNoMenu" />
      <FieldRef ID="{BA3C27EE-4791-4867-8821-FF99000BAC98}" Name="PermMask" />
      <FieldRef ID="{1df5e554-ec7e-46a6-901d-d85a3881cb18}" Name="Author" />
      <FieldRef ID="{3881510a-4e4a-4ee8-b102-8ee8e2d0dd4b}" Name="CheckedOutUserId" />
      <FieldRef ID="{c63a459d-54ba-4ab7-933a-dcf1c6fadec2}" Name="_SourceUrl" />
      <FieldRef ID="{26d0756c-986a-48a7-af35-bf18ab85ff4a}" Name="_HasCopyDestinations" />
      <FieldRef ID="{6b4e226d-3d88-4a36-808d-a129bf52bccf}" Name="_CopySource" />
      <FieldRef ID="{fdc3b2ed-5bf2-4835-a4bc-b885f3396a61}" Name="_ModerationStatus" />
      <FieldRef ID="{7841bf41-43d0-4434-9f50-a673baef7631}" Name="_UIVersion" />
      <FieldRef ID="{03e45e84-1992-4d42-9116-26f756012634}" Name="ContentTypeId" />
    </FieldRefs>
  </Field>
 
 
 
 
  <Field ID="{B5C29F91-5A68-4D11-9F83-F68E10C7136D}"
         Name="DocTitleOrName"
         StaticName="DocTitleOrName"
         DisplayName="Document"
         Type="Computed"
         ShowInDisplayForm="FALSE"
         ShowInEditForm="FALSE"
         ShowInNewForm="FALSE"
         Description="Link to Title or FileName"
         AuthoringInfo=" (linked to document with menu)"
         Overwrite="TRUE">
    <FieldRefs>
      <!-- just so we get all of the required fields in the view query and the xsl has all the goodies we need will work -->
      <FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title"/>
      <FieldRef ID="{687c7f94-686a-42d3-9b67-2782eac4b4f8}" Name="FileLeafRef" />
      <FieldRef ID="{56605df6-8fa1-47e4-a04c-5b384d59609f}" Name="FileDirRef" />
      <FieldRef ID="{30bb605f-5bae-48fe-b4e3-1f81d9772af9}" Name="FSObjType" />
      <FieldRef ID="{1d22ea11-1e32-424e-89ab-9fedbadb6ce1}" Name="ID" />
      <FieldRef ID="{105f76ce-724a-4bba-aece-f81f2fce58f5}" Name="ServerUrl" />
      <FieldRef ID="{0c5e0085-eb30-494b-9cdd-ece1d3c649a2}" Name="HTML_x0020_File_x0020_Type" />
      <FieldRef ID="{39360f11-34cf-4356-9945-25c44e68dade}" Name="File_x0020_Type" />
      <FieldRef ID="{9d30f126-ba48-446b-b8f9-83745f322ebe}" Name="LinkFileNameNoMenu" />
      <FieldRef ID="{BA3C27EE-4791-4867-8821-FF99000BAC98}" Name="PermMask" />
      <FieldRef ID="{1df5e554-ec7e-46a6-901d-d85a3881cb18}" Name="Author" />
      <FieldRef ID="{3881510a-4e4a-4ee8-b102-8ee8e2d0dd4b}" Name="CheckedOutUserId" />
      <FieldRef ID="{c63a459d-54ba-4ab7-933a-dcf1c6fadec2}" Name="_SourceUrl" />
      <FieldRef ID="{26d0756c-986a-48a7-af35-bf18ab85ff4a}" Name="_HasCopyDestinations" />
      <FieldRef ID="{6b4e226d-3d88-4a36-808d-a129bf52bccf}" Name="_CopySource" />
      <FieldRef ID="{fdc3b2ed-5bf2-4835-a4bc-b885f3396a61}" Name="_ModerationStatus" />
      <FieldRef ID="{7841bf41-43d0-4434-9f50-a673baef7631}" Name="_UIVersion" />
      <FieldRef ID="{03e45e84-1992-4d42-9116-26f756012634}" Name="ContentTypeId" />
    </FieldRefs>
  </Field>
  

注意:如果你看过“内置”字段,它们在 FieldRefs 元素末尾都有一个名为 DisplayPattern 的额外项。我们不处理这些,因为那只是为了兼容旧版本的 SharePoint。我们仅使用此字段定义来指定我们要将所有其他引用的字段包含在查询响应中。将 Type 属性指定为“Computed”(计算)基本上意味着“好的 SharePoint,给我这些数据,XSL 会处理其余的。” 

保存并关闭。 

更新说明:我将 ShowInXxxForm 属性的 FALSE 值大写了。Apparently for some properties this makes a difference and some it doesn't. I guess these are some of those that it does...(显然,对于某些属性来说,这很重要,而对于另一些则不。我猜这些是其中重要的部分……) 

我们刚才做了什么?

现在我们已经创建了一个 SharePoint 场解决方案,它将被打包成 WSP 存档。它将包含一个具有两个站点列定义的功能。我们可以立即部署它,但这对我们没有好处。即使计算列可以按原样添加到列表中,我们仍然有两个问题:我们在站点列中看不到计算字段(但它们在那里!),因此无法将它们添加到列表中。Secondly,我们没有 XSL 样式表来指示渲染我们新创建的“满是亮点的”字段。

让我们先处理样式表。 

SharePoint 会直接从 main.xsl 中加载默认的 fldtypes.xsl,但 XsltListViewWebPart 在渲染时也会加载 %ProgramFiles%\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\Layouts\XSL 文件夹中所有名为 fldtypes*.xsl 的其他文件。 这意味着我们需要创建一个样式表,其中包含一套与我们新字段匹配的模板,渲染它们,并将它们部署为一个正确命名的样式表在正确的位置。 

我们将通过在项目中添加一个映射到 Layouts 的文件夹来开始: 

只需右键单击解决方案资源管理器中的项目节点(DocLinkTitles),然后选择 **Add -> SharePoint "Layouts" Mapped Folder**(添加 -> SharePoint “Layouts” 映射文件夹)。 在此项目文件夹中放置的任何内容都将部署到服务器上相应的目录 **\14\TEMPLATE\Layouts**。

你会看到 Visual Studio 已经友好地为我们创建了一个 DocLinkTitles 文件夹,位于 Layouts 之下,这样我们的内容就可以与内置的以及其他人的解决方案稍微分开。这很好,但是我们需要将样式表放在 XSL 目录中,还记得吗?所以,在 Layouts 下创建一个新文件夹,不要在 DocLinkTitles 下面。 在 XSL 目录中,创建一个名为 fldtypes_doclinktitles.xsl 的空文件。 

将样式表内容粘贴到其中。 

<?xml version="1.0" encoding="utf-8"?>
<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" ddwrt:oob="true">
  <xsl:output method="html" indent="no"/>
 
  <xsl:template match="FieldRef[@Name='DocTitleOrNameNoMenu']" mode="Computed_body" priority="9.0">
    <xsl:param name="thisNode" select="."/>
    <xsl:param name="ShowAccessibleIcon" select="0"/>
    <xsl:param name="folderUrlAdditionalQueryString" select="''"/>
    <xsl:param name="Position" select="1" />
    <xsl:call-template name="TitleOrFileNameBody">
      <xsl:with-param name="thisNode" select="$thisNode" />
      <xsl:with-param name="ShowAccessibleIcon" select="$ShowAccessibleIcon"/>
      <xsl:with-param name="folderUrlAdditionalQueryString" select="$folderUrlAdditionalQueryString"/>
    </xsl:call-template>
  </xsl:template>
 
  <xsl:template match="FieldRef[@Name='DocTitleOrName']" mode="Computed_body" priority="9.0">
    <xsl:param name="thisNode" select="."/>
    <xsl:param name="ShowAccessibleIcon" select="0"/>
    <xsl:param name="folderUrlAdditionalQueryString" select="''"/>
    <xsl:param name="Position" select="1" />
    <xsl:variable name="ID">
      <xsl:call-template name="ResolveId">
        <xsl:with-param name="thisNode" select ="$thisNode"/>
      </xsl:call-template>
    </xsl:variable>
    <xsl:variable name="PermMask">
      <xsl:choose>
        <xsl:when test="$thisNode/@PermMask != ''">
          <xsl:value-of select="$thisNode/@PermMask"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="$ExternalDataListPermissions"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <xsl:choose>
      <xsl:when test="$EcbMode or $NoAJAX">
        <xsl:choose>
          <xsl:when test="$MasterVersion=4">
            <!-- Client JS uses 'itx' string to decide whether to create AJAX menu or not -->
            <!-- If AJAX is enabled, then we must include the 'itx' string at the end of the class -->
            <xsl:variable name="ClassName">
              <xsl:choose>
                <xsl:when test="$NoAJAX or $EcbMode">ms-vb</xsl:when>
                <xsl:otherwise>ms-vb itx</xsl:otherwise>
              </xsl:choose>
            </xsl:variable>
            <div class="{$ClassName}" onmouseover="OnItem(this)" CTXName="ctx{$ViewCounter}" id="{$ID}" Field="{@Name}"
              Url="{$thisNode/@FileRef.urlencodeasurl}" DRef="{$thisNode/@FileDirRef}" Perm="{$PermMask}" Type="{$thisNode/@HTML_x0020_File_x0020_Type}"
              Ext="{$thisNode/@File_x0020_Type}"
              OType="{$thisNode/@FSObjType}"
              COUId="{$thisNode/@CheckedOutUserId}" HCD="{$thisNode/@_HasCopyDestinations.value}"
              CSrc="{$thisNode/@_CopySource}" MS="{$thisNode/@_ModerationStatus.}" CType="{$thisNode/@ContentType}"
              CId="{$thisNode/@ContentTypeId}" UIS="{$thisNode/@_UIVersion}" SUrl="{$thisNode/@_SourceUrl}"
              Icon="{$thisNode/@HTML_x0020_File_x0020_Type.File_x0020_Type.mapall}"
              EventType="{$thisNode/@EventType}">
              <xsl:if test="$IsDocLib">
                <xsl:attribute name="sred">
                  <xsl:value-of select="$thisNode/@serverurl.progid"/>
                </xsl:attribute>
                <xsl:attribute name="defaultio">
                  <xsl:value-of select="$XmlDefinition/List/@DefaultItemOpen"/>
                </xsl:attribute>
                <xsl:attribute name="cout">
                  <xsl:value-of select="$thisNode/@IsCheckedoutToLocal"/>
                </xsl:attribute>
              </xsl:if>
              <xsl:call-template name="TitleOrFileNameBody">
                <xsl:with-param name="thisNode" select="$thisNode" />
                <xsl:with-param name="ShowAccessibleIcon" select="$ShowAccessibleIcon"/>
                <xsl:with-param name="folderUrlAdditionalQueryString" select="$folderUrlAdditionalQueryString"/>
              </xsl:call-template>
            </div>
            <!-- render the markup for list item chevron from server side -->
            <div class="s4-ctx" onmouseover="OnChildItem(this.parentNode); return false;">
              <span>&#160;</span>
              <a onfocus="OnChildItem(this.parentNode.parentNode); return false;" onclick="PopMenuFromChevron(event); return false;" href="javascript:;" title="{$open_menu}">
              </a>
              <span>&#160;</span>
            </div>
          </xsl:when>
          <xsl:otherwise>
            <table height="100%" cellspacing="0" class="ms-unselectedtitle" onmouseover="OnItem(this)" CTXName="ctx{$ViewCounter}" id="{$ID}"
             Url="{$thisNode/@FileRef.urlencodeasurl}" DRef="{$thisNode/@FileDirRef}" Perm="{$PermMask}" Type="{$thisNode/@HTML_x0020_File_x0020_Type}"
             Ext="{$thisNode/@File_x0020_Type}"
             OType="{$thisNode/@FSObjType}"
             COUId="{$thisNode/@CheckedOutUserId}" HCD="{$thisNode/@_HasCopyDestinations.value}"
             CSrc="{$thisNode/@_CopySource}" MS="{$thisNode/@_ModerationStatus.}" CType="{$thisNode/@ContentType}"
             CId="{$thisNode/@ContentTypeId}" UIS="{$thisNode/@_UIVersion}" SUrl="{$thisNode/@_SourceUrl}"
             DocIcon="{$thisNode/@HTML_x0020_File_x0020_Type.File_x0020_Type.mapall}"
             EventType="{$thisNode/@EventType}">
              <xsl:if test="$IsDocLib">
                <xsl:attribute name="sred">
                  <xsl:value-of select="$thisNode/@serverurl.progid"/>
                </xsl:attribute>
                <xsl:attribute name="defaultio">
                  <xsl:value-of select="$XmlDefinition/List/@DefaultItemOpen"/>
                </xsl:attribute>
                <xsl:attribute name="cout">
                  <xsl:value-of select="$thisNode/@IsCheckedoutToLocal"/>
                </xsl:attribute>
              </xsl:if>
              <tr>
                <td width="100%" class="ms-vb">
                  <xsl:call-template name="TitleOrFileNameBody">
                    <xsl:with-param name="thisNode" select="$thisNode" />
                    <xsl:with-param name="ShowAccessibleIcon" select="$ShowAccessibleIcon"/>
                    <xsl:with-param name="folderUrlAdditionalQueryString" select="$folderUrlAdditionalQueryString"/>
                  </xsl:call-template>
                </td>
                <td>
                  <img src="https://codeproject.org.cn/_layouts/images/blank.gif" width="13" style="visibility:hidden" alt="" />
                </td>
              </tr>
            </table>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:when>
      <xsl:otherwise>
        <xsl:choose>
          <xsl:when test="$MasterVersion=4">
            <div class="ms-vb itx" onmouseover="OnItem(this)" CTXName="ctx{$ViewCounter}" id="{$ID}" Field="{@Name}" Perm="{$PermMask}" EventType="{$thisNode/@EventType}">
              <xsl:call-template name="TitleOrFileNameBody">
                <xsl:with-param name="thisNode" select="$thisNode" />
                <xsl:with-param name="ShowAccessibleIcon" select="$ShowAccessibleIcon"/>
                <xsl:with-param name="folderUrlAdditionalQueryString" select="$folderUrlAdditionalQueryString"/>
              </xsl:call-template>
            </div>
            <!-- render the markup for list item chevron from server side -->
            <div class="s4-ctx" onmouseover="OnChildItem(this.parentNode); return false;">
              <span>&#160;</span>
              <a onfocus="OnChildItem(this.parentNode.parentNode); return false;" onclick="PopMenuFromChevron(event); return false;" href="javascript:;" title="{$open_menu}">
              </a>
              <span>&#160;</span>
            </div>
          </xsl:when>
          <xsl:otherwise>
            <table height="100%" cellspacing="0" class="ms-unselectedtitle itx" onmouseover="OnItem(this)" CTXName="ctx{$ViewCounter}" id="{$ID}" Field="{@Name}" Perm="{$PermMask}" EventType="{$thisNode/@EventType}">
              <tr>
                <td width="100%" class="ms-vb">
                  <xsl:call-template name="TitleOrFileNameBody">
                    <xsl:with-param name="thisNode" select="$thisNode" />
                    <xsl:with-param name="ShowAccessibleIcon" select="$ShowAccessibleIcon"/>
                    <xsl:with-param name="folderUrlAdditionalQueryString" select="$folderUrlAdditionalQueryString"/>
                  </xsl:call-template>
                </td>
                <td>
                  <img src="https://codeproject.org.cn/_layouts/images/blank.gif" width="13" style="visibility:hidden" alt="" ddwrt:insideECB=""/>
                </td>
              </tr>
            </table>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
 
 
  <xsl:template name="TitleOrFileNameBody">
    <xsl:param name="thisNode" select="."/>
    <xsl:param name="ShowAccessibleIcon" select="0"/>
    <xsl:param name="folderUrlAdditionalQueryString" select="''"/>
    <xsl:choose>
      <!-- if it's a folder, call LinkFilenameNoMenu and let the system do it's normal thing with it-->
      <xsl:when test="$thisNode/@FSObjType='1'">
        <xsl:call-template name="LinkFilenameNoMenu">
          <xsl:with-param name="thisNode" select="$thisNode"/>
          <xsl:with-param name="folderUrlAdditionalQueryString" select="$folderUrlAdditionalQueryString"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <!-- warning: this code has optimization in webpart. Change it must change the webpart code too!-->
        <a onfocus="OnLink(this)" href="{$thisNode/@FileRef}" onmousedown="return VerifyHref(this,event,'{$XmlDefinition/List/@DefaultItemOpen}','{$thisNode/@HTML_x0020_File_x0020_Type.File_x0020_Type.mapcon}','{$thisNode/@serverurl.progid}')"
           onclick="return DispEx(this,event,'TRUE','FALSE','{$thisNode/@File_x0020_Type.url}','{$thisNode/@File_x0020_Type.progid}','{$XmlDefinition/List/@DefaultItemOpen}','{$thisNode/@HTML_x0020_File_x0020_Type.File_x0020_Type.mapcon}','{$thisNode/@HTML_x0020_File_x0020_Type}','{$thisNode/@serverurl.progid}','{$thisNode/@CheckoutUser.id}','{$Userid}','{$XmlDefinition/List/@ForceCheckout}','{$thisNode/@IsCheckedoutToLocal}','{$thisNode/@PermMask}')">
          <xsl:call-template  name="TitleValue">
            <xsl:with-param name="thisNode" select="$thisNode"/>
            <xsl:with-param name="ShowAccessibleIcon" select="$ShowAccessibleIcon"/>
          </xsl:call-template>
        </a>
        <xsl:if test="$thisNode/@Created_x0020_Date.ifnew='1'">
          <xsl:call-template name="NewGif">
            <xsl:with-param name="thisNode" select="$thisNode"/>
          </xsl:call-template>
        </xsl:if>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
 
 
  <xsl:template name="TitleValue">
    <xsl:param name="thisNode" select="."/>
    <xsl:param name="ShowAccessibleIcon" select="0"/>
    <xsl:variable name="titlevalue" select="$thisNode/@Title"/>
    <xsl:choose>
      <xsl:when test="$titlevalue=''">
        <!-- no title? empty title? k. filename. -->
        <xsl:value-of select="$thisNode/@FileLeafRef.Name" />
      </xsl:when>
      <xsl:otherwise>
        <xsl:choose>
          <xsl:when test="$HasTitleField">
            <!-- if it's also at top level, it's already encoded, decode it.-->
            <xsl:value-of disable-output-escaping="yes" select="$titlevalue" />
          </xsl:when>
          <xsl:otherwise>
            <xsl:choose>
              <xsl:when test="string-length($titlevalue) = 0">
                <!-- no title? empty title? k. filename. -->
                <xsl:value-of select="$thisNode/@FileLeafRef.Name" />
              </xsl:when>
              <xsl:otherwise>
                <xsl:value-of select="$titlevalue" />
              </xsl:otherwise>
            </xsl:choose>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:otherwise>
    </xsl:choose>
    <xsl:choose>
      <xsl:when test="$ShowAccessibleIcon">
        <img src="https://codeproject.org.cn/_layouts/images/blank.gif" class="ms-hidden" border="0" width="1" height="1" alt="{$idPresEnabled}" />
      </xsl:when>
      <xsl:otherwise></xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>
  

保存。 

这个样式表有什么作用?

这个样式表包含一些模板,这些模板大致基于 LinkFilename 的模板。我有两个“顶层”模板,一个使用 XPath 表达式 "FieldRef[@Name='DocTitleOrNameNoMenu']" 匹配 DocTitleOrNameNoMenu,另一个使用 XPath 表达式 "FieldRef[@Name='DocTitleOrName']" 匹配 DocTitleOrName。 第一个只是调用另一个模板 **TitleOrFileNameBody**,第二个先生成 ECB 菜单,然后再调用相同的 **TitleOrFileNameBody** 模板。 (制作 ECB 菜单三明治!)

如果发现该项是一个文件夹(即 FSObjType 变量设置为 '1'),它将直接调用用于“LinkFilename”的内置 SharePoint 模板。 文件夹没有 Title 属性,所以我们让它使用默认行为。 

如果没有 Title 属性,它将回退到文件名。没问题。 

注意到我在顶层模板的属性上设置了另外两个有趣的属性 **mode="Computed_body"** 和 **priority="9.0"**。 优先级并非绝对必要,但这是为了确保我的模板会被应用而不是任何其他匹配的模板,因为我是一个贪婪无情的人,我想独享我字段的所有渲染。 SharePoint 没有其他模板会匹配,但有人可能会在明天部署一个来对付我!9.0 是最高优先级,所以我是稳操胜券。

匹配和模式属性是我“钩子”,用于接入 SharePoint 内置的 fldtypes.xsl 样式表。在其深处,它会对任何没有其他渲染方式的“Computed”(计算)字段调用一个“通用” **<xsl:apply-templates match="." mode="Computed_body">**,将控制权转交给任何具有适当匹配表达式并指定“Computed_body”模式的其他模板。因为我们的模板同时具备这两个条件,所以它会得到青睐并渲染内容。 

这很好,但是…… 

现在,如果你部署它,并且找到一种方法将这些字段添加到列表中,它们就会像雨后初晴一样渲染,但是……我们如何将它们附加到列表,因为它们没有显示在站点列中?

我的最后一个绝活 

我创建了一个自定义应用程序页面,它将允许我将这些“坏小子”连接到列表。 

我只是在 **Layouts\DocLinkTitles** 下添加了一个 **New Item -> Application Page**(新建项 -> 应用程序页面),命名为 FieldAdder.aspx。

 

页面的内容很简单,一个包含网站上文档库的列表框,以及一个用于将字段添加到这些列表的按钮。 

FieldAdder.aspx (仅“Main”内容占位符的内容,不是整个文件) 

<div class="description">
        Select which lists (by control+clicking) into which you'd like to inject the DocTitleOrName fields.
        You will have to add them to the view.
    </div>
    <asp:ListBox runat="server" ID="lboxDocLibs" SelectionMode="Multiple">
    </asp:ListBox>
    <asp:Label runat="server" Font-Bold="true" EnableViewState="false" ID="message" />
    <br />
    <asp:Button runat="server" OnClick="doButton_click" ID="doButton" Text="Do it!" />
    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
    <asp:Button runat="server" OnClick="doneButton_click" ID="doneButton" Text="Done" />
  

FieldAdder.aspx.cs   

        protected override void OnInit(EventArgs e) {
            base.OnInit(e);
 
            // bind the dropdown
            foreach (SPList list in Web.Lists) {
                if (list.BaseTemplate == SPListTemplateType.DocumentLibrary) {
                    lboxDocLibs.Items.Add(new ListItem(list.Title));
                }
            }
        }
 
        protected void Page_Load(object sender, EventArgs e) {
        }
 
        protected void doButton_click(object sender, EventArgs e) {
            try {
                foreach (ListItem item in lboxDocLibs.Items) {
                    if (item.Selected) {
                        DoList(item.Text);
                    }
                }
            }
            catch (Exception ex) {
                SPUtility.TransferToErrorPage("There was an error adding the fields: " + ex.Message);
            }
 
            SPUtility.TransferToSuccessPage(
                    "The fields were successfully added! You will still need to add them to any views in which you want them to appear.");
        }
 
        private void DoList(string text) {
 

            var list = Web.Lists[text];
            if (list != null) {
 
                // only add them if they aren't there already.
                if (!list.Fields.Contains(DocTitleOrNameId)) {
                    var withMenu = Web.AvailableFields[DocTitleOrNameId];
                    list.Fields.Add(withMenu);
                }
                
                if (!list.Fields.Contains(DocTitleOrNameNoMenuId)) {
                    var withoutMenu = Web.AvailableFields[DocTitleOrNameNoMenuId];
                    list.Fields.Add(withoutMenu);
                }
 
            }
        }
 

        private static readonly Guid DocTitleOrNameNoMenuId = new Guid("{F936A51C-9E26-45AA-A3A5-63357913E7E9}");
        private static readonly Guid DocTitleOrNameId = new Guid("{B5C29F91-5A68-4D11-9F83-F68E10C7136D}");
 

所以……现在构建并部署解决方案。确保你的部署过程包括 IISRESET,这样 SharePoint 在重新启动时会重新扫描新的 XSL 文件。 

让我们去我们新部署的应用程序页面,并将这些字段推入我们的共享文档库: 

Image depicting the use of the field adding application page. 

注意:如果你使用的是下载中的版本,我添加了一个“未公开”的 Custom Action(自定义操作),它将 FieldAdder 页面的链接添加到 Site Settings(站点设置)中。  

现在,转到文档库并将字段添加到视图中。 

 

我将现有的 Name 和 Title 列放在页面上,与这两个新列并列,这样你就可以看到它们在功能上几乎相同——但文本不是文件名! 

瞧!它奏效了! 

 

你可以根据自己的风险部署附带的 WSP。 我不提供任何形式的保修。你也可以根据需要编辑/修改它。当然,你可以创建任何其他自定义字段渲染,但这只是展示了示例。 

历史 

2012/7/19 - 第一轮,文章创建。
2012/8/17 - 修正了字段定义属性中 FALSE 的大写。

© . All rights reserved.