CRUD knockoutjs grid and MVVM pattern


You can create a CRUD knockoutjs grid using the MVVM pattern (this works with linq enabled and linq not enabled). In this example you have a classic web page that use the jquery plugin "zPageGrid2.js". In this page linq is enabled with paging size 10. You have a detail template, a create template and a edit/modify template. In the query projection method "applyTempKo(mapKnock)", you have specified a callback function "mapKnock(data)" that it will create the viewmodel (using the data returned from the ajax rest call). On the main template "temp1" you can see some html attribute: jcustomaction , jdeleteaction , jdetailaction , jcreateaction , jmodifyaction. These tell the the plugin that there are some action that need to be registred. The tcustomCallBack, tdeleteCallBack actions will be specified by the plugin properties: tcustomCallBack, tdeleteCallBack. They will be raised when you clik on the link. When you click on the jdetailaction, jcreateaction, jmodifyaction action link, will be opened a popup and will be applied a template. The template is specified inside the associated panel. You can declare this panel by the plugin properties: tdetailPanel, tdetailContainer, tcreatePanel, tcreateContainer, tmodifyPanel, tmodifyContainer. If you use a create popup template you need to specify a callback function "tcreateViewModel" that create an empty viewmodel.

As you can see, inside the create panel, there are two html attribute: jinsertaction, data-jcancelaction. When you click on the link will be called the callback function. You can specify those function by the plugin properties: tinsertCallBack, tcancelCallBack. If you click on tcancelCallBack the popup will be closed automaticaly. If you click on tinsertCallBack the popup will not be closed automaticaly. You can close it calling the methods closeCreateDialogFeilure()/closeCreateDialogSuccess().

As you can see, inside the modify panel, there are two html attribute: jupdateaction, data-jcancelaction. When you click on the link will be called the callback function. You can specify those function by the plugin properties: tupdateCallBack, tcancelCallBack. If you click on tcancelCallBack the popup will be closed automaticaly. If you click on tupdateCallBack the popup will not be closed automaticaly. You can close it calling the methods closeModifyDialogFeilure()/closeModifyDialogSuccess().

In the create/modify popup you may use some htmlhelper to create the imput fields. If you set the plugin property "tknockoutValidation=true" , you can use the mvc3 jquery validation.

You can request the data to server (the action method) through an ajax call. On the query projection you must specify th method "applyTempKo(mapKnock)". As you can see, there is a function parameter. It is the callback function that create the viewmodel. If you use this method without param, will be create automaticaly a view model with only properties.

After you have create a new entry, if you want see it on the grid you must call the "refresh()" method.

@using JQueryLinq
@model Application1.CustomerViewModel2

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section header{
   ...
}

<h2>Index - Page08</h2>

<div>
   <input id="gbutton1" type="button" value="Search" />&nbsp;
   <input id="gbutton2" type="button" value="MoveF"  />&nbsp;
   <input id="gbutton3" type="button" value="MoveP"  />&nbsp;
   <input id="gbutton4" type="button" value="MoveN"  />&nbsp;
   <input id="gbutton5" type="button" value="MoveL"  />&nbsp;
</div>

<br /><br />

<div id="gpane1" style="display:none">
  <div>
    <table class="viewmodel">    
    <tr>
      <td>City: </td>
      <td><input id="text1" data-bind="value: city" /></td>
    </tr>  
    <tr>
      <td>Country: </td>
      <td><input id="text2" data-bind="value: country" /></td>
    </tr> 
    </table>
    <br /><br />
    <input id="gbutton6" type="button" value="Find" />
  </div>
</div>

<div id="gpane2" style="display:none">
  <table id="jtable">
  <tr>
    <td colspan="4"> &nbsp; </td>
    <td data-sort="CustomerID"> CustomerID </td>
    <td data-sort="CompanyName"> CompanyName </td>
    <td data-sort="Address"> Address </td>
    <td data-sort="City"> City </td>
    <td data-sort="City"> Value1 </td>
    <td data-sort="City"> Value2 </td>
    <td data-sort="City"> Total </td>
  </tr>
  <tbody id="jlist" data-bind="template: {name: 'temp1', foreach: $data}"></tbody>
  </table>   
</div>

<div id="gdialog1" style="display:none;height:100px">
  <div id="jlist1" data-bind="template:'jdetailTemplate'"></div>
</div>

<div id="gdialog2" style="display:none;height:100px">
  <div id="jlist2" data-bind="template:'jmodifyTemplate'"></div> 
  <div>
    <a href="#" data-jinsertaction>Insert</a>&nbsp;
    <a href="#" data-jcancelaction>Cancel</a>&nbsp;
  </div>
</div>

<div id="gdialog3" style="display:none;height:100px">
  <div id="jlist3" data-bind="template:'jmodifyTemplate'"></div> 
  <div>
    <a href="#" data-jupdateaction>Update</a>&nbsp;
    <a href="#" data-jcancelaction>Cancel</a>&nbsp;
  </div>
</div>

