Search code examples
asp.net-mvclistviewkendo-uitempdatatransport

Kendo ListView: Add/Create not reflected until browser session is refreshed


I'm using MVC and a Kendo (UI v2018.3.1017) ListView to manage a list I refer to as 'MaximoList'. To make the list available to multiple controllers, I'm leveraging the MVC provided "TempData" object. On Create (and Destroy), I see that the MaximoList object is properly reflecting changes. But to reflect the change in the UI, I have to refresh (F5) the (Chrome) browser.

CSHTML reference to the control is fairly straightforward:

    <div class="col-md-3">
      <div class="form-group">
        <label style="white-space: nowrap;">Maximo Id's</label>
        <a id="btnAddMaximoEntry" onclick=window.allIsos.addMaximoEntry() href="#" title='Add Maximo' style='margin-left: 8px'><i class='fa fa-upload'></i></a>
        <div class="" id="divMaximoList">
        </div>
      </div>
    </div>

The JS definition for the control is:

function addMaximoListView(isoId, outageId) {
  maximoListDataSource = new kendo.data.DataSource({
    transport: {
      read: { url: "/request/GetMaximoList", type: "GET", dataType: "json", cache: false },
      create: { url: "/request/AddMaximoEntry", type: "Post", dataType: "json", cache: false },
      destroy: { url: "/request/DeleteMaximoEntry", type: "POST", dataType: "json", cache: false },
      update: {},
      parameterMap: function (data, type) {
        if (type == "read") { return { IsoId: isoId, OutageId: outageId }; }
        if (type == "create") { return { MaximoId: data.MaximoId }; }
        if (type == "destroy") { return { id: data.Id }; }
      },
      sortable: true,
      pageable: true,
    },
    requestEnd: function (e) {
      console.log(e.type);
      console.log(e.response);
    },
    columns: [
      { field: "Id", Title: "Id", width: "0px" },
      { field: "MaximoId", Title: "MaximoId", width: "50px" },
      { command: ["edit", "destroy"], title: "&nbsp;", width: "250px" }
    ],
    autoSync: false,
    schema: {
      model: {
        id: "Id",
        fields: {
          Id: { editable: false },
          MaximoId: { editable: true, type: "string" },
        }
      },
    },
  });
  var getTemplate = ""
    + "<div>"
    + "    <a onclick=window.allIsos.deleteMaximo(#:Id#) href='\\#'><i class='fa fa-trash'></i></a>"
    + '#:MaximoId#</div>';
  var editTemplate = ""
    + "<div>"
    + "  <input type='text' data-bind='value: MaximoId' name='MaximoId' required='required' />"
    + "  <div>"
    + "    <a class='k-button k-update-button' href='\\#'><span class='k-icon k-i-check'></span></a>"
    + "    <a class='k-button k-cancel-button' href='\\#'><span class='k-icon k-i-cancel'></span></a>"
    + "  </div>"
    + "</div>";
  $("#divMaximoList").kendoListView({
    template: kendo.template(getTemplate),
    editTemplate: kendo.template(editTemplate),
    dataSource: maximoListDataSource,
    pageSize: 5,
    dataBound: function (e) {
      console.log("ListView is bound and ready to render.");
    }
  });
};

The JS definition to Add items to the list is:

var addMaximoEntry = function () {
  var listView = $("#divMaximoList").data("kendoListView");
  listView.add();
};

Debugging the local app with a fresh restart, here is the control exposing testing data from the API/Database:

enter image description here

I've clicked my ADD button, and before accepting the entry, please note the value entering as 'NotWorking' for my new MaximoId:

enter image description here

After accepting the input, note that the control has added a new entry but, it's a ghost of one of the original items:

enter image description here

However after refreshing the page (F5), note when the GET fires pulling from server-side TempData object, the actual 'NotWorking' item was properly received by the server, and that updated TempData object is passed to the UI (which includes the new entry):

enter image description here

The API method for the Create/Add is as follows - I'm returning to the UI, the JSON representation of the updated MaximoList object and using that updated object, am refreshing to TempData["MaximoList"]:

