SharePoint RESTサービスとKnockout.jsで遊んでみた

いぜん、この辺のポスト
SharePoint2013 & Knockout.js で遊んでみた(その3)
でKnockout.jsを使ってSharePointリストのCRUDを試みましたが、その時心残りだったのが、jQuery Library for SharePoint Web Servicesというライブラリを使っていたこと、アイテム更新時に差分だけの更新ができなかったことでした。

で、かねてよりRESTサービスを使って何とかできないかなぁ・・・と考えていたのですが、先日こちらの記事を読みまして、
SharePoint REST サービスを使用したアイテムの CRUD 方法 – Japan SharePoint Support Team Blog – Site Home – TechNet Blogs
ここに記載のサンプルコードを元にトライしてみることにしました。(SharePoint2013のオンプレ環境です)

画面イメージはこんな感じです。
20150109

事前準備として、サイト内に「testList」という名前のカスタムリストを作成し、「Body」という一行テキスト列を追加しておきます。

ではソースコードです。
以下を、ページのスクリプトエディタ内にぺちょっと貼っていただけばOKです。

<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
<script langauge="JavaScript">
var weburl = "http://[your host]/[your site]";
var listTitle = "testList";
var myDigest = null;

function ItemsModel() {
  var self = this;
  self.Items = ko.observableArray([]);
  GetItems();

  function GetItems() {
      $.ajax({
      url: weburl + "/_api/Web/Lists/GetByTitle('" + listTitle + "')/Items",
      type: "GET",
      headers: {
                "accept": "application/json;odata=verbose",
                "Content-Type": "application/json;odata=verbose",
                "x-requestforceauthentication": true
              },
      success: function(data){
                if (data.d.results) {
                  self.Items(data.d.results);
                }
              },
      error: function (xhr) { alert(xhr.status + ": " + xhr.statusText) }
    });
  }

  function runWithFormDigest(fn){
    if (myDigest == null){
      $.ajax({
        url: weburl + "/_api/contextinfo",
        type: "POST",
        contentType: "application/x-www-url-encoded",
        dataType: "json",
        headers: {
                  "accept": "application/json;odata=verbose",
                },
        contentLength: 0,
        beforeSend: function (xhr) { xhr.withCredentials = true; },
        success: function (data) {
                  if (data.d) {
                    myDigest = data.d.GetContextWebInformation.FormDigestValue;
                    fn(); 
                  }
                }
      });
    } else {
      fn();
    }
  }

  self.AddRow = function() {
    self.Items.push({
      ID: "New",
      Title: "",
      Body: "",
    });
  };

  self.DelItem = function(data){
    var id = data.ID;
    if (id !== "New") {
      runWithFormDigest(function(){
        $.support.cors = true;
        $.ajax({
          url: weburl + "/_api/Web/Lists/GetByTitle('" + listTitle + "')/Items(" + id + ")",
          type: "POST",
          headers: {
                    "X-HTTP-Method":"DELETE",
                    "accept": "application/json;odata=verbose",
                    "Content-Type": "application/json;odata=verbose",
                    "x-requestforceauthentication": true,
                    "X-RequestDigest": myDigest,
                    "IF-MATCH": "*"
                  },
          success: function(xhr){ alert("completed.")},
          error: function (xhr) { alert(xhr.status + ": " + xhr.statusText) }
        });
      });
    };
    self.Items.remove(data);
  };

  self.SaveItem = function(data){
    var id = data.ID;
    var title = data.Title;
    var body = data.Body;
    if(id!= "New"){
      runWithFormDigest(function(){
        $.support.cors = true;
        $.ajax({
          url: weburl + "/_api/Web/Lists/GetByTitle('" + listTitle + "')/Items(" + id + ")",
          type: "POST",
          data: JSON.stringify({ '__metadata': { 'type': 'SP.Data.TestListListItem' }, 'Title': title, 'Body': body }),
          headers: {
                    "X-HTTP-Method":"MERGE",
                    "accept": "application/json;odata=verbose",
                    "Content-Type": "application/json;odata=verbose",
                    "x-requestforceauthentication": true,
                    "X-RequestDigest": myDigest,
                    "IF-MATCH": "*"
                  },
          success: function(xhr){ GetItems(); alert("completed.");},
          error: function (xhr) { alert(xhr.status + ": " + xhr.statusText) }
        });
      });
    } else {
      runWithFormDigest(function(){
        $.support.cors = true;
        $.ajax({
          url: weburl + "/_api/Web/Lists/GetByTitle('" + listTitle + "')/Items",
          type: "POST",
          data: JSON.stringify({ '__metadata': { 'type': 'SP.Data.TestListListItem' }, 'Title': title, 'Body': body }),
          headers: {
                    "accept": "application/json;odata=verbose",
                    "Content-Type": "application/json;odata=verbose",
                    "x-requestforceauthentication": true,
                    "X-RequestDigest": myDigest
                  },
          success: function(xhr){ GetItems(); alert("completed.")},
          error: function (xhr) { alert(xhr.status + ": " + xhr.statusText) }
        });
      });
    }
  }
}