<script id="temp1" type="text/html">
  <tr class="pagingbase-ui-selected-ou"> 
    <td><a href="#" data-jcustomaction>Custom</a></td>
    <td><a href="#" data-jdeleteaction>Delete</a></td>
    <td><a href="#" data-jdetailaction>Detail</a></td>
    <td><a href="#" data-jmodifyaction>Modify</a></td>
    <td> ${CustomerID} </td>
    <td> ${CompanyName} </td>
    <td> ${Address} </td>
    <td> ${City} </td>
    <td> ${Value1} </td>
    <td> ${Value2} </td>
    <td> ${Total} </td>
  <tr>
</script>

<script id="jdetailTemplate" type="text/x-jquery-tmpl">
  <b>CustomerID: </b><span data-bind="text: CustomerID" /></span><br>
  <b>CompanyName: </b><span data-bind="text: CompanyName" /></span><br>
  <b>City: </b><span data-bind="text: City" /></span><br>
  <b>Value1: </b><span data-bind="text: Value1" /></span><br>
  <b>Value2: </b><span data-bind="text: Value2" /></span><br>
  <b>Total: </b><span data-bind="text: Total" /></span><br>
</script>

<script id="jcreateTemplate" type="text/x-jquery-tmpl">
@using (Html.BeginForm()) {
<div> 
  <p>
    <b>CustomerID: </b>@Html.KoTextBoxStrFor(model => model.CustomerID)<br>
    @Html.ValidationMessageFor(model => model.CustomerID)
  </p>
  <p>
    <b>CompanyName: </b>@Html.KoTextBoxStrFor(model => model.CompanyName)<br>
    @Html.ValidationMessageFor(model => model.CompanyName)
  </p> 
  <p>
    <b>City: </b>@Html.KoTextBoxStrFor(model => model.City)
    @Html.ValidationMessageFor(model => model.City)
  </p>
  <p>
    <b>Value1: </b>@Html.KoTextBoxIntFor(model => model.Value1)
  </p>
  <p>
    <b>Value2: </b>@Html.KoTextBoxIntFor(model => model.Value2)
  </p>    
  <h3><b>Total: </b><span data-bind="text: Total"></span></h3>
</div>
}
</script>

<script id="jmodifyTemplate" type="text/x-jquery-tmpl">
@using (Html.BeginForm()) {
<div> 
  <p>
    <b>CustomerID: </b>@Html.KoTextBoxStrFor(model => model.CustomerID)<br>
    @Html.ValidationMessageFor(model => model.CustomerID)
  </p>
  <p>
    <b>CompanyName: </b>@Html.KoTextBoxStrFor(model => model.CompanyName)<br>
    @Html.ValidationMessageFor(model => model.CompanyName)
  </p> 
  <p>
    <b>City: </b>@Html.KoTextBoxStrFor(model => model.City)
    @Html.ValidationMessageFor(model => model.City)
  </p>
  <p>
    <b>Value1: </b>@Html.KoTextBoxIntFor(model => model.Value1)
  </p>
  <p>
    <b>Value2: </b>@Html.KoTextBoxIntFor(model => model.Value2)
  </p>    
  <h3><b>Total: </b><span data-bind="text: Total"></span></h3>
</div>
}
</script>


/// <reference path="jquery-1.5.1.js" />
/// <reference path="knockout-2.0.0.js" />

$(document).ready(function () {
    var settings = {
        tpane1: 'gpane1',
        tpane2: 'gpane2',
        tcontainer: "jlist",
        tformViewModel: function () {
            return {
                city: ko.observable(""),
                country: ko.observable("")
            };
        },
        tcreateViewModel: function () {
            return new dataItem2();
        },
        tcustomCallBack: function (obj) {
            customfrm(obj);
        },
        tdeleteCallBack: function (obj) {
            deletefrm(obj);
        },
        tinsertCallBack: function (obj) {
            insertfrm(obj);
        },
        tupdateCallBack: function (obj) {
            updatefrm(obj);
        },
        tcancelCallBack: function (obj) {
            cancelfrm(obj);
        },
        tcacheEnabled: false,
        tdetailPanel: "gdialog1",
        tdetailContainer: "jlist1",
        tcreatePanel: "gdialog2",
        tcreateContainer: "jlist2",
        tmodifyPanel: "gdialog3",
        tmodifyContainer: "jlist3",  
        tknockoutValidation: true,
        tlinqEnabled: true,
        ttype: "2"
    };
    $("#gpane2").gridTemplate(settings, searchfrm).
      bind('popupdetail', function (event) {
          //alert("databound");
      }).
     bind('popupcreate', function (event) {
         //alert("popupcreate");
      }).
      bind('popupmodify', function (event) {
          //alert("databound");
      }).
      bind('databound', function (event) {
          //alert("databound");
      });
});

function searchfrm(context) {
    context.clearSearch();

    var r = context.koWhereObjectAnd();

    context.from("/Grid4/GetDataJson2").pagingWithSize(10).where(r.value, 
       r.param).orderBy("Country,City desc").applyTempKo(mapKnock);
};