[HttpPost, Route("AddMaximoEntry")]
public ActionResult AddMaximoEntry(string MaximoId)
{
  var ignoreCase = StringComparison.InvariantCultureIgnoreCase;
  try
  {
    List<MaximoEntry> maximoList = TempData["MaximoList"] != null ? TempData["MaximoList"] as List<MaximoEntry> : new List<MaximoEntry>();

    var maximoId = MaximoId;// unnecessary variable, but used for testing against multiple method confgurations

    var maximoEntry = new MaximoEntry();
    maximoEntry.MaximoId = maximoId;

    if (maximoList.Where(s => s.MaximoId.Equals(maximoEntry.MaximoId, ignoreCase)).FirstOrDefault() == null)
      maximoList.Add(maximoEntry);

    TempData["MaximoList"] = maximoList as List<MaximoEntry>;
    return Json(maximoList, JsonRequestBehavior.AllowGet);
  }
  catch (Exception ex)
  {
    var msg = $"Excepton: {ex.Message}. Inner Expection: {(ex.InnerException == null ? "N/A" : ex.InnerException.ToString())}";
    return new JsonResult
    {
      Data = new
      {
        id = 0,
        Success = false,
        Msg = "Something went wrong while adding to the Maximo List. Please contact system support!"
      }
    };
  }
}

And the API GET is as follow:

[HttpGet, Route("GetMaximoList")]
public ActionResult GetMaximoList(int IsoId, int OutageId)
{
  try
  {
    List<MaximoEntry> maximoList = ODataGetMaximoList(IsoId, OutageId);
    return Json(maximoList, JsonRequestBehavior.AllowGet);
  }
  catch (Exception ex)
  {
    var msg = $"Excepton: {ex.Message}. Inner Expection: {(ex.InnerException == null ? "N/A" : ex.InnerException.ToString())}";
    return new JsonResult
    {
      Data = new
      {
        id = 0,
        Success = false,
        Msg = "Something went wrong while getting Maximo List. Please contact system support!"
      }
    };
  }
}

