构建 HTML5 应用程序

向 SharePoint 列表添加 HTML5 拖放功能

Andrey Markeev

下载代码示例

Microsoft SharePoint 企业平台具有悠久的发展历史和广泛的各种功能,这就是为什么它的反应速度总是不够快,无法迎合新兴 Web 技术发展趋势的原因。 尽管大量企业均使用 SharePoint 并且致力于提供包罗万象的功能,但是 SharePoint 在沉浸式 UI 方面(例如 HTML5 和 CSS3)仍然滞后于现代的 CMS 产品。

在我看来,HTML5 不仅是一项热门的新技术,而且的确具有许多实用的优势: 它简单、方便且内容丰富,差不多受所有现代浏览器的支持(包括移动设备浏览器)。 此外,HTML5 和 JavaScript 正逐渐成为 Windows 桌面编程的主要技术。

所以 HTML5 毫无疑问也应在 SharePoint 中占有一席之地,使门户变得更加简单易用。 改善 SharePoint 界面对业务用户有着实际的帮助,因为可以让他们更快且更好地工作。

不幸的是,SharePoint 不具有任何内置 HTML5 功能,但它具有极大的灵活性。 在本文中,我将演示如何通过简单操作将 HTML5 拖放支持添加到 SharePoint 中,并且会使标准界面更加简洁,如图 1 所示。

Drag and Drop in SharePoint
图 1 SharePoint 中的拖放功能

为实现此功能,我将使用一个重要的 SharePoint 构建基块,这也是我最喜欢的 SharePoint 工具之一,即 XsltListViewWebPart 及其 XSL 转换(有关详细信息,请参阅 MSDN 库中的相应页面,网址为 bit.ly/wZVSFx)。

为什么不是自定义 Web 部件?

一如既往,SharePoint 提供了大量可能的实现方案,而选择您认为最适合的方案非常重要。

在解决 HTML5 拖放问题时,考虑到拖放功能主要是管理数据,许多 SharePoint 开发者可能倾向于构建自定义 Web 部件,在这种情况下该部件的行为就像普通的 ASP.NET 控件: 数据存储在标准 SharePoint 列表中,通过对象模型或 SPDataSource 控件检索该数据,并在 ASCX 标记和 ASP.NET 控件的帮助下呈现该数据。

简单、干净、清晰... 但这是最佳选择吗?

两年前,我的回答是肯定的。 但现在,我更喜欢使用 XsltListViewWebPart 的 XSL 转换自定义该 Web 部件。 我为什么改变了想法呢?

从 SharePoint 2010 开始,几乎所有类型的列表视图(只有日历例外)均通过该 Web 部件显示。 想象一下: 所有这些数据类型,所有这些不同的视图、样式和列表类型,所有种类繁多的数据都是使用 XsltListViewWebPart 及其 XSL 转换加以呈现的。 这是一个灵活且强大的工具。

如果您决定构建自己的自定义 Web 部件,以呈现某些用于显示列表数据的 HTML5 标记,那么您将丢失所有内置功能。 根据我的经验,那损失可大了。 另外,我还没见到有哪个自定义 Web 部件能够最终至少实现半数现成的 XsltListViewWebPart 功能。

所以,我的计划是重复使用现有功能,而不是创建类似的自定义 Web 部件,因为那样会大大降低灵活性和性能。

实际上,XsltListViewWebPart 包括有许多非常有用的功能。 它集成在 SharePoint Designer 中,支持所有可能的 Web 部件连接并正确显示所有 SharePoint 数据类型。 它支持分组、小计、分页、项目上下文菜单、内联编辑、项目选择、状态指示等等。 它具有上下文功能区界面,提供用于排序和筛选的 UI,还提供一些基本的视图样式及更多功能。 总之,XsltListViewWebPart 具有许多很有用的强大功能,而使用自定义 Web 部件方法则很难重新实现这些功能。

XsltListViewWebPart

XsltListViewWebPart 为开发者提供众多集成点: 一个编程接口、一个 CAML 接口,当然还有 XSL 转换以及参数绑定。 但是请不要忘记,所有这些对象和属性在客户端对象模型中都有其自己的表现形式,因此您甚至可以从 JavaScript 或 Silverlight 访问 XsltListViewWebPart。

