Webgrid needs to update with new content in MVC - javascript

I am still very new to MVC, JavaScript, and jQuery so please bear with me.
I have a webgrid that contains different terms and their translations. The list of terms is dependent on the 'VMID' chosen from the drop down list above the grid. (Or at least it would be, if it were working correctly.)
The left-most column has an edit link for each term that leads to a Boostrap modal, which is populated with all the values assigned to the ID chosen in that drop down list. I need the terms in the grid to also depend on the value chosen from that list.
The approach I am currently trying goes like this (only pasting the bits relevant to the question) -
Main view (strongly typed with model reference, not included):
<div class="container-fluid">
<div style=" margin-bottom: 1.4%;">
<table>
<tr>
<td style="font-size: medium; margin-bottom: 5px">
#Model.lblVMID:
</td>
<td>
#Html.DropDownListFor(model => model.VMID, new System.Web.Mvc.SelectList(Model.locations, "Key", "Value"), new { #class = "form-control", id = "ddlVMID", onchange = "RepopGrid()" })
</td>
</tr>
</table>
</div>
<div class="table-scrollable well" id="termGrid">
#{Html.RenderPartial("_TermGrid", Model);}
</div>
</div>
<script type="text/javascript">
function RepopGrid() {
VMID = $("#ddlVMID").val();
ShowLoadingDialog();
$.ajax({
url: URLPrefix() + "/Terminology/RepopGrid/" + VMID,
type: "POST",
success: function () {
HideLoadingDialog();
},
error: function (jqXHR, textStatus, errorThrown) {
HideLoadingDialog();
ShowAlert(false, 'Failed to change location\r\n' + errorThrown);
}
})
}
</script>
Partial view (strongly typed with model reference, not included. Same model that the main view uses):
#Model.grid.GetHtml(columns: Model.columns, alternatingRowStyle: "info", nextText: "Next",
previousText: "Previous", tableStyle: "table")
Controller:
public ActionResult Index()
{
TerminologyModel model = new TerminologyModel(clsUtils.PreferredVMID());
return View(model);
}
[HttpPost]
public ActionResult RepopGrid(int VMID)
{
TerminologyModel model = new TerminologyModel(VMID);
return PartialView("_TermGrid", model);
}
The model accepts an 'int VMID' and uses that to retrieve the list of terms from the database, then a foreach runs through each term and assigns them to the grid. This works fine, so I didn't feel a need to post it here (it's a bit long, because there are some special columns that need extra work to get set up).
We have a route configuration file that maps URLS to their corresponding actions in the controllers, in this case:
routes.MapRoute(
name: "TerminologyRepopGrid",
url: "Terminology/{action}/{VMID}",
defaults: new { controller = "Terminology", action = "RepopGrid", VMID = UrlParameter.Optional }
);
I'm not familiar with Ajax, so I'm probably using it completely wrong.
This approach is based on a few places where I've read to put the grid in a partial view, so that's what I've done here.
After I choose a new option, I can see that a whole new grid is being returned in Chrome's element inspector, but that grid is not being applied on top of the existing one.
Again, I have been searching and trying and reading and experimenting and I just can't figure out why mine won't work.