$(document).ready(function () {
  ko.applyBindings(new ItemsModel());
});
</script> 
<div id="mainContent">
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>Title</th>
        <th>Body</th>
        <th></th>
        <th></th>
      </tr>
    </thead>
    <tbody data-bind='foreach: Items'>
      <tr>
        <td data-bind="text:ID"></td>
        <td><input data-bind='value: Title' /></td>
        <td><input data-bind='value: Body' /></td>
        <td><input type='button' value='Save' data-bind='click: $root.SaveItem' /></td>
        <td><input type='button' value='Delete' data-bind='click: $root.DelItem'/><td>
      </tr>
    </tbody>
  </table>
  <input type="button" value="Add New" data-bind='click: AddRow' />
</div>

なお、上で引用したSharePoint Support Teamさんのブログではアイテムの追加と更新のとき、「type」に「SP.ListItem」を渡していましたが、私の環境ではうまくいきませんでした。
なんでだろう・・・と悩みつつ、こちらを読むと
REST を使用したリスト アイテムの操作

この操作を実行するには、リストの ListItemEntityTypeFullName プロパティを知っていて、それを HTTP 要求本文の type の値として渡す必要があります。

とあったので、上記ソースコードのような書き方になっています。
私のソースコードでうまくいかない場合は、Support Teamさんのブログを参照してください。

※ListItemEntityTypeFullName プロパティは、ブラウザのアドレスバーに以下を入力してEnterすると取得できます。

http://[your host]/[your site]/_api/lists/getbytitle('testList')?$select=ListItemEntityTypeFullName

RESTサービスだと値をJSON形式で取れるので、SPServicesを使っていたときよりだいぶシンプルに書けたのではないかと思います。

広告

SharePoint2013 & Knockout.js で遊んでみた(その3)

今さら言うのもなんですが、やったことをブログに書き起こしてみると自分がいかによくわからないままなんとな~く作っていたかがわかりますね・・・
(しかしそれでも動いてしまうのだから、システムって恐ろしい)

それはさておき、このシリーズの最後の投稿です。
前回View部分を定義しましたので、次にViewModelを定義します。
このポストをおおいにパクって参考にしてます)

function Link(data) {
    this.SiteName = ko.observable(data.SiteName);
    this.SiteURL = ko.observable(data.SiteURL);
    this.SortOrder = ko.observable(data.SortOrder);
    this.ID = ko.observable(data.ID);
}