由此看来,XsltListViewWebPart 真的是一个强大的工具。 的确如此,所有这种特定于 SharePoint 的 XML(或 XSLT)乍看上去让人觉得有点胆怯,但是我将向您展示一些操作技巧以帮助您解开疑惑。

应用场景

在深入探讨实现细节之前,让我先来描述一下整体的方案。

我需要将 HTML5 拖放功能插入 SharePoint 列表视图中,以允许用户将单元格从一个列表拖到另一个列表。 我将在示例中使用一个“任务”列表和一个“执行者”列表,以便项目经理可以通过将执行者拖放到相应的“任务”列表单元格中,来轻松地分配和重新分配任务。

您可能知道,HTML5 引入了一些新的拖放属性,其中最重要的是“draggable”属性。 同时还提供众多事件来处理拖放过程的各个阶段。 可以使用相应的属性(例如“ondragstart”和“ondragend”等等)附加这些事件的处理程序功能。 (有关详细信息,请参阅万维网联盟 [W3C] HTML5 规范草案的第 7.6 章,网址为:bit.ly/lNL0FO。)

在本示例中,这意味着我只需要使用 XSLT 将某些基本属性添加到特定列表视图单元格中,并且可能还需要使用其他一些自定义属性来附加数据值(通过拖曳完成)。 最后,我将需要为处理程序功能提供相应的 JavaScript 代码。

首要步骤

我需要两个列表。 我可以从标准任务模板创建一个“任务”列表,或者可以只创建一个自定义列表并添加一些列,其中包括一个必需的“指派给”站点列。 还需另外创建一个“执行者”自定义列表,即,添加类型为“用户或用户组”的“执行者”列,使其成为必需、带索引且唯一的列。

“执行者”列表应仅显示用户姓名;所以实际上它不需要标准的“职务”列。 为隐藏该列,我转至列表设置,启用内容类型管理,然后转至“项目”内容类型,单击“职务”列以隐藏该列,如图 2 所示。

Making the Title Column Hidden in SharePoint List Settings
图 2 在 SharePoint 列表设置中隐藏“职务”列

使用示例数据填充这些列表,然后为我的仪表板创建一个 Web 部件页,其中我并排添加了这些列表(“任务”列表在左侧,“执行者”列表在右侧),如图 3 所示。

Adding the Lists to the SharePoint Dashboard
图 3 将列表添加到 SharePoint 仪表板

现在,我创建了包含数据的列表, 是时候开始实际实现拖放功能了。

SharePoint Designer

Microsoft SharePoint Designer 是一个用于快速开发 SharePoint 应用程序的完全免费的工具。 与 SharePoint 2007 相比,SharePoint 2010 进行了极大改进,现在即使是对于开发者来说,它也是极其有用的。 您可以使用 SharePoint Designer GUI 生成一些非常复杂的 XSLT 代码,然后将其复制并粘贴到您的 Visual Studio 项目中,而不需要手动编写容易出现键入错误并且不能保证内容完整的 XML/XSLT。 在实际项目中我通常采用这种方法,我保证这能节省很多时间。

我打开了 SharePoint Designer 并导航到先前创建的仪表板页。 在“指派给”列中选择一个单元格(右键单击,然后选择“选择”|“单元格”)。 下面就是见证奇迹的时刻: 在状态栏中,我看到指向负责显示该特定单元格的 XSL 模板(以及此模板中相应 HTML 标记)的路径(参见图 4)。

The Path to the Current XSL Template in SharePoint Designer
图 4 SharePoint Designer 中指向当前 XSL 模板的路径

此信息对于确定要替代哪个 XSL 模板以更改单元格标记非常有用。 您可以在 14/TEMPLATE/LAYOUTS/XSL 文件夹中找到模板的原始代码,并在自己的 XSLT 文件中或在 XsltListViewWebPart 的 <Xsl> 标记中使用该代码。