I moved the drop down list to the partial view where the grid is, wrapped everything in an Ajax Form, removed the "RepopGrid" JavaScript and controller actions, and added a parameter to the Index action for a VMID. If the VMID is null or empty (when the page is first loaded or refreshed), it uses the default VMID to generate the model. If a valid VMID is received, then it uses that number to generate the model instead.
Here is the new code for those who might be looking for a similar solution (like last time, only the relevant parts):
Index.cshtml -
<div class="table-scrollable well" id="termGrid">
#Html.Partial("_TermGrid", Model)
</div>
<div class="modal fade" id="editTerm" tabindex="-1" role="dialog" aria-labelledby="editTerm-label" aria-hidden="true">
<div class="modal-dialog" style="width: 290px">
<div class="modal-content" style="width: 290px">
<div class="modal-header" style="border-bottom: none; padding-bottom: 0px;">
<h4 id="lblParamName" align="center"></h4>
</div>
<div class="modal-body" id="editTermBody" style="padding: 8px">
</div>
</div>
</div>
</div>
Partial View -
#{
var ajaxOptions = new AjaxOptions()
{
OnSuccess = "OnSuccess",
OnFailure = "OnFailure",
OnBegin = "OnBegin",
HttpMethod = "Post"
};
using (Ajax.BeginForm("Index", "Terminology", ajaxOptions))
{
<div class="container-fluid" id="termGrid">
<div style=" margin-bottom: 1.4%;">
<table>
<tr>
<td style="font-size: medium; margin-bottom: 5px">
#Model.lblVMID<label>: </label>
</td>
<td>
#Html.DropDownListFor(model => model.VMID, new System.Web.Mvc.SelectList(Model.locations, "Key", "Value"), new { #class = "form-control", id = "ddlVMID", onchange = "this.form.submit()" })
</td>
</tr>
</table>
</div>
</div>
}
}
#Model.grid.GetHtml(columns: Model.columns, alternatingRowStyle: "info", nextText: "Next",
previousText: "Previous", tableStyle: "table")
<script type="text/javascript">
function OnSuccess(data, textStatus, jqXHR) {
HideLoadingDialog();
}
function OnFailure(data, textStatus, jqXHR) {
HideLoadingDialog();
ShowAlert(false, "Oops! Something went wrong. :(");
}
function OnBegin() {
ShowLoadingDialog();
VMID = $("#ddlVMID").val()
ShowLoadingDialog();
$.ajax({
url: URLPrefix() + "/Terminology/Index/" + VMID,
type: "POST",
success: function () {
HideLoadingDialog();
},
error: function (jqXHR, textStatus, errorThrown) {
HideLoadingDialog();
ShowAlert(false, 'Failed to change location\r\n' + errorThrown);
}
})
}
</script>
Controller -
public ActionResult Index(string VMID)
{
if (string.IsNullOrEmpty(VMID))
{
TerminologyModel model = new TerminologyModel(clsUtils.PreferredVMID());
return View(model);
}
else
{
TerminologyModel model = new TerminologyModel(int.Parse(VMID));
return View(model);
}
}
The model's code has not changed since the question was originally asked.

Related

Cannot work out how to access property of self in ViewModel

