Shawn
Shawn

Reputation: 5260

knockout.js binding issue when trying to refresh data

I am using knockout.js data binding. At the page load the binding works fine and data is shown on the page correctly. Then I try to push data back to the database and the push is successful. The database receives the data OK.

The problem comes when I try to reload the data upon push success. At this time the binding already happen once (at the page load). If I don't bind it again the data on the page does not refresh. If I do the binding again knockout.js issues an error "cannot bind multiple times". If I do a cleanup before rebinding I receive an error "nodeType undefined". Can anyone tell me what I have missed here? I am using ASP.NET MVC 4.0 with knockout.js 3.0.0.

Controller:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplJSON.Controllers
{
    public class KnockoutController : Controller
    {
        //
        // GET: /Knockout/

        public ActionResult Index()
        {
            return View();
        }

        [HttpGet]
        public JsonResult GetProductList()
        {
            var model = new List<Product>();
            try
            {
                using (var db = new KOEntities())
                {
                    var product = from p in db.Products orderby p.Name select p;
                    model = product.ToList();
                }
            }
            catch (Exception ex)
            { throw ex; }
            return Json(model, JsonRequestBehavior.AllowGet);
        }
        // your controller action should return a JsonResult. There's no such thing as a controller action that returns void. You have specified dataType: 'json' so return JSON. That's it. As far as what parameter this controller action should take, well, from the JSON you are sending ({"Name":"AA"}) it should be a class that has a Name property of type string. 
        //  modify your action signature to look like this: [HttpPost] public ActionResult SaveProduct (Product product) { ... return Json(new { success = true }); }. Or get rid of this dataType: 'json' attribute from your AJAX request if you don't want to return JSON. In this case you could return simply status code 201 (Created with empty response body): return new HttpStatusCodeResult(201);.
        [HttpPost]
        public ActionResult SaveProduct(Product product)
        {
            using (var db = new KOEntities())
            {
                db.Products.Add(new Product { Name = product.Name, DateCreated = DateTime.Now });
                db.SaveChanges();
            }
            return Json(new { success = true });
        }
    }
}

View:

@{
    ViewBag.Title = "Knockout";
}

<h2>Knockout</h2>
<h3>Load JSON Data</h3>
<div id="loadJson-custom" class="left message-info">
    <h4>Products</h4>
    <div id="accordion" data-bind='template: { name: "product-template", foreach: product }'>
    </div>
</div>

<h3>Post JSON Data</h3>
<div id="postJjson-custom" class="left message-info">
    <h4>Add Product</h4>
    <input id="productName" /><br />
    <button id="addProduct">Add</button>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/knockout")
    @Scripts.Render("~/bundles/livequery")
    <script src="/Scripts/jquery.livequery.min.js"></script>
    <style>
        #accordion { width: 400px; }
    </style>
    <script id="product-template" type="text/html">
        <h3><a data-bind="attr: {href:'#', title: Name, class: 'product'}"><span data-bind="text: Name"></span></a></h3>
        <div>
            <p>Here's some into about <span data-bind="text: Name" style="font-weight: bold;"></span> </p>
        </div>
    </script>

    <script>
      var isBound;
        function loadJsonData() {
            $.ajax({
                url: "/knockout/GetProductList",
                type: "GET",
                contentType: "application/json",
                dataType: "json",
                data: {},
                async: true,
                success: function (data) {
                    var loadJsonViewModel = {
                        product: ko.observableArray(),
                        init: function () {
                            loadAccordion();
                        }
                    };
                    var a = $('loadJson-custom');
                    loadJsonViewModel.product = ko.mapping.fromJS(data);
                    if (isBound) { }
                    else
                    {
                      ko.applyBindings(loadJsonViewModel, $('loadJson-custom')[0]);
                      isBound = true;
                    }
                    loadJsonViewModel.init();
                }
            });
        }
        // push data back to the database
        function pushJsonData(productName) {
            var jData = '{"Name": "' + productName + '"}';
            $.ajax({
                url: "/knockout/SaveProduct",
                type: "POST",
                contentType: "application/json",
                dataType: "json",
                data: jData,
                async: true,
                success: function (data, textStatus, jqXHR) {
                  console.log(textStatus + " in pushJsonData: " + data + " " + jqXHR);
                  //ko.cleanNode($('loadJson-custom')[0]);
                    loadJsonData(); 

                },
                error: function (jqXHR, textStatus, errorThrown) {
                  alert(textStatus + " in pushJsonData: " + errorThrown + " " + jqXHR);
                }
            });
        }

        function loadAccordion() {
          $("#accordion > div").livequery(function () {
            if (typeof $("#accordion").data("ui-accordion") == "undefined")
            {
              $("#accordion").accordion({ event: "mouseover" });
            }
            else
            {
              $("#accordion").accordion("destroy").accordion({ event: "mouseover" });
            }
          });
        }

        $(function () {
          isBound = false;
            loadJsonData();
            $("#addProduct").click(function () {
                pushJsonData($("#productName").val());
            });
        });
    </script>
}