function LinkModel() {

    var self = this;
    self.Links = ko.observableArray([]);

    // get my personal link.
    $().SPServices({
        operation: "GetListItems",
        async: false,
        webURL: ”MySiteのURL”,
        listName: "MyLinks",
        CAMLViewFields: "<ViewFields Properties='True' />",
        CAMLQuery: "<Query><OrderBy><FieldRef Name='SortOrder'/></OrderBy></Query>",
        completefunc: function (xData, Status) {
            var spsData = $(xData.responseXML).SPFilterNode("z:row").SPXmlToJson({ includeAllAttrs: true, removeOws: true });
            if (spsData) {
                $.each(spsData, function (k, l) {
                    var arrOrder = (l.SortOrder + "").split(".");
                    self.Links.push(new Link({
                        SiteName: l.Title,
                        SiteURL: l.SiteURL,
                        SortOrder: arrOrder[0],
                        ID: l.ID
                    }))
                });
            } // end if
        } // end complete func
    }); // end SPServices

    self.addSite = function() {
        self.Links.push({
            SiteName: "",
            SiteURL: "",
            SortOrder: "",
            ID: "New"
        });
    };

    self.removeSite = function(data){
        if (data.ID !== "New") {
            var batch = "<Method ID='1' Cmd='Delete'><Field Name='ID'>" + data.ID() + "</Field></Method>";
            $().SPServices({
                operation: "UpdateListItems",
                async: false,
                webURL: ”MySiteのURL”, 
                listName: "MyLinks",
                updates: "<Batch OnError='Continue'>" + batch + "</Batch>",
                completefunc: function (xData, Status) {
                    alert("completed");
                }
            });
        };
        self.Links.remove(data);
    }; // end removeSites

    self.saveSites = function(){
        var ret = self.Links();
        var i = 1;
        var batch = "";
        for(var n = 0, len = ret.length; n < len; n++){
            if(ret[n].ID != "New"){
                batch += "<Method ID='" + i + "' Cmd='Update'><Field Name='ID'>" + ret[n].ID() + "</Field><Field Name='SortOrder'>" + ret[n].SortOrder() + "</Field><Field Name='Title'>" + ret[n].SiteName() + "</Field><Field Name='SiteURL'>" + ret[n].SiteURL() + "</Field></Method>";
            } else {
                batch += "<Method ID='" + i + "' Cmd='New'><Field Name='ID'>" + ret[n].ID + "</Field><Field Name='SortOrder'>" + ret[n].SortOrder + "</Field><Field Name='Title'>" + ret[n].SiteName + "</Field><Field Name='SiteURL'>" + ret[n].SiteURL + "</Field></Method>";
            }/
            i++;
        }

        $().SPServices({
            operation: "UpdateListItems",
            async: false,
            webURL: ”MySiteのURL”, 
            listName: "MyLinks",
            updates: "<Batch OnError='Continue'>" + batch + "</Batch>",
            completefunc: function (xData, Status) {
                alert("completed");
            }
        });
    }; // end saveSites

} // end LinkModel

ここで、”observable”、”observableArray”っていうのが出てきますが、これは特殊な JavaScript オブジェクトで、ViewModelのプロパティの変更をViewに知らせることができます(逆もまたしかり)。”observable”は単一のオブジェクト、”observableArray”は配列に対し使います。
こう書くことでViewModel側でViewに表示させるべきデータが変われば自動的にViewが変わるので、スクリプト内でHTMLを書き換える必要がありません。
追加の場合はViewとバインドしてる配列に新しい要素を追加し、削除の場合は要素を削除、更新の場合は配列の中身をwebサービスでしぇあぽにばばーっと送っているだけです。DOM要素は一切さわってません。

なお、observableについて詳しくは本家のドキュメントをみてください。

さいごに、ViewとViewModelを関連付けます。これは簡単。

$(document).ready(function () {
    ko.applyBindings(new LinkModel());
});

あら不思議、これでKnockout.jsをつかって自前画面からSharePointのリストを操作できるようになりました!

しかし心残りなのは、更新処理のときに差分ではなく全部更新になっていること。アイテム数が少なければ全とっかえでもいいですが、数が増えるとどうなんだ・・・という感じです。
差分をとるうまいやり方を思いつけず今回はこの形ですが、できれば差分更新にしたいところ。よいアイデアあれば教えてください。
あ、あと、エラーとか例外とかいっさい処理してませんのであしからず(笑)

結局よくわからなかったところは、Modelは出てこなかったけど?!ってことです(笑)
SharePointで作る場合、何をViewModelとし、何をModelとするのか、自分の中で整理がついてないので、今後の課題ですね・・・