I am very new to js and html - trying to make a basic front end for a C# web api.
I'm making a simple app for tracking bugs. I have a panel for the list of bugs, where I can click "Details" to see more info on each bug (I would post an image, but my reputation is too low). Then a new panel opens with with the details of the bug, including a button to close the bug, ie change set the status to "closed". It's with this button that I have the problem.
I have this in my Index.cshtml:
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Bugs</h2>
</div>
<div class="panel-body">
<ul class="list-unstyled" data-bind="foreach: bugs">
<li>
<strong><span data-bind="text: Title"></span></strong>: <span data-bind="text: Description"></span>
<small>Details</small>
</li>
</ul>
</div>
</div>
<div class="alert alert-danger" data-bind="visible: error"><p data-bind="text: error">
</p></div>
<!-- ko if:detail() -->
#* Bug Detail with Close Button *#
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Detail</h2>
</div>
<table class="table">
<tr><td>Title</td><td data-bind="text: detail().Title"></td></tr>
<tr><td>Description</td><td data-bind="text: detail().Description"></td></tr>
<tr><td>Status</td><td data-bind="text: detail().Status"></td></tr>
<tr><td>Created</td><td data-bind="text: detail().Created"></td></tr>
<tr><td>Owner</td><td data-bind="text: detail().Owner"></td></tr>
</table>
<div class="panel-body">
<form class="form-horizontal" data-bind="submit: closeBug(detail())">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
</div>
</div>
</div>
<!-- /ko -->
Then this is the relevant stuff in app.js:
var ViewModel = function () {
var self = this;
self.bugs = ko.observableArray();
self.error = ko.observable();
self.detail = ko.observable();
self.getBugDetail = function (item) {
ajaxHelper(bugsUri + item.Id, 'GET').done(function (data) {
self.detail(data);
});
}
var bugsUri = '/api/bugs/';
function ajaxHelper(uri, method, data) {
self.error(''); // Clear error message
return $.ajax({
type: method,
url: uri,
dataType: 'json',
contentType: 'application/json',
data: data ? JSON.stringify(data) : null
}).fail(function (jqXHR, textStatus, errorThrown) {
self.error(errorThrown);
});
}
// get open bugs
function getAllBugs() {
ajaxHelper(bugsUri, 'GET').done(function (data) {
self.bugs(data);
});
}
// Fetch the initial data.
getAllBugs();
//close bug
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: self.localDetail.OwnerId,
Description: self.localDetail.Description,
Status: "closed",
Title: self.localDetail.Title,
Created: self.localDetail.Created
};
ajaxHelper(bugsUri + self.localDetail.Id, 'DELETE', self.localDetail.Id);
ajaxHelper(bugsUri, 'POST', closedBug).done(function (item) {
self.bugs.push(item);
});
}
};
To update the status of a bug, I want to take the Id of the bug currently open in the detail panel and create an identical bug except with Status set to "closed". The trouble is that there's always a problem access self.localDetail in the new variable closedBug. I've tried it here by parameterizing the closeBug method, but I've also tried accessing self.Detail, but it's done no good, so I'm here. My next step, if this fails, is to create a separate panel entirely with a form for bugId which closes the bug when you submit, but it would be better to be in the bug details window.
you're already passing localDetail as the param in the closeBug fn, so you don't need to refer to it by adding self. Try this (removed all references to self.):
//close bug
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: localDetail.OwnerId,
Description: localDetail.Description,
Status: "closed",
Title: localDetail.Title,
Created: localDetail.Created
};
ajaxHelper(bugsUri + localDetail.Id, 'DELETE', localDetail.Id);
ajaxHelper(bugsUri, 'POST', closedBug).done(function (item) {
self.bugs.push(item);
});
}
Your first issue is in your submit binding itself. It's being called as soon as it's rendered, not on submit. You want to pass the function object (optionally with bound arguments) instead of calling it in your html.
Explicit Bound Arguments
<form class="form-horizontal" data-bind="submit: closeBug.bind(null, detail)">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
which will bind null as the this value of the function and pass the detail observable as the first argument. With this, your closeBug looks like
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: localDetail().OwnerId
}
}
Note, you want to unwrap it in the handler, not the html so you get the latest value and not the initial value.
Binding Context as this
Alternatively (and in more idiomatic knockout fashion) you can bind to the function object and it will be called with the binding context as this (the same as explicitly using closeBug.bind($data)).
<form class="form-horizontal" data-bind="submit: closeBug">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
self.closeBug = function () {
var closedBug = {
OwnerId: this.localDetail().OwnerId
}
}
Aside: this may be helpful for better understanding this, self, and function.bind

View not updating with new resultset when button clicked