Solution

  • Originally, the API method for ADD(CREATE) passed back the entire List (JSON Array) of Maximo entries.

    Solution is to pass back only the updated version of the model object initially passed to the ADD(CREATE) method, including the newly assigned "id" value. Newly created entries will have a negative index value which I'll later use at the DB level within a MERGE statement (ie negative values for the primary key shouldn't exist in the DB table) to sync the DB.

    If not already clear, I'm using the TempData["MaximoList"] object to maintain the state of the Kendo ListView Control across API controllers. Saving the control state to the DB (leveraging the TempData object) will occur in a more generic controller method which doesn't manage state (adds/deletes/etc.) to the TempData object.

    I've also reworked the DESTROY (Delete) components to remove deleted items from the (UI) list only on a successful API call, and have cleaned up the JS by removing a number of unnecessary sets to the control not required to reflect the state of TempData object.

    My API Method for ADD method is now:

    [HttpPost, Route("AddMaximoEntry")]
    public JsonResult AddMaximoEntry(MaximoEntry model)
    {
      var ignoreCase = StringComparison.InvariantCultureIgnoreCase;
      try
      {
        List<MaximoEntry> maximoList = TempData["MaximoList"] != null ? TempData["MaximoList"] as List<MaximoEntry> : new List<MaximoEntry>();
    
        var minId = maximoList.Where(w => w.id <= 0);
        model.id = minId.Count() == 0 ? -1 : minId.Select(s => s.id).Min() - 1;
    
        if (maximoList.Where(s => s.maximoId.Equals(model.maximoId, ignoreCase)).FirstOrDefault() == null)
          maximoList.Add(model);
    
        TempData["MaximoList"] = maximoList as List<MaximoEntry>;
        return Json(model, JsonRequestBehavior.AllowGet);
      }
      catch (Exception ex)
      {
        var msg = $"Excepton: {ex.Message}. Inner Expection: {(ex.InnerException == null ? "N/A" : ex.InnerException.ToString())}";
        return new JsonResult
        {
          Data = new
          {
            Success = false,
            Msg = "Something went wrong while adding to the Maximo List. Please contact system support!"
          }
        };
      }
    }
    

    I've also updated my DELETE method, as an HTTPDELETE method, and returning a status indicator to alert the calling JS of operation outcome:

    [HttpDelete, Route("DeleteMaximoEntry")]
    public JsonResult DeleteMaximoEntry(int id)
    {
      try
      {
        List<MaximoEntry> maximoList = TempData["MaximoList"] != null ? TempData["MaximoList"] as List<MaximoEntry> : new List<MaximoEntry>();
    
        var itemToRemove = maximoList.SingleOrDefault(r => r.id == id);
        if (itemToRemove != null)
          maximoList.Remove(itemToRemove);
    
        TempData["MaximoList"] = maximoList as List<MaximoEntry>;
    
        return Json(new { success = true, responseText = $"id {id} deleted from TempData object" });
      }
      catch (Exception ex)
      {
        var msg = $"Excepton: {ex.Message}. Inner Expection: {(ex.InnerException == null ? "N/A" : ex.InnerException.ToString())}";
        return new JsonResult
        {
          Data = new
          {
            Success = false,
            Msg = $"Something went wrong while deleteing Maximo Entry (id {id}). Please contact system support!"
          }
        };
      }
    }
    

    The JS control definition for SELECT (READ) and CREATE(Add) is:

    function addMaximoListView(isoId, outageId) {
      maximoListDataSource = new kendo.data.DataSource({
        transport: {
          batch: false,//default is false
          read: { dataType: "json", cache: false, type: "GET", url: "/request/GetMaximoList" },
          create: { dataType: "json", cache: false, type: "POST", url: "/request/AddMaximoEntry" },
          parameterMap: function (data, type) {
            if (type == "read") { return { IsoId: isoId, OutageId: outageId }; }
            if (type == "create") { return { model: data }; }
          },
        },
        requestEnd: function (e) {
          console.log(e.type);
          console.log(e.response);
        },
        autoSync: false,
        serverFiltering: true,
        schema: {
          model: {
            id: "id",
            fields: {
              maximoId: { editable: true, type: "string" },
            }
          },
        },
      });
      var getTemplate = ""
        + "<div>"
        + "  <a onclick=window.allIsos.deleteMaximo(#:id#) href='\\#'><i class='fa fa-trash'></i></a>"
        + "  #:maximoId#"
        + "</div > ";
      var editTemplate = ""
        + "<div>"
        + "  <input type='text' data-bind='value: maximoId' name='MaximoId' required='required' />"
        + "  <div>"
        + "    <a class='k-button k-update-button' href='\\#'><span class='k-icon k-i-check'></span></a>"
        + "    <a class='k-button k-cancel-button' href='\\#'><span class='k-icon k-i-cancel'></span></a>"
        + "  </div>"
        + "</div>";
    
      $("#divMaximoList").kendoListView({
        template: kendo.template(getTemplate),
        editTemplate: kendo.template(editTemplate),
        dataSource: maximoListDataSource,
        dataBound: function (e) {
          console.log("ListView is bound and ready to render.");
        }
      });
    };
    

    For DESTROY (DELETE), I've added these JS methods (hope to embed these elements into the control defintion):

    var deleteMaximo = function (id) {
      executeDeleteMaximo(id, "Are you sure you want to delete this Maximo entry?");
    }
    
    function executeDeleteMaximo(id, confirmMsg) {
      common.LoadingGif("#mainView", false);
      common.ConfirmDialog(true, confirmMsg,
        function (reason) {
          if (reason == "NO") {
            common.LoadingGif("#mainView", true);
            return false;
          }
          $.ajax({
            type: "DELETE",
            data: { id: id },
            url: "/request/DeleteMaximoEntry",
            success: function (response) {
              var data = response;
              if (data.success) {
                var myDataSource = $("#divMaximoList").data().kendoListView.dataSource;
                var item = myDataSource.get(id);//need a check before attempting remove
                myDataSource.remove(item);
    
              }
              common.LoadingGif("#mainView", true);
              window.scrollTo(0, 0);
            },
            error: function (jqXHR, textStatus, errorThrown) {
              var e = errorThrown;
            }
          });
        });
    }