SharePoint2013 & Knockout.js で遊んでみた(その2)

前回の続きです。

いまさらですが、Knockout.jsってなんなのってあたりをおさらい。

Knockout.jsとは、MVVM(Model-View-View Model)設計パターンに基づいたアプリケーションの構築をサポートするJava Scriptフレームワークです。
特徴をKnockout.js日本語サイトからまんま引用しますと、

宣言型バインディング
UIに必要なのは ViewModel (シンプルなモデルオブジェクト) とデータバインドだけ。
ややこしいDOM操作なしで、動的なインターフェイスを作ることができます。
UIの自動更新
ViewModel のプロパティが変更されると、自動的にUIの関連付けられた部分を更新します。
依存関係のトラッキング
データの結合や変換を実現するためのデータ間の関係チェーンを暗黙的に設定します。
UIテンプレート
幾重にもネストされたテンプレートも、バインドされた ViewModel を用いて素早くUIを生成します。

MVVMって・・・とか深いところはとても説明できる自信がないので、興味があればこの辺とかみておいてください。
http://www.buildinsider.net/web/bookjslib111/89

SharePoint2013になってクライアントサイドでの自由度はかなり上がりましたが、とはいえ既存のwebパーツを使わず、自前の画面でSharePointとどうたらこうたらしようとなると、jQueryだけではかなり苦しくなってきます。画面構築とロジックが絡まりあって、コードの見通しも悪くなりますしね・・・
そういった面で、Knockout.jsを使うとスクリプト内でのDOM操作が必要なくなる、というのはかなり魅力です。スクリプト内では、画面のことは考えずデータ操作とイベント処理だけに専念すればよいわけです。
また、ユーザーさんとやり取りしながら作ってるとよくありがちな、度重なる画面仕様の変更にも強い(はず)です(スクリプト内でDOMを生成してたりすると、あとで必ずといってよいほど泣きを見るので・・・)

さて、前置きはこの辺で。
Knockout.jsをつかってなんかするぞー!という場合、最低限以下の作業が必要になります。

  • Knockout.js本体の読み込み(当たり前ですね?!)
  • Viewの定義(基本、画面のHTMLです)
  • ViewModelの定義(Viewに表示するデータの操作やViewから呼び出されるイベントハンドラを書きます)
  • ViewとViewModelの関連付け

Knockout.jsの読み込みは割愛させていただいて(すいません・・・)、まずViewを定義します。

<div class="MyLinkDiv">
    <table>
        <thead>
            <tr>
                <th>ID&</th>
                <th>Name</th>
                <th>URL</th>
                <th>Order</th>
                <th></th>
            </tr>
        </thead>
        <tbody data-bind='foreach: Links'>
            <tr>
                <td data-bind='text:ID'></td>
                <td><input data-bind='value: SiteName' /></td>
                <td><input data-bind='value: SiteURL' /></td>
                <td><input data-bind='value: SortOrder' /></td>
                <!-- 削除ボタン -->
                <td><input type='button' value='Delete' data-bind='click: $root.removeSite'/><td>
            </tr>
        </tbody>
    </table>
    <!-- 新規アイテム追加ボタン -->
    <input type='button' value='Add New' data-bind='click: addSite' />
    <!-- 保存ボタン -->
    <input type='submit' value='Save Links' data-bind='click: saveSites' />
</div>

ここでは”data-bind”という属性がキモになります。
inputタグにdata-bind=’value: SiteName’という記述がありますが、これで「このinputのvalueと、View Modelで定義したSiteNameプロパティをひもづけるよ!」と定義してることになります。
ここの”value”を「バインディング」といい、Knockoutではいろいろと用意されています。また、SiteNameの方はここではプロパティですが、そのほか式などもひもづけできます。

また、”foreach”バインディングを使うことで、テーブルやリストといった繰り返し処理の必要なマークアップとView Model内の配列をひもづけることができます。上記のように書くことで、配列内の要素ぶんだけViewのテーブルに行が出力されることになります。