但是,我不需要处理这些又大又复杂的 XSLT 文件来实现我的目标。 相反,我可以使用 SharePoint Designer 的条件格式功能,该功能旨在根据特定条件突出显示包含特殊格式的特定行或单元格。 您不需要任何特殊的技能即可使用该功能;GUI 使其简单易用。 但在幕后,它全部是使用 XSLT 实现的。 因此,SharePoint Designer 包括一种可供随时使用的图形 XSLT 生成器,现在我打算使用它来满足自己的需要。

我选择一个单元格,单击功能区中的“条件格式”按钮,然后选择“设置列格式”,如图 5 所示。

Setting Conditional Formatting in SharePoint Designer
图 5 在 SharePoint Designer 中设置条件格式

接下来,我创建一个不可能的条件,ID 等于 0,如图 6 所示。

The Conditional Formatting Dialog in SharePoint Designer
图 6 SharePoint Designer 中的“条件格式”对话框

然后,单击“设置样式”按钮并随机选择一些样式(例如“text-decoration: underline”)。 单击“确定”并切换到“代码视图”选项卡,在其中我找到了生成的代码;当然,它位于 XsltListViewWebPart 控件的 <Xsl> 标记内。

XSL 转换

现在,我准备开始修改“指派给”单元格的标记。 “指派给”列是用于拖曳执行者的“数据接受者”,所以我需要提供“ondragover”、“ondragenter”、“ondragleave”和“ondrop”属性,这些属性将指向相应的 JavaScript 事件处理程序功能。

前面段落中 SharePoint Designer 生成的代码包含具有以下签名的 XSL 模板:

<xsl:template name="FieldRef_printTableCell_EcbAllowed.AssignedTo"
  match="FieldRef[@Name='AssignedTo']" mode="printTableCellEcbAllowed"
  ddwrt:dvt_mode="body" ddwrt:ghost="" xmlns:ddwrt2="urn:frontpage:internal">