function mapKnock(data) {
    return ko.observableArray(ko.utils.arrayMap(data, function (item) {
            return new dataItem1(item);
        })
    );
};

function dataItem1(item) {
    var that = this;  
    this.CustomerID = ko.observable(item.CustomerID);
    this.CompanyName = ko.observable(item.CompanyName);
    this.Address = ko.observable(item.Address);
    this.City = ko.observable(item.City);
    this.Value1 = ko.observable(0);
    this.Value2 = ko.observable(0);
    this.Total = ko.observable(0);

    this.Value1.subscribe(function (value) {
        var ret = that.Value1() + that.Value2();
        that.Total(ret);
    });

    this.Value2.subscribe(function (value) {
        var ret = that.Value1() + that.Value2();
        that.Total(ret);
    });
};

function dataItem2(item) {
    var that = this;
    this.CustomerID = ko.observable("aa");
    this.CompanyName = ko.observable("bb");
    this.Address = ko.observable("cc");
    this.City = ko.observable("dd");
    this.Value1 = ko.observable(0);
    this.Value2 = ko.observable(0);
    this.Total = ko.observable(0);

    this.Value1.subscribe(function (value) {
        var ret = that.Value1() + that.Value2();
        that.Total(ret);
    });

    this.Value2.subscribe(function (value) {
        var ret = that.Value1() + that.Value2();
        that.Total(ret);
    });
};

function deletefrm(obj) {
    $.ajax({ url: "/Grid4/DeleteViewModel2/", type: 'post',
        data: ko.toJSON(obj.dataitemJs),
        contentType: 'application/json',
        error: function (request, state, error) {
            alert("Ajax error:" + error);         
        },
        success: function (result) {
            alert(result);
        }
    });
};

function insertfrm(obj) {
    $.ajax({ url: "/Grid4/InsertViewModel2/", type: 'post',
        data: ko.toJSON(obj.dataitemJs),
        contentType: 'application/json',
        error: function (request, state, error) {
            obj.context.closeCreateDialogFeilure();
            alert("Ajax error:" + error);
        },
        success: function (result) {
            //obj.context.refresh();
            obj.context.closeCreateDialogSuccess();
            alert(result);
        }
    });
};

function updatefrm(obj) {   
    $.ajax({ url: "/Grid4/UpdateViewModel2/", type: 'post',
        data: ko.toJSON(obj.dataitemJs),
        contentType: 'application/json',
        error: function (request, state, error) {
            obj.context.closeModifyDialogFeilure();
            alert("Ajax error:" + error);
        },
        success: function (result) {
            obj.context.closeModifyDialogSuccess();
            alert(result);
        }
    });
};

function customfrm(obj) {
    alert("custom:" + obj.dataitemJs.CustomerID);
};

function cancelfrm(obj) {
    alert("cancel:" + obj.dataitemJs.CustomerID);
};


[OutputCache(Duration = 0, VaryByParam = "*")]
 public ActionResult GetDataJson2(RequestLinq linq)
 {
     linq.SetSecurity(true, true, true);
     var query = Repository.GetRepository<Customer>().Query();
     var result = (object)query.JQuery(linq);
     return Json(result, JsonRequestBehavior.AllowGet);
 }

[HttpPost]
[OutputCache(Duration = 0, VaryByParam = "*")]
public JsonResult InsertViewModel2(CustomerViewModel2 entity)
{
    var message = "Insert2 error: " + entity.CustomerID;
    if (ModelState.IsValid == true)
    {
        if (entity.City == "pippo")
            throw new Exception("pippo");
        message = "Insert2 ok: " + entity.CustomerID;
    }
    return Json(message);
 }

[HttpPost]
[OutputCache(Duration = 0, VaryByParam = "*")]
public JsonResult UpdateViewModel2(CustomerViewModel2 entity)
{
    var message = "Update2 error: " + entity.CustomerID;
    if (ModelState.IsValid == true)
    {
        if (entity.City == "pippo")
            throw new Exception("pippo");
        message = "Update2 ok: " + entity.CustomerID;
    }
    return Json(message);
 }

 [HttpPost]
 [OutputCache(Duration = 0, VaryByParam = "*")]
 public JsonResult DeleteViewModel2(CustomerViewModel2 entity)
 {
     var message = "Delete2 error: " + entity.CustomerID;
     if (ModelState.IsValid == true)
     {
         if (entity.City == "pippo")
             throw new Exception("pippo");
         message = "Delete2 ok: " + entity.CustomerID;
     }
     return Json(message);
 }      
 


In this way you can create a MVC jquery knockoutjs grid with paging, filter and sort functions.

Classic page with knockout MVVM
jQuery mobile page with knockout MVVM
CRUD knockoutjs grid and MVVM pattern
jQuery mobile dynamic content with a knockoutjs template and MVC
Create a PhoneGap application using html5/javascript
How using RavenDB in a html5/javascript application
Knockoutjs and LINQ for JavaScript

Last edited Mar 17, 2012 at 8:51 PM by mastefano64, version 34

Comments

No comments yet.