I am building a small ASP.NET Core 3.1 web application.
When application loads, a default View is presented with a set of records. On the View, I have a panel with search filters. When user selects an option from one of the DropDown and clicks "Search", the new set of rows should be loaded.
I have added the entire functionality as below but the View is not getting updated with the new result.
[HTML Button]
<table style="margin-left:2%;margin-top:2%">
<tr style="width:200px">
<td style="width:100px">Location</td>
<td style="width:300px">
#Html.DropDownList("ddlLocation", new List<SelectListItem>
{
new SelectListItem{ Text="-- Select --", Value = "0" },
new SelectListItem{ Text="North", Value = "1" },
new SelectListItem{ Text="South", Value = "2" },
new SelectListItem{ Text="West", Value = "3" }
}, new { #id = "ddlLocation" })
</td>
</tr>
</table>
<div style="margin: 0;position: relative;top: 30%;left:45%;right:45%">
<input type="button" id="btnSearch" value="Filter Rows" onClick="searchRows()" />
</div>
[Javascript Code]
function searchRows(TemplateData) {
var e = document.getElementById('ddlLocation');
var FilterText = e.options[e.selectedIndex].text;
#*var data = "#Newtonsoft.Json.JsonConvert.SerializeObject(Model)";*#
$.ajax({
url: '/Search/' + FilterText,
type: 'GET',
data: TemplateData,
success: function (TemplateData) {
$("#divData").html(TemplateData);
alert(TemplateData);
}
});
}
[Controller Action Method]
[HttpGet]
[Route("~/Search/{FilterText}")]
public IActionResult Search(string FilterText)
{
ModelState.Clear();
List<TemplateData> templateList = new List<TemplateData>();
templateList = ImportExcel(FilterText);
return View(templateList.AsEnumerable());
}
Please note that the 'templateList' in the above action method is fetching correct set and invoking View but the View is not updating.
I read somewhere that one Nuget Package for Runtime Compilation needs to be added. I tried to add it but it throws error: "Not compatible with ASP.NET Core 3.1".

Use MVC Session to store Client-side values (e.g. filter text) between visits

In an MVC View, is there an efficient way to store client-side values for use on subsequent page visits?
Typical scenario
An Index page has a table that's getting a bit long so I add a filter (I know paging is another option) and use an input control with some JavaScript to limit the table rows without having to perform another "Get" from the server.
This works fine but, if I navigate off (say) into an edit page then return back to the Index page, the filter is clearly no longer there.
After a bit of searching I never found anything simple so I post my meagre answer below.
The View contains a form at the top of the page into which a user can type filter text (on form "Get", text is set from a session value):-
<form id="frmEdit">
#Html.AntiForgeryToken()
<div class="form-group row">
<div class="col-sm-6">
#Html.ActionLink("Create New", "Create", null, new { #class = "nav-item nav-link" })
</div>
<label for="search" class="col-sm-2 col-form-label text-right">Filter</label>
<div class="col-sm-4">
<input type="text" placeholder="Filter" class="form-control" id="search" value=#Session["SparesSectionFilter"]>
</div>
</div>
</form>
A script section contains the filtering JavaScript but also a postback to the controller
#section Scripts{
<script type="text/javascript">
// on load
PerformFilter();
// hook up events
$(function () {
$("input#search").on("keydown keyup", function () {
PerformFilter();
// post back to session for reuse
$.post('SparesSections/Session_Add', { __RequestVerificationToken: $('[name=__RequestVerificationToken]').val(), itemName: 'SparesSectionFilter', itemValue: $("#search").val() });
});
})
</script>
}
I have a custom base-class for my controller into which I've added the following actions. These are usable from any controller using this class. The Razor view loads the session value but I've included a "Get" in the controller for client-side options.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Session_Add(string itemName, string itemValue)
{
Session.Add(itemName, itemValue);
return Json(new { itemName = itemName, itemValue = itemValue }, JsonRequestBehavior.AllowGet);
}
[HttpGet]
public ActionResult Session_Get(string itemName)
{
return Json(new { itemName = itemName, itemValue = Session[itemName] ?? string.Empty }, JsonRequestBehavior.AllowGet);
}

Child view model modying a different child viewmodel