正如您可能知道的那样,XSL 模板可以通过名称或条件互相调用。 第一种类型的调用是使用“xsl:call-template”元素执行的,它与函数调用(例如您在 C# 中使用的调用)非常相似。

第二种方法则比较受欢迎并且要灵活得多: 通过使用“xsl:apply-templates”元素,您可以指定模式和参数(使用 XPath 选择,以便它能够实际包含许多元素),而无需指定任何特定的模板名称。 对于每个参数元素,将使用“match”属性匹配相应的模板。 您可以这么认为,即该方法与 C# 中的重载类似。

如您在前面的代码中所见到的一样,此模板将匹配“FieldRef”元素,其中 Name 属性等于“AssignedTo”。同样,相应 xsl:apply-template 调用的“mode”属性必须等于“printTableCellEcbAllowed”。所以此模板本质上就是显示字段值的标准函数的重载。 并且此重载将仅与“指派给”字段值匹配。

现在让我们看看该模板中的内容,如图 7 所示(为了清晰起见,删除了一些代码)。

图 7 XSL 模板的内容

<xsl:template match="FieldRef[@Name='AssignedTo']" mode="printTableCellEcbAllowed" ...>
  <xsl:param name="thisNode" select="."/>
  <xsl:param name="class" />
  <td>
    <xsl:attribute name="style">
      <!-- ...
-->
    </xsl:attribute>
    <xsl:if test="@ClassInfo='Menu' or @ListItemMenu='TRUE'">
      <xsl:attribute name="height">100%</xsl:attribute>
      <xsl:attribute name="onmouseover">OnChildItem(this)</xsl:attribute>
    </xsl:if>
    <xsl:attribute name="class">
      <!-- ...
-->
    </xsl:attribute>
    <xsl:apply-templates select="." mode="PrintFieldWithECB">
      <xsl:with-param name="thisNode" select="$thisNode"/>
    </xsl:apply-templates>
  </td>
</xsl:template>

如您所见,该模板包含两个 xsl:param 元素、一个 <td> 元素、几个 xsl:attribute 元素以及一个 xsl:apply-templates 元素,这些元素将导致应用一些较低级别的模板。

为实现我的拖放目标,我只将拖放属性添加到 <td> 元素中,如下所示:

<td ondragover="return UserDragOver(event, this)" ondragenter=
  "return UserDragOver(event, this)" ondragleave=
  "UserDragLeave(event, this)" ondrop="UserDrop(event, this)">

很简单,不是吗?

或者,如果您在 SharePoint 环境中部署了 jQuery,那么可以考虑使用 jQuery .on 方法附加 JavaScript 事件处理程序。

“指派给”列的标记已准备就绪(我稍后再编写处理程序)。 现在该自定义“执行者”列表了。

切换回“设计视图”选项卡,在“执行者”列中选择一个单元格,并重复条件格式设置以生成 XSLT 代码。 然后,添加 onstartdrag 属性以确保拖放功能可以正常运行(这里,我不需要 draggable 属性,因为“用户或用户组”字段值呈现为链接,并且根据规范,链接默认情况下将 draggable 属性设置为“true”):

<td ondragstart="UserDragStart(event, this)">

太棒了, 但是,我将如何跟踪数据? 如何确定拖曳的是哪个执行者? 很明显,我需要他的登录名,最好是他的 ID。 在我看来,从 TD 元素的内部解析 ID 相当复杂。

关键是,在 XSLT 中,“用户或用户组”类型的任何字段都具有用户 ID,并且可以通过简单的 XPath 查询轻松检索该用户 ID。

在该查询中,我需要指出当前元素的值。 在所有标准 XsltListViewWebPart 模板中,通常将当前元素引用为 $thisNode 参数。 要检索用户的 ID,您需要指向 $thisNode 参数的属性,方法是使用与“用户或用户组”列相同的名称并在其末尾附加“.id”。

以下是我的查询:

<td ondragstart="UserDragStart(event, {$thisNode/Executor.id}, this)">

大括号用于在属性值中包括 XPath 表达式。

标记已准备就绪并随时可供实际使用,但最好再处理一下代码以便重复使用。

使模板可重复使用

您可能已注意到,模板已可靠地绑定到特定列名称,并且这些模板只用于特定列表。 但是,实际上只需修改这些模板即可将其重新用于包含其他列名称的其他列表。

首先,如果您查看了我之前提供的模板签名,就会发现以下属性:

match="FieldRef[@Name='AssignedTo']"

很明显,这会将模板绑定到“指派给”列。 要使该模板的用途再广泛一点,您可以将名称绑定替换为类型绑定,以使所有“用户或用户组”列都将匹配。 那就做吧! 代码如下:

match="FieldRef[@Type='User']"

该相同的修改也适用于第二个模板,其中 FieldRef 元素与“执行者”字段的内部名称匹配。

现在,因为我可以创建“用户或用户组”类型的任何列以及任何列表,所以我需要将一些其他信息传递到我的 JavaScript 处理程序。 执行拖放时,我需要更新“任务”列表中“指派给”列的值,所以,我需要知道列的名称和列表的 GUID。

如之前所述,SharePoint XSLT 具有一些可全局使用的标准参数。 其中一个参数是 $List,它存储当前列表的 GUID。 可以从匹配的 FieldRef 元素中轻松检索字段的内部名称。

所以,我将列表 GUID 和列内部名称传递给 UserDrop 处理程序,如下所示(为了清晰起见,我省略了“ondragenter”、“ondragover”和“ondragleave”属性):

<td ...
ondrop="UserDrop(event, this, '{$List}', '{./@Name}'">

“.”指向之前已通过模板匹配的当前 FieldRef 元素。

接下来,我需要使用下面的代码清除“执行者”列表 XSLT 中 ID 参数的“执行者”字符串:

<td ondragstart="UserDragStart(event, {$thisNode/@*[name()=
  concat(current()/@Name, '.id')]}, this)">

模板现在已准备就绪并且可以重复使用,我将编写相应的 JavaScript 代码以实现处理程序。

编写 JavaScript 处理程序

尽管要编写四个不同的处理程序,但它们大都很简单,并且显而易见。

通常,我建议将此代码放在单独的 JavaScript 文件中。 通常最好使用 Visual Studio 创建此代码,因为您可以利用 IntelliSense 的优势。 但在某些情况下,可以针对此目的将该代码放在 XSLT 中,以替代根模板(match=“/”)。 这将允许您在 JavaScript 代码中使用一些 XSLT 变量和参数,同时还意味着您不必担心如何部署 JavaScript 文件。

接下来让我们看看以下处理程序的代码:DragStart、DragEnter、DragOver、DragLeave 和 Drop。

在 UserDragStart 处理程序中,您需要初始化数据传输。 这意味着您需要在特定 HTML5 DataTransfer 对象中存储拖曳数据,如下所示:

function UserDragStart(e, id, element) {
  e.dataTransfer.effectAllowed = 'copy';
  e.dataTransfer.setData('Text', id + '|' + element.innerHTML);
}

请注意,用户 ID 不是已传输数据的唯一组成。 我还添加了 <td> 元素的内部 HTML,以避免在放置数据后必须刷新页面(有关详细信息,请参阅图 8 中的 UserDrop 处理程序代码)。

图 8 Drop 事件处理程序

function UserDrop(e, toElement, listGuid, columnName) {
  // Terminate the event processing
  if (e.stopPropagation)
      e.stopPropagation();
  // Prevent default browser action
  if (e.preventDefault)
      e.preventDefault();
  // Remove styles from the placeholder
  toElement.style.backgroundColor = '';
  //toElement.className = '';
  // iid attribute is attached to tr element by SharePoint
  // and contains ID of the current element
  var elementId = toElement.parentNode.getAttribute('iid').split(',')[1];
  // Transferred data
  var data = e.dataTransfer.getData('Text');
  var userId = data.split('|')[0];
  var userLinkHtml = data.split('|')[1];
  // Setting value of the field using SharePoint
  // EcmaScript Client Object Model
  var ctx = new SP.ClientContext.get_current();
  var list = ctx.get_web().get_lists().getById(listGuid);
  var item = list.getItemById(elementId);
  item.set_item(columnName, userId);
  item.update();
  // Asynchronous call
  ctx.executeQueryAsync(
    function () { toElement.innerHTML = userLinkHtml; },
    function (sender, args) { alert('Drag-and-drop failed.
Message: ' + args.get_message()) }
    );
  return false;
}

本例中 DragEnter 和 DragOver 事件的处理程序是相同的,所以我对它们使用一个函数。 您应该在这些事件中指示用户可以将其数据放置在此处。 根据规范,为实现此目的,您应该调用 event.preventDefault 方法(如果可用),然后返回 false。

DragEnter/DragOver 处理程序是将自定义样式应用到拖放占位符以告知用户他实际上可以放置其拖曳数据的最佳位置。 为了简化示例,我将使用内联 CSS 样式,但在实际项目中,建议使用 CSS 类(请见图 9 中的注释行)。

图 9 DragOver 和 DragLeave 事件处理程序

function UserDragOver(e, toElement) {
  // Highlight the placeholder
  toElement.style.backgroundColor = '#efe';
  // toElement.className = 'userDragOver';
  // Denote the ability to drop
  if (e.preventDefault)
      e.preventDefault();
  e.dataTransfer.dropEffect = 'copy';
  return false;
}
function UserDragLeave(e, toElement) {
  // Remove styles
  toElement.style.backgroundColor = '';
  // toElement.className = '';

很明显,在 DragLeave 中我需要删除之前应用的样式,如图 9 所示。

图 8 的 Drop 事件中,需要执行两项重要的操作:

  1. 应用更改。 使用 SharePoint EcmaScript 客户端对象模型,将字段值设置为传输的用户 ID。
  2. 将当前单元格 (TD) 中的内部 HTML 代码替换为传输代码,以便显示更改而不刷新页面。

现在,所有处理程序均已实现,您可以部署 Java­Script 了。 实际上,有多种方法可实现这个目的: 自定义母版页、使用委派控制、创建自定义操作并将位置设置为“ScriptLink”,或者如我之前所述,将 JavaScript 包括在 XSLT 中。

这里最简单的方法是使用 SharePoint Designer 自定义母版页文件,因为它不需要任何特殊的技能。 但是,由于您是一名开发者,您需要收集所有这些 JavaScript 和 XSLT 自定义项并创建可部署的解决方案。 好吧,让我们开始吧!

构建可部署的解决方案

要创建可发送给客户且随时可用的解决方案,您需要执行一些非常简单的步骤:

  1. 打开 Visual Studio,然后创建一个空的 SharePoint 项目。 然后在“项目设置”对话框中选择“部署为场解决方案”。
  2. 将 SharePoint“布局”映射文件夹添加到您的项目中,然后向其中添加三个新文件: UserDragHandlers.js、UserDragProvider.xsl 和 UserDragConsumer.xsl。
  3. 将之前创建的 JavaScript 代码和 XSLT 代码(在 SharePoint Designer 中生成)粘贴到相应的文件中。
  4. 添加一个空元素项目项,打开 Elements.xml 并在其中粘贴以下代码:
            <CustomAction ScriptSrc="/_layouts/SPDragAndDrop/UserDragHandlers.js" Location="ScriptLink" Sequence="10" />
    这会将您 JavaScript 文件的链接部署到所有站点页面。
  5. 最后,将事件接收器添加到 Feature1(已在添加空元素项目项时自动创建)中,取消注释 FeatureActivated 方法并在其中粘贴下面的代码:
var web = properties.Feature.Parent as SPWeb;
        SPList list;
        SPView view;
        list = web.Lists["Tasks"];
        view = list.DefaultView;
        view.XslLink = "../SPDragAndDrop/UserDragConsumer.xsl";
        view.Update();
        list = web.Lists["Executors"];
        view = list.DefaultView;
        view.XslLink = "../SPDragAndDrop/UserDragProvider.xsl";
        view.Update();

好了,就是这些,大功告成! 很简单,不是吗? 现在,如果您使用之前创建的“任务”和“执行者”列表构建和部署解决方案,然后创建包含这些列表的各自默认视图的仪表板,那么您的仪表板将具备便利的拖放功能。

浏览器支持

到目前为止,除 Opera 之外的几乎所有主要浏览器都支持 HTML5 拖放功能。 我已测试了 Internet Explorer 9.0.8112.16421、Chrome 16.0.912.75 和 Firefox 3.6.12,它们都完全与上述解决方案兼容。 我希望这些浏览器的一些较早版本也具有兼容性。 实际上,Safari 也应兼容,但在撰写这篇文章时,它在其 HTML5 拖放功能实现的过程中出现了一些奇怪的 bug,阻碍了解决方案按预期的方式运行。

接下来会怎样呢?

在该解决方案中,XSLT 模板或 JavaScript 都不包含任何特定于列表的代码,即,无硬编码 GUID、姓名、职务或其他类似内容。 所以,可以将这些转换应用到任何列表或文档库,并且您可以将拖放支持添加到任何“用户或用户组”类型的列。 这很不错,不是吗?

很明显,也可通过相同的方法拖曳其他类型的列,所以实际上您可以创建一个解决方案,让所有 SharePoint 列表中的所有单元格都可拖曳,然后在仪表板页面上,用户可以在列表之间拖曳单元格,这同样很方便。

您可以尝试实现按行拖放,这很具挑战性。 方法本质上是一样的,但为了获得正确的模板,您应使用行条件格式。 可以将其用于链接列表元素,或者更改它们的顺序。 例如,您可以将快速启动菜单中的“回收站”链接作为拖放接受者,用它从列表中删除项目不失为一个好办法。

您还可以执行更多操作。 只要不断思考、实验和尝试,您的 SharePoint 门户将变得更加友好。

Andrey Markeev 是 SharePoint Server MVP,他是俄罗斯 Softline Group 公司的一名 SharePoint 开发者。 Markeev 是 SharePoint StackExchange (bit.ly/w9e4NP) 排名前 10 位的专家,多个 CodePlex 开源项目的创建者,并经常发表博客和演讲。 请关注他的微博:twitter.com/amarkeev

衷心感谢以下技术专家对本文的审阅: Michael NemtsevBrandon Satrom