あ、あとですね、”$root”というのは「ルートコンテキストにあたるメインの ViewModel であり、最上位のコンテキスト」ってことなんですが、まあここではView Modelの”removeSite”っていう関数を呼び出してるんだと便宜的に思っといてください。
(バインディングコンテキストについては、詳しくはこちらをみてください)

なお、データバインディングについてもっと知りたい場合は、先ほど紹介した日本語サイトや、本家のドキュメントを参照してください。

おっと、意外と長くなってしまった・・・なかなか佳境に入りませんが、今回はこの辺で。

て、今回はまったくSharePoint出てきませんでしたね?!(汗)。次回はきっと登場します・・・

SharePoint2013 & Knockout.js で遊んでみた(その1)

ひさびさのSharePoint2013ネタです。長くなりそうなので、何回かに分けてお送りします(の予定)。
今回は下準備ということで、Knockout使い始めるまでのあれこれです。

今はやりのJavaScriptのMVC系フレームワーク使ってなんかやってみたいな~、と思いたち、お勉強も兼ねちょっと遊んでみました。

【実現したいこと】
・ユーザーさんが個別にカスタマイズできるリンク集的なウィジェットを作る
・リンクのデータは各ユーザの個人サイトに置く
・登録編集はそのリンクリストを直でいじるんではなく、ポータル内にインターフェースを置く

まあー表示くらいはwebサービスなりでデータをとってきて、jQueryでDOMをがりがり・・・でできなくはないですが、リンクの登録編集とかの実装になると気が遠くなりそうですよね?!私はなりそうです。
で、これはひょっとしてフレームワークの出番ではないか?!と短絡的に考えました。

最終的に、登録編集フォームとしてこんな感じのものを作ってみました。(小さくてすいません)
140521_2

さて、手始めに同じことを考えてる人はいないだろうか?いやいるはず!世界は広い!といつものようにググってみたら、こんなものが。
http://sympmarc.com/2013/09/10/spservices-stories-16-beginning-sharepoint-development-with-kosp-knockout-for-sharepoint-rest-api-and-sp-services/

おおっ。私の愛するSPServicesを使ってできそうではないですか。
とはいうものの、Knockout.js使ったことないので(汗)、とりあえず本家サイトでお勉強します。
http://knockoutjs.com/

いろんなとこで書かれてますが、ほんとここのチュートリアルは良くできてます。触ってて楽しい。いつまでも遊んでたい・・・のですが、そうしてると仕事にならないので適当なところで切り上げます(涙。

しかし困ったことに、上の記事はデータの表示方法のサンプルです。
やりたいのは登録・編集・削除だし、どうしよう~、と調べてたら本家サイトのExampleが使えそうだったので、これを参考に実装してみることに。
http://knockoutjs.com/examples/gridEditor.html
SPServicesを使ってのリストのアップデートはこの辺を参考にすればよさそう。
http://spservices.codeplex.com/wikipage?title=UpdateList&referringTitle=Lists

方針の決まったところで、おもむろに必要なファイルをDLします。
Knockout.js※導入方法もこのページに載ってますね。
http://knockoutjs.com/downloads/index.html
SPServices
http://spservices.codeplex.com/
KoSpJs
http://kosp.codeplex.com/

KoSpJsは、

KoSp provides custom knockout binding handlers that enables easy binding of json data from SharePoint lists retrieved via REST API with oData queries and  SP Services with CAML queries with client side controls.

との触れ込みなので実装が楽になるかも?と入れてみました。上で参照してる記事でも使ってますしね・・・

で、いつものようにDLしたファイルをカスタマイズファイル専用置場へアップロードしておきます。あ、jQueryもアップロードしてない場合はお忘れなく。

さいごに、自分の個人用サイトのなかにリンクを登録するリストをあらかじめ作っておきます。
これはもうカスタムリストでちゃっちゃと作りました。

リストタイトル:MyLinks
追加した列:
SiteURL (Single line of text)
SortOrder (Number)

これで、下準備は完了です(のはず)。

今回はこの辺で。続きはまた・・・