Upvotes: 0

Views: 2253

Answers (2)

Satish Singh
Satish Singh

Reputation: 2339

Here is a complete solution for your question. I have just implemented and checked. Please have a look.

I have created a class for getting some records ie: Records.cs.

public static class Records
{
    public static IList<Student> Stud(int size)
    {
        IList<Student> stud = new List<Student>();

        for (int i = 0; i < size; i++)
        {
            Student stu = new Student()
                              {
                                  Name = "Name " + i,
                                  Age = 20 + i
                              };
            stud.Add(stu);
        }
        return stud;
    }
}

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Here is a controller for the respective view.

//
    // GET: /HelpStack/
    private static IList<Student> stud = Records.Stud(10);
    public ActionResult HelpStactIndex()
    {
        return View();
    }

    [HttpGet]
    public JsonResult GetRecord()
    {
        return Json(stud, JsonRequestBehavior.AllowGet);
    }

    [HttpPost]
    public void PostData(Student model)
    {
        //do the required code here as All data is in "model"
    }

Here is a view as HTML, I have taken two section one for list and other to Add records

<div id="loadJson-custom">
<h4>Student</h4>
<table width="100%">
    <tr>
        <td style="width: 50%">
            <div id="accordion" data-bind='template: { name: "product-template", foreach: Student }'>
            </div>
        </td>
        <td valign="top">
            <div id="NewStudent">
                <input type="text" data-bind="value: Name" /><br />
                <input type="number" data-bind="value: Age" /><br />
                <input type="submit" data-bind="click: Save" value="AddNew" />
            </div>
        </td>
    </tr>
</table>

Here is your scripts for Knockoutjs.

<script id="product-template" type="text/html">

    <h3><a data-bind="attr: { href: '#', title: Name, class: 'product' }"><span data-bind="    text: Name"></span></a>
        <br />
        Age: <span data-bind="text: Age"></span>
    </h3>
    <div>
        <p>Here's some into about <span data-bind="text: Name" style="font-weight: bold;"></span></p>
    </div>
</script>

<script type="text/javascript">
    //Model for insert new record
    var Model = {
        Name: ko.observable(''),
        Age: ko.observable(''),
    };

    var Student = ko.observableArray([]); // List of Students

    function loadData() { //Get records
        $.getJSON("/HelpStack/GetRecord", function (data) {
            $.each(data, function (index, item) {
                Student.push(item);
            });
        }, null);
    }

    function Save() { //Save records
        $.post("/HelpStack/PostData", Model, function () { //Oncomplete i have just pushed the new record.

            // Here you can also recall the LoadData() to reload all the records
            //Student.push(Model);
            Student.unshift(Model); // unshift , add new item at top

        });
    }
    $(function () {
        loadData();
        ko.applyBindings(Model, $('#NewStudent')[0]);
    });
</script>

Upvotes: 1

Anwar Javed
Anwar Javed

Reputation: 377

You are declaring your model inside loadJsonData function success callback, & creating new object on every success callback, move the model outside that function, create an object & use it inside loadJsonData function, it will fix the issue.

Upvotes: 1

Related Questions