I have a main View Model for my screen. It consists of 2 child view models.
One handles the registration section.
One handles the login section.
One handles the menu section (If authenticated and what menu items can appear, as well as the "Welcome "Username" type stuff).
$(document).ready(function () {
// Create the main View Model
var vm = {
loginVm: new LoginViewModel(),
registerVm: new RegisterViewModel(),
layoutVm: new LayoutViewModel()
}
// Get the Reference data
var uri = '/api/Reference/GetTimezones';
$.getJSON({ url: uri, contentType: "application/json" })
.done(function (data) {
vm.registerVm.Timezones(data);
});
// Bind.
ko.applyBindings(vm);
});
Once my Login model's "Login" method completes, I want to set the "IsAthenticated" value within the Menu model, as well as some other user info.
So in my login model, I have a SignIn method.
$.post({ url: uri, contentType: "application/json" }, logindata)
.done(function (data) {
toastr[data.StatusText](data.DisplayMessage, data.Heading);
if (data.StatusText == 'success') {
alert($parent.layoutVm.IsAuthenticated());
}
else {
}
})
.fail(function () {
toastr['error']("An unexpected error has occured and has been logged. Sorry about tbis! We'll resolve it as soon as possible.", "Error");
});
The alert code is my testing. I am hoping to access (and set) the IsAuthenticated property of the layoutVm model. That's one of the child models on my main View model.
However, "$parent" is not defined.
How can I update values in the layoutVm, from my loginVm?
$parent is part of the binding context, which is only available during the evaluation of the data-bind (i.e. to the binding handler).
In your viewmodel structure, you'll have to come up with a way to communicate between models yourself. For example, by passing parent view models, or by passing along shared observables. The problem you're describing can be solved by using data-bind="visible: $root.userVM.IsAuthenticated", like I answered in your previous question.
If you'd like to go with the other approach, here's an example on how to share an observable between viewmodels.
var ChildViewModel = function(sharedObs) {
this.myObs = sharedObs;
this.setObs = function() {
this.myObs(!this.myObs());
}.bind(this);
}
var RootViewModel = function() {
this.myObs = ko.observable(false);
this.vm1 = new ChildViewModel(this.myObs);
this.vm2 = new ChildViewModel(this.myObs);
this.vm3 = new ChildViewModel(this.myObs);
}
ko.applyBindings(new RootViewModel());
div { width: 25%; display: inline-block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="with: vm1">
<h4>vm1</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm2">
<h4>vm2</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm3">
<h4>vm3</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
Note that each of the child view models also have write permission, so you'll have to be careful to not accidentally update the observable

Not able to refresh the partial view after a form submission from another partial view within it, using ajax in mvc razor

In my website I have a Facebook like chat page. I have implemented it with basic form submission method that will refresh the whole page when I post something. now I need to change it using ajax/jquery so that it should only refresh my partial views. I have written code for that and I changed my views by adding scripts.
My main Message view is like (sample):
#model myModel
<h2>
Message Board<small class="on-right"/>
</h2>
// Text box for Adding Message(post)
#Html.TextArea("Message", new { #placeholder = "Add a post", id = "Message" })
<input type="button" id="Post" value="Post"/>
// partial view that displays all the messages along with its comments
<div id="messagelist">
#{
Html.RenderPartial("_posts", Model.MessageList);
}
</div>
script for message page:
$('#Post').click(function () {
var url = "/MyController/Messages";
var Message = $("#Message").val();
$("#Message").val("");
$.post(url, { Message: Message }, function (data) {
$("#messagelist").html(data);
_post partial view:
#model IEnumerable<Model.MessageList>
//Foreach loop for displaying all the messages
#foreach (var item in Model)
{
<div >
#Html.DisplayFor(model => item.UserName)
#Html.DisplayFor(model => item.MessageText)
//Foreach loop for displaying all the comments related to each message
#foreach (var item1 in item.Comments)
{
#item1.UserName
#item1.MessageText
}
</div>
//partial view for adding comments each for messages
#Html.Partial("Comment", new ModelInstance { MessageId = item.MessageId })
}
Comment partial view (I am using ajax form submit):
#model ModelInstance
//form for submitting a message instance with parent message id for adding a comment to the parent message
#using (Ajax.BeginForm("Comment", "MyController", new AjaxOptions { UpdateTargetId = "messagelist" }))
{
#Html.AntiForgeryToken() #Html.ValidationSummary(true)
<div>
#Html.HiddenFor(modelItem => Model.MessageId)
#Html.TextBoxFor(modelItem => Model.CommentText, new { #placeholder = "leave a comment" })
<button class="btn-file" type="submit"></button>
</div>
}
Controller actions (sample):
public ActionResult Messages(string Message)
{
------------------------------
create messag object
---------------------
add to database
-------------------
fetch again for refreshing
------------------------
return PartialView("_posts", refreshed list);
}
public ActionResult Comment(StudiMessageDetails Comment)
{
------------------------------
create messag object
---------------------
add to database
-------------------
fetch again for refreshing
return PartialView("_posts", msgDetails);
}
Now the Posting message and posting comment is working perfectly. also when I post a message it only refreshes my main message view.
But when I post a comment it is giving me the refreshed partial view only. it is not getting bound to the 'div id=messagelist' and not giving me the full page. Can anybody tell where I am going wrong ? please help.
Your Ajax.BeginForm() is replacing the contents of <div id="messagelist"> with the html from return PartialView("_posts", msgDetails);. I suspect the model msgDetails contains only the details of the comment's associated message so that's all your seeing.
I suggest to rethink your design, particularly the Messages() method, which after saving the message is calling the database to get all messages and returning the complete list - you already have all the data on the page so this seems completely unnecessary and just degrades performance. You could simplify it with the following
Partial view (note the partial is for one message, not a collection)
#model Model.Message
<div class="message">
<div class=username">#Model.UserName</div>
<div class=messagetext">#Model.MessageText</div>
<div class="commentlist">
#foreach (var comment in Model.Comments)
{
<div class="comment">
<div class="username">#comment.UserName<div>
<div class="commenttext">#comment.MessageText<div>
</div>
}
</div>
<div>
#Html.TextBox("CommentText", new { placeholder = "Leave a comment", id = "" }) // remove the id attribute so its not invalid html
<button class="addcomment" data-id="#Model.MessageId">Add Comment</button>
</div>
</div>
Main View
#model myModel
...
#Html.TextArea("Message", new { placeholder = "Add a post" }) // no point adding the id - its already added for you
<input type="button" id="Post" value="Post" />
<div id="messagelist">
#foreach(var message in Model.MessageList)
{
#{ Html.RenderPartial("_posts", message); }
}
</div>
<div id="newmessage"> // style this as hidden
#{ Html.RenderPartial("_posts"); }
</div>
Scripts
// Add a model or ViewBag property for the current user name
var userName = JSON.parse('#Html.Raw(Json.Encode(ViewBag.UserName))');
$('#Post').click(function () {
var url = '#Url.Action("Message", "MyController")'; // dont hardcode!
var message = $('#Message').val();
$.post(url, { MessageText: message }, function (data) {
if(data) {
// clone the new message, update message id and add to DOM
var html = $('#newmessage').clone();
message.children('.username').text(userName);
message.children('.messagetext').text(message);
message.children('.newcomment').children('button').data('id', data);
$('#messagelist').perpend(html); // add at top of list
$('#Message').val(''); // clear message text
} else {
// something went wrong
}
});
});
$('.addcomment').click(function() {
var url = '#Url.Action("Comment", "MyController")';
var comment = $(this).prev('input');
var messageID = $(this).data('id');
var list = $(this).closest('.message').children('.commentlist');
$.post(url, { MessageID: messageID, CommentText comment.val() }, function (data) {
if (data) {
// add message
var html = $('<div><div>').addClass('comment');
html.append($('<div><div>').addClass('username').text(userName));
html.append($('<div><div>').addClass('commenttext').text(commentText));
list.append(html); // add to end of existing comments
comment.val(''); // clear existing text
} else {
// something went wrong
}
});
});
Controller
[HttpPost]
public ActionResult Message(string MessageText)
{
// create new message object and save to database
int ID = // the ID of the new message
return Json(ID); // if exception, then return null;
}
[HttpPost]
public ActionResult Comment(int MessageID, string CommentText)
{
// create new comment object and save to database
return Json(true); // if exception, then return null;
}

Categories