建置 HTML5 應用程式

將 HTML5 拖放功能新增到 SharePoint 清單

Andrey Markeev

下載代碼示例

Microsoft SharePoint 企業平臺具有悠久的發展歷史和廣泛的各種功能,這就是為什麼它的反應速度總是不夠快,無法迎合新興 Web 技術發展趨勢的原因。儘管大量企業均使用 SharePoint 並且致力於提供包羅萬象的功能,但是 SharePoint 在沉浸式 UI 方面(例如 HTML5 和 CSS3)仍然滯後于現代的 CMS 產品。

在我看來,HTML5 不僅是一項熱門的新技術,而且的確具有許多實用的優勢:它簡單、方便且內容豐富,差不多受所有現代流覽器的支援(包括移動設備流覽器)。此外,HTML5 和 JavaScript 正逐漸成為 Windows 桌面程式設計的主要技術。

所以 HTML5 毫無疑問也應在 SharePoint 中佔有一席之地,使門戶變得更加簡單易用。改善 SharePoint 介面對業務使用者有著實際的説明,因為可以讓他們更快且更好地工作。

不幸的是,SharePoint 不具有任何內置 HTML5 功能,但它具有極大的靈活性。在本文中,我將演示如何通過簡單操作將 HTML5 拖放支援添加到 SharePoint 中,並且會使標準介面更加簡潔,如圖 1 所示。


圖 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 所示。


圖 2 在 SharePoint 清單設置中隱藏“職務”列

使用示例資料填充這些清單,然後為我的儀錶板創建一個 Web 部件頁,其中我並排添加了這些清單(“任務”清單在左側,“執行者”清單在右側),如圖 3 所示。


圖 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)。


圖 4 SharePoint Designer 中指向當前 XSL 範本的路徑

此資訊對於確定要替代哪個 XSL 範本以更改儲存格標記非常有用。您可以在 14/TEMPLATE/LAYOUTS/XSL 資料夾中找到範本的原始代碼,並在自己的 XSLT 檔中或在 XsltListViewWebPart 的 <Xsl> 標記中使用該代碼。

但是,我不需要處理這些又大又複雜的 XSLT 檔來實現我的目標。相反,我可以使用 SharePoint Designer 的條件格式功能,該功能旨在根據特定條件突出顯示包含特殊格式的特定行或儲存格。您不需要任何特殊的技能即可使用該功能;GUI 使其簡單易用。但在幕後,它全部是使用 XSLT 實現的。因此,SharePoint Designer 包括一種可供隨時使用的圖形 XSLT 生成器,現在我打算使用它來滿足自己的需要。

我選擇一個儲存格,按一下功能區中的“條件格式”按鈕,然後選擇“設置列格式”,如圖 5 所示。


圖 5 在 SharePoint Designer 中設置條件格式

接下來,我創建一個不可能的條件,ID 等於 0,如圖 6 所示。


圖 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# 中的重載類似。

As you can see in the preceding code, this template will match “FieldRef” elements, where the Name attribute is equal to “AssignedTo.” Also, the “mode” attribute of the corresponding xsl:apply-template call must be equal to “printTableCellEcbAllowed.” So this template is essentially an overload for the standard function that displays fields’ values. 並且此重載將僅與“指派給”欄位值匹配。

現在讓我們看看該範本中的內容,如圖 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)">

Cool. 但是,我將如何跟蹤資料? 如何確定拖曳的是哪個執行者? 很明顯,我需要他的登錄名,最好是他的 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']"

很明顯,這會將範本綁定到“指派給”列。 要使該範本的用途再廣泛一點,您可以將名稱綁定替換為類型綁定,以使所有“使用者或使用者組”列都將匹配。 So be it! 代碼如下:

